How to implement undo/redo using MVVM
Introduction
One feature that many users demand is a neatless undo/redo integration. This means that the application allows the user to revert any modification he made - one by one - back to the start of the application and than eventually reapply them again. This improves the usability a lot, because it allows the user to carelessly use an unclear command, because he is certain, that he can undo it if he was wrong. Today undo/redo has gotten almost standard for any modern data editing application.
The MVVM-Pattern
Because of the strong databinding functionality in WPF, most applications are using the popular MVVM (Model-View-ViewModel) pattern. The idea of this pattern is basically to define a class that aggregates all data and commands for a certain view and provides them to the view as properties where it can bind to. Changes on properties are notified by an event on the
INotifyPropertyChanged
interface.A concept of implementing undo/redo
A classical approach to implement undo/redo is to allow changes on the model only through commands. And every command should be invertible. The user than executes an action, the application creates a command, executes it and puts an inverted command on the undo-stack. When the user clicks on undo, the application executes the top-most (inverse) command on the undo-stack, inverts it again (to get the original command again) and puts it on the redo-stack. That's it.
Scenario 1: Executing an action
Scenario 2: Undoing an action
Adoption for WPF
We start with a base class that implements the
INotifyPropertyChanged
interface and provides a private methodNotify(string propertyName)
.public class NotifyableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void Notify(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
Then we build the base class
TrackableObject
where all model objects or view models that are directly bound to the view should inherit from.public class TrackableObject : NotifyableObject { private readonly List<ITrackable> _trackableItems = new List<ITrackable>(); public bool HasChanges { get { return _trackableItems.Any(i => i.HasChanges); } } public IModificationTracker ModificationTracker { get; set; } protected TrackableValue<T> RegisterTrackableValue<T>(string propertyName, T defaultValue = default(T)) { var property = new TrackableValue<T>(propertyName, Modify, Notify, defaultValue); _trackableItems.Add(property); return property; } protected TrackableCollection<T> RegisterTrackableCollection<T>() { var collection = new TrackableCollection<T>(Modify); _trackableItems.Add(collection); return collection; } private void Modify(Action doAction, Action undoAction, Action notification) { var modification = new Modification(doAction, undoAction, notification); modification.Execute(); ModificationTracker.TrackModification(modification); } }
To simplify the generation of modifactions when changing a property value, we build a generic wrapper for each property called
TrackableValue
.public class TrackableValue<T> : ITrackable { private readonly string _propertyName; private readonly Action<Action, Action, Action> _modifyCallback; private readonly Action<string> _notifyAction; private T _value; public TrackableValue(string propertyName, Action<Action, Action, Action> modifyCallback, Action<string> notifyAction, T defaultValue) { _propertyName = propertyName; _modifyCallback = modifyCallback; _notifyAction = notifyAction; _value = defaultValue; } public bool HasChanges { get { return _originalValue.Equals(_value); } } public T Value { get { return _value; } set { var oldValue = _value; _modifyCallback(() => _value = value, () => _value = oldValue, () => _notifyAction(_propertyName)); } } }
To same thing we need to do for collections to track add/remove of items from a collection
public class TrackableCollection<T> : IList<T>, ITrackable { private readonly Action<Action, Action, Action> _modifyCallback; private readonly List<T> _list = new List<T>(); private readonly List<T> _originalList = new List<T>(); public TrackableCollection(Action<Action, Action, Action> modifyCallback) { _modifyCallback = modifyCallback; } public event EventHandler<EventArgs<T>> ItemAdded; public event EventHandler<EventArgs<T>> ItemRemoved; public event EventHandler CollectionChanged; public bool HasChanges { get { if( _list.Count == _originalList.Count) { return _list.Where((item, index) => !item.Equals(_originalList[index])).Any(); } return true; } } public void AcceptChanges() { _originalList.Clear(); _originalList.AddRange(_list); } public IEnumerator<T> GetEnumerator() { return _list.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public void Add(T item) { _modifyCallback(() => { _list.Add(item); ItemAdded.Notify(this, new EventArgs<T>(item)); }, () => { _list.Remove(item); ItemRemoved.Notify(this, new EventArgs<T>(item)); }, OnCollectionModified); } public void Clear() { var items = new T[_list.Count]; _list.CopyTo(items); _modifyCallback(() => { _list.ForEach(i => ItemRemoved.Notify(this, new EventArgs<T>(i))); _list.Clear(); }, () => { _list.AddRange(items); _list.ForEach(i => ItemAdded.Notify(this, new EventArgs<T>(i))); }, OnCollectionModified); } public bool Contains(T item) { return _list.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { _list.CopyTo(array, arrayIndex); } public bool Remove(T item) { var result = _list.Contains(item); _modifyCallback(() => { _list.Remove(item); ItemRemoved.Notify(this, new EventArgs<T>(item)); }, () => { _list.Add(item); ItemAdded.Notify(this, new EventArgs<T>(item)); }, OnCollectionModified); return result; } public int Count { get { return _list.Count; } } public bool IsReadOnly { get { return false; } } public int IndexOf(T item) { return _list.IndexOf(item); } public void Insert(int index, T item) { _modifyCallback(() => { _list.Insert(index, item); ItemAdded.Notify(this, new EventArgs<T>(item)); }, () => { _list.Remove(item); ItemRemoved.Notify(this, new EventArgs<T>(item)); }, OnCollectionModified); } public void RemoveAt(int index) { var item = _list[index]; _modifyCallback(() => { _list.Remove(item); ItemRemoved.Notify(this, new EventArgs<T>(item)); }, () => { _list.Insert(index, item); ItemAdded.Notify(this, new EventArgs<T>(item)); }, OnCollectionModified); } public T this[int index] { get { return _list[index]; } set { var oldItem = _list[index]; _modifyCallback(() => { _list[index] = value; ItemAdded.Notify(this, new EventArgs<T>(value)); }, () => { _list[index] = oldItem; ItemRemoved.Notify(this, new EventArgs<T>(oldItem)); }, OnCollectionModified); } } private void OnCollectionModified() { CollectionChanged.Notify(this, EventArgs.Empty); } }
Comments
Post a Comment