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 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));
}
}
}
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);
}
}