Click here to Skip to main content
Email Password   helpLost your password?

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

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:

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralGreat job.....
Dhavalkumar P Patel
5:47 11 Jan '10  
Very very nice dude.....


Excellent work on project........


Thanks............... Smile Thumbs Up Smile Smile Smile Smile Smile Smile

Dhavalkumar Patel

GeneralRe: Great job.....
SmackWacker
8:00 12 Jan '10  
Thank you for your great compliment! Smile

Unknown error...
...known bug.

GeneralConvert code from vb.net to c#
thungphan
0:50 7 Jan '10  
Thanks for your article. Please post code in c#. I'm a newbie in c# and trying to looking for undo/redo framework. But your code is in vb.net, it's difficult to me.
GeneralRe: Convert code from vb.net to c#
SmackWacker
2:40 7 Jan '10  
Most .NET languages are just a few syntax changes away from each other. You can convert the code using a free online tool, like the free tool on developerfusion.com. With some copy-paste actions the code is C# in no time. Good luck!
GeneralVery Nice piece of work...
rspercy60
12:38 1 Jan '10  
This is exactly what i was looking for. I am building
a windows type explorer with LogicNP controls and they
do not have SelectAll, Undo, Redo. I had to create my own SelectAll,
and then I found this. WORKS GREAT!

Thank you for your contribution.

rspercy
If "You wash your feet and find a pair of socks " Then
"You ARE a Redneck"
End If

GeneralRe: Very Nice piece of work...
SmackWacker
5:19 3 Jan '10  
Thanks for the compliment and I'm interested in your end result. Maybe you can post a link when you're done. Would be nice and could be another example for others on how to use this framework. Thanks again and good luck!
GeneralRe: Very Nice piece of work...
rspercy60
13:31 3 Jan '10  
I am building a Windows type explorer using LogicNP's components. I have added a burner to it
and I have your stuff added in but have not started working on that yet. I am having a problem
converting a listbox item to a IMAPI.Interop IMediaItem. I see how it was done in C# but
It is a bit different in VB.net. I am almost done with The_Explorer and will be posting it soon.

rspercy
If "You wash your feet and find a pair of socks " Then
"You ARE a Redneck"
End If

GeneralRe: Very Nice piece of work...
rspercy60
17:04 3 Jan '10  
Just implimented you framework..."Works Perctly". It's very user-friendly. This was a very good Idea.
THNX for the app.

rspercy
If "You wash your feet and find a pair of socks " Then
"You ARE a Redneck"
End If

GeneralCode update: Small fix in the Monitor function of class SimpleControlMonitor
SmackWacker
12:14 29 Dec '09  
The function Monitor in class SimpleControlMonitor needed a little fix when using inherited controls. Using TypeName to determine the control type seemed like a good idea at first but inherited controls won't fit when passing the Monitor function. The name of the inherited control is for example SampleTextBox and won't match any of the names in the list of the select case statement.

Previous:
    Select Case TypeName(AControl)
Case "TextBox", "ComboBox", "DateTimePicker", _
"NumericUpDown", "MaskedTextBox"

Fix:
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

The code is somewhat longer but now an inherited control, for example SampleTextBox, will also fit when passing the monitor control. The TypeOf RTTI* statement goes a deeper and compares not only the current class type but also all of the inherited classes. Assuming that SampleTextBox inherits from TextBox it will fit when passing the monitor function.

* RTTI - RunTime Type Information

PURPOSE: Delays program execution until designated condition is indicated.

GeneralHow to implement
youngsc1
16:16 12 Nov '09  
This code is fantastic and will be a big help to me however there's one problem, I'm having trouble implementing it. The way that I currently have my app setup is to use the undo & redo on the main menu control. The forms are being placed into a tabcontrol, some of the controls may further be inside a panel. I've tried passing the active form and the focused control but when the breakpoint hits "canundo" it's always false. How would I go about using this where I have a form on a tabcontrol? It seems in the demo everything is on the top of the form so I'm having trouble understanding and admittetdly I'm no where near the level you appear to be in terms of knowledge. Any help would be appreciated, TIA
GeneralRe: How to implement
SmackWacker
5:36 15 Nov '09  
Thanks for the compliment. The situation you have shouldn't normally be a problem. To verify it I did some testing on the demo application. If you add a tab control to the demo form and put some text field controls on the tab pages, the framework will automatically provides undo redo functionality for the text fields on the tab pages. This is because there is no monitor for a tab control so it will move on to each tab page. There is also no monitor for a tab page so it will move on to the controls on the tab pages. That will be the text fields you just added to it.
So, to setup the framework correctly you must create an instance of the UndoRedoManager and passing it the parent control of all other controls. Like in the example this is the main form. If you pass it the main menu control it will not work. This is because the parent of the menu control is your main form and the controls are also added to the main form. It also will only look at the control itself and not into it’s actions. So if a click on a menu item opens a form, this form isn’t handled by the UndoRedoManager. The newly opened form must be added manually by calling the method Monitor with the new control (or form in this case). Be aware that the actions on this form will be monitored but aren’t automatically removed when this form is closed. So each form must have it’s own UndoRedoManager and therefore all the undo/redo commands are destroyed when the form is closed. Or you should add functionality so undo/redo commands will be removed from the stacks if a form is closed in case you want one global UndoRedoManager.
Well, I hope this somewhat helps you to solve your implementation problem. If not, please supply example code or upload/send an example of what you want to accomplish. This way I can see the actual problem and supply you with a solution for it.

PURPOSE: Delays program execution until designated condition is indicated.

GeneralRe: How to implement
youngsc1
3:31 16 Nov '09  
Great reply thanks much. I think I got it, I'll work on it more today and if I have any problems I'll post the code.


Last Updated 29 Dec 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010