An Automated Undo/Redo History Based on Common Interfaces

I’m developing a small data management application, and I wanted an Undo/Redo. As I already have all my properties implementing INotifyPropertyChanging/Changed (INPC) and I’m using collections that implement INotifyCollectionChanged (INCC), I decided it’d be silly to not use those interfaces to handle tracking changes as history.

My first pass at implementation involves setting up an IHistory interface and matching History implementation. I create IMememto objects to store on future or past stacks for redo or undo, respectively.  We handle monitoring objects using a little reflection along with a couple conventions.

Handling INotifyPropertyChanging and INotifyPropertyChanged

Implementing this was fairly easy. When I start monitoring an object, I check for the necessary interfaces and subscribe to them:

I use the INotifyPropertyChanging event to grab the old value, turning it into a memento and placing that memento in it’s own stack. Once the INotifyPropertyChanged event comes through I move that memento over to the current history stack. This might be a touch of overkill, since when the changing event comes through it should be safe to put it on the stack, but better safe than sorry in this case.

Handling INotifyCollectionChanged

A slightly trickier option. I check the properties of a target object, looking for ones that implement INCC. If the property also implements IEnumerable, then I can walk the objects there are monitor them if desired, too. The tricky part comes with Add/Remove.

If the collections on an object are read-only, then there may be methods on the parent object for adding/removing objects and to handle housekeeping chores. So my implementation will check for such methods. It can always fall back and check the collection itself for Add/Remove or ICollection interface, etc, but I assume if such methods exist on the parent object, they are preferable. There are catches:

  1. I can assume the name of the collection is pluralized, but the methods probably won’t be. Singularizing English words is hard.
  2. I have no way of knowing what the verb portion of the method name is. I can build in some conventions, but I won’t think of them all.

An (silly) example:

class Train() {
    ObservableCollection<TrainCar> cars;
    ReadOnlyObservableCollection<TrainCar> Cars { get; private set; }

    public void CoupleCar(TrainCar car) {
        // process car, validation, etc.
        this.cars.Add(car);
    }

    public void DecoupleCar(TrainCar car) {
        // process car, etc.
        this.Cars.Remove(car);
    }
}

I’d have no way of knowing to use the Couple/Decouple methods instead of Add/Remove. In general, though, the convention of looking for Add/Remove (plus a few others), should be safe.

The actual implementation was relatively straightforward. I only implemented a very rudimentary singularization method, but it could be easily expanded. After I have the singular name of the list, its a matter of determining what the type of the collection is via generic type parameters and then finding a single-parameter method for both add and remove using the singular name. I store the method as an action in a dictionary keyed to the collection that will be raising the INCC events so I’ll know which to use later. Here is the code for identifying the add method (note I’ve not yet implemented the code to check for add/remove methods on the collection itself):

private void IdentifyAddMethod(object target, Type type, PropertyInfo property, object targetCollection)
{
    var singularName = property.Name.Singularize();
    var parameterType = (targetCollection as IEnumerable).GetCollectionItemType();
    // search for add methods on the target - Add/Append/???
    var method = type.GetSingleParameterMethod(parameterType, singularName, "Add", "Append");
    if (method != null)
    {
        var addAction = method.ToAction(target);
        this.addActions.Add(targetCollection, addAction);
        return;
    }
    // check property type for ICollection or Add/???
    throw new NotImplementedException();
}

Transactions

I wanted to automatically detect, via Reflection, where a change was coming from and to automatically merge related changes into transactions, but that proved a bit beyond my skills. My best idea of looking at the call stack quickly went nowhere. Instead I decided to add StartTransaction() and EndTransaction() methods to the IHistory interface, to allow the UI (or whatever portion is handling the IHistory) to manage marking groups of changes as related. In the end, it’s at that segment of the program where such things matter. For example, clicking “Create Doodad” in the UI could create a doodad, add it to a list, and set a few default values. An undo operation should undo all of these things, and only the UI would really know all the steps taken, so I leave it to the UI to mark those transactions.

Order of Operations

This turned out easier than I expected. When I add a memento, if I’m in a transaction, it goes into the transaction CompoundMemento object, which is an object that only stores other mementos. Otherwise, I can assume it’s new data and add it to the past stack. I can do this since during an Undo() or Redo(), I’ll start up a new transaction to hold the changes that spring out of that. Then, once the transaction is over, I’ll decide where to push that transaction memento. Here is the relevant code:

private void AddMemento(IMemento memento)
{
    if (!this.isUndo && !this.isRedo)
    {
        // destroy the redo stack since we're in new territory
        this.futureMementos.Clear();
    }
    if (this.isTransactionActive)
    {
        this.transactionMemento.AddMemento(memento);
        return;
    }
    // undo and redo are handled as transactions, and this isn't a transaction so it must be new changes
    this.pastMementos.Push(memento);
}

And then when the transaction is complete I decide where to stick the transaction memento:

public void EndTransaction()
{
    // only add the transaction memento if something actually changed during the transaction
    if (this.isTransactionActive && this.transactionMemento.HasMementos)
    {
        if (this.isUndo)
        {
            this.futureMementos.Push(this.transactionMemento);
        }
        else
        {
            this.pastMementos.Push(this.transactionMemento);
        }
    }
    this.isTransactionActive = false;
    this.transactionMemento = null;
}

Improvements

Monitor Multiple Objects

Right now I’m specifying a single root node only, along with any objects I find searching for collections among its properties. This makes sense for the object model I’m using in the targeted project, but that may change and it limits the usefulness of the project itself. Allowing for multiple objects to be monitored would be ideal, and should be easily achieved if I…

Split Responsibilities

Split the object graph up further. Currently the single History class knows how to monitor and react to both the INPC and INCC interfaces, but it’s pretty long (300 lines) and it definitely doesn’t follow the SRP. What I can do is to split the History object out into an IHistoryMonitorCoordinator that handles adding/removing monitors on objects, an IHistory object that handles collecting mementos and the actual Undo/Redo, and multiple IHistoryMonitor services that can monitor for different events (one each for INPC and INCC, plus any others desired). This should get us nicely to a very testable, IoC structure.

Circular References

I’m not currently checking to ensure I don’t wind up in an infinite loop traversing object graphs. Some simple checks would be easy to add but would increase memory usage (a simple Dictionary to store the monitored objects).

Transactions

While I’m currently requiring manual calls to the StartTransaction() and EndTransaction() methods, I could use attributes to mark methods that require it, using AOP to automatically inject the calls into, say, a ViewModel if that’d be appropriate.

Concurrency

Haven’t tested for it. Need to!

Performance

Haven’t tested for it. Need to!

An Aside on AOP

When using Fody to implement the INPC events you really can keep very nice, clean POCO objects for the model entities. Not littering the code with INPC or bothering about history, but still having all this great functionality really feels like magic.

An Aside on INCC

I hate the INCC interface. No, that’s not true, the interface is fine. It’s the NotifyCollectionChangedEventArgs class I hate. The docs are terrible as to what the various Actions mean, and some of those actions simply make no sense at all for some collections. For example: “Move” and “Replace” don’t make sense as actions for non-indexed or keyed collections. I’d much prefer a more general purpose interface, along with implementation-specific versions for more specific types and bindings. The class is confusing enough that there are multiple posts about it. Unfortunately, it’s “legacy” now, so I guess I have to learn to live with it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s