Click here to Skip to main content
15,879,613 members
Articles / Desktop Programming / WPF

Multilevel Undo and Redo Implementation in C# - Part II (Using Command Pattern)

Rate me:
Please Sign up or sign in to vote.
4.93/5 (52 votes)
17 Feb 2009CPOL9 min read 169.3K   3.5K   103   18
How Command Pattern and Stack can be used to implement Undo/Redo operation in C#

Introduction

This is Part 2 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 requirements. 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 the previous state. 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 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 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 this approach keeps state? In Command pattern, we keep the changes of a single operation in an ICommand object which is intended for this particular type of operation as state.

About Command Pattern

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

How Can We Model Undo/Redo Operation for an Arbitrary Application Using Command Pattern?

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

Step 1

First identify the operations for which you are going to support Undo/Redo operation and identify the objects on which the operations will be applicable and also the container of Undo/Redo operation.

Step 2

Make command class inheriting from ICommand for each of the identified operation. Each command class will contain attribute that it needs to make Undo/Redo of its representing operation. The ICommand Interface is in the following:

C#
interface ICommand
   {
       void Execute();
       void UnExecute();
   }

In the Execute() method, you will perform the action with the command's attribute. In Unexecuted () method, you will perform the undo operation with the command's attribute. Here command's attribute holds the changes its corresponding command needs to make undo/redo operation and also the reference on which object it makes the change.

Special Note: If the same operation behaves differently on different objects, then you have to make multiple commands for this operation. That is a command of this operation per object type.

Step 3

Then make a class named UndoRedo which contains two stacks. The first one is for Undo operation and the second one is for Redo operation. This class implements Undo method, Redo method and a number of InsertInUnDoRedo methods to insert ICommand object in Undo/Redo system. When InsertInUnDoRedo method is called, then the ICommand object is inserted into Undo stack to make the operation Undo/Redo Enable and clear the Redostack.

In Undo operation:

  • First check whether UndoStack is empty or not. If empty, then return otherwise proceed.
  • Then pop an ICommand object from the UndoStack.
  • Then push this command to RedoStack.
  • Then invoke the Unexecute method of the  I<code>command object.

In Redo operation:

  • First check whether RedoStack is empty or not. If empty, then return otherwise proceed.
  • Then  pop an Icommand object  from the RedoStack.
  • Then push this command to Undostack.
  • Then invoke the execute method of the Icommand object. 

Step 4

When you are performing different operations in your application, make a Command object of this operation type and push it into Undo/Redo system using method call InsertInUnDoRedo. When you need to do an undo operation, then just call Undo method of the UndoRedo class from your application and when you need to do a redo operation, then just call redo operation from your application.

Sample Application Description

Here a simple WPF drawing application is used as an example to incorporate undo/redo operation. This WPF sample application supports four operations: Object Insert, Object Delete, Object Move, and Object Resize and has two types of geometric objects: 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 Part 3, the implementation is shown using Memento pattern.

Undo/Redo Implementation of the Sample Application Using Command Pattern

Undo/Redo implementation for the sample application Using Command Pattern Approach is discussed in the following:

Step 1

Here there are four operations in the sample application and these are move, resize, Insert and delete. The objects are rectangle, Ellipse and the Container is a WPF Canvas.

Step 2

Now we will make four command classes for each of the four operations inheriting for ICommand interface.

C#
class MoveCommand : ICommand
   {
       private Thickness _ChangeOfMargin;
       private FrameworkElement _UiElement;

       public MoveCommand(Thickness margin, FrameworkElement uiElement)
       {
           _ChangeOfMargin = margin;
           _UiElement = uiElement;
       }

       #region ICommand Members

       public void Execute()
       {
           _UiElement.Margin = new Thickness(_UiElement.Margin.Left +
           _ChangeOfMargin.Left, _UiElement.Margin.Top
                   + _ChangeOfMargin.Top, _UiElement.Margin.Right +
           _ChangeOfMargin.Right, _UiElement.Margin.Bottom +
           _ChangeOfMargin.Bottom);
       }

       public void UnExecute()
       {
           _UiElement.Margin = new Thickness(_UiElement.Margin.Left -
           _ChangeOfMargin.Left, _UiElement.Margin.Top -
               _ChangeOfMargin.Top, _UiElement.Margin.Right -
           _ChangeOfMargin.Right, _UiElement.Margin.Bottom -
           _ChangeOfMargin.Bottom);
       }

       #endregion
   }

As move operation only changes the margin of geometric object, move command will contain change of margin, the geometric object reference. In the move command, the Execute method just accomplishes the changes with the  _UiElement geometric object by adding the margin change and the Unexecute method undoes the operation by subtracting the already applied changes. To do so, it subtracts the margin change from the geometric object UIelement.

C#
class ResizeCommand : ICommand
   {
       private Thickness _ChangeOfMargin;
       private double _ChangeofWidth;
       private double _Changeofheight;
       private FrameworkElement _UiElement;

       public ResizeCommand(Thickness margin, double width,
           double height, FrameworkElement uiElement)
       {
           _ChangeOfMargin = margin;
           _ChangeofWidth = width;
           _Changeofheight = height;
           _UiElement = uiElement;
       }

       #region ICommand Members

       public void Execute()
       {
           _UiElement.Height = _UiElement.Height + _Changeofheight;
           _UiElement.Width = _UiElement.Width + _ChangeofWidth;
           _UiElement.Margin = new Thickness
       (_UiElement.Margin.Left + _ChangeOfMargin.Left,
       _UiElement.Margin.Top
               + _ChangeOfMargin.Top, _UiElement.Margin.Right +
       _ChangeOfMargin.Right, _UiElement.Margin.Bottom +
       _ChangeOfMargin.Bottom);
       }

       public void UnExecute()
       {
           _UiElement.Height = _UiElement.Height - _Changeofheight;
           _UiElement.Width = _UiElement.Width - _ChangeofWidth;
           _UiElement.Margin = new Thickness(_UiElement.Margin.Left -
       _ChangeOfMargin.Left, _UiElement.Margin.Top -
           _ChangeOfMargin.Top, _UiElement.Margin.Right -
       _ChangeOfMargin.Right, _UiElement.Margin.Bottom -
       _ChangeOfMargin.Bottom);
       }

       #endregion
   }

Resize operation changes the margin, height, width of the geometric object so resize command holds change of margin, change of height, change of width and the reference of the geometric object. In the resize command, the Execute method just accomplishes the changes with the  _UiElement geometric object by adding the margin change, height change and width change. The Unexecute method undoes the operation by subtracting the already applied changes. To do so, it subtracts the margin change, height change and width change from the geometric object _UIelement.

C#
class InsertCommand : ICommand
   {
       private FrameworkElement _UiElement;
       private Canvas _Container;

       public InsertCommand(FrameworkElement uiElement, Canvas container)
       {
           _UiElement = uiElement;
           _Container = container;
       }

       #region ICommand Members

       public void Execute()
       {
           if (!_Container.Children.Contains(_UiElement))
           {
               _Container.Children.Add(_UiElement);
           }
       }

       public void UnExecute()
       {
           _Container.Children.Remove(_UiElement);
       }

       #endregion
   }

As insert operation inserts a geometric object in a panel, insert command holds the geometric object and the reference of the Canvas. In insert command, Execute method adds the geometric object to the Canvas and Unexecute method removes the geometric object from the Canvas.

C#
class DeleteCommand : ICommand
    {
        private FrameworkElement _UiElement;
        private Canvas _Container;

        public DeleteCommand(FrameworkElement uiElement, Canvas container)
        {
            _UiElement = uiElement;
            _Container = container;
        }

        #region ICommand Members

        public void Execute()
        {
            _Container.Children.Remove(_UiElement);
        }

        public void UnExecute()
        {
            _Container.Children.Add(_UiElement);
        }

        #endregion
    } 

As delete operation deletes a geometric object from a panel, delete command holds the geometric object and the reference of the Canvas. In delete command, Execute method removes the geometric object from the Canvas and Unexecute method adds the geometric object to the Canvas.

Step 3

Now we will implement the UndoRedo class according to the description of generic approach.

C#
public class UnDoRedo
  {
      private Stack<ICommand> _Undocommands = new Stack<ICommand>();
      private Stack<ICommand> _Redocommands = new Stack<ICommand>();

      private Canvas _Container;

      public Canvas Container
      {
          get { return _Container; }
          set { _Container = value; }
      }

      public void Redo(int levels)
      {
          for (int i = 1; i <= levels; i++)
          {
              if (_Redocommands.Count != 0)
              {
                  ICommand command = _Redocommands.Pop();
                  command.Execute();
                  _Undocommands.Push(command);
              }

          }
      }

      public void Undo(int levels)
      {
          for (int i = 1; i <= levels; i++)
          {
              if (_Undocommands.Count != 0)
              {
                  ICommand command = _Undocommands.Pop();
                  command.UnExecute();
                  _Redocommands.Push(command);
              }

          }
      }

      #region UndoHelperFunctions

      public void InsertInUnDoRedoForInsert(FrameworkElement ApbOrDevice)
      {
          ICommand cmd = new InsertCommand(ApbOrDevice, Container);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      }

      public void InsertInUnDoRedoForDelete(FrameworkElement ApbOrDevice)
      {
          ICommand cmd = new DeleteCommand(ApbOrDevice, Container);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      }

      public void InsertInUnDoRedoForMove
      (Point margin, FrameworkElement UIelement)
      {
          ICommand cmd = new MoveCommand(new Thickness
          (margin.X, margin.Y, 0, 0), UIelement);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      }

      public void InsertInUnDoRedoForResize
  (Point margin, double width, double height, FrameworkElement UIelement)
      {
          ICommand cmd = new ResizeCommand(new Thickness
      (margin.X, margin.Y, 0, 0), width, height, UIelement);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      }

      #endregion
  }

The first stack _Undocommands holds the undoable operation and the second stack _Redo commands hold the redoable operation. When InsertInUnDoRedo method is called, the Icommand object is inserted into _Undocommands to make the command the undoable operation and clear the Redostack. Here level determines how many times you want to do undo. That's why you have to run undo redo operation level times in undoredo method.

Step 4

When you are performing different operations in your application, make a Command object of this operation type and push it into Undo/Redo system using method call InsertInUnDoRedo. 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 system.

Change Management When Using Command Pattern

When you need to support Undo/ Redo for a new operation in your application, you only need to add a new command object. I think that is good maintainability.

Advantages and Disadvantages When Using Command Pattern

Its maintainability is good and does not hold any redundant information. It is not memory intensive. From my point of view, the Command pattern is the best way to implement multi-level Undo/Redo.

The only disadvantage of command pattern is you have to make Command type equal to the number of operations whether an operation is very small or big. As operations increase, commands increase.

Sample Code

Here, a project has been attached which shows the Undo/Redo implementation using command 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 – 14/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

 
PraiseVery Good article Pin
Member 147932945-Apr-20 11:08
Member 147932945-Apr-20 11:08 
QuestionGreat Pin
flash-mmb1-May-17 23:30
flash-mmb1-May-17 23:30 
QuestionIf I only perform redo/undo action on a list, can I make the list static and simply call it in the commands? Pin
Member 1297812010-Apr-17 9:34
Member 1297812010-Apr-17 9:34 
QuestionVery good work! Pin
hernandes.asasoft10-Oct-16 1:39
hernandes.asasoft10-Oct-16 1:39 
GeneralMy vote of 5 Pin
cpsglauco31-May-16 7:07
cpsglauco31-May-16 7:07 
QuestionAbsolutely awesome article! Pin
Pixipulp31-Mar-15 2:19
Pixipulp31-Mar-15 2:19 
QuestionWhy are you not execute()ing the commands? Pin
Baahu13-Jul-13 20:09
Baahu13-Jul-13 20:09 
GeneralMy vote of 5 Pin
pjdixit3-Apr-13 4:13
pjdixit3-Apr-13 4:13 
SuggestionRedo/Undo for Delete Pin
schmiddd29-Mar-13 7:05
schmiddd29-Mar-13 7:05 
GeneralGreat explanation! Pin
radsd12-Nov-12 20:18
radsd12-Nov-12 20:18 
GeneralMy vote of 5 Pin
Kanasz Robert28-Sep-12 7:16
professionalKanasz Robert28-Sep-12 7:16 
QuestionDeNem Pin
Algerius11-Jul-12 20:41
Algerius11-Jul-12 20:41 
Question5s Pin
Ziad Khoury8-Jan-12 23:54
Ziad Khoury8-Jan-12 23:54 
QuestionNice article to read. Pin
siddhartha.mukherjee@wipro.com21-Nov-11 18:58
siddhartha.mukherjee@wipro.com21-Nov-11 18:58 
GeneralMy vote of 5 Pin
jim lahey12-Oct-11 21:50
jim lahey12-Oct-11 21:50 
GeneralMy vote of 5 Pin
Reiss17-Aug-11 2:22
professionalReiss17-Aug-11 2:22 
GeneralWell done. Pin
_cephei16-May-11 22:00
_cephei16-May-11 22:00 
GeneralGreat job!!! Pin
rejectkosta28-Feb-10 21:52
rejectkosta28-Feb-10 21:52 

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.