Microsoft Dot Net Master

Microsoft Dot Net Master
Microsoft Dot Net Master

Friday, May 25, 2012

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

3 comments:

  1. Hi Sandeep,

    can you provide an example project with your classes? What structure do the interface classes have (like ITrackable, IModificationTracker, ...)?

    Best regards,
    Kleemns

    ReplyDelete
  2. Hmmm... http://www.wpftutorial.net/UndoRedo.html

    ReplyDelete
  3. you copied all your articles from wpftutorial.net,, i can see that also in the watermark of the picture.

    ReplyDelete