Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A simple framework for adding undo/redo support

0.00/5 (No votes)
21 Jul 2005 1  
A framework for adding undo/redo support to a Windows Forms application is presented.

Introduction

A simple framework for adding undo/redo support to a Windows Forms application is presented here. The framework consists of a small collection of classes and interfaces that helps you to manage invocation of undo/redo functionality. Of course, the framework itself does not perform the underlying undo or redo functionality. This is something application-specific that you need to provide as you extend the framework.

The framework

There are three main classes/interfaces that I want to describe. They are coded within the same source file (UndoSupport.cs) and namespace (UndoSupport) in the attached demo project.

UndoCommand: This is an abstract class that represents an undoable or redoable operation or command. It provides virtual Undo() and Redo() methods which your derived command classes can override in order to perform the underlying undo/redo functionality.

    public abstract class UndoCommand : IUndoable
    {
        // Return a short description of the cmd

        // that can be used to update the Text

        // property of an undo or redo menu item.

        public virtual string GetText()
        {
            // Empty string.

            return "";
        }

        public virtual void Undo()
        {
            // Empty implementation.

        }

        public virtual void Redo()
        {
            // Empty implementation.

        }
    }

In a class that inherits from UndoCommand, you also have the option of not overriding the virtual Undo() and Redo() methods. Instead, you can treat the derived command class like a data class and simply provide extra fields, properties, or methods that an external class (one that implements the IUndoHandler interface, as discussed below) can use to perform the actual undo/redo functionality.

IUndoHandler: This is an optional interface that your application classes can implement if you don't want a particular UndoCommand class to perform the underlying undo/redo functionality itself. Use of this interface allows you to keep all of the undo/redo logic within a single class if you like (e.g., the class that implements IUndoHandler), and use the UndoCommand classes only for storing the data (e.g. snapshots of application state) that is needed to perform undo/redo.

    public interface IUndoHandler
    {
        void Undo(UndoCommand cmd);
        void Redo(UndoCommand cmd);
    }

UndoManager: This is the primary class in the framework. As you perform operations in your application, you create command objects and add them to the undo manager. The undo manager handles when to invoke undo/redo functionality for you. When you add a new command, you can optionally specify an IUndoHandler to perform undo/redo of that command. It is possible to mix command objects that can perform undo/redo on their own together with command objects that rely on an IUndoHandler implementation. The UndoManager class is designed to be used directly within undo/redo menu item event handlers and undo/redo menu item state update functions (which makes it easy to implement standard Edit | Undo and Edit | Redo menu item functionality).

    public class MyForm : System.Windows.Forms.Form
    {
        ...
        private void OnEditUndoClick(object sender, System.EventArgs e)
        {
            // Perform undo.

            m_undoManager.Undo();
        }
    }

For reference, here is the public interface of the UndoManager class:

    public class UndoManager
    {
        // Constructor which initializes the 

        // manager with up to 8 levels

        // of undo/redo.

        public UndoManager() {...}
        
        // Property for the maximum undo level.

        public int MaxUndoLevel {...}

        // Register a new undo command. Use this method after your

        // application has performed an operation/command that is

        // undoable.

        public void AddUndoCommand(UndoCommand cmd) {...}
        
        // Register a new undo command along with an undo handler. The

        // undo handler is used to perform the actual undo or redo

        // operation later when requested.

        public void AddUndoCommand(UndoCommand cmd, 
                           IUndoHandler undoHandler) {...}
        
        // Clear the internal undo/redo data structures. Use this method

        // when your application performs an operation that cannot be undone.

        // For example, when the user "saves" or "commits" all the changes in

        // the application, or when a form is closed.

        public void ClearUndoRedo() {...}
        
        // Check if there is something to undo. Use this method to decide

        // whether your application's "Undo" menu item should be enabled

        // or disabled.

        public bool CanUndo() {...}
        
        // Check if there is something to redo. Use this method to decide

        // whether your application's "Redo" menu item should be enabled

        // or disabled.

        public bool CanRedo() {...}
        
        // Perform the undo operation. If an undo handler was specified, it

        // will be used to perform the actual operation. Otherwise, the

        // command instance is asked to perform the undo.

        public void Undo() {...}
        
        // Perform the redo operation. If an undo handler was specified, it

        // will be used to perform the actual operation. Otherwise, the

        // command instance is asked to perform the redo.

        public void Redo() {...}
        
        // Get the text value of the next undo command. Use this method

        // to update the Text property of your "Undo" menu item if

        // desired. For example, the text value for a command might be

        // "Draw Circle". This allows you to change your menu item Text

        // property to "&Undo Draw Circle".

        public string GetUndoText() {...}
        
        // Get the text value of the next redo command. Use this method

        // to update the Text property of your "Redo" menu item if desired.

        // For example, the text value for a command might be "Draw Line".

        // This allows you to change your menu item text to "&Redo Draw Line".

        public string GetRedoText() {...}
        
        // Get the next (or newest) undo command. This is like a "Peek"

        // method. It does not remove the command from the undo list.

        public UndoCommand GetNextUndoCommand() {...}
        
        // Get the next redo command. This is like a "Peek"

        // method. It does not remove the command from the redo stack.

        public UndoCommand GetNextRedoCommand() {...}
        
        // Retrieve all of the undo commands. Useful for debugging,

        // to analyze the contents of the undo list.

        public UndoCommand[] GetUndoCommands() {...}
        
        // Retrieve all of the redo commands. Useful for debugging,

        // to analyze the contents of the redo stack.

        public UndoCommand[] GetRedoCommands() {...}
    }

