Click here to Skip to main content
Click here to Skip to main content

Simple Undo/redo library for C#/.NET

By , 22 Sep 2012
Rate this:
Please Sign up or sign in to vote.

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:

  1. UndoRedoManager.Instance() -Get the instance of UndoRedoManager singleton object. 
  2. 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      

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)
{
    //Do not add if the person is already in the list
    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: 

  1. UndoMethods - This class library consists of the UndoManager class and other supporting classes. 
  2. UndoPatternSample - This sample demonstrates the various usages of UndoManager class.
  3. DrawToolkit projects- This is a modified version of  DrawTools from CodeProject with undo/redo functionality.  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Y Sujan
Team Leader
Canada Canada
I have been coding since 1984 in a variety of languages (starting with Basic on Z80 Spectrum).

Comments and Discussions

 
BugAn off-by-one bug in the code Pinmemberthe21st19-Feb-14 2:52 
QuestionComparison to traditional implementations (Memento etc) PinmemberDaniël Trommel16-Aug-13 22:49 
AnswerRe: Comparison to traditional implementations (Memento etc) PinmemberY Sujan18-Aug-13 11:53 
QuestionRe: Comparison to traditional implementations (Memento etc) PinmemberDaniël Trommel26-Oct-13 3:46 
SuggestionSuggestion to use Stacks ("StackFixedSize") PinmemberDaniël Trommel16-Aug-13 22:27 
GeneralRe: Suggestion to use Stacks ("StackFixedSize") PinmemberY Sujan18-Aug-13 11:55 
QuestionUndo/Redo Framework PinmemberE.F. Nijboer24-May-13 1:10 
QuestionQuestion PinmemberFatCatProgrammer24-Sep-12 5:38 
AnswerRe: Question PinmemberY Sujan24-Sep-12 10:02 
GeneralMy vote of 5 Pinmemberkanalbrummer17-Sep-12 2:07 
QuestionThanks PinmemberAlbarhami13-Sep-12 3:27 
AnswerRe: Thanks PinmemberE.F. Nijboer24-May-13 1:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 22 Sep 2012
Article Copyright 2012 by Y Sujan
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid