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 : Model { 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; } }I 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.
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 Undoable { 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(); } }So 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:
[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.
Comments
Post a Comment