Introduction
A useful application normally has a conventional feature called "undo" or "undo-redo". It is extremely handy and I can't imagine a cyber world without undo-redo. With that said, it is not easy to implement undo-redo features. Convenience for users and headache for developers usually refer to the same thing. To my knowledge, there are no .NET libraries that have enough capability to handle all undo-redo cases because this feature is very specific to every single application. The purpose of this article is to generalize undo-redo as much as possible and to suggest implementation methodology.
Background
There is a well known design pattern called Memento. It is a common way of storing and restoring application states. I am not going to talk about that in detail. For more information on Memento pattern or application states, start reading from this article on Wikipedia.
The Memento pattern specifies a state. Translating to C# in the traditional way, we get an interface with one property.
interface IMemento
{
object State
{
get;
}
}
The above interface is very generic, but it is too generic to contain essential information. It is pretty much just an object, and doesn't give any hints on what to do with it.
An interface that describes behaviors is usually more useful than those describing properties, because they are more flexible and extensible. So my solution to this is redefining the word Memento as "an object that is capable of restoring a target object to one of its previous states". Let's visualize this statement by translating it to C#.
interface IMemento<T>
{
IMemento<T> Restore(T target);
}
Note that the Restore() method returns an IMemento<T> instance; this is to facilitate redo. The return type can be changed to void if redo doesn't have to be supported.
This simple interface (with a single method) may seem useless to you at first glance. Now let's look into how it can be used to facilitate undo and redo. The following are some code snippets from class UndoRedoHistory<T>.
The Undo() method takes the Memento from the top of undo stack and calls Restore. The returned Memento is then pushed onto the redo stack.
public void Undo()
{
inUndoRedo = true;
IMemento<T> top = undoStack.Pop();
redoStack.Push(top.Restore(subject));
inUndoRedo = false;
}
The Redo() method is just the opposite of the Undo() method.
public void Redo()
{
inUndoRedo = true;
IMemento<T> top = redoStack.Pop();
undoStack.Push(top.Restore(subject));
inUndoRedo = false;
}
The Do() method registers a Memento so that it can be undone. At the same time, the redo stack is cleared.
public void Do(IMemento<T> m)
{
if (inUndoRedo)
throw new InvalidOperationException(
"Involking do within an undo/redo action.");
redoStack.Clear();
undoStack.Push(m);
}
Using the code
Now that the undo-redo history is in place, the only thing left is to write concrete implementations of IMemento<T>, which is quite application-dependent.
There are two demo projects written to demonstrate the power of this pattern. They have the same functions and user interfaces. Their difference lies in how undo and redo are implemented. I named them "action based undo-redo" and "serialization based undo-redo" respectively.
Action Based Undo-Redo
Every change made to the tracked object is stored in one memento. Mementos are capable of restoring states by carrying out the inverse action.
history.Do(new AddShapeMemento());
shapepool.Add(GetShape(e));
history.BeginCompoundDo();
foreach (Shape s in toBeRemoved)
{
int index = shapepool.IndexOf(s);
history.Do(new RemoveShapeMemento(index, s));
shapepool.RemoveAt(index);
}
history.EndCompoundDo();
The mechanism is very efficient in memory usage. However, the mementos within the undo-redo history are inter-dependent. If one is corrupted, the entire history gets corrupted. What's worse, it is very tedious to implement, as each single action corresponds to its own Memento subtype.
Serialization Based Undo-Redo
The tracking system doesn't care about what changes are made to the tracked object. Whenever there is a change, it stores the complete state of the object by serializing it.
history.Do(new ShapePoolMemento(shapepool));
shapepool.Add(GetShape(e));
history.Do(new ShapePoolMemento(shapepool));
foreach (Shape s in toBeRemoved)
{
int index = shapepool.IndexOf(s);
shapepool.RemoveAt(index);
}
As opposed to action based undo-redo, this method is very easy to implement and the mementos are independent of each other. But it has a fatal pitfall - huge memory consumption, if the tracked object is large. As a result, poor time performance will be observed.
Hybrid Undo-Redo (Conceptual)
As you see in the previous examples, the IMemento<T> interface is really flexible to extend. It is also possible, though I didn't implement a demo project, to create a hybrid undo-redo mechanism that combines the power of the two mechanisms mentioned above. The memory consumption and implementation simplicity should be balanced before making design decisions.
Although I'm trying to list as many implementation methods as possible, I won't be surprised if someone comes up and tells me that there is a totally new and much better way of implementing the IMemento<T> interface. So if you do have any good ideas on this, tell me or just leave a note in the comment area at the bottom.
Points of Interest
Compound Memento
You might have already noticed the two strange methods BeginCompoundDo() and EndCompoundDo() in the previous section. They are used to create an internal CompoundMemento<T>, so that all invocations of Do() method occuring between them are grouped together and treated as a single action.
A CompoundMemento<T> is an IMemento<T> with a wrapper Restore(). See code snippet below.
public class CompoundMemento<T> : IMemento<T>
{
private List<t>> mementos;
public IMemento<T> Restore(T target)
{
CompoundMemento<T> inverse = new CompoundMemento<T>();
for (int i = mementos.Count - 1; i >= 0; i--)
inverse.Add(mementos[i].Restore(target));
return inverse;
}
}
Controlling Capacity
The size of the undo and redo stacks are usually constrained. Unfortunately System.Collections.Generics.Stack<T> doesn't allow us to control it's capacity. So a generic class named RoundStack<T> was implemented for this purpose. It is round as the name suggests. The bottom items of the stack are trimmed off automatically when the capacity exceeds the predefined limit. Space and time overhead is almost zero. Note that no thread safety is ensured, which may be updated later.
History
- 15 March 2007 - First Draft
| You must Sign In to use this message board. |
|
|
 |
