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 theINotifyPropertyChanged
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 theINotifyPropertyChanged
interface and provides a private method Notify(string propertyName)
.
public class NotifyableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void Notify(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
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); } }
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)); } } }
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); } }
Hi Sandeep,
ReplyDeletecan you provide an example project with your classes? What structure do the interface classes have (like ITrackable, IModificationTracker, ...)?
Best regards,
Kleemns
Hmmm... http://www.wpftutorial.net/UndoRedo.html
ReplyDeleteyou copied all your articles from wpftutorial.net,, i can see that also in the watermark of the picture.
ReplyDelete