Click here to Skip to main content
Click here to Skip to main content

An Undo/Redo Buffer Framework

, 2 Jun 2005
Rate this:
Please Sign up or sign in to vote.
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:

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();
// 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:

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)
    {
    }
  }

  /// <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

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
GeneralAn alternate PinmemberY Sujan19-Aug-13 7:11 
GeneralRe: An alternate PinprotectorMarc Clifton19-Aug-13 7:37 
GeneralRe: An alternate PinprofessionalY Sujan19-Aug-13 19:19 
GeneralMy vote of 2 Pinmemberpaulsasik3-May-10 4:38 
QuestionFrameWork 1.1 Pinmembersudarshanbs30-Jun-09 23:16 
Generalundo-redo for listview Pinmembermm3107-Apr-07 11:23 
GeneralProblem with classes whose properties updated through PropertyGrid PinmemberX-treem1-Dec-05 4:29 
GeneralRe: Problem with classes whose properties updated through PropertyGrid PinmemberPDHB17-Mar-06 5:29 
GeneralRe: Problem with classes whose properties updated through PropertyGrid Pinmemberlhayes001-Apr-08 13:18 
GeneralRe: Problem with classes whose properties updated through PropertyGrid PinmemberMichael Brian Clarke10-Nov-08 9:07 
Questiongenerics c#2.0? PinmemberMP3Observer2-Jun-05 22:26 
AnswerRe: generics c#2.0? PinprotectorMarc Clifton3-Jun-05 0:57 
QuestionCompare to Command pattern undo/redo? PinmemberDr Herbie2-Jun-05 22:01 
AnswerRe: Compare to Command pattern undo/redo? PinprotectorMarc Clifton3-Jun-05 0:59 
AnswerRe: Compare to Command pattern undo/redo? PinmemberDejaVudew3-Jun-05 3:31 
GeneralRe: Compare to Command pattern undo/redo? PinmemberWillemM7-Jun-05 3:38 
GeneralNice article :-) PinstaffNishant Sivakumar2-Jun-05 17:56 
GeneralRe: Nice article :-) PinprotectorMarc Clifton3-Jun-05 3:57 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 2 Jun 2005
Article Copyright 2005 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid