Catnap Games

Simple undo/redo system for C#

Posted by Tom, 2009-03-19 Permalink Latest posts List of all posts

So you want to add an undo/redo system to your program? OK.

First, we need to separate the GUI from the code that actually does stuff. This is sometimes called "a command design pattern". Each command should know how to "do" stuff and how to "undo" it. It may be as simple as this:

public interface Command
{
void Do();
void Undo();
}

Second, we need a class that will keep track of what commands have been applied and which ones haven't. Let's call this class History. It knows where we are and what to do (or undo) next. Again, pretty simple. So let's add some more functionality while we're at it. Our History class will notify us when a command is executed and will know if there are any unsaved changes. Here goes:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


public class History {

private List commands = new List();
private int lastExecuted = -1;
private int lastSaved = -1;

public delegate void Changed(bool haveUnsavedChanges);
public event Changed OnChanged = (h) => { };

public void Clear() {
commands.Clear();
lastExecuted = -1;
lastSaved = -1;

OnChanged(false);
}


public void Save() {
lastSaved = lastExecuted;

OnChanged(false);
}


public bool Modified {
get { return lastSaved != lastExecuted; }
}


public int Size
{
get { return commands.Count; }
}


public int LastExecuted
{
get { return lastExecuted; }
}


public void Limit(int numCommands) {
while (commands.Count > numCommands) {
commands.RemoveAt(0);
if (lastExecuted >= 0) {
lastExecuted--;
}
if (lastSaved >= 0) {
lastSaved--;
}
}
}


public void Add(Command command, bool execute) {
if (lastExecuted + 1 < commands.Count) {
int numCommandsToRemove = commands.Count
- (lastExecuted + 1);
for (int i = 0; i < numCommandsToRemove; i++) {
commands.RemoveAt(lastExecuted + 1);
}
lastSaved = -1;
}
if (execute) {
command.Do();
}
commands.Add(command);
lastExecuted = commands.Count - 1;

OnChanged(true);
}


public void Undo() {
if (lastExecuted >= 0) {
if (commands.Count > 0) {
commands[lastExecuted].Undo();
lastExecuted--;
OnChanged(lastExecuted != lastSaved);
}
}
}


public void Redo() {
if (lastExecuted + 1 < commands.Count) {
commands[lastExecuted + 1].Do();
lastExecuted++;
OnChanged(lastExecuted != lastSaved);
}
}

}


You can drop these two classes into your project and start using them right away.

Or try this simple demo showing how to implement some specific commands.

Precompiled: www.catnapgames.com/downlo…
VS2008 project with source code: www.catnapgames.com/downlo…

You may run into a situation where you think you need to undo/redo multiple commands at once. For example, you have an editor that allows the user to select and change multiple objects simultaneously. There is a simple solution for this - implement a single additional Command - one that keeps a nested list of other commands and calls their Do() and Undo() methods all at once. You may want to reverse the call order for the Undo() to keep out of trouble.

Also, if you're wondering what that "execute" flag is good for in the History.Add() method, imagine an editor that allows moving objects by dragging them. You probably don't want to have a separate Move command for every tiny mouse move, do you? What you really want is a single Command that will put the object back into the place where it was when the user pressed the mouse button and started dragging it, right? Well, just remember the original coordinates and keep updating the object yourself with each mouse move. When the mouse button is released, create a Move command, give it the original and the new coordinates and add it to the history without executing it.

Comments

So why exactly did you not use a Stack&lt;T&gt; for this? That would have removed about 60% of your code.
Jonathan Holland 2009-03-19 20:00
It wouldn't.
Tom 2009-03-19 20:31
What a fruitful discussion
Troll 2009-03-20 11:26
Oh, I'm sure Jonathan can work it out himself. Here's an explanation for Mr. Troll.

The undo/redo mechanism doesn't work like a stack. You cannot pop an item once you undo it, because there would be nothing left to redo.

So you need to keep it and at the same time remember what you've already undone to be able to remove all undone items in the list when you add a new action in the middle. Hence, a list and an additional index.
Tom 2009-03-21 15:47
Agree with Jonathan Holland. Usually two stacks are created. One for undo and one for redo. While performing undo/redo operations you just do pushing/popping between them. I would definitely go reducing 60% of code...
Denis Vuyka 2009-04-03 17:15
You can take a look at my old article related to Undo/Redo stacks if you are interested:
http://dvuyka.spaces.live.com/blog/cns!305B02907E9BE19A!231.entry
Denis Vuyka 2009-04-03 17:18
Oh I see, that's pretty neat. Thanks!
Tom 2009-04-05 18:53
I've thought about this a bit but I don't see a clean way to keep track of the last save point that indicates that the object being edited has been modified (i.e. the current state is not saved).

It works so that if, for instance, after making a few actions, you call Save(), then do a Redo() and an Undo() the History will indicate &quot;not modified since last Save()&quot;. Which I think is the correct behaviour but it requires more code than just setting a flag when a command is added or when Undo() or Redo() are invoked.

So while I do admit that a two Stack&lt;Command&gt; solution might be more concise in itself, this additional feature would complicate quite a bit as well.
Tom 2009-04-07 18:24
will thi undo redo in the database. Cause for my project i have a database with a whole bunch of tables. Example: if i add a course to the database, then when i undo will it remove it from the database and the redo it back into the database

Please let me know soon. Anyone knows?
Mac 2009-12-01 14:19

Your comment

captcha