-
-
Save bradphelan/5395652 to your computer and use it in GitHub Desktop.
[Fact] | |
public void UndoStackChangeNotificationSpec() | |
{ | |
var stack = new UndoStack<A>(new A(10, null)); | |
var psubj = stack.ImmutablePropertySubject(x => x.P); | |
int current = 0; | |
psubj.Subscribe(c => current = c); | |
psubj.OnNext(20); | |
stack.Current.P.Should().Be(20); | |
current.Should().Be(20); | |
psubj.OnNext(30); | |
stack.Current.P.Should().Be(30); | |
current.Should().Be(30); | |
psubj.OnNext(40); | |
stack.Current.P.Should().Be(40); | |
current.Should().Be(40); | |
stack.Undo(); | |
stack.Current.P.Should().Be(30); | |
current.Should().Be(30); | |
stack.Undo(); | |
stack.Current.P.Should().Be(20); | |
current.Should().Be(20); | |
stack.Undo(); | |
stack.Current.P.Should().Be(10); | |
current.Should().Be(10); | |
// Verify we don't pop the initial value | |
stack.Undo(); | |
stack.Current.Should().NotBe(null); | |
current.Should().Be(10); | |
} |
interface Immutable { }; | |
static class ImmutableExtension | |
{ | |
public static T Set<T,V> | |
(this T This, Expression<Func<T, V>> prop, V value) | |
where T : class, Immutable | |
{ | |
var names = ReactiveUI.Reflection.ExpressionToPropertyNames(prop).ToList(); | |
return This.Set(names, value); | |
} | |
public static T Set<T,V> | |
(this T This, List<string> names, V value) | |
where T : class, Immutable | |
{ | |
var name = names.First(); | |
var rest = names.Skip(1).ToList(); | |
if (names.Count==1) | |
{ | |
var copy = This.ShallowClone(); | |
copy.SetPrivatePropertyValue(names.First(), value); | |
return copy as T; | |
}else{ | |
var copy = This.ShallowClone(); | |
var subtree = copy | |
.GetPrivatePropertyValue<Immutable>(name) | |
.Set(rest, value); | |
copy.SetPrivatePropertyValue(names.First(), subtree ); | |
return copy as T; | |
} | |
} | |
public static object ShallowClone(this object o) | |
{ | |
var flags = BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; | |
return o.GetType().InvokeMember("MemberwiseClone", flags, null, o, null); | |
} | |
/// <summary> | |
/// Sets a _private_ Property Value from a given Object. Uses Reflection. | |
/// Throws a ArgumentOutOfRangeException if the Property is not found. | |
/// </summary> | |
/// <typeparam name="T">Type of the Property</typeparam> | |
/// <param name="obj">Object from where the Property Value is set</param> | |
/// <param name="propName">Propertyname as string.</param> | |
/// <param name="val">Value to set.</param> | |
/// <returns>PropertyValue</returns> | |
public static void SetPrivatePropertyValue<T>(this object obj, string propName, T val) | |
{ | |
Type t = obj.GetType(); | |
if (t.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) == null) | |
throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName)); | |
t.InvokeMember(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, obj, new object[] { val }); | |
} | |
/// <summary> | |
/// Returns a _private_ Property Value from a given Object. Uses Reflection. | |
/// Throws a ArgumentOutOfRangeException if the Property is not found. | |
/// </summary> | |
/// <typeparam name="T">Type of the Property</typeparam> | |
/// <param name="obj">Object from where the Property Value is returned</param> | |
/// <param name="propName">Propertyname as string.</param> | |
/// <returns>PropertyValue</returns> | |
public static T GetPrivatePropertyValue<T>(this object obj, string propName) | |
{ | |
if (obj == null) throw new ArgumentNullException("obj"); | |
PropertyInfo pi = obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); | |
if (pi == null) throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName)); | |
return (T)pi.GetValue(obj, null); | |
} | |
} |
class A : Immutable | |
{ | |
public int P { get; private set; } | |
public B B { get; private set; } | |
public A(int p, B b) | |
{ | |
P = p; | |
B = b; | |
} | |
} | |
class B : Immutable | |
{ | |
public int P { get; private set; } | |
public C C { get; private set; } | |
public B(int p, C c) | |
{ | |
P = p; | |
C = c; | |
} | |
} | |
class C : Immutable | |
{ | |
public int P { get; private set; } | |
public C(int p) | |
{ | |
P = p; | |
} | |
} | |
namespace Utils.Spec | |
{ | |
public class ImmutableObjectPatternSpec | |
{ | |
[Fact] | |
public void OhMyLord() | |
{ | |
var a = new A | |
( p:10 | |
, b: new B | |
( p: 20 | |
, c : new C(30))); | |
var a_ = a.Set(p => p.B.C.P, 10); | |
a.Should().NotBe(a_); | |
a.B.C.P.Should().Be(30); | |
a_.B.C.P.Should().Be(10); | |
} | |
} | |
} |
[Fact] | |
public void UndoStackLensSpec() | |
{ | |
var stack0 = new UndoStack<A>(new A(1, new B(1,new C(5)))); | |
// Create a lens onto A.B a lens is ISubject<T> but | |
// also remembers the property path it was generated | |
// from | |
var bLens = stack0.ImmutablePropertySubject(x => x.B); | |
// Create a sub lens to A.B.C.P | |
var bp = bLens.ForProperty(x => x.C.P); | |
// Change the property | |
bp.OnNext(10); | |
// Verify that it is changed in the undo stack | |
stack0.Current.B.C.P.Should().Be(10); | |
stack0.Undo(); | |
// Verify that undo still works | |
stack0.Current.B.C.P.Should().Be(5); | |
} |
class UndoStack<T> | |
where T : class, Immutable | |
{ | |
private Stack<T> ChangeHistory = new Stack<T>(); | |
public T Set<V> | |
(Expression<Func<T, V>> prop, V value) | |
{ | |
var last = ChangeHistory.Peek(); | |
var next = last.Set(prop, value); | |
ChangeHistory.Push(next); | |
return next; | |
} | |
public T Undo() | |
{ | |
ChangeHistory.Pop(); | |
return ChangeHistory.Peek(); | |
} | |
public UndoStack(T initial) | |
{ | |
ChangeHistory.Push(initial); | |
} | |
} |
[Fact] | |
public void UndoStackSpec() | |
{ | |
var stack = new UndoStack<A>(new A(10, null)); | |
stack.Current().B.Should().Be(null); | |
stack.Set(x => x.B, new B(20, null)); | |
stack.Current().B.Should().NotBe(null); | |
stack.Current().B.P.Should().Be(20); | |
stack.Undo(); | |
stack.Current().B.Should().Be(null); | |
} |
No way. That's a lot of boiler plate and a lot of code to get wrong. My solution just requires tagging with Immutable and all properties are Copy and Update compatible immediately.
The sort of typical cut and paste errors that this kind of code generates are things like this.
return new DataObject
{
Id = id.ValueOrDefault(Id),
Value = value.ValueOrDefault(Id),
Object = obj.ValueOrDefault(Object)
};
Note the Id on the right hand side of the Value assignment. Easy
to do cause you copy the above line and forget to change the
name in both places. If the types are the same then you get
no compiler error and you then have to write tests for this
behaviour which will probably generate copy paste errors.
You should read through the old ReactiveUI.Serialization code, it did something similar to implement object persistence a-la Google Wave, where you could traverse object "history"
Cool, but I'd rather use this approach: http://v2matveev.blogspot.com/2010/05/copy-and-update-in-c.html