|
|
 |
|
 |
Lu, Very nice article. Did any one notice an increase in memory usage while testing this undo-redo implementation. I have noticed some. And it does not go away even by forcing GC. I am looking into it. Let me know if you have seen it too, and any possible pointers to fix this.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I don't think there are leaks. Calling the GC() method doesn't force the garbage collector to actually run the collection. try minimize your window and restore it again.
Never trust a computer you can't throw out a window.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
Hi, I'm a newbie. I tried your code but I cannot figure it out how to use it for the Nodes of a tree (ex. undo deleted nodes).
Can you give me a short code example?
Thanks
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Isn't it a disadvantage if the UndoRedoHistory refers to a certain type? In your example it is bound to the ShapePool since it is the only thing that changes. What if something different exists that should be (un-/re-)done?
Consider a (not so realistic) text that is held in a String beside the ShapePool. Modifications to the shapes and to the text (that is displayed in a TextBox at the bottom of the form) would lead to states that are recorded by the history. But the changes should be recorded chronologically so the undo history could be: 1. change text 2. add shape 3. change text 4. change text 5. remove shape
How would you do this? What would be the type that you use for the UndoRedoHistory?
Cheers, Robert
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The generic T here refers to the type of the target object whose state is to be restored. If text changes occurs to a shapepool, the change is reflected by the corresponding memento, say TextChagnedMemento<ShapePool>. The change information is held within the class, and the type in <> mains ShapePool because it is ShapePool (or its sub-elements) whose states changes.
The TextChangedMemento should be something like this:
public class TextChangedMemento<ShapePool> : IMemento<ShapePool> { private string oldText; public TextChangedMemento(string oldText) { this.oldText = oldText; } ... ... }
Never trust a computer you can't throw out a window.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
> If text changes occurs to a shapepool
But what if the text exists beside the ShapePool, not in the ShapePool? So the text is independent of the shapes. However, the UndoRedoHistory should reflect the changes that are done variously in the ShapePool and the TextBox chronologically correct. Is this possible with your solution? A TextChangedMemento wouldn't be useful, right?
Consider that the ShapePool is just a small part of the business model, so don't regard the text as an exception.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
In this case, there should be something bigger than ShapePool, to be put within <>. There are maybe cases that information to be tracked is loosely scattered the program, like your example of TextBox. In this case, undo redo will definitely be messy. A better practice is to put all the information you care within well design structures, one of which is a root level class. And this root will be used as the target object type.
Never trust a computer you can't throw out a window.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
Run the program; Draw some forms; Ctrl-Z, Ctrl-Z, Ctrl-Z; Ctrl-Y, Ctrl-Y, Ctrl-Y;
No problem.
Run the program; Draw some forms; Menu Edit/Undo; Ctrl-Y;
Ctrl-Y does not redo.
Anderson
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Thank you very much for the report. It's a good remind for the rest of reader that the demo projects are just "demo" project. They are far from perfect and are just examples of showing how this pattern can be used.
Never trust a computer you can't throw out a window.
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Hi all, Can anyone or the author can pass me same implemantation in VS 2003(.Net 1.1) I tried but faced some problems with this .. <T> How to implement this on VS 2003
Appriciate early reply....
- Regds
- Buddhi From Sri Lanka -
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I'm very sorry, but this <T> (which is called generics by the way) is only supported by .NET Framework 2.0 and above. If the reason you are using VS2003 is you don't have a copy of VS2005, you can download "Visual C# 2005 Express Edition" here: http://msdn.microsoft.com/vstudio/express/ Too bad if you have to use VS2003 for the time being, try upgrade later.
VALUE LIFE; STAY AWAY FROM CODING.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Very simple and elegant solution you have demonstrated. Nicely done.
That's why I enjoy coding, simple/elegant solutions are what makes it all worthwhile.
If only I could of found similar simple/elagant solutions when I was doing my mathematic degree lol!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
You could use the action-base technique discussed in the article. I've implemented and undo-redo for an entire variable size table in another project, so list shouldn't be a big problem.
VALUE LIFE; STAY AWAY FROM CODING.
|
| Sign In·View Thread·PermaLink | 1.00/5 |
|
|
|
 |
|
|
 |
|
 |
Nice article about Undo-Redo support. Truly it is a tedious job for the developer to implement it.
We used to use Command Pattern or Functor[Modern C++ Design:Generic Programming and Design Patterns Applied] for implementing Undo-Redo. Using Memento Pattern was always overlooked. It is a great idea, different from others[including Me]. Really impressed by the article.
Thanks a lot for sharing such a good code, ideas and especially the Hybrid idea.
How soon 'not now' becomes 'never'. Every accomplishment starts with the decision to try. Shoot for the moon. Even if you miss, you'll land among the stars.
My Website & Blog
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
 |
Thanks for the kind words. Actually, I've been working on an demo of the hybrid idea. It takes long time, not because it's complicated, but because I was trying to make it as simple as possible. I also tried to make it as generic as possible, hopefully it could be immediately applied to small scale data structure.
VALUE LIFE; STAY AWAY FROM CODING.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Many thanks for the article - I've often wondered about a good way to do undo/redo (but never actually explored the idea).
Serialization (generally) outputs a text version of the binary state of the object. Text is nice to manipulate: consider that source code control systems often use file deltas to save only the changes, not the whole text, of a source file check-in.
A hybrid Memento could easily do the same: use a good difference engine to identify the inserted / deleted / modified parts of the full text representation of the binary state of the app; and store for each new IMemento the only the (set of) difference(s).
A further improvement on memory consumption, at the expense of performance, might be to compress successive mementos. Text often compresses very well - especially when it has a number of characteristic patterns (like XML does, as most applications do with the type information about the objects that are a part of the application's state).
A further optimization might be to recognize any hierarchical structure to the data of the application, and instead of maintaining a 'flat' serialized representation of the application, use true objects to contain the "serialization" - as follows: Any unique data to an element is serialized to text, contained in a "serialization object" that corresponds to that element only; All contained elements are 'serialized' as distinct objects, that are referenced by their parent object.
For example, a Family Tree application, might implement a "Couple" node and a "Person" node; Couple nodes have start and end dates (marriage; divorce or death of one or both partners), plus other node-specific information like images of the two people, the place where the marriage/divorce/etc. was registered, the guests, etc.; Children of that Couple node are stored as (an ordered, if needed) collection of contained (references to) Person nodes. Now, any edit to the Couple node only results in a serialized value delta of that node's specific data.
How this idea differs from an action oriented Memento (which at first look it appears to resemble) is that it tracks the results of the actions, not the actions themselves.
Pure Action Idea Most (all?) apps have a finite set of actions - represented by mouse actions (drag, click, etc.) and/or menu options.
If each possible type of action is implemented by an object - (that derives from IActionable, perhaps and, required, IMemento) that exposes IActionable.Execute(object []Parameters) then the controller (in the model/view/controller pattern) is simply an engine that accepts an IActionable and an array of values (list indexes; mouse positions; mouse button information; etc.) and invokes the object's Execute method.
The controller then passes the IActionable object and its parameter array to the History mechanism which, when it needs to undo or redo the action, calls either IMemento.Undo or IMemento.Redo (this saves it from having to know about IActionable, since Redo will call Execute applying the stored parameters).
To facilitate Undo I might make the Execute method return an Object that encapsulates the "before touch points" - i.e. the before values of the affected pieces of state that the action is about to change. This would be stored with the Action object and its Execute parameters in the History mechanism.
LookSharp "Use what talents you possess; The woods would be very silent if no birds sang there except those that sang best." (William Blake)
|
| Sign In·View Thread·PermaLink | 4.00/5 |
|
|
|
 |
|
|
 |
|
|
 |
|
 |
This article makes the non-trivial seem trivial. It’s simple, clear, useful, and includes a great pair of examples. What more could you ask for. Great article!
Keith Rule
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
|
 |
|
|