|
||||||
How to implement undo/redo using MVVMIntroductionOne 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-PatternBecause 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 A concept of implementing undo/redoA 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 WPFWe start with a base class that implements the 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 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 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); } }
Last modified: 2011-12-23 09:14:09
Copyright (c) by Christian Moser, 2011.
Comments on this articleShow all comments
|
||||||