Rico Suter's blog.
 


In one of my recent projects, I had to implement an undo/redo mechanism for an observable object graph. The usual approach to this problem is to implement commands which operate on the object graph and provide Do() and Undo() methods. The problem was, that a great portion of the business logic was already implemented and was not command-based. This is why I had to find a solution which works transparently on the property change events of an object graph whose objects implement the INotifyCollectionChanged interface and use the ObservableCollection class for collection properties.

The solution to the problem was to enhance the change tracking so that the undo/redo commands can be built transparently. To make this possible, the following features had to be implemented:

  1. Enhance the property changed events so that the old and new values are provided in the event arguments. With this feature, it is possible to transparently create undo/redo commands based on the change events.
  2. Implement a base observable class so that change events from child objects (i.e. references to other observable objects) are also reported on the parent object. This way the root of the object graph can listen for all changes in the graph, not only for changes on own properties.
  3. Implement an undo redo manager which listens for these graph property changes and creates undo/redo commands based on these events.

The requirements from point 1 and 2 have been implemented in the class GraphObservableObject which exposes a new event called GraphPropertyChanged. This event works the same way as the PropertyChanged event except that it also publishes child property changes and provides the old and new values of the changed property. The observable object automatically registers callbacks to its child objects which implement the INotifyPropertyChanged or INotifyCollectionChanged interfaces so that the whole object graph can be observed. As described before, the GraphPropertyChangedEventArgs object additionally contains the old and new property values so that the property can be restored when undoing an action. Using this GraphObservableObject we can now implement an observable class:

public class Person : GraphObservableObject
{
    private string _firstName; 
    private string _lastName; 

    public string FirstName
    {
        get { return _firstName; }
        set { Set(ref _firstName, value); }
    }

    public string LastName
    {
        get { return _lastName; }
        set { Set(ref _lastName, value); }
    }
}

After implementing the GraphObservableObject class, we need to implement the UndoRedoManager which takes a root GraphObservableObject and manages an undo/redo stack based on the object graph’s changes. To do so, the manager listens for the GraphPropertyChanged event of the root object and internally creates undo/redo commands based on the property change events. The created commands contain all necessary information: The parent object, the property name and the old and new values. If an observed collection has changed, the old collection is stored so that the whole collection can be restored if needed. Now, the UndoRedoManager class can be used as follows:

var person = new Person 
{
    FirstName = "John", 
    LastName = "Doe"
};

var undoRedoManager = new UndoRedoManager(person);
person.FirstName = "James"; 

undoRedoManager.Undo();     
Console.WriteLine(person.FirstName); // prints "John"

undoRedoManager.Redo();     
Console.WriteLine(person.FirstName); // prints "James"

The nice thing with this approach is that it scales very well: Only the property values are saved for undo, not the whole object graph. Also the solution works almost transparent without thinking or knowing about the undo/redo feature. The only problem is, that often multiple property changes should be grouped together so that an undo action restores multiple property values. In the current implementation, all synchronous property changes are grouped together to one single undo/redo command set. However if you need to group multiple asynchronous changes into one command set then you have to find a way to inform the UndoRedoManager about the scope of the undo/redo transaction. It is not implemented yet, but I’m thinking about something like:

using (new UndoRedoTransaction(mgr)) 
{ 
    ...
}

What do you think of this undo/redo solution?

Source code

The implementations of the mentioned classes can be found here:



Discussion