New CodePlex project: a simple Undo/Redo framework

I just created a new CodePlex project: http://undo.codeplex.com

What

It's a simple framework to add Undo/Redo functionality to your applications, based on the classical Command design pattern. It supports merging actions, nested transactions, delayed execution (execution on top-level transaction commit) and possible non-linear undo history (where you can have a choice of multiple actions to redo).

The status of the project is Stable (released). I might add more stuff to it later, but right now it fully satisfies my needs. It's implemented in C# 3.0 (Visual Studio 2008) and I can build it for both desktop and Silverlight. The release has both binaries.

Existing Undo/Redo implementations

I do realize that my project is the reinvention of the wheel at its purest, existing implementations being most notably:

However I already have three projects that essentially share the exact same source code, so I decided that it would be good to at least extract this code into a reusable component, so perhaps not only me but someone else might find it useful too.

It's open-source and on CodePlex, so I also have a chance of benefiting from it if someone contributes to it :)

History

It all started in 2003 when I first added Undo/Redo support to the application that I was developing at that time. I followed the classical Command design pattern, together with Composite (for nested transactions) and Strategy (for plugging various, possibly non-linear undo buffers).

Then I needed Undo/Redo for my thesis, so I just took the source code and improved it a little bit. Then I started the Live Geometry project, took the same code and improved it there a little bit, fixing a couple of bugs. Now the mess is over, and I'm finally putting the code in one place :)

A good example of where this framework is used is the Live Geometry project (http://livegeometry.codeplex.com). It defines several actions such as AddFigureAction, RemoveFigureAction, MoveAction and SetPropertyAction.

Actions

Every action encapsulates a change to your domain model. The process of preparing the action is explicitly separated from executing it. The execution of an action might come at a much later stage after it's been prepared and scheduled.

Any action implements IAction and essentially provides two methods: one for actually doing the stuff, and another for undoing it.

 /// <summary>
/// Encapsulates a user action (actually two actions: Do and Undo)
/// Can be anything.
/// You can give your implementation any information it needs to be able to
/// execute and rollback what it needs.
/// </summary>
public interface IAction
{
    /// <summary>
    /// Apply changes encapsulated by this object.
    /// </summary>
    void Execute();

    /// <summary>
    /// Undo changes made by a previous Execute call.
    /// </summary>
    void UnExecute();

    /// <summary>
    /// For most Actions, CanExecute is true when ExecuteCount = 0 (not yet executed)
    /// and false when ExecuteCount = 1 (already executed once)
    /// </summary>
    /// <returns>true if an encapsulated action can be applied</returns>
    bool CanExecute();

    /// <returns>true if an action was already executed and can be undone</returns>
    bool CanUnExecute();

    /// <summary>
    /// Attempts to take a new incoming action and instead of recording that one
    /// as a new action, just modify the current one so that it's summary effect is 
    /// a combination of both.
    /// </summary>
    /// <param name="followingAction"></param>
    /// <returns>true if the action agreed to merge, false if we want the followingAction
    /// to be tracked separately</returns>
    bool TryToMerge(IAction followingAction);

    /// <summary>
    /// Defines if the action can be merged with the previous one in the Undo buffer
    /// This is useful for long chains of consecutive operations of the same type,
    /// e.g. dragging something or typing some text
    /// </summary>
    bool AllowToMergeWithPrevious { get; set; }
}

Both methods share the same data required by the action implementation and are supplied when you create an action instance.

ActionManager

Your domain model (business objects) will likely have an instance of ActionManager that keeps track of the undo/redo buffer and provides the RecordAction(IAction) method. This method adds an action to the buffer and executes it. And then you have ActionManager.Undo(), ActionManager.Redo(), CanUndo(), CanRedo() and some more stuff.

As a rule, the thing that works for me is that I generally have two APIs: one that is public and lazy (i.e. it just creates an action and adds it to the buffer), and the other which is internal and eager, that does the actual work. Action implementation just calls into the eager API, while the public API is lazy and creates actions transparently for the consumer.

History

Right now I only have a SimpleHistory. Instead of having two stacks, I have a state machine, where Undo goes to the previous state and Redo goes to the next state, if available. Each graph edge stores an action (implementation of IAction). As the current state transitions along the graph edge, IAction.Execute or UnExecute is being called, depending on the direction in which we go (there is a logical "left" and "right" in this graph, which kind of represents "future" and "past").

 

image

It's possible for this linked list to become a tree, where you try something out (way1), don't like it, undo, try something else (way2), like it even less, undo, and choose to go back and redo way1. However this is not implemented yet.

Transactions

Transactions are groups of actions viewed as a single action (see Composite design pattern).

Here's a typical usage of a transaction:

 public void Add(IEnumerable<IFigure> figures)
{
    using (Transaction.Create(ActionManager))
    {
        figures.ForEach(Add);
    }
}

If an action is recorded while a transaction is open (inside the using statement), it will be added to the transaction and executed only when the top-level transaction commits. This effectively delays all the lazy public API calls in the using statement until the transaction commits. You can specify that the actions are not delayed, but executed immediately - there is a corresponding overload of Transaction.Create specifically for that purpose.

Note that you can "misuse" this framework for purposes other than Undo/Redo: one prominent example is navigation with back/forward.

Update: I just posted some samples for the Undo Framework: http://blogs.msdn.com/kirillosenkov/archive/2009/07/02/samples-for-the-undo-framework.aspx