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

Undo/Redo Framework

0.00/5 (No votes)
16 Oct 2013 1  
Undo/Redo framework for editing controls in a Windows application.

Introduction

When I was looking for a generic implementation of undo/redo functionality, I found all kinds of articles and examples, but not what I wanted. What I wanted was undo/redo functionality for editing controls that could be added to existing forms and would even work with custom controls. To my surprise, I couldn’t find a generic solution, so I crafted the most generic undo/redo code I could think of, and of course, sharing it with you all.

Background

When added to a project that was way past its initial development stage, I was asked to add undo/redo functionality to the application. I already did some maintenance on this application, but to add this, I had two options. The first was to shoehorn it into all the forms manually and contribute to the degradation of the code (as I have seen is done so often and came along too many times). That is absolutely not for me, so I went for the second option, and that is to create a flexible and generic way of solving this, and maybe even helping the rest of the world tackle this once and for all.

Using the Code

There are three main class types that are important, they are:

  • UndoRedoManager: Responsible for the overall undo/redo functionality.
  • UndoRedoMonitor: Will bind to a specific control to monitor the actions and provide undo/redo information.
  • UndoRedoCommand: Every action is stored as a command which knows how to perform the undo and redo.

UndoRedoManager

This is the class responsible for the overall undo/redo function. When taking a look from the front, this is the class you provide with a control to monitor, which in most cases will be a form, and calls the undo or redo method when necessary. As an extra, you can provide it a list of controls it has to exclude. When taking a look from the back, this is the class that is holding the undo/redo stacks and some state information accordingly. This class also provides methods used by UndoRedoMonitors for storing the UndoRedoCommands, with some extra code to prevent new undo/redo commands to be added when performing an undo or redo request.

How do we register a monitor? Not needed! The manager will scan the assembly for classes that inherit from BaseUndoRedoMonitor and will store the type information in a shared list, and is only done the first time a new UndoRedoManager instance is created. Bind the manager to a control If you want to add undo/redo functionality to a form called MyForm, you simply create a new UndoRedoManager and pass the MyForm as parameter, and that’s it. The way this binding works is actually quite simple. The manager will simply try to find a monitor for each control on the form, and bind a monitor if a match is found. If there is no monitor found for the given control, as would be the case for a panel, it will try to find a monitor for each control of that control recursively. A nice advantage is that user controls, that are composed of controls which already have a monitor class for it, don’t need to create specialized monitors because they are automatically supported. The only thing not yet explained above is how the manager is able to somehow know which type of monitor class is needed for a certain control. The trick here is that the manager, when instantiated, will create an instance of each type of monitor found earlier. Because there is only one edit control which has focus, there is no need to create separate monitors for each control. With all the monitor objects created, the manager can now simply ask each monitor if they are able to monitor a certain control and bind it accordingly.

BaseUndoRedoMonitor

This is the base class of a monitor, and is responsible for adding handlers to the necessary events so changes can be monitored. For most controls, the monitor must also hold temporary data between events. This is because, other than I assumed, the EventArgs object that is passed to the event delegate has no information at all, something I really would expect in an OnChange event. Here is the outline of the BaseUndoRedoMonitor:

Public MustInherit Class BaseUndoRedoMonitor
    Public Sub New(ByVal AUndoRedoManager As UndoRedoManager)
    Public Property UndoRedoManager() As UndoRedoManager
    Public ReadOnly Property isUndoing() As Boolean
    Public ReadOnly Property isRedoing() As Boolean
    Public ReadOnly Property isPerformingUndoRedo() As Boolean
    Public Sub AddCommand(ByVal UndoRedoCommandType As UndoRedoCommandType, _
                          ByVal UndoRedoCommand As BaseUndoRedoCommand)
    Public MustOverride Function Monitor(ByVal AControl As Control) As Boolean
End Class

Most properties and functions are just getting or passing information from and to the UndoRedoManager. The only function that is important is the Monitor function. This is the function used by the UndoRedoManager that will be called to check if the given control fits this monitor. When this is the case, the events handlers are added as necessary, and true is returned so the manager knows that the search for a monitor is completed. Below, you see the monitor implementation for simple text controls (TextBox, ComboBox, DateTimePicker, NumericUpDown, MaskedTextBox):

