BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Algorithms and Data Structures
.NET 2.0+

Generic Multi-Level Undo and Redo

Most modern software applications include undo and redo features. Undo allows one or more activities to be reverted. Redo allows previous undo actions to be reversed. This article explains how to create a generic, multi-level undo and redo class.

State Property

The state in the byte array is deserialized back to an object during an undo or redo operation. This is handled by the get accessor of the read-only State property. A memory stream is created from the array and the binary formatter is used to generate a new object of the correct type. This will essentially be a deep clone of the original object that is loaded into the Value property of the Undoable<T> instance. If you have references to the wrapped value, such as when using data binding, these reference need to be refreshed following an undo or redo.

Add the following property to the class:

public T State
{
    get
    {
        using (MemoryStream stream = new MemoryStream(_stateData))
        {
            return (T)_formatter.Deserialize(stream);
        }
    }
}

Implementing Undoable<T>

The other class that we need to create is Undoable<T>, based upon the IUndoable<T> interface. This is the public class that wraps a type for which you wish to provide undo and redo functionality. The class includes the current state of the object and two stacks containing the undo and redo states. Stacks are ideal for this purpose as they return the historical states in the expected order.

Undo states should be stored whenever you wish to record a state that could be recalled. To serialize a state and add it to the undo stack you call the SaveState method. Two Boolean properties indicate whether it is possible to perform an undo or a redo. These properties should be checked when determining whether the functions are made available to the user.

The interface is as follows:

public interface IUndoable<T>
{
    bool CanRedo { get; }
    bool CanUndo { get; }
    T Value { get; set; }
    void SaveState();
    void Undo();
    void Redo();
}

Creating the Class

The Undoable<T> class implements IUndoable<T>. To create it, add a new class file and modify the declaration as follows:

public class Undoable<T> : IUndoable<T>
{
}

State Variables

Three private, class-level variables are used to hold the current state and the undo and redo history. The current state is held in the _value variable, which is of the type declared in the class' type parameter. This property should be used when working with or displaying the current data.

The historical states are held in a pair of generic stacks. Each holds IUndoState<T> objects that control the serialization and deserialization of the state data.

Stack<IUndoState<T>> _redoStack;
Stack<IUndoState<T>> _undoStack;
T _value;

The three state variables are initialised during instantiation. The two stacks are created empty and the current state is passed in via the constructor's single parameter.

public Undoable(T value)
{
    _value = value;
    _redoStack = new Stack<IUndoState<T>>();
    _undoStack = new Stack<IUndoState<T>>();
}

Value Property

The current state can be set or read using the Value property. This is a standard property with the backing store variable, "_value".

public T Value
{
    get { return _value; }
    set { _value = value; }
}

CanUndo and CanRedo Properties

The CanUndo and CanRedo properties return Boolean values that determines whether either operation is possible. If the relevant stack contains a state that can be restored, the property returns true. If the stack is empty, the property returns false. You should check these flags before allowing the Undo or Redo methods to be called, as executing these operations without a state to restore throws an exception.

public bool CanRedo
{
    get { return _redoStack.Count != 0; }
}

public bool CanUndo
{
    get { return _undoStack.Count != 0; }
}
6 July 2011