Introduction
This article provides an example framework for undo/redo functionality without using Command or Momento pattern. The framework provides undo/redo stack and support for collated undo/redo.
Note: This is not intended to replace full blown Command or Memento pattern implementation but just demonstrates a simplistic way of performing undo/redo operations. This is especially useful for adding undo/redo support for existing applications.
Background
Recently a friend of mine asked for some help on implementing an undo/redo functionality in an existing application. The obvious choice for such an approach would be either Command or Memento pattern but that would have required making drastic changes to the application.
The solution provided in this article demonstrates a simplistic way to introduce undo/redo functionality at a chosen level of granularity (property/operation/multiple-operations).
Simple undo operation example
The key to using this library is to introduce undo behaviors at the relevant points of action. Using a very simple example, if one of your objects has a property as follows:
public int Age
{
get { return _age; }
set
{
_age = value;
NotifyPropertyChanged("Age");
}
}
If you would like to implement undo behavior at this property's level (as opposed to UI level) then you should add the line shown below
public int Age
{
get { return _age; }
set
{
UndoRedoManager.Instance().Push(a => this.Age=a, _age, "Change age");
_age= value;
NotifyPropertyChanged("Age");
}
}
The line UndoRedoManager.Instance().Push(a=> Age=a, _age, "Change age")
can be broken down as follows:
UndoRedoManager.Instance()
-Get the instance of UndoRedoManager
singleton object. Push
- Push an undo record on the undo stack with the following data
a=> Age=a
- The method to be called to perform undo. In this case, we are just declaring a in-place lambda expression which calls
the Age set property accessor. _age
- The data to be passed to the method. In this case, this member variable contains the current value of the age before it is changed. "Change age"
- is the description of the undo operation (optional).
So when an undo operation is called after one sets the Age property, then the lambda expression specified above gets called effectively resetting the age to the original value. Basically you are creating the Undo record at places where an undo is required. There is no requirement to have a lambda expression - you can create your undo methods as non-anonymous methods.
Note that the UndoRedoManager
takes care of the condition in which this lambda expression is called in the context of an ongoing undo operation, in which case, the new lambda expression will be added to the redo stack. You will never explicitly add a redo operation to the stack.
The signature of the UndoRedoManager.Push
operation is <t><t>
public void Push<t>(UndoRedoOperation<t> undoOperation, T undoData, string description = "")
As you can see that the data type is a template parameter and can be of any type.
Fundamentally, you will be pushing undo record (state) to the stack at any place where you want the user to be able to perform undo's. This is similar to maintaining a list of Commands being executed in the Command pattern and then calling Command.Undo when an undo needs to be performed.
Slightly more complex example
To test the
UndoRedoManager
class, I downloaded and modified the DrawTools code from Code project and added undo-redo functionality to the supplied code. Given below is an example of how I added undo functionality to the Move operation of
DrawLine.cs class.
public override void Move(int deltaX, int deltaY)
{
UndoRedoManager.Instance().Push((dummy) => Move(-deltaX, -deltaY), this);
startPoint.X += deltaX;
startPoint.Y += deltaY;
endPoint.X += deltaX;
endPoint.Y += deltaY;
Invalidate();
}
Here is another example
public override void Normalize()
{
UndoRedoManager.Instance().Push(r => DrawRectangle.GetNormalizedRectangle(r), rectangle);
rectangle = DrawRectangle.GetNormalizedRectangle(rectangle);
}
Consolidating multiple undo operations
Imagine the case where you are setting the Name and Age of the person in two different calls as shown below
Person p = new Person();
p.Name = "new name";
p.Age = p.Age+1;
Assuming that both Name and Age setters create undo records, the above code will result in two undo records in the undo stack. If you want to consolidate these into one undo record then you can surround the above with a transaction.
using (new UndoTransaction("optional description))
{
p.Name = "new name";
p.Age = p.Age+1;
}
This will cause the two undo records to be counted as one undo record. Another example would be in cases where one undoable operation
may call another set of operations which are undoable themselves e.g.,
private Person AddPerson(Person person)
{
Person personInList = _personList.Find(p => p.ID == person.ID);
if (personInList != null)
{
return personInList;
}
UndoRedoManager.Instance().Push(p => RemovePerson(p), person,"Add Person");
personListBindingSource.Position = personListBindingSource.Add(person);
return person;
}
private void btnAddTran_Click(object sender, EventArgs e)
{
using (new UndoTransaction("Add Person"))
{
Person p = new Person() {};
AddPerson(p);
p.Name = "<Change Name>";
p.Age = 0;
}
}
In this case, if the UndoTransaction
was not used in btnAddTran_Click
function, the undo stack would have contained 3 undo records (one for AddPerson, one for Name change, one for Age change) instead of just 1 record.
UndoRedoManager operations
Apart from the Push operation describe above, the
UndoRedoManager
provides the following operations and events:
Undo()
-This is called to perform an Undo operation. One should check if there are undo operations in the stack before calling this method. Redo()
-This is used to perform a Redo operation.
One should check if there are redo operations in the stack before calling this method. HasUndoOperations
/HasRedoOperations
- These can be called to determine if there are any undo/redo records in the undo stack.MaxItems
-This sets/gets the maximum number of items to be stored in the stack.UndoStackStatusChanged
/RedoStackStatusChanged
-These events are fired when items are added/removed from the undo stack. e.g., These events can be used to set the state of undo/redo menu items.
Redos
Redos are automatically handled by the UndoRedoManager
class. The user just has to push undo operations to the
UndoRedoManager
stack.
Contents of attached solution
The attached solution consists of the following projects:
- UndoMethods - This class library consists of the
UndoManager
class and other supporting classes. - UndoPatternSample - This sample demonstrates the various usages of
UndoManager
class. - DrawToolkit projects- This is a modified version of DrawTools from
CodeProject with undo/redo functionality.