Click here to Skip to main content
15,881,831 members
Articles / Desktop Programming / WPF

Multilevel Undo and Redo Implementation in C# - Part III (Using Memento Pattern)

Rate me:
Please Sign up or sign in to vote.
4.90/5 (37 votes)
16 Feb 2009CPOL8 min read 110.1K   2.6K   91   10
How Memento Pattern and Stack can be used to implement Undo/Redo operation in C#.

Introduction

This is Part 3 in a series of articles on writing Multilevel Undo and Redo implementation in C#. This series shows Undo/Redo implementation in three approaches for the same problem along with how we can implement Undo/redo for different scenarios using  these approaches. The approaches are using Single Object Representing Change, Command Pattern and Memento Pattern.

As we know, there is no generic solution for Undo/Redo and Undo/Redo implementation is very specific for each application. For this reason, at first each part in this series of articles discusses how that approach can model an arbitrary application, then it shows the implementation for a sample application. Here Undo/Redo operation is implemented using three different approaches for the same application so that you can compare each approach implementation with the others and pick one that best fits your requirements. The advantages and disadvantages of each approach are also discussed in each part.

Can You Read Each Part Independently?

Here you can read each part independently. To make each part independently readable, necessary information is repeated in each part.

Background

The approach that I have described in part I of this series of articles was written by me in a WPF commercial application when I was not aware of any pattern. After getting some pattern knowledge, I discovered that my solution had many design problems but I could not change the implementation as the application is working well according to requirement. Now by writing this series of articles, I am actually trying to learn from my mistakes. I have written this series of three part articles as a learning exercise, and I expect comments from you about the write up. Please let me know if you have any suggestions.

Basic Idea about Undo/Redo Implementation

As we know, an application changes its state after every operation. As an application is operated, it changes its state. So if someone would like to do undo, he has to go to the previous state. So to enable going to the previous state, we need to store the states of the application while it runs. To support redo, we have to go to the next state from the present state.

To implement Undo/Redo, we have to store the states of the application and go to the previous state for undo and have to go to the next state for redo. So we have to maintain the states of the application to support Undo/Redo. To maintain the states of an application in all the three approaches we uses two stacks. One stack contains the states for undo operation. The second stack contains the states for redo operation. Undo operation pops the undo stack to get the previous state and sets the previous state to the application. In the same way, redo operation pops the redo stack to get to the next state and sets the next state to the application.

Now we know Implementing Undo Redo operation is all about keeping state after each operation of the application. Now the question is how does this approach keep state? In memento pattern, we are keeping state of the container as the state of the application. 

About Memento Pattern

Here I am not going to discuss about the Memento pattern. You can read about this pattern here and here

How Can We Model Undo/Redo Operation for an Arbitrary Application using Memento Pattern? 

The Memento pattern stores the application state before every operation to implement Multi-level Undo/ Redo. To implement Undo/Redo operation using memento pattern, memento represents a state of the Container object and MementoOriginator creates a memento (state) of the Container object. Caretaker performs safe keeping of memento (state) into two stacks, one is for undo and another is for redo and it returns undo memento and redo memento. An Undo/Redo class will use Caretaker to get Undo memento (state) and Redo memento (state) and performs the undo/redo operation.  

How an arbitrary application can be modeled using memento pattern is discussed in the following steps:

Step 1

Identify the container for which you are going to support Undo/Redo. Then identify the objects the container holds and the properties of the container that can be changed over time during different operations.

Step 2

Then make a memento class to hold these objects and the changeable properties of the container so that this memento can represent a state of the container after each operation on the container.

Step 3

Then make a MementoOriginator class whose responsibility is making a memento of the container at any point and setting a memento to the container.This class implements two methods getMemento() and setMemento(Memento memento). getMemento() method makes a memento of the container and returns it to the caller. SetMemento(Memento memento) method seta the memento(State) to the container.

Step 4

Then make a Caretaker Class which keeps memento(state) into two stacks. One stack holds memento (state) for Undo operations and another one holds memento (state) for redo operations. It implements three methods getUndoMemento (), getRedoMemento (), InsertMementoForUndoRedo (Memento memento). GetUndoMemento () returns a memento for undo operation. GetRedoMemento() returns a memento for redo operation. InsertMementoForUndoRedo(Memento memento) inserts a memento into Undo /Redo plumbing  and clears the Redostack.

Step 5

Then make Undo/Redo class which implements the following IUndoRedo interface:

C#
interface IUndoRedo
 {
     void Undo(int level);
     void Redo(int level);
     void SetStateForUndoRedo();
 }

In Undo operation:

  • First get the UndoMemento from the Caretaker 
  • Then set the Undomemento to the container using the MementoOriginator

In Redo operation:

  • First get the RedoMemento from the Caretaker 
  • Then set the REdomemento to the container using the MementoOriginator

In SetStateForUndoRedo operation:

  • Get the current memento (state) using the MementoOriginator
  • Then insert the current memento (state) in the Caretaker to support Undo/Redo plumbing. 

Step 6

After each operation of your application, call the method SetStateForUndoRedo() of UndoRedo class to make that operation Undo Redo enabled.

Sample Application Description

Here a simple WPF drawing application is used as an example to incorporate undo/redo operation. This WPF sample application support four operations: Object Insert, Object Delete, Object Move, and Object Resize and has two types of geometric object: Rectangle and Polygon. It uses Canvas as container to contain these geometric objects.

Now in this series of articles, we see how we can give Undo/Redo support in these four operations. In the Part 1, the implementation is shown Using Single Object Representing Change Approach. In Part 2, the   implementation is shown using command pattern and in Part3, the implementation is shown Using Memento pattern.

Undo/Redo Implementation of the Sample Application Using Memento Pattern

Step 1

Here container is the Canvas which holds Uielement objects and no properties of the container is changed over time during different operations. 

Step 2

Now we will make the following memento class to hold Uielement objects as Canvas state.

C#
public class Memento
 {
     private List<UIElement> _ContainerState;

     public List<UIElement> ContainerState
     {
         get { return _ContainerState; }
     }
     public Memento(List<UIElement> containerState)
     {
         this._ContainerState = containerState;
     }
 }

Now this memento represents a state of the Canvas.

Step 3

Then the following MementoOriginator is made which makes a memento of the Canvas using deep copy of its objects and sets a memento to the Canvas.

C#
public class MementoOriginator
  {
      private Canvas _Container;

      public MementoOriginator(Canvas container)
      {
          _Container = container;
      }

      public Memento getMemento()
      {
          List<UIElement> _ContainerState = new List<UIElement>();

          foreach (UIElement item in _Container.Children)
          {
              if (!(item is Thumb))
              {
                  UIElement newItem = DeepClone(item);
                  _ContainerState.Add(newItem);
              }
          }

          return new Memento(_ContainerState);

      }

      public void setMemento(Memento memento)
      {
          _Container.Children.Clear();
          Memento memento1 = MementoClone(memento);
          foreach (UIElement item in memento1.ContainerState)
          {
              ((Shape)item).Stroke = System.Windows.Media.Brushes.Black;
              _Container.Children.Add(item);
          }
      }

      public Memento MementoClone(Memento memento)
      {
          List<UIElement> _ContainerState = new List<UIElement>();

          foreach (UIElement item in memento.ContainerState)
          {
              if (!(item is Thumb))
              {
                  UIElement newItem = DeepClone(item);
                  _ContainerState.Add(newItem);
              }
          }

          return new Memento(_ContainerState);

      }
      private UIElement DeepClone(UIElement element)
      {
          string shapestring = XamlWriter.Save(element);
          StringReader stringReader = new StringReader(shapestring);
          XmlTextReader xmlTextReader = new XmlTextReader(stringReader);
          UIElement DeepCopyobject = (UIElement)XamlReader.Load(xmlTextReader);
          return DeepCopyobject;
      }
  }

GetMemento()  method  makes a deep copy of the UIelement collection of the canvas to make a memento and returns this memento to the caller. SetMemento(Memento memento) method at first clears the Canvas and then sets the memento by adding each object of memento to the Canvas. DeepClone(UIElement element) method just makes a deep copy of an UIelement object.

Step 4

The following Caretaker class is made which keeps memento (state) into two stacks. Undo stack holds Memento (state) for Undo operation and Redo stack holds memento (state) for redo operation.

C#
class Caretaker
 {
     private Stack<Memento> UndoStack = new Stack<Memento>();
     private Stack<Memento> RedoStack = new Stack<Memento>();

     public Memento getUndoMemento()
     {
         if (UndoStack.Count >= 2)
         {
             RedoStack.Push(UndoStack.Pop());
             return UndoStack.Peek();
         }
         else
             return null;
     }
     public Memento getRedoMemento()
     {
         if (RedoStack.Count != 0)
         {
             Memento m = RedoStack.Pop();
             UndoStack.Push(m);
             return  m;
         }
         else
             return null;
     }
     public void InsertMementoForUndoRedo(Memento memento)
     {
         if (memento != null)
         {
             UndoStack.Push(memento);
             RedoStack.Clear();
         }
     }
     public bool IsUndoPossible()
     {
         if (UndoStack.Count >= 2)
         {
             return true;
         }
         else
             return false;

     }
     public bool IsRedoPossible()
     {
         if (RedoStack.Count != 0)
         {
             return true;
         }
         else
             return false;
     }

 }

Step 5

Then the following UndoRedo class is implemented:

C#
public class UndoRedo : IUndoRedo
{
    Caretaker _Caretaker = new Caretaker();
    MementoOriginator _MementoOriginator = null;
    public event EventHandler EnableDisableUndoRedoFeature;

    public UndoRedo(Canvas container)
    {
        _MementoOriginator = new MementoOriginator(container);

    }
    public void Undo(int level)
    {
        Memento memento = null;
        for (int i = 1; i <= level; i++)
        {
            memento = _Caretaker.getUndoMemento();
        }
        if (memento != null)
        {
            _MementoOriginator.setMemento(memento);

        }
        if (EnableDisableUndoRedoFeature != null)
        {
            EnableDisableUndoRedoFeature(null, null);
        }
    }

    public void Redo(int level)
    {
        Memento memento = null;
        for (int i = 1; i <= level; i++)
        {
            memento = _Caretaker.getRedoMemento();
        }
        if (memento != null)
        {
            _MementoOriginator.setMemento(memento);

        }
        if (EnableDisableUndoRedoFeature != null)
        {
            EnableDisableUndoRedoFeature(null, null);
        }
    }

    public void SetStateForUndoRedo()
    {
        Memento memento = _MementoOriginator.getMemento();
        _Caretaker.InsertMementoForUndoRedo(memento);
        if(EnableDisableUndoRedoFeature != null)
        {
            EnableDisableUndoRedoFeature(null,null);
        }
    }

    public bool IsUndoPossible()
    {
        return _Caretaker.IsUndoPossible();

    }
    public bool IsRedoPossible()
    {
       return  _Caretaker.IsRedoPossible();
    }
}

In Undo method, we execute UndoOperation to the level number of times. In each undo operation, we get the UndoMemento from the Caretaker and set the Undomemento to the canvas using the MementoOriginator. In Redo method, we execute RedoOperation to the level number of times. In each RedoOperation, we get the RedoMemento from the Caretaker and set the Redomemento to the Canvas using the MementoOriginator. In SetStateForUndoRedo operation, we get the current memento (state) using the MementoOriginator and then insert the current memento (state) in the Caretaker to support undo/redo plumbing.                 

Step 6

After each operation of this application, we called the SetStateForUndoRedo() method of UndoRedo class to make that operation UndoRedo enable. When Undo is clicked from UI, we call the Undo method of UndoRedo class and when Redo is clicked from UI, we call the redo method of UndoRedo class.

Here I did not explicitly set the size of Undo stack and Redo stack, so the number of undo redo states the application can hold will be based on the memory of the system.

Change Management When Using Memento Pattern

In memento pattern, if you would like to add a new operation, generally you need not make any change in Undo/Redo code to make the operation undo redo enabled. As in memento approach, we keep a deep copy of the overall application state. 

Advantages and Disadvantages When Using Memento Pattern

Its advantage is that it performs well in change management.

As in memento pattern we are keeping state of the container, it is memory intensive. Here you have to make a deep copy of all of the objects and properties the container holds. It can be a problem if you cannot make a deep copy for any of them.

Sample Code

Here, a project has been attached which shows the Undo/Redo implementation using memento pattern.

Conclusion

Thanks for reading this write up. I hope that this article will be helpful for some people. If you guys have any questions, I would love to answer. I always appreciate comments.

History

  • Initial release – 15/02/09

License

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


Written By
Software Developer (Senior) CP
Australia Australia
I am an Independent Contractor in Brisbane, Australia. For me, programming is a passion first, a hobby second, and a career third.

My Blog: http://weblogs.asp.net/razan/






Comments and Discussions

 
GeneralMy vote of 5 Pin
cpsglauco31-May-16 7:08
cpsglauco31-May-16 7:08 
QuestionUndo/Redo for Canvas area Wpf Pin
Sagar Pulidindi31-May-13 5:00
Sagar Pulidindi31-May-13 5:00 
GeneralMy vote of 5 Pin
Kanasz Robert28-Sep-12 7:18
professionalKanasz Robert28-Sep-12 7:18 
Questioncapacity of undo and redo stacks Pin
mafoti26-Jul-11 5:16
mafoti26-Jul-11 5:16 
AnswerRe: capacity of undo and redo stacks Pin
Brighto Twumasi11-Jul-12 23:59
Brighto Twumasi11-Jul-12 23:59 
Generalhave 5 Pin
Pranay Rana26-Jan-11 6:01
professionalPranay Rana26-Jan-11 6:01 
GeneralRe: have 5 Pin
Razan Paul (Raju)26-Jan-11 13:55
Razan Paul (Raju)26-Jan-11 13:55 
GeneralMemento pattern Pin
Carl Warman29-Sep-10 23:01
Carl Warman29-Sep-10 23:01 
GeneralThank you ... and alternative approaches Pin
Henrik Jonsson20-Feb-09 7:55
Henrik Jonsson20-Feb-09 7:55 
GeneralWould checkpoint/redo be workable as an approach? [modified] Pin
supercat917-Feb-09 17:04
supercat917-Feb-09 17:04 

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

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