Code available on GitHub
I'm currently building a UI with a semi-complex input-form that requires the ability to 'undo, redo, undo, redo' user actions ad-infinitum. Not only does it require the ability to undo the text typed in text-boxes etc it needs the ability to undo (or redo) button clicks that add\remove complex properties (reference types).
For example imagine I have the following model, where there is a hierarchical structure to the data - parent-child relationships, the Widget has one settable value type property - 'Name' & a collection of child Widgets accessed via the 'Children' property, this property can not set by the caller- the caller has to use the 'AddChild' & 'RemoveChild' methods to affect the enumerable 'Children' property. I want the ability to 'undo, redo, undo, redo' on these two properties.
I'm currently building a UI with a semi-complex input-form that requires the ability to 'undo, redo, undo, redo' user actions ad-infinitum. Not only does it require the ability to undo the text typed in text-boxes etc it needs the ability to undo (or redo) button clicks that add\remove complex properties (reference types).
For example imagine I have the following model, where there is a hierarchical structure to the data - parent-child relationships, the Widget has one settable value type property - 'Name' & a collection of child Widgets accessed via the 'Children' property, this property can not set by the caller- the caller has to use the 'AddChild' & 'RemoveChild' methods to affect the enumerable 'Children' property. I want the ability to 'undo, redo, undo, redo' on these two properties.
public class Widget : ModelI know already the kind of functionality required, an implementation of the Command Pattern, specifically the Memento Pattern. A quick Google found some interesting implementations much of which I didn't like for a couple of reason, one being no separation from UI - here or to complex (to many interfaces) - here. A couple more examples were just to old, not using modern languages features like Lambda expressions. In fact the use of Lambdas should make this a trivial exercise. The solution I want should be capable of being used within an MVC implementation without being tied to it, in fact it should know nothing about either the Model or the View. I envisage it being used from inside the Controller. This fits well with the UI I'm currently building, an MVVM implementation.
{
private int? id;
private Widget parent;
private string name;
private readonly ObservableCollection<Widget> children;
public Widget()
{
children = new ObservableCollection<Widget>();
}
public int? Id
{
get { return id; }
private set
{
SetPropertyAndNotify(ref id, value, () => Id);
}
}
public string Name
{
get { return name; }
set
{
SetPropertyAndNotify(ref name, value, () => Name);
}
}
public Widget Parent
{
get { return parent; }
private set
{
SetPropertyAndNotify(ref parent, value, () => Parent);
}
}
public IEnumerable<Widget> Children
{
get { return children; }
}
public Widget AddChild(Widget child)
{
if (children.Contains(child))
{
return this;
}
child.Parent = this;
children.Add(child);
return this;
}
public Widget AddChild(IEnumerable<Widget> childs)
{
foreach (var child in childs)
{
AddChild(child);
}
return this;
}
public Widget RemoveChild(Widget child)
{
if (!children.Contains(child))
{
return this;
}
child.Parent = null;
children.Remove(child);
return this;
}
public Widget RemoveAllChildren()
{
foreach (var child in children.ToList())
{
child.Parent = null;
children.Remove(child);
}
return this;
}
}
The solution I came up with has 2 classes, the first is the Memento we wish to be able to 'undo-redo'. As you can see the use of Action delegate syntax to remove any explicit knowledge of how the 'undo-redo' steps will be performed. There are 2 constructors one for when both undo & redo is supported and the other when only undo is supported:
public class Memento
{
public Action Undo { get; private set; }
public Action Redo { get; private set; }
public Memento(Action undo)
{
Undo = undo;
Redo = () => { };
}
public Memento(Action undo, Action redo)
{
Undo = undo;
Redo = redo;
}
}
public class UndoableSo to round a couple of test written using MSpec. The first one shows the Undo'ing of setting a simple (value type) property on the model:
{
private readonly Stack<Memento> undoStack;
private readonly Stack<Memento> redoStack;
public Undoable()
{
undoStack = new Stack<Memento>();
redoStack = new Stack<Memento>();
}
public void Add(Action undoAction, Action redoAction)
{
undoStack.Push(new Memento(undoAction, redoAction));
redoStack.Clear();
}
public void Undo()
{
if (undoStack.Count == 0)
{
return;
}
var current = undoStack.Pop();
current.Undo();
redoStack.Push(current);
}
public void Redo()
{
if (redoStack.Count == 0)
{
return;
}
var current = redoStack.Pop();
current.Redo();
undoStack.Push(current);
}
public void Clear()
{
redoStack.Clear();
undoStack.Clear();
}
}
[Subject("Undoable")]
public class when_undoing_a_value_type_property_modification
{
private Establish context = () =>
{
undoable = new Undoable();
widget = new Widget {Name = OriginalName};
};
private Because of = () =>
{
undoable.Add(() => { widget.Name = OriginalName; }, () => { widget.Name = NewName; });
widget.Name = NewName;
undoable.Undo();
};
private It should_undo_setting_the_name_on_a_widget = () => widget.Name.ShouldEqual(OriginalName);
private static string OriginalName = "Original Name - " + Guid.NewGuid();
private static string NewName = "New name - " + Guid.NewGuid();
private static Widget widget;
private static Undoable undoable;
}
The second shows the Redo'ing of adding a child Widget to the parent:
[Subject("Undoable")]
public class when_redoing_a_reference_type_property_modification
{
private Establish context = () =>
{
undoable = new Undoable();
parent = new Widget { Name = "Parent Widget" };
child = new Widget { Name = "Child Widget" };
};
private Because of = () =>
{
undoable.Add(() => parent.RemoveChild(child), () => parent.AddChild(child));
parent.AddChild(child);
undoable.Undo();
undoable.Redo();
};
private It parent_widget_should_cotain_child_widget = () => parent.Children.Contains(child).ShouldEqual(true);
private It parent_widget_children_collection_should_not_be_empty = () => parent.Children.Count().ShouldNotEqual(0);
private static Widget parent;
private static Widget child;
private static Undoable undoable;
}
I've pushed this to GitHub here.
0 comments:
Post a Comment