Tomus Blogus Noteworthy coding techniques and discussion

23Apr/11Off

Implementing Undo/Redo support in .NET applications with lambda expressions

The Undo/Redo problem

You know when your application feels like it could use an "Undo" button: when you can make a mistake and be faced with a depressing choice between reloading from a previous save or trying to repair (and often redo) the work that was destroyed. In my case was an editor for 2D animated sequences that could be dropped into an XBox game. It felt rough edged and incomplete and not something I felt comfortable about releasing.

In older .NET versions, implementing an undo/redo pattern was not simple. The approach is to break down every possible user action and encapsulate them in classes with a "do()" and "undo()" method. You need to put special thought into what these objects would need to store - if the action involves deleting something, you have to be sure to store exactly what is deleted so that the "undo()" method can properly put it back. This results in a large library of very small classes to maintain.

How lambdas help

Fortunately the problem has become a lot simpler in .NET 3.0, with the introduction of lambda expressions. These allow you to write code in such a way that it is not executed immediately but instead creates an object that you can execute later. Lambda expressions support closures, meaning any variables referred to by the expression are automatically wrapped up into the object so that they will be available later when it is executed.

These properties make it perfect for defining undo-able user actions. We drop them into the code at the exact point we wish to perform the action, writing the code almost exactly like we would write it without do/undo support, and with full access to all the data and variables we would have access to at that point. The language automatically creates an object that we can squirrel away for later and we don't have to define and maintain a whole set of classes. The problem becomes a lot more manageable.

A undo/redo class

We start with a basic class to represent an undoable action.

public class DoUndoAction
{
	public Action DoAction { get; private set; }
	public Action UndoAction { get; private set; }

	public DoUndoAction(Action doAction, Action undoAction)
	{
		DoAction = doAction;
		UndoAction = undoAction;
	}
}

Pretty straight forward really. The DoAction does the action, the UndoAction undoes it.

The "Action" class is a predefined .NET delegate type that specifies a function taking no parameters and returning no result. There's no reason we couldn't have declared our own delegate type, but "Action" already exists and does exactly what we need. (If you're not already familiar with the Action and Func generic delegates in .NET, I recommend looking them up. They can be quite handy).

We use lambda expressions to create these actions and define them with code.

Say we want to delete the 5th element from a list of 10 integers. Our DoAction would simply delete the 5th element. Our UndoAction must insert whatever was deleted back into the 5th position. We can encapsulate this in a DoUndoAction:

var theList = new List { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3 };
int prevValue = theList[5];
var doUndo = new DoUndoAction(
	() => { theList.RemoveAt(5); },
	() => { theList.Insert(5, prevValue); }
);

The () => {} part is the lambda expression. In this case defining an "anonymous function" taking no arguments and performing the code inside the {} brackets. I won't go into the specific syntax of lambda expressions here, but you can see that they are quite similar to what you would write if you were simply dropping the code straight into your program. The expressions refer to "theList", and "prevValue" variables, which means they will automatically store their values at the point the lambda expressions are defined so that they can use them later when they are executed. This is quite simple and powerful at the same time.

Managing do/undo actions

Next we need a stack to store our do/undo actions. Actually we will use two - one for undo and one for redo. When we undo an action, we remove it from the undo stack and add it to the redo stack. And vice-versa to redo it. We'll bundle this up into a single "UndoManager" object.

public class UndoManager
{
    private readonly Stack doStack = new Stack();
    private readonly Stack undoStack = new Stack();

    public bool CanUndo { get { return undoStack.Any(); } }
    public bool CanRedo { get { return doStack.Any(); } }

    public void DoAndAdd(DoUndoAction newAction)
    {
        newAction.DoAction();
        undoStack.Push(newAction);
        doStack.Clear();
    }

    public void Undo()
    {
        Debug.Assert(undoStack.Any());
        var a = undoStack.Pop();
        a.UndoAction();
        doStack.Push(a);
    }

    public void Redo()
    {
        Debug.Assert(doStack.Any());
        var a = doStack.Pop();
        a.DoAction();
        undoStack.Push(a);
    }
}

The logic is straightforward. When we want to perform an undoable action, we wrap it up in a DoUndoAction object and pass it to DoAndAdd(). This performs the action, adds it to the undo stack and clears out any "redo" actions we might have queued up.

The CanUndo and CanRedo properties let us know whether we have actions waiting to be undone/redone, which we can invoke with Undo()/Redo() respectively.

I've put together a small example of a working program which you can download here.

Things to add

The above code is enough to implement a basic workable undo/redo system, but you might consider these suggestions to take it a bit further:

  • Limit the number of undos, to preserve memory.
  • Detect if your data has been changed since it was saved.
    A simple way is to clear the undo stack after saving. Thus there are unsaved changes whenever the stack is not empty.
    However, this means users can only undo back to the point when it was last saved, which may not be desirable. So an alternative approach is to record the undo action that was at the top of the stack when the file is saved.
  • Include a description of the action (to display in the Edit->Undo menu item etc)
  • Include a "bookmark"
    If your application has a number of contexts to navigate, you may want to add a "bookmark" to each undo/redo action so that the app can jump back to the place that the change was made.
    Otherwise it may not be obvious that the "Undo" action has worked if the user is looking at a different tab.
    (How you implement the "bookmark" will depend on the type of application, and its different views and contexts.)
Comments (2) Trackbacks (0)
  1. Nice work! How about using WeakReferences for low items in the undo list?

    • Like a chain of items linked by weak references?
      Sounds like it could work in theory (I haven’t actually used weak references for anything myself yet).


Trackbacks are disabled.