共用方式為


Samples for the Undo Framework

I just added some samples for the Undo Framework. You can find the samples in the source code or download them from the project website.

MinimalSample

First is a simple Console App sample called MinimalSample. Here’s the full source code:

 using System;
using GuiLabs.Undo;

namespace MinimalSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Original color");

            SetConsoleColor(ConsoleColor.Green);
            Console.WriteLine("New color");

            actionManager.Undo();
            Console.WriteLine("Old color again");

            using (Transaction.Create(actionManager))
            {
                SetConsoleColor(ConsoleColor.Red); // you never see Red
                Console.WriteLine("Still didn't change to Red because of lazy evaluation");
                SetConsoleColor(ConsoleColor.Blue);
            }
            Console.WriteLine("Changed two colors at once");

            actionManager.Undo();
            Console.WriteLine("Back to original");

            actionManager.Redo();
            Console.WriteLine("Blue again");
            Console.ReadKey();
        }

        static void SetConsoleColor(ConsoleColor color)
        {
            SetConsoleColorAction action = new SetConsoleColorAction(color);
            actionManager.RecordAction(action);
        }

        static ActionManager actionManager = new ActionManager();
    }

    class SetConsoleColorAction : AbstractAction
    {
        public SetConsoleColorAction(ConsoleColor newColor)
        {
            color = newColor;
        }

        ConsoleColor color;
        ConsoleColor oldColor;

        protected override void ExecuteCore()
        {
            oldColor = Console.ForegroundColor;
            Console.ForegroundColor = color;
        }

        protected override void UnExecuteCore()
        {
            Console.ForegroundColor = oldColor;
        }
    }
}

Here we define a new action called SetConsoleColorAction, and override two abstract methods: ExecuteCore() and UnExecuteCore(). In the ExecuteCore(), we change the color to the new color and backup the old color. In UnExecuteCore() we rollback to the backed up old color. We pass the action all the context information it needs (in our case, the desired new color). We rely on the action to backup the old color and store it internally.

The philosophy is to only store the smallest diff as possible. Try to avoid copying the entire world if you can only save the minimal delta between states.

Next, pay attention to the SetConsoleColor method. It wraps creating the action and calling RecordAction on it. It helps to create an API that abstracts away the action instantiation so that it is transparent for your callers. You don’t want your callers to create actions themselves every time, you just want them to call a simple intuitive API. Also, if for whatever reason you’d like to change or remove the Undo handling in the future, you’re free to do so without breaking the clients.

Finally, the source code in Main shows how you can intersperse your API calls with calls to Undo and Redo. It also shows using a transaction to group a set of actions into a single “multi-action” (Composite design pattern). You can call your API within the using statement, but the actions’ execution is delayed until you commit the transaction (at the end of the using block). That’s why you don’t see the console color changing to red in the middle of the block. If you undo a transaction, it will undo all the little actions inside it in reverse order.

WinFormsSample

The second sample is called WinFormsSample. It shows a windows form that let’s you edit properties of a business object:

image

You can change the text of both name and age, and the values will be mapped to the business object. You can also “Set Both Properties” which illustrates transactions. Then you can click Undo and it will rollback the state of your object to the previous state. The UI will update accordingly.

There is a trick in the code to avoid infinite recursion: on textbox text change, update the business object, fire an event, update the textboxes, update the business object again, etc... We use a boolean flag called “reentrancyGuard” that only enables the TextChanged events if the textbox modification was made by user, and not programmatically. If we updated the textboxes as a results of the business object change, no need to update the business object.

Note: If this was WPF, I would just use two-way data binding, but I wanted to keep the sample as simple as possible and use only basic concepts.

Action merging

Another thing worth mentioning that this sample demonstrates is action merging. As you type in the name in the textbox ‘J’, ‘o’, ‘e’, you don’t want three separate actions to be recorded, so that you don’t have to click undo three times. To enable this, an action can determine if it wants to be merged with the next incoming action. If the next incoming action is similar in type to the last action recorded in the buffer, they merge into a single action that has the original state of the first action and the final state of the new action. This feature is very useful for recording continuous user input such as mouse dragging, typing and other events incoming at a high rate that you want to record as just one change.

We update the visual state of the Undo and Redo buttons (enabled or disabled) to determine if the actionManager can Undo() or Redo() at the moment. We call the CanRedo() and CanUndo() APIs for this.

Hopefully this has been helpful and do let me know if you have any questions.

Comments

  • Anonymous
    July 02, 2009
    Great code Kirill! Easy to understand. :) Leniel Macaferi http://leniel.net

  • Anonymous
    July 03, 2009
    This is nice. Is there an AbstractAction<T>, so we could use a more generic action?

  • Anonymous
    July 06, 2009
    Leniel: Thanks! Bruce: I don't have a generic AbstractAction<T>. What's your usecase? It's simple to add one. Thanks, Kirill

  • Anonymous
    July 09, 2009
    Uh... This isn't more than the command pattern in a processed form.

  • Anonymous
    July 09, 2009
    Yup, that's what I wrote here: http://blogs.msdn.com/kirillosenkov/archive/2009/06/29/new-codeplex-project-a-simple-undo-redo-framework.aspx I should have linked to the original post.

  • Anonymous
    July 15, 2009
    Funny - I wrote something very similar about 3 wks ago... UndoObject<T> (I didn't write the merge piece... not needed for what I was doing) One big difference is I use 2 Stack<T> Objects, and a Current Value property.  Current Value returns UndoStack.peek();  The CanUndo and CanRedo are evaluations of the Count property of each stack.  Obviously when you undo you pop the undo stack and push the result to the redo stack.

  • Anonymous
    January 25, 2011
    where is the code for the winform sample?

  • Anonymous
    January 26, 2011
    It's all on http://undo.codeplex.com

  • Anonymous
    August 15, 2011
    I am trying to understand your work. I have to say its relly nice stuff. i am wundering what is what i would need to ake it work for a canvas.

  • Anonymous
    August 15, 2011
    Dan, I have a sample of it working with Canvas - see the source code at livegeometry.codeplex.com, and also http://layout.codeplex.com.

  • Anonymous
    August 15, 2011
    thanks Kirill. I would like to know if i could use part of your work for my Tesys. I hope that is fine for you. I have to say the livegeometry is very interesting.

  • Anonymous
    August 15, 2011
    Dan, please go ahead. It's free and open source. Send me a link when you're done, I'd be curious what you're building.

  • Anonymous
    August 16, 2011
    Hi, my work is a tool for crating mesurment chains. I am drawing evriting on a Canvas conteiner. I am loading the sybolic from a .png file. I just have huge Problems about the undo/redo funktionality. i just dont understand how to conect it. That is why i want to try to use and analize your code so i may find out how can i do it. the Consept is not so dificult, the part of begining its the hard part what i dont understand. Thans Kirill