Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / C#
Article

An Undo/Redo Buffer Framework

Rate me:
Please Sign up or sign in to vote.
4.60/5 (23 votes)
2 Jun 20052 min read 140.2K   1.6K   110   19
The basic framework to implement undo/redo functionality.

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:

  1. Do
  2. Undo
  3. 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:

Image 1

As illustrated, "state1" is preserved prior to performing the action, which in this case is "Enter".

Let's look at a second action, "Add":

Image 2

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:

C#
UndoBuffer buffer;
...
if (buffer.AtTop)
{
  buffer.PushCurrentState(GetStateMemento());
}
IMemento mem=buffer.Undo();
// restore state

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:

Image 3

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:

Image 4

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:

Image 5

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:

C#
using System;
using System.Collections;

using Clifton.Architecture.Memento;

namespace Clifton.Architecture.UndoRedo
{
  public class UndoRedoException : ApplicationException
  {
    public UndoRedoException(string msg) : base(msg)
    {
    }
  }

  /// <summary>
  /// A class that manages an array of IMemento objects, implementing
  /// undo/redo capabilities for the IMemento originator class.
  /// </summary>
  public class UndoBuffer
  {
    protected ArrayList buffer;
    protected int idx;

    /// <summary>
    /// Returns true if there are items in the undo buffer.
    /// </summary>
    public bool CanUndo
    {
      get {return idx > 0;}
    }

    /// <summary>
    /// Returns true if the current position in the undo buffer will
    /// allow for redo's.
    /// </summary>
    public bool CanRedo
    {
      // idx+1 because the topmost buffer item is the topmost state
      get {return buffer.Count > idx+1;}
    }

    /// <summary>
    /// Returns true if at the top of the undo buffer.
    /// </summary>
    public bool AtTop
    {
      get {return idx == buffer.Count;}
    }

    /// <summary>
    /// Returns the count of undo items in the buffer.
    /// </summary>
    public int Count
    {
      get {return buffer.Count;}
    }

    /// <summary>
    /// Returns the action text associated with the Memento that holds
    /// the last known state. An empty string is returned if there are
    /// no items to undo.
    /// </summary>
    public string UndoAction
    {
      get
      {
        string ret=String.Empty;
        if (idx > 0)
        {
          ret=((IMemento)buffer[idx-1]).Action;
        }
        return ret;
      }
    }

    /// <summary>
    /// Returns the action text associated with the Memento that holds
    /// the current state. An empty string is returned if there are
    /// no items to redo.
    /// </summary>
    public string RedoAction
    {
      get
      {
        string ret=String.Empty;
        if (idx < buffer.Count)
        {
          ret=((IMemento)buffer[idx]).Action;
        }
        return ret;
      }
    }

    /// <summary>
    /// Sets the last Memento action text.
    /// </summary>
    public string LastAction
    {
      set
      {
        if (idx==0)
        {
          throw new UndoRedoException("Invalid index.");
        }
        ((IMemento)buffer[idx-1]).Action=value;
      }
    }

    /// <summary>
    /// Constructor.
    /// </summary>
    public UndoBuffer()
    {
      buffer=new ArrayList();
      idx=0;
    }

    /// <summary>
    /// Saves the current state. This does not adjust the current undo indexer.
    /// Use this method only when performing an Undo and AtTop==true, so that 
    /// the current state, before the Undo, can be saved, allowing a Redo to 
    /// be applied.
    /// </summary>
    /// <param name="mem"></param>
    public void PushCurrentState(IMemento mem)
    {
      buffer.Add(mem);
    }

    /// <summary>
    /// Save the current state at the index position. Anything past the
    /// index position is lost.
    /// This means that the "redo" action is no longer possible.
    /// Scenario--The user does 10 things. The user undo's 5 of them, then
    /// does something new.
    /// He can only undo now, he cannot "redo". If he does one 
    /// undo, then he can do one "redo".
    /// </summary>
    /// <param name="mem">The memento holding the current state.</param>
    public void Do(IMemento mem)
    {
      if (buffer.Count > idx)
      {
        buffer.RemoveRange(idx, buffer.Count-idx);
      }
      buffer.Add(mem);
      ++idx;
    }

    /// <summary>
    /// Returns the current memento.
    /// </summary>
    public IMemento Undo()
    {
      if (idx==0)
      {
        throw new UndoRedoException("Invalid index.");
      }
      --idx;
      return (IMemento)buffer[idx];
    }

    /// <summary>
    /// Returns the next memento.
    /// </summary>
    public IMemento Redo()
    {
      ++idx;
      return (IMemento)buffer[idx];
    }

    /// <summary>
    /// Removes all state information.
    /// </summary>
    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Gary R. Wheeler29-Dec-21 10:47
Gary R. Wheeler29-Dec-21 10:47 
GeneralAn alternate Pin
Y Sujan19-Aug-13 7:11
Y Sujan19-Aug-13 7:11 
GeneralRe: An alternate Pin
Marc Clifton19-Aug-13 7:37
mvaMarc Clifton19-Aug-13 7:37 
GeneralRe: An alternate Pin
Y Sujan19-Aug-13 19:19
Y Sujan19-Aug-13 19:19 
GeneralMy vote of 2 Pin
paulsasik3-May-10 4:38
professionalpaulsasik3-May-10 4:38 
QuestionFrameWork 1.1 Pin
sudarshanbs30-Jun-09 23:16
sudarshanbs30-Jun-09 23:16 
Generalundo-redo for listview Pin
mm3107-Apr-07 11:23
mm3107-Apr-07 11:23 
GeneralProblem with classes whose properties updated through PropertyGrid Pin
X-treem1-Dec-05 4:29
X-treem1-Dec-05 4:29 
GeneralRe: Problem with classes whose properties updated through PropertyGrid Pin
PDHB17-Mar-06 5:29
PDHB17-Mar-06 5:29 
GeneralRe: Problem with classes whose properties updated through PropertyGrid Pin
Lea Hayes1-Apr-08 13:18
Lea Hayes1-Apr-08 13:18 
GeneralRe: Problem with classes whose properties updated through PropertyGrid Pin
Michael Brian Clarke10-Nov-08 9:07
Michael Brian Clarke10-Nov-08 9:07 
Questiongenerics c#2.0? Pin
MP3Observer2-Jun-05 22:26
MP3Observer2-Jun-05 22:26 
AnswerRe: generics c#2.0? Pin
Marc Clifton3-Jun-05 0:57
mvaMarc Clifton3-Jun-05 0:57 
QuestionCompare to Command pattern undo/redo? Pin
Dr Herbie2-Jun-05 22:01
Dr Herbie2-Jun-05 22:01 
AnswerRe: Compare to Command pattern undo/redo? Pin
Marc Clifton3-Jun-05 0:59
mvaMarc Clifton3-Jun-05 0:59 
AnswerRe: Compare to Command pattern undo/redo? Pin
DejaVudew3-Jun-05 3:31
DejaVudew3-Jun-05 3:31 
GeneralRe: Compare to Command pattern undo/redo? Pin
WillemM7-Jun-05 3:38
WillemM7-Jun-05 3:38 
GeneralNice article :-) Pin
Nish Nishant2-Jun-05 17:56
sitebuilderNish Nishant2-Jun-05 17:56 
GeneralRe: Nice article :-) Pin
Marc Clifton3-Jun-05 3:57
mvaMarc Clifton3-Jun-05 3:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.