Challenges in implementing the Undo Redo functionality
Here are some of the points to be noted
- Undo/Redo is an EDIT operation
Operation1 + Undo(on Operation1) = 2 Edit operations.
This will mean that the state of the object(lastModified) is changed even though there is no change in the content.
Hence any mechanism to know whether there are any changes made to the ObjectModel state must be based on the difference in the content and cannot just be based on its lastModified info.
- Maintaining the edits in a Stack(FirstInLastOut behaviour) will enforce the undo/redo implementation to be sequential in the reverse order in which the operations were performed. It cannot offer the flexibility to offer the undo/redo functionality for the edits which need not correspond to the latest edit operation.
- Each edit must implement the CanUndo() and CanRedo() to validate the undo/redo operation. This is very essential to ensure that the object model on which edit operation is performed via the undo/redo implementation does not push the underlying object model to an Invalid state.
Each edit might have a cached object instance associated with it when the edit is initialized. When trying to perform the Undo/Redo operation using the cached object, validation is must to ensure that using the cached object doesnt push the underlying object model to an Invalid State, resulting in a total disaster.
- Each edit must implement Dispose() to ensure that objects cached in the edit are disposed and they don’t eat up the memory.
In addition to all these, for a robust implementation of undo/redo here are some tips
1. Undo Redo for an edit operation on the objectModel should only be dependent on the objects in the objectModel. It should never be dependent on the objects used in the UI which are used for displaying the content of the objectModel.
2. Always update the object model before updating the UI, so that the UI displayed will always truly represents the object model.
3. The undo/redo implementation will deal with cached instance of the object model but working with caching instances of the UI objects should be avoided and instead the UI objects which represent the object model must be retreived at run time.
Implementing Undo/Redo
Here is a code snippet for implementing the Undo/Redo.
Sample Code
//interface defining the edit
public interface IUndoableEdit
{
//Perform the Undo operation
bool Undo();
//Perform the Redo operation
bool Redo();
//validates whether Undo operation can be performed on this edit //in the current context
bool CanUndo();
//validates whether Redo operation can be performed on this edit //in the current context
bool CanRedo();
void Dispose();
}
//Class providing the infrastructure for manging the IUndoableEdits
public class UndoStack
{
//stack is used to store the edits..First In Last Out
private Stack undoStack;
private Stack redoStack;
public UndoStack()
{
//initialize the stack
redoStack = new Stack(100);
undoStack = new Stack(100);
}
public void AddEdit(IUndoableEdit edit)
{
//validate input arguments
undoStack.Push(edit);
}
//Clear all the edits stored in the Stack.
public void ClearAll()
{
undoStack.Clear();
redoStack.Clear();
}
public void Undo()
{
if(undoStack.Count == 0)
{
Console.Beep();
return;
}
IUndoableEdit edit = undoStack.Pop();
if(edit.CanUndo() && edit.Undo())
{
if(edit.CanRedo() )
{
redoStack.Push(edit)
}
}
else
{
edit.Dispose();
}
}
public void Redo()
{
if(redoStack.Count == 0)
{
Console.Beep();
return;
}
IUndoableEdit edit = redoStack.Pop();
if(edit.CanRedo() && edit.Redo())
{
if(edit.CanUndo() )
{
undoStack.Push(edit)
}
}
else
{
edit.Dispose();
}
}
}
Explore the Undo/Redo - Part II
Here is the list of the types and variations of the Undo/Redo functionalty.
1.Blind Undo/Redo
The Undo/Redo functionality without specifying the operation which will be performed.
2.Instructional Undo/Redo
The menu item offering the Undo/Redo functionality also displays the operation which will performed. This makes use of the DisplayName of the edit to be performed.
3.Single Undo/Redo
The typical notepad style supporting Undo/Redo only for the last operation performed.
4.Multiple Undo/Redo
Multiple Undo/Redo supports unlimited no of edits for all the opertaions performed.
5.Multiple Undo/Redo with the UI to choose the number of edits.
Its the typical MS word style. The UI(toolBar Button) shows the DisplayName of all the edits in the Undo/Redo stack providing the option to choose the multiple edits to be performed. This provides the option of performing Undo on the last N opertaion at one shot.
More sophisticated implementation would be allow the user to choose the edit which need not correspond to the last edit operation.
For Eg:
Operation 1- Delete a line
OPeration2- Find Replace operation performed for a particular word
What if user wants to perform Undo for operation1 but NOT for operation2?
My next post will be on the Challenges that I faced in implementing Multiple (Instructional) Undo/Redo .
Explore the Undo/Redo - Part I
Imagine preparing your project report in MS Word without having to use Ctrl Z/Y. You will bulb for sure, its going to be very very painful.
That’s the power of Undo/Redo. We take it for granted in any standard, user-friendly software.
- It acts as a Saviour, during the times of distress
- It makes the learning process like a ride on a Highway.
- It allows the users to explore the software with a confidence that he can always recover easily from the mistakes, during the learning process.
Q. But why does a computer application need the Undo/Redo functionality, when the underlying piece of code will never ever go wrong it its output. You run the same operation 1000 times with the same inputs you are guaranteed to get the same output every time?
Ans. The Undo/Redo is meant for the User and not the application itself. Yes the computer program never makes a mistake but humans do. They are by default error prone.
What the designed applications treats as an invalid input for an operation might be a perfectly valid input from the users side.
The role of Undo/Redo comes into picture when an error(s) is made and the user realises it and express a strong intent to recover from the mistake(s) made.
The application must always assume that the user is always right.
Let’s solve the Recursion
The code posted below is a very simple recursive code for performing the search operation. Reading the code will force you to follow a visually cyclic path but instead visualising the search operation as a traversal along the tree from the root node to the leaf node of the last top level node follows a straight visual path.
Most of the single recursive methods can be split into group of atomic methods .
//iterate the collection (this is where the recursion is imposed)
//validate the input object against the object of interest
——————————————————————–
//Finds the first treeNode encountered in the Hunt operation, which is tagged to the //specified object.
//Returns Null, if the search operation fails to find a node tagged to the object specified Or if the specified object/nodeCollection is Null.
private TreeNode FindNode(object taggedObject, TreeNodeCollection nodes)
{
//validate inputs
if (taggedObject == null || nodes == null)
{ return null; }
TreeNode nodeToReturn = null;
foreach (TreeNode node in nodes)
{
if (Equals(node.Tag, taggedObject))
{
return node;
}
//Now search in children of this node.
nodeToReturn = FindNode(taggedObject, node.Nodes);
if (nodeToReturn != null)
{
return nodeToReturn;
}
}
//no matching node found. Hence return null!!
return null;
}
How do you dish out the current obect in the context?
The environment may dish out all the possible operations that could be performed on the current object in the context to the user from different sources. It could be from the right click context menu on the current object or the cute icon in the toolbar or the menuItem in the global menu bar or a tree layout in the side bar or an icon in the tray.
The same set of operations may be offered by multiple sources. Then
Q.How can we manage to keep all of them in sync?
>The context specific menu items must be populated dynamically. They must never be cached with the current object
>All the menuItems/icons must be tagged to their associated actions , i.e, if a certain operation is disabled for the given context then all the UI components offering that particular operation must be disabled or should not be populated at all.
To achieve this, possibly you must have a registration mechanism of all the operations against the UI components offering them.
>Populate the ContextMenu with the selection oriented commands
>Populate the ContextMenu with a fixed set of comands for each selection type and then enable/disable them to reflect the selection state.
This will help the user to see an expected and same set of consistent context menu and will make navigation easier.
>The context specific menu must be consistent across all the object displayed in the editor.
>If the same object appears in more than one editor, then same contextMenu must be available.
>Classify and group the menuItems using menu separators.
>Provide the common operations on the editor itself, which are not selection specific rather than on each of the object. For eg: Reload, SelectAll, Back, Undo/Redo, Find, Zoom
Q. How can the editor cope up when the menuItems, which might possibly be contributed by a add-ons and plugins?
>There must be a registration mechanism to allow the menuItems dispayed in the editor to be registered with the HostControl.
>Implement a command filter for each object type in the editor.
Some dumb questions to ask…
>Whats the best way to provide response to the functionality from the editor but inturn being offered by different objects?
>If the functionality of an operation is provided by a specific object in the editor, then that particular functionality must be exposed as public method inorder to allow the editor to provide response to the menuItemClick.
>If for the sake of consistency, all the context specific menuItems are being populated by the editor, then the editor must be able to exactly locate and identify the current Object and must enable/disable menuItems accordingly but how to do it cleanly?
>If the effect of an operation being performed by using object1 or object2 is same, then why have functionality available from both objects?
Writing about it so far has helped me in clearing some blockages and probably would be in a better state to comment more on it once I solve it completely.
Coding vs Programming
Coding is a mere typing exercise to express the logical sequence of steps in the form of a well indented paragraph. It is specific to syntax and the language.
Programming is problem solving exercise, which forces you think continuously. Its a two step process.
First and the most crucial step is to “Define the Problem Statement”. This is the most important and difficult step as all the use cases which needs to be satisfied must be clearly identified. The accuracy of the solution will depend on this step. This doesn’t depend on how you express. It could be a flow chart or diagrams depicting the problem statement.
Second step is to provide a logical sequence of the steps. The elegance, re-usability and scalability of the solution will depend on this step, as domain/technology specific knowledge is essential.
After some experience in using the msdn, I am confident that any software technology can be easily learnt by spending some time with a well documented API.
Hence a solid first step, is essential for a rock solid solution.
Count your LOC!!
How many Lines of Code(LOC), have you produced?
For those wondering, how many lines of code have they generated, here is a sample code in C#.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace LOC
{
public class Program
{
private const string BUILDER =@”X:\DesignTime\Builder”;
static void Main(string[] args)
{
if (args.Length == 0)
{
PrintUsageAndExit();
}
string path = args[0];
if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
{
PrintUsageAndExit();
}
DirectoryInfo parent = new DirectoryInfo(path);
int lines = CountLines(parent);
Console.WriteLine(”The Total Number of Lines is :” + lines);
}
private static void PrintUsageAndExit()
{
Console.WriteLine(”Expecting a Directory with CS Files”);
Environment.Exit(1);
}
private static int CountLines(DirectoryInfo dir)
{
int lines = 0;
foreach (FileInfo f in dir.GetFiles())
{
if (f.Name.EndsWith(”.cs”))
{
lines += CountLines(f);
}
}
DirectoryInfo[] children = dir.GetDirectories();
foreach (DirectoryInfo d in children)
{
lines += CountLines(d);
}
return lines;
}
private static int CountLines(FileInfo file)
{
StreamReader reader = new StreamReader(file.FullName);
int lines = 0;
while (!reader.EndOfStream)
{
string line = reader.ReadLine().Trim();
if (string.IsNullOrEmpty(line) ||
line.StartsWith(”//”))
{
continue;
}
lines++;
}
reader.Close();
return lines;
}
}
}
Comments(0)