Implementing Undo-Redo with Aspects

by Gael Fraiteur on 23 Nov 2010

Download the source code for this post.

Undo-redo is one of those features we don’t notice any more. It is however one of the most annoying to program because it requires so much menial code. Fortunately, aspect-oriented programming (AOP) renders this task virtually effortless.

There are two ways to implement an undo-redo feature. The first way, in a model-view-controller, is to put the feature inside the controller (each command implementation in the controller is responsible for generating another command, reversing the effect of the first one). The second way is to record all changes on the model. We’ll focus on this one and demonstrate how you could use an aspect to automatically record any change to domain objects.

Designing the undo-redo system

First, let me remind you that aspect-oriented programming is not opposed to object-oriented programming in any way; therefore a good aspect-oriented design must always start with a good object-oriented design.

In our design, every single change to a model object (such as changing the value of a field or added an object to a collection) is modeled as an object implementing the interface IUndoItem, with the following semantics:

public interface IUndoItem
{
  void Redo();
  void Undo();
}

Next we need an UndoManager. The manager has two kinds of clients (or consumers). First, domain objects (model) will call its Record operation whenever they are changed, so the manager will maintain a list of IUndoItem. Secondly, UI objects (or, possibly, the controller) will call its operations Undo and Redo. The core of the UndoManager is a double linked list of undo items and a cursor to the current item. When the UI invokes the Undo operation, the UndoManager invokes the Undo operation of the current item, and moves the cursor backward. When the UI invokes the Redo operation, the UndoManager invokes the Redo operation of the next item and moves the cursor forward.

Since the purpose of this article is to demonstrate the use of aspects and not to design or implement a general undo-redo system, I will not spend more time on the architecture and implementation of the manager.

Implementing the aspect

What do we want the aspect to do? Well, we need to intercept any change to a domain object field, wrap it into an IUndoItem, and record it using the UndoManager. This is pretty easy to do, since we can use aspects derived from LocationInterceptionAspect to hook any change to any field.

/// 
/// Aspect that, when applied on a field (directly or indirectly, using multicasting),
/// registers all changes to that field into the .
/// 
[Serializable]
[MulticastAttributeUsage( MulticastTargets.Field )]
public class UndoableAttribute : LocationInterceptionAspect
{
    /// 
    /// Method invoked whenever a field value is changed.
    /// 
    /// Information about the field being changed and 
    /// the new value.
    public override void OnSetValue( LocationInterceptionArgs args )
    {
        // Store the new and old values.
        object newValue = args.Value;
        object oldValue = args.GetCurrentValue();

        // If we're not changing anything, don't do anything.
        if ( Equals( newValue, oldValue ) )
            return;

        // Change the field value (or invoke the next aspect in chain).
        base.OnSetValue( args );

        // Record the change in the UndoManager.
        UndoManager.Record( new UndoField( args.Binding, args.Instance, oldValue, newValue ) );
    }

    /// 
    /// Encapsulates a change of field value as an .
    /// 
    private class UndoField : IUndoItem
    {
        private readonly ILocationBinding binding;
        private object instance;
        private readonly object oldValue;
        private readonly object newValue;

        public UndoField( ILocationBinding binding, object instance, 
object oldValue, object newValue ) { this.binding = binding; this.instance = instance; this.oldValue = oldValue; this.newValue = newValue; } public void Redo() { this.binding.SetValue(ref this.instance, Arguments.Empty, newValue ); } public void Undo() { this.binding.SetValue(ref this.instance, Arguments.Empty, oldValue ); } public bool CanUndo { get { return true; } } public bool CanRedo { get { return true; } } } }

The implementation is pretty simple, as you can read yourself: the method OnSetValue is invoked whenever a change of a field value is attempted; this method encapsulates the change as an instance of the UndoField class, and records it in the UndoManager.

The only difficulty is maybe to set the value of the field. We could use reflection (FieldInfo.SetValue), but it has two drawbacks: it is slow, and it would cause a recursion of the aspect: the change done during an undo operation would be recorded in the UndoManager. PostSharp addresses this issue with the notion of binding. When you assign a value to a field, you usually just think that you assign a value to a field, but PostSharp sees that a little differently: it considers you’re sending a message to the field, and this message is “set the value to x”. PostSharp considers aspects as filters between the sender and the receiver of the message. Every filter receives a binding: this is where the filter should send the message. A binding binds to the next filter in the chain or to the final receiver. The point of this message-passing mechanism is that it’s robust: you can’t mess with the order of aspects. Also, note that the message-passing mechanism is only a mental model; in reality, this is implemented as normal method calls, and is very resource-effective.

Using the aspect

Since the aspect class behaves as a custom attribute we could apply it to any field we want to make undoable. Of course, this is not very convenient, and that’s why we will use a combination of custom attribute multicasting (by applying on a class, we implicitly apply it to all fields of that class) and custom attribute inheritance (by applying on a class, we implicitly apply it to all derived classes). Supposing that the common ancestor of all domain classes is Entity, we would write:

[Undoable(AttributeInheritance = MulticastInheritance.Multicast)]
abstract class Entity
{
    public int ID;
}

That’s all you need to track changes to all fields of your domain objects. However, that’s not the end of the story: you still need to ensure that the type of all fields of domain classes is either a domain class itself, either value types (hint: see CompileTimeValidate in PostSharp documentation), and you have implement the design pattern for collection types (using standard OOP, this time).

Now you can see how AOP offers an elegant solution where conventional OOP alone requires a lot of code duplication. Indeed, in most projects, there are plenty of opportunities to use aspects.

Happy PostSharping!

-gael