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
{
public virtual string GetText()
{
return "";
}
public virtual void Undo()
{
}
public virtual void Redo()
{
}
}
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)
{
m_undoManager.Undo();
}
}
For reference, here is the public interface of the UndoManager
class:
public class UndoManager
{
public UndoManager() {...}
public int MaxUndoLevel {...}
public void AddUndoCommand(UndoCommand cmd) {...}
public void AddUndoCommand(UndoCommand cmd,
IUndoHandler undoHandler) {...}
public void ClearUndoRedo() {...}
public bool CanUndo() {...}
public bool CanRedo() {...}
public void Undo() {...}
public void Redo() {...}
public string GetUndoText() {...}
public string GetRedoText() {...}
public UndoCommand GetNextUndoCommand() {...}
public UndoCommand GetNextRedoCommand() {...}
public UndoCommand[] GetUndoCommands() {...}
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)
{
AddABCCommand cmd = new AddABCCommand(m_displayTB.Text);
m_displayTB.Text += "ABC ";
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)
{
m_undoMenuItem.Enabled = m_undoManager.CanUndo();
m_redoMenuItem.Enabled = m_undoManager.CanRedo();
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
- 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.