Introduction
An undo/redo feature is a "must-have" in today's applications. In conjunction with the Memento design pattern that I discuss here, it is very easy to implement. Throughout this article, I will be illustrating how an undo/redo buffer works using the example of an RPN calculator stack. For an actual RPN calculator example that implements undo/redo, you can read my article here (while this article is about event pools, it also implements the Memento pattern and undo/redo capability for the calculator).
Architecture
An undo/redo buffer primarily implements three methods:
- Do
- Undo
- Redo
The Do Method
This method records the current state, and this is very important, before the action takes place, as illustrated by this diagram:

As illustrated, "state1" is preserved prior to performing the action, which in this case is "Enter".
Let's look at a second action, "Add":

Again, the current state is saved before the action, in this case an "Add" is taken.
The Undo Method
The undo method has a complexity to it--before performing the undo, the current state, if at the top of the undo buffer, must be preserved. If we don't do this, we won't be able to "redo" the state we are about to "undo". Therefore, any time you need to do an undo in your code, you have to do something like this:
UndoBuffer buffer;
...
if (buffer.AtTop)
{
buffer.PushCurrentState(GetStateMemento());
}
IMemento mem=buffer.Undo();
The effect of this is to create an entry at the head of the buffer, while the Undo command leaves the current state index at next to head instead, as illustrated here:

By pushing state4, we can now redo our last action.
The Redo Method
The redo method simply increments the state indexer and returns the Memento at that index:

One More Complexity
Let's say that we've restored our state all the way back to state1. The application then performs a "Do" of "newState". When this happens, all "redo" actions beyond the current state index are cleared:

Implementation
The implementation includes some additional properties:
bool CanUndo
bool CanRedo
bool AtTop
int Count
string UndoAction
string RedoAction
Besides the obvious first four properties, the UndoAction
and RedoAction
properties return the action string associated with the Memento
instance at the appropriate index. As I described in my article on Mementos, I prefer to put the contextual information in the memento itself, as this is part of the state information, rather than manage it separately.
The class implementation is straightforward and presented here in its entirety:
using System;
using System.Collections;
using Clifton.Architecture.Memento;
namespace Clifton.Architecture.UndoRedo
{
public class UndoRedoException : ApplicationException
{
public UndoRedoException(string msg) : base(msg)
{
}
}
public class UndoBuffer
{
protected ArrayList buffer;
protected int idx;
public bool CanUndo
{
get {return idx > 0;}
}
public bool CanRedo
{
get {return buffer.Count > idx+1;}
}
public bool AtTop
{
get {return idx == buffer.Count;}
}
public int Count
{
get {return buffer.Count;}
}
public string UndoAction
{
get
{
string ret=String.Empty;
if (idx > 0)
{
ret=((IMemento)buffer[idx-1]).Action;
}
return ret;
}
}
public string RedoAction
{
get
{
string ret=String.Empty;
if (idx < buffer.Count)
{
ret=((IMemento)buffer[idx]).Action;
}
return ret;
}
}
public string LastAction
{
set
{
if (idx==0)
{
throw new UndoRedoException("Invalid index.");
}
((IMemento)buffer[idx-1]).Action=value;
}
}
public UndoBuffer()
{
buffer=new ArrayList();
idx=0;
}
public void PushCurrentState(IMemento mem)
{
buffer.Add(mem);
}
public void Do(IMemento mem)
{
if (buffer.Count > idx)
{
buffer.RemoveRange(idx, buffer.Count-idx);
}
buffer.Add(mem);
++idx;
}
public IMemento Undo()
{
if (idx==0)
{
throw new UndoRedoException("Invalid index.");
}
--idx;
return (IMemento)buffer[idx];
}
public IMemento Redo()
{
++idx;
return (IMemento)buffer[idx];
}
public void Flush()
{
buffer.Clear();
idx=0;
}
}
}
Conclusion
In conjunction with the Memento
class, the UndoBuffer
class lets you implement fully-featured undo/redo capability in your application with very re-usable components. The only task on your part is to implement the serialization/deserialization of your state information.