The TestUndo application

The demo project (TestUndo) shows the framework in action. It's a simple Windows application with just a single form. All of the undo/redo framework code is in the UndoSupport.cs file as mentioned earlier. All of the application code that uses the framework is contained within the MainForm.cs file. Below is a snapshot of the TestUndo application:

The MainForm is divided into two group box sections. The top section is the Test Area and offers a simple GUI that allows you to perform some undoable operations. These operations consist of appending short strings to a multiline display textbox. There are three buttons that allow you to add a specific string. A different undo command class (derived from UndoCommand) is associated with each button. The first two command classes (AddABCCommand and Add123Command) do not implement their own undo/redo functionality. They rely on an IUndoHandler implementation to perform the actual undo/redo. The IUndoHandler reference must be specified when these types of commands are added to the undo manager.

    public class MainForm : System.Windows.Forms.Form, IUndoHandler
    {
        ...
        private void OnAddABC(object sender, System.EventArgs e)
        {
            // Create a new command that saves the "current state"

            // of the display textbox before performing the AddABC

            // operation.

            AddABCCommand cmd = new AddABCCommand(m_displayTB.Text);

            // Perform the AddABC operation.

            m_displayTB.Text += "ABC ";

            // Add the new command to the undo manager. We pass in

            // "this" as the IUndoHandler.

            m_undoManager.AddUndoCommand(cmd, this);
        }
    }

The third command class (AddXYZCommand) does implement its own undo/redo functionality. That's why in its constructor, the display textbox is passed in. The AddXYZCommand class needs to access the display textbox in order to perform undo/redo by itself.

    class AddXYZCommand : UndoCommand
    {
        private string  m_beforeText;
        private TextBox m_textBox;

        public AddXYZCommand(string beforeText, TextBox textBox)
        {
            m_beforeText = beforeText;
            m_textBox = textBox;
        }

        public override string GetText()
        {
            return "Add XYZ";
        }

        public override void Undo()
        {
            m_textBox.Text = m_beforeText;
        }

        public override void Redo()
        {
            m_textBox.Text += "XYZ ";
        }
    }

To undo the add operations, the MainForm provides a main menu with Edit | Undo and Edit | Redo menu items. You can use these menu items or their keyboard shortcuts to perform undo/redo of the three types of add operations. The Clear button clears the display textbox and also clears all outstanding undo/redo commands (since I have deemed this particular operation as being not undoable). As you perform add, undo, or redo operations, you can access the Edit menu and see how the undo and redo menu item text changes. For example, instead of simply displaying "Undo", you can see the undo menu item displaying "Undo Add ABC" after you press the Add ABC button.

    public class MainForm : System.Windows.Forms.Form, IUndoHandler
    {
        ...
        private void OnEditMenuPopup(object sender, System.EventArgs e)
        {
            // Update the enabled state of the undo/redo menu items.

            m_undoMenuItem.Enabled = m_undoManager.CanUndo();
            m_redoMenuItem.Enabled = m_undoManager.CanRedo();

            // Change the text of the menu items in order

            // specify what operation to undo or redo.

            m_undoMenuItem.Text = "&Undo";
            if ( m_undoMenuItem.Enabled )
            {
                string undoText = m_undoManager.GetUndoText();
                if ( undoText.Length > 0 )
                {
                    m_undoMenuItem.Text += " " + undoText;
                }
            }
            m_redoMenuItem.Text = "&Redo";
            if ( m_redoMenuItem.Enabled )
            {
                string redoText = m_undoManager.GetRedoText();
                if ( redoText.Length > 0 )
                {
                    m_redoMenuItem.Text += " " + redoText;
                }
            }
        }
    }

The bottom section of the MainForm shows you what's happening behind the scenes in the data structures maintained by the UndoManager class. The UndoManager is implemented using an ArrayList to store the history of commands for undo purposes, and a Stack to store commands for redo purposes. You can see command objects being shuffled between the two data structures as you invoke the undo or redo menu items. By default, the UndoManager supports up to eight levels of undo (meaning it can backtrack up to 8 commands). You can use the MainForm GUI to test different values for the maximum undo level (but note that changing the level will cause existing undo/redo commands to be cleared).

That's basically all for the TestUndo application. I wrote it (MainForm.cs) primarily to illustrate how to use the UndoManager class, to exercise all of its public methods, and to provide some confidence to the reader that the internal undo/redo state is being managed properly (according to "standard" undo/redo behavior). My intent was to create a simple application to demonstrate the above, rather than focus on creating a real application with actual graphics, cut and paste, or text editing commands. There are other articles on CodeProject which discuss this in relation to undo/redo and I encourage you to check those out as well.

Summary

The presented framework can be a starting point for adding undo/redo support to your own Windows Forms applications. The most important part about using the framework is figuring out how to partition your undoable user operations into command classes, deciding what extra fields you need to store in those classes, and then figuring out how to actually undo and redo those operations. In simple cases, undo can be implemented by saving the current state of some application variables before you perform the operation (so that when it is time to undo, you just restore that archived state). In more complex situations, such as when your user operations involve database or data structure manipulations, you will need to be able to write routines to reverse (undo) or re-apply (redo) those manipulations. Discussion of how to implement such commands I believe is application-specific and beyond the scope of what I wanted to demonstrate in this article.

History

  • July 9th, 2005
    • Initial revision.
  • July 10th, 2005
    • Added some clarifications regarding the scope and purpose of the test application, based on initial feedback from Marc Clifton.
  • July 19th, 2005
    • Minor updates to code blocks in the article text.

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