Public Class SimpleControlMonitor : Inherits BaseUndoRedoMonitor
 
    Private Data As String
 
    Public Sub New(ByVal AUndoRedoManager As UndoRedoManager)
        MyBase.New(AUndoRedoManager)
    End Sub
 
    Public Overrides Function Monitor(ByVal AControl As System.Windows.Forms.Control) As Boolean
        If TypeOf AControl Is TextBox Or _
           TypeOf AControl Is ComboBox Or _
           TypeOf AControl Is DateTimePicker Or _
           TypeOf AControl Is NumericUpDown Or _
           TypeOf AControl Is MaskedTextBox Then
            AddHandler AControl.Enter, AddressOf Control_Enter
            AddHandler AControl.Leave, AddressOf Control_Leave
            Return True
        End If
        Return False
    End Function
 
    Private Sub Control_Enter(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Data = CType(sender, Control).Text
    End Sub
 
    Private Sub Control_Leave(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Dim CurrentData As String = CType(sender, Control).Text
        If Not String.Equals(CurrentData, Data) Then
            AddCommand(UndoRedoCommandType.ctUndo, _
                       New SimpleControlUndoRedoCommand(Me, sender, Data))
        End If
    End Sub
End Class

The code is actually very simple. The monitor function checks the type of a given control, and if this is one of the simple text controls, the enter and leave handlers are added. With these handlers, it is possible to monitor the changes. The text of the control is stored when entered, and when leaving the control, an UndoRedoCommand is added for undo purposes if the text has changed. As you may have noticed, events used and the kind of properties monitored are up to you and depend on your implementation. The above implementation will only monitor text changes, but could, for example, easily be extended to monitor other changes as needed.

BaseUndoRedoCommand

This is the base class for the actual undo/redo information, and is responsible to perform the actual undo and redo action. When derived classes call the undo or redo methods of this base class, it will automatically create the opposite undo or redo command accordingly. This is made possible with a little trick. See the code below, and perhaps you will spot it.

Public MustInherit Class BaseUndoRedoCommand
 
    Private _UndoRedoMonitor As BaseUndoRedoMonitor
    Private _UndoRedoControl As Control
    Private _UndoRedoData As Object
 
    Public ReadOnly Property UndoRedoMonitor() As BaseUndoRedoMonitor...
    Public ReadOnly Property UndoRedoControl() As Control...
    Protected Property UndoRedoData() As Object...
 
    Protected Sub New()
        Throw New Exception("Cannot create instance with the default constructor.")
    End Sub
 
    Public Sub New(ByVal AUndoRedoMonitor As BaseUndoRedoMonitor, _
                   ByVal AMonitorControl As Control)
        Me.New(AUndoRedoMonitor, AMonitorControl, Nothing)
    End Sub
 
    Public Sub New(ByVal AUndoRedoMonitor As BaseUndoRedoMonitor, _
                   ByVal AMonitorControl As Control, ByVal AUndoRedoData As Object)
        _UndoRedoMonitor = AUndoRedoMonitor
        _UndoRedoControl = AMonitorControl
        _UndoRedoData = AUndoRedoData
    End Sub
 
    Protected Sub AddCommand(ByVal UndoRedoCommandType As UndoRedoCommandType, _
                             ByVal UndoRedoCommand As BaseUndoRedoCommand)
        UndoRedoMonitor.AddCommand(UndoRedoCommandType, UndoRedoCommand)
    End Sub
 
    Public Overridable Sub Undo()
        AddCommand(UndoRedoCommandType.ctRedo, Activator.CreateInstance(Me.GetType, _
                                               UndoRedoMonitor, UndoRedoControl))
    End Sub
 
    Public Overridable Sub Redo()
        AddCommand(UndoRedoCommandType.ctUndo, _
                   Activator.CreateInstance(Me.GetType, UndoRedoMonitor, UndoRedoControl))
    End Sub
 
    Public Overridable Sub Undo(ByVal RedoData As Object)
        AddCommand(UndoRedoCommandType.ctRedo, Activator.CreateInstance(Me.GetType, _
                                               UndoRedoMonitor, UndoRedoControl, RedoData))
    End Sub
 
    Public Overridable Sub Redo(ByVal UndoData As Object)
        AddCommand(UndoRedoCommandType.ctUndo, Activator.CreateInstance(Me.GetType, _
                                               UndoRedoMonitor, UndoRedoControl, UndoData))
    End Sub
 
    Public MustOverride Function CommandAsText() As String
 
    Public Overrides Function ToString() As String
        Return CommandAsText()
    End Function
 
End Class

The trick here is that the base class knows the class type of the inherited class, and uses this information to create a new UndoRedoCommand. Let’s look at an implementation example to see how this works.

Public Class SimpleControlUndoRedoCommand : Inherits BaseUndoRedoCommand
 
    Protected ReadOnly Property UndoRedoText() As String
        Get
            Return CStr(UndoRedoData)
        End Get
    End Property
 
    Public Sub New(ByVal AUndoMonitor As BaseUndoRedoMonitor, _
                   ByVal AMonitorControl As Control)
        MyBase.New(AUndoMonitor, AMonitorControl)
        UndoRedoData = UndoRedoControl.Text
    End Sub
 
    Public Sub New(ByVal AUndoMonitor As BaseUndoRedoMonitor, _
           ByVal AMonitorControl As Control, ByVal AUndoRedoData As String)
        MyBase.New(AUndoMonitor, AMonitorControl, AUndoRedoData)
    End Sub
 
    Public Overrides Sub Undo()
        MyBase.Undo()
        UndoRedoControl.Text = UndoRedoText
    End Sub
 
    Public Overrides Sub Redo()
        MyBase.Redo()
        UndoRedoControl.Text = UndoRedoText
    End Sub
 
    Public Overrides Function CommandAsText() As String...
End Class

When looking at the implementation above, the code is pretty simple to understand. First of all, you have the constructors that are pretty important. The first one takes the monitor and control parameter, and will also store the current state of the control. The second one will take the state of the control as an extra parameter and store this. Pretty straightforward so far. The implementation of the Undo and Redo methods are, in this case, very simple and the same, with the exception of the call they make to the base class. As you may have noticed, the plot now tightens, and also see how a redo is possible after doing an undo and vice versa.

In the Undo and Redo methods of the base class, a new object is created of the inherited class type which is an object of SimpleControlUndoRedoCommand, in this case. For a simple control, it is sufficient to store only the current text, which is done in the constructor. So when an undo is performed, the base class will automatically create a redo command, which stores the current text and calls the manager class to store it in the redo stack. This way, there is no further need to do extra coding, and everything is handled within the base class. Remember that for this to work, it is important that the signature of the constructors remains the same and the UndoRedoData needed is stored only in the UndoRedoData property. If additional undo/redo information is needed, it should then be wrapped into a struct or class. In some cases, with some radiobuttons for example, it is not possible to extract the needed information necessary from the given control. When a radiobutton is selected, another is deselected, and to undo or redo this, specific UndoRedoData is needed. This is why it is possible to provide this information when calling the Undo or Redo methods of the base class.

Points of Interest

With this undo/redo framework, there is no excuse anymore why your Windows application doesn’t have undo/redo capabilities. It is very easy to extend it with your own monitor and command classes to serve even the most exotic controls imaginable.

Download the example and experience the ease of this framework. The data (text/selection) properties of the following controls are already implemented:

  • TextBox
  • ComboBox
  • DateTimePicker
  • NumericUpDown
  • MaskedTextBox
  • ListBox (single and multi-select)
  • CheckBox
  • RadioButton
  • MonthCalendar
  • ListView (Label text changes)

History

  • Update: 2009 12 29: Recently I discovered that user controls did not working properly because the RTTI function TypeName is narrowing the code to only the specific controls in the list. Derived controls don't work in the old code and this is fixed with the updated code
  • Update: 2013 10 16: Added a new example in a separate source file for ListView label text changes. Hopefully this will make it easier for others to understand and implement their own undo/redo functionality for other actions and controls. To get into edit mode: Select the item and then click the label again for the selected item. Then wait for some time. It is just like a file rename but without the option of F2 to get into edit mode quickly

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