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