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:
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.
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
.
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
.
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.
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.
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