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