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

An Undo/Redo Buffer Framework

By , 2 Jun 2005
 

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

About the Author

Marc Clifton
United States United States
Member
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.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 2memberpaulsasik3 May '10 - 4:38 
QuestionFrameWork 1.1membersudarshanbs30 Jun '09 - 23:16 
Generalundo-redo for listviewmembermm3107 Apr '07 - 11:23 
GeneralProblem with classes whose properties updated through PropertyGridmemberX-treem1 Dec '05 - 4:29 
GeneralRe: Problem with classes whose properties updated through PropertyGridmemberPDHB17 Mar '06 - 5:29 
GeneralRe: Problem with classes whose properties updated through PropertyGridmemberlhayes001 Apr '08 - 13:18 
GeneralRe: Problem with classes whose properties updated through PropertyGridmemberMichael Brian Clarke10 Nov '08 - 9:07 
I was browsing and saw your post. Despite how long ago and that maybe you have forgotten about it, would you happen to have any info about the 'TestApp.StaticObject' mentioned in your post. Also, are you 100% sure that multiple items were in fact selected and what is the end result you were after when creating/using the PropertyGrid object?
 
v/r,
 
Mike Poke tongue | ;-P
Questiongenerics c#2.0?memberMP3Observer2 Jun '05 - 22:26 
AnswerRe: generics c#2.0?protectorMarc Clifton3 Jun '05 - 0:57 
QuestionCompare to Command pattern undo/redo?memberDr Herbie2 Jun '05 - 22:01 
AnswerRe: Compare to Command pattern undo/redo?protectorMarc Clifton3 Jun '05 - 0:59 
AnswerRe: Compare to Command pattern undo/redo?memberDejaVudew3 Jun '05 - 3:31 
GeneralRe: Compare to Command pattern undo/redo?memberWillemM7 Jun '05 - 3:38 
GeneralNice article :-)staffNishant Sivakumar2 Jun '05 - 17:56 
GeneralRe: Nice article :-)protectorMarc Clifton3 Jun '05 - 3:57 

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 2 Jun 2005
Article Copyright 2005 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid