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

Multilevel Undo and Redo Implementation in C# - Part I (Using Single Object Representing Change Approach)

, 17 Feb 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
How single object Representing Change Approach and Stack can be used to implement Undo/Redo operation in C#

Introduction

This is Part 1 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 others and can pick one which 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 suggestion.

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 have to 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 use 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 this approach, changes of a single operation are kept in an object in which some attributes are redundant for this operation as state because here a single object is used to hold all types of action data.

What is the Single Object Representing Change Approach?

First of all, I want to say sorry because this name is given by me. Here a single object represents all the changes of all operations in an application. So after executing an operation, when you make an object of this type with the changes of the operation, you only use subset of attributes of this object and remaining attributes remain unused. For example, you have two operations in an application; these are height change and width change. So here this type of object contains two attributes: height and width. After executing height change method, when you make Change object, you will only set the height field of the change object and the other field remains unused. 

How Can We Model Undo/Redo Operation for an Arbitrary Application using Single Object Representing Change Approach?

How an arbitrary application can be modeled using Single Object Representing Change Approach is discussed in the following steps:

Step 1

First identify for what operations you would like to support Undo/Redo. Then, identify in what container you will support undo/redo and also on what objects you would like to support undo/redo.

Step 2

Then identify the attributes that need to be saved for further use to handle each and every undo/redo operation. 

Step 3

Then make a class (ChangeRepresentationObject) which contains all the attributes to support Undo/Redo for all operations. Also make an action type enum which will represent all the operations. This action type enum will be part of the ChangeRepresentationObject class.

Step 4

Than make a class named UndoRedo which contains two stacks of type ChangeRepresentationObject. One stack for undo operation and another operation id for redo operation. The class will implement the following interface: 

  interface IUndoRedo
    {
        void Undo(int level);
        void Redo(int level);
        void InsertObjectforUndoRedo(ChangeRepresentationObject dataobject);
    } 

Step 5

Then make the implementation of the methods: Undo, Redo, InsertObjectforUndoRedo.

In each Undo operation:

  • First you will check whether Undo stack is empty or not.
  • If not, then pop a ChangeRepresentationObject and push it to redo stack. 
  • Check the action type.
  • Then based on action type, perform the undo operation using the ChangeRepresentationObject attributes. 

In each Redo operation, you will do almost the same as Undo.

  • First you will check whether Redo stack is empty or not.
  • If not, then pop a ChangeRepresentationObject and push it to undo stack.
  • Check the action type.
  • Then based on action type, perform the redo operation using the ChangeRepresentationObject attributes.

In InsertObjectforUndoRedo operation, you will just insert the data object into Undo Stack and clear the Redo stack.

Step 6

Then before performing each operation, call the InsertObjectforUndoRedo method to support undo/redo for all those operations. When Undo is clicked from UI, just call the Undo method of UndoRedo class and when Redo is clicked from UI, just call the redo method of UndoRedo class.

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 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 Single Object Representing Change Approach

Undo/Redo implementation for the sample application using Single Object Representing Change Approach is discussed in the following:

Step 1

We will identify for what operations we are supporting Undo/Redo. Here for four operations we are supporting Undo/Redo. These are: Object Insert, Object Delete, Object Move, Object Resize. We are going to support  Undo /Redo for Rectangle and Ellipse objects and here Container is Canvas.

Step 2

Now we will identify the parameters that need to be saved for further use to handle Undo/Redo. Geometric object moves its margin changes, so to support Undo/Redo for object move we need to keep margin. As object resizes, it changes its height, width and margin. So to support Undo/Redo for object resize, we need to keep height, width and margin. To support Undo /Redo for insert and delete, we will keep the reference of the geometric object.

Step 3

Now we will make ChangeRepresentationObject which contains Margin, Height, Width, Action type and geometric object reference to support Undo/Redo for all operations. Here geometric object reference is kept to get the reference when we want to make Undo/Redo on it. Also make an action type enum which will represent insert, delete, move and resize operation. This action type enum is used as part of the ChangeRepresentationObject.

 public enum ActionType
    {
        Delete = 0,
        Move = 1,
        Resize = 2,
        Insert = 3
    } 
    public class ChangeRepresentationObject
    {
        public ActionType Action;
        public Point Margin;
        public double Width;
        public double height;
        public FrameworkElement UiElement;
    }

Steps 4 & 5

Than we make a class named UndoRedo which contains two stacks of type ChangeRepresentationObject. One stack is for undo operation and another operation id for redo operation. The class code is given below:

  public partial class UnDoRedo : IUndoRedo
    {
        private Stack<ChangeRepresentationObject> _UndoActionsCollection = 
					new Stack<ChangeRepresentationObject>();
        private Stack<ChangeRepresentationObject> _RedoActionsCollection = 
					new Stack<ChangeRepresentationObject>();
        
        #region IUndoRedo Members

        public void Undo(int level)
        {
            for (int i = 1; i <= level; i++)
            {
                if (_UndoActionsCollection.Count == 0) return;

                ChangeRepresentationObject Undostruct = _UndoActionsCollection.Pop();
                if (Undostruct.Action == ActionType.Delete)
                {
                    Container.Children.Add(Undostruct.UiElement);
                    this.RedoPushInUnDoForDelete(Undostruct.UiElement);
                }
                else if (Undostruct.Action == ActionType.Insert)
                {
                    Container.Children.Remove(Undostruct.UiElement);
                    this.RedoPushInUnDoForInsert(Undostruct.UiElement);
                }
                else if (Undostruct.Action == ActionType.Resize)
                {
                    if (_UndoActionsCollection.Count != 0)
                    {
                        Point previousMarginOfSelectedObject = new Point
			(((FrameworkElement)Undostruct.UiElement).Margin.Left, 
                            ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                        this.RedoPushInUnDoForResize(previousMarginOfSelectedObject, 
			Undostruct.UiElement.Width, 
                            Undostruct.UiElement.Height, Undostruct.UiElement);
                        Undostruct.UiElement.Margin = new Thickness
			(Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
                        Undostruct.UiElement.Height = Undostruct.height;
                        Undostruct.UiElement.Width = Undostruct.Width;
                    }
                }
                else if (Undostruct.Action == ActionType.Move)
                {
                    Point previousMarginOfSelectedObject = new Point
			(((FrameworkElement)Undostruct.UiElement).Margin.Left,
                        ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                    this.RedoPushInUnDoForMove(previousMarginOfSelectedObject, 
							Undostruct.UiElement);
                    Undostruct.UiElement.Margin = new Thickness
			(Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
                }
            }
        }

        public void Redo(int level)
        {
            for (int i = 1; i <= level; i++)
            {
                if (_RedoActionsCollection.Count == 0) return;

                ChangeRepresentationObject Undostruct = _RedoActionsCollection.Pop();
                if (Undostruct.Action == ActionType.Delete)
                {
                    Container.Children.Remove(Undostruct.UiElement);
                    this.PushInUnDoForDelete(Undostruct.UiElement);
                }
                else if (Undostruct.Action == ActionType.Insert)
                {
                    Container.Children.Add(Undostruct.UiElement);
                    this.PushInUnDoForInsert(Undostruct.UiElement);
                }
                else if (Undostruct.Action == ActionType.Resize)
                {
                    Point previousMarginOfSelectedObject = new Point
			(((FrameworkElement)Undostruct.UiElement).Margin.Left,
                        ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                    this.PushInUnDoForResize(previousMarginOfSelectedObject, 
						Undostruct.UiElement.Width, 
                        Undostruct.UiElement.Height, Undostruct.UiElement);
                    Undostruct.UiElement.Margin = new Thickness
			(Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
                    Undostruct.UiElement.Height = Undostruct.height;
                    Undostruct.UiElement.Width = Undostruct.Width;
                }
                else if (Undostruct.Action == ActionType.Move)
                {
                    Point previousMarginOfSelectedObject = new Point
			(((FrameworkElement)Undostruct.UiElement).Margin.Left, 
                        ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                    this.PushInUnDoForMove(previousMarginOfSelectedObject, 
							Undostruct.UiElement);
                    Undostruct.UiElement.Margin = new Thickness
				(Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
                }
            }
        }

        public void InsertObjectforUndoRedo(ChangeRepresentationObject dataobject)
        {
            _UndoActionsCollection.Push(dataobject);_RedoActionsCollection.Clear();
        }

       #endregion 

Step 6

Before performing each operation, call the InsertObjectforUndoRedo method. 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 that the application can hold will be based on the memory of system.

Change Management when using Single Object Representing Change Approach

If you want to support Undo /Redo for a new operation in single object representing change approach, you have to change a couple of things. You have to modify the object representing change, action type Enum and also change the code of undo and redo method. So its maintainability is low.

Advantages and Disadvantages when using Single Object Representing Change Approach

Its advantage is its simplicity to implement as without knowing any design pattern, you can implement undo/Redo.  

Maintainability is low. The object that represents this approach contains extra information as here a single object is used to hold all types of action data. For example, for move we should keep only move related data and for resize, we should keep only resize related data. So we are keeping redundant data.  As the number of operations increase, the redundancy increases. It is not a good object oriented design.

Sample Code

Here, a project has been attached which shows the Undo/Redo implementation Using Single Object Representing Change Approach. 

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)

Share

About the Author

Razan Paul (Raju)
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

 
GeneralAn alternate PinmemberY Sujan19-Aug-13 8:11 
QuestionUndo/Redo for Canvas area Wpf PinmemberRama Sagar Pulidindi31-May-13 6:00 
QuestionLeveraging undo-redo mechanism for implementing macro recording and plat back functionality Pinmemberpjdixit3-Apr-13 2:39 
AnswerRe: Leveraging undo-redo mechanism for implementing macro recording and plat back functionality PinmemberRazan Paul (Raju)3-Apr-13 5:10 
GeneralMy vote of 5 Pinmemberpjdixit3-Apr-13 2:34 
QuestionGreat Explanation PinmemberZiad Khoury9-Jan-12 0:53 
QuestionMy vote of 5 PinmemberFilip D'haene23-Jun-11 1:22 
GeneralMy vote of 5 Pinmemberk-fun9-Jul-10 18:10 
GeneralI would use IEditableObject and IEditableCollection implementations PinmvpSacha Barber19-Feb-09 6:32 
GeneralRe: I would use IEditableObject and IEditableCollection implementations PinmemberRazan Paul (Raju)19-Feb-09 8:17 

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 | Terms of Use | Mobile
Web04 | 2.8.1411023.1 | Last Updated 17 Feb 2009
Article Copyright 2009 by Razan Paul (Raju)
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid