Click here to Skip to main content
Click here to Skip to main content

A Simple, Action Based, Undo/Redo Framework

By , 16 Feb 2013
 
Demo Application Snapshot

Introduction

This article presents a simple way to add undo/redo support (http://en.wikipedia.org/wiki/Undo) to your applications. The framework is able to handle various scenarios. However, I am not claiming that it is able to handle all possible scenarios, nor that it is the smartest framework.

Basic C++ & STL understanding is required. To understand the demo application, basic MFC understanding is also required.

I have used Microsoft Visual C++ and Windows XP Professional. The framework can easily be ported to other operating systems.

How to

The framework is based on a simple concept: action (command) objects. An action, or command object encapsulates a user request. Actions have numerous advantages over ad-hoc handling of user requests (http://en.wikipedia.org/wiki/Command_pattern).

In the Box

The framework is enclosed in kis namespace (http://en.wikipedia.org/wiki/KISS_Principle) and contains the following:

  1. An action interface, C_Action
  2. An action executor interface, C_ActionExecutor
  3. A default action executor creator, CreateActionExecutor_Default
  4. A smart pointer template, C_SharedPtr (http://en.wikipedia.org/wiki/Smart_pointer)

Using the Framework

To use the framework, you must follow three steps:

  1. Set up the framework.
  2. Write the actions.
  3. Use the actions.

Setting up the Framework

  1. Unpack the KisWinBin.zip file. The default path I am using is d:\. This is also the path used by the demo application.
  2. Prepare your project:
    1. Add the kis path to your project.
    2. Add the kis.lib library to your project.
    3. Add the kis.dll library to your project output directory.
    4. Add the #include “kis.h” directive to your STDAFX.H file.
  3. Add an action executor member to your document class. All actions are executed through an action executor. A default executor is provided. You may write your own executors in a way that takes advantage of specific cases.
    class C_Document
    {
    protected:
      kis::SP_ActionExecutor m_spActionExecutor;
      
    public:
      C_Document( /*...*/ )
      :
      m_spActionExecutor( kis::CreateActionExecutor_Default() )
      {
        // ...
      }
      // ...
    };

Writing Actions

  1. An action class is a class derived from C_Action. C_Action is fully documented in the header file (kis_action.h) so you should be able to write actions with no help.
  2. Ideally, you should write action classes for every user request in your application; it does not matter whether the request should or should not be undo-able.
  3. Use exceptions to let the client know something went wrong.
  4. Make sure the execution is atomic in the sense that it never leaves the target in an invalid state.
  5. Use action creators to create action objects; do not expose the derived action class.
    //
    // interface file
    //
    SP_Action CreateAction_Clear ( C_Document* a_pTarget );
    
    //
    // implementation file
    //
    namespace
    {
    class C_Action_Clear : public C_Action
    {
    public:
      C_Action_Clear ( C_Document* a_pTarget )
    // ...
    };
    } // namespace
    
    SP_Action CreateAction_Clear ( C_Document* a_pTarget )
    {
      return SP_Action( new C_Action_Clear( a_pTarget ) );
    }

Using Actions

Actions cannot be directly executed. An action object protects most of its methods. To execute an action, you need an action executor object.

  1. Executing an action:
    kis::SP_Action spAction = CreateAction_Clear( this );
    // ...
    try
    {
      m_spActionExecutor->Execute( spAction );
    }
    catch ( /*exceptions threw by your action*/ )
    {
      // ...
    }
  2. Un-executing the last executed actions:
    // ...
    m_spActionExecutor->Unexecute( 1 ); // un-executes last executed action
    // ...
  3. Re-executing the last un-executed actions:
    // ...
    m_spActionExecutor->Reexecute( 2 ); // re-executes last two executed actions
    // ...

Questions?

How do I change history size?

The history size is managed by two methods: C_ActionExecutor::GetMaxBytesCount and C_ActionExecutor::SetMaxBytesCount. Rate of consumption of history space depends on how large the action objects are. The size of an action object is given by C_Action::GetBytesCount.

How large is an action object?

It depends. The method C_Action::GetBytesCount responds to C_ActionExecutor’s question “How much memory did you consume?”. Your derived C_Action object should never answer “zero”, because the size of your action is sizeof(C_Action), at least. The more accurate the answer is, the better the estimation of history usage is.

/*override*/ unsigned int C_DemoAction_InMemoryClearDrawing::GetBytesCount() const
{
  return sizeof( *this ) + m_pClearedDrawing->GetBytesCount();
}

Keeping the history memory usage low may be accomplished by using temporary files. In this case, the number of allocated bytes is decreased by the size of streamed object.

/*override*/ unsigned int C_DemoAction_ClearDrawing::GetBytesCount() const
{
  return sizeof( *this ); // size of cleared drawing not added
}

/*override*/ bool C_DemoAction_ClearDrawing::Execute()
{
  m_pDoc->m_Drawing.Write( m_Stream );
  m_pDoc->m_Drawing.Delete();
  return true;
}

How do I disable/enable the history memory?

If the size of history memory is set to zero, then the history is disabled. Setting the size to a value greater than zero enables it.

How do I list the history?

You may iterate through history using C_ActionExecutor’s methods GetUnexecuteCount, GetReexecuteCount, GetUnexecuteName, GetReexecuteName.

Why can’t I directly use the action object?

The short answer is the framework is not supposed to work that way. [There is a joke (real story?) about an IQ test run on a group of cops. The test consisted of a board with oddly shaped holes and corresponding pegs which should be fitted in the holes. The result of the test was 1% of the cops were very smart and 99%... very strong.]

Let us say Execute, Unexecute and Reexecute methods are public. Some clients would be tempted to write code like this:

void C_Document::OnClear_WithPublicActionExecute()
{
  SP_Action spAction = CreateAction_Clear();
  spAction->Execute();
}

Seems fine until you realize the client forgot to save the executed action into history. Did he forget? (This would be really embarrassing if the product was already delivered.) Did he really intend to locally execute the action? It is hard to tell.

Hiding action’s Execute, Unexecute and Reexecute methods makes the framework less error prone. Let us say that you know nothing about the framework and you want to add a Clear command handler in an already existing project. First you will try to directly call the Execute method. You will soon realize Execute is protected. "Why? How can I execute the action?" You discover that you need an action executor. "Why do I need it?... Aaa, undo/redo!"

It is true it may be necessary to execute some actions with no need to undo them. This is a rare case, in my opinion, and it may be easily solved using a local action executor. Using a local action executor clearly states developer’s intention to locally use the action.

How can I locally execute one or more actions?

Since the execute method is not public, you cannot directly execute an action. In order to execute it, you first need to create a local action executor and then call the executor’s execute method.

void C_Document::LocalActionExample()
{
  // the following line will generate an error: cannot access protected member
  // CreateAction_Clear( this )->Execute(); 

  kis::SP_ActionExecutor spActionExecutor = kis::CreateActionExecutor_Default();
  
  try
  {
    spActionExecutor->Execute( CreateAction_Clear( this ) );
    spActionExecutor->Execute( CreateAction_Insert( this, "abc" ) );
  }
  catch ( ... )
  {
    spActionExecutor->Unexecute( spActionExecutor->GetUnexecuteCount() ); // rollback
  }
}

The Demo Application

The demo is a simple drawing application. It implements actions for following user requests:

  • Add a random segment. This action shows how to create an action that does not require parameters.
  • Change history size. This action shows how to: create a non undo-able action, acquire parameters using a dialog box, and change the history size.
  • Add a segment/rectangle/ellipse. These actions show how to acquire parameters using the mouse and how to add a new graphic object to the drawing.
  • Clear the whole drawing. This action shows how to keep history memory usage low.

Although it is possible to list the whole history (the same way applications like Microsoft Word do), the undo/redo user interface elements list only the most recent action.

History

  • May 16, 2006: Article first published
  • June 14, 2006: "Questions?" section changed
  • February 03, 2009: Minor bug fixed
  • February 16, 2013: Typo 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

zdf

Romania Romania
Just a humble programmer.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5membermanoj kumar choubey9-May-13 2:43 
Nice
QuestionBuild errormvpMichael Haephrati מיכאל האפרתי20-Feb-13 7:54 
While trying to build your code (after converting to VS2010) I get these messages:
 
1>------ Rebuild All started: Project: kis, Configuration: Debug Win32 ------
1>Error: The device is not ready.
1>
2>------ Rebuild All started: Project: KisActionDemo, Configuration: Debug Win32 ------
2>Error: The device is not ready.
2>
========== Rebuild All: 0 succeeded, 2 failed, 0 skipped ==========

AnswerRe: Build errormemberzdf21-Feb-13 0:12 
Some of project paths are not relative to project directory. Please change them (search for "d:"). I will fix this the next time.
Regards,
ZDF Smile | :)

What is good is twice as good if it's simple.

QuestionbinariesmvpMichael Haephrati מיכאל האפרתי20-Feb-13 7:52 
It would be great if you could place a test executable as part of the binaries zip.
AnswerRe: binariesmemberzdf21-Feb-13 0:08 
I will keep this in mind for the next update. A simple command line application, maybe.
Regards,
ZDF Smile | :)

What is good is twice as good if it's simple.

GeneralMy vote of 5mvpMichael Haephrati מיכאל האפרתי20-Feb-13 7:51 
Excellent idea
GeneralRe: My vote of 5memberzdf21-Feb-13 0:06 
Thank you!
Regards,
ZDF Smile | :)

What is good is twice as good if it's simple.

Generalit is Very goodmemberntanta12-Feb-09 19:29 
it is Very good
GeneralRe: it is Very goodmemberzdf21-Feb-13 0:06 
Thank you!
Regards,
ZDF Smile | :)

What is good is twice as good if it's simple.

GeneralLooks well-designed. I'm intrigued to know how C_ActionExecutor has access to C_Action's protected methods though.memberwtwhite9-Feb-09 17:03 
I can't see any friend C_ActionExecutor; declaration inside C_Action. Nor is C_ActionExecutor a derived class of C_Action (which would certainly be strange, but would be another way to get things done). So how on earth does C_ActionExecutor_Default::Execute() call the protected method C_Action::Execute()? I can't even see how this compiles! Please enlighten me!
 
TIA,
WTJW
AnswerRe: Looks well-designed. I'm intrigued to know how C_ActionExecutor has access to C_Action's protected methods though.memberzdf10-Feb-09 0:05 
Check line 21 and 22 in kis_Action.cpp.
 
Thank you,
ZDF Smile | :)

What is good is twice as good if it's simple.

GeneralRe: Looks well-designed. I'm intrigued to know how C_ActionExecutor has access to C_Action's protected methods though.memberwtwhite10-Feb-09 0:40 
Ouch!
 
For those of you without the source code handy, those lines are:
 
#define protected public
#include "kis.h"
Maybe I'm nitpicking, but your code now violates the One Definition Rule and may produce undefined behaviour -- technically, the compiler is allowed to produce a different memory layout for the two versions of C_Action (although in this case I can't think why it would in practice).
 
Could you please change this to just use friend instead? That's what it's for! I know it's a bit of a pain because friendship isn't transitive, so you'd need to reorganise things a little bit. Here is one way to do it:
 
1. Introduce a new class, C_ActionProxy. This is just a simple class that holds a pointer to a C_Action object. It contains only a private single-argument constructor and public methods that forward to each of the protected methods in C_Action. Grant friendship to C_ActionExecutor from this class.
2. Grant friendship to C_ActionProxy from C_Action.
3. Add a non-virtual protected method proxy() to C_ActionExecutor that takes a single pointer to C_Action and returns a C_ActionProxy object initialised with that pointer. This method can be inline. In fact it can even be static.
4. In the code for C_ActionExecutor_Default, replace all calls to methods of C_Action (e.g. spAction->Execute() with calls to the forwarding functions in C_ActionProxy (e.g. proxy(spAction)->Execute()).
 
This design maintains the desirable property that the only way to call the protected methods of C_Action is to derive a class from C_ActionExecutor.
 
What do you think?
AnswerRe: Looks well-designed. I'm intrigued to know how C_ActionExecutor has access to C_Action's protected methods though.memberzdf10-Feb-09 2:35 
I do not see any problem with the access specifier trick (I admit this is not orthodox coding, though). I do not have the entirely text regarding the ODR but I am pretty sure the declaration trick does not violates the ODR. I doubt the protected/public specifier changes the class signature.
 
Adding additional classes will make the framework more complicated, which I was hardly trying to avoid while adding as much functionality as possible. Please note that C_Action is also used by C_StackOfSPAction.
 
Maybe there is a cleaner, smarter solution but I do not see it right now. Please feel free to change the code the way you like it. Please let me know if you reach a more acceptable solution.
 
Thank you for reading my code,
ZDF Smile | :)

What is good is twice as good if it's simple.

GeneralVerifying undo/redo consistencymembersupercat93-Feb-09 5:49 
I work mostly in VB.net, so I'm not sure how I'd adapt something for C++, but I am interested in how to handle a good undo/redo framework. One of my big concerns in implementing such a thing is ensuring that undo/redo functions actually behave sensibly. I have a couple programs I've bought which I 'almost' really like, but the "undo" doesn't consistently return to the state before the last action and the "redo" doesn't consistently return to the state before the "undo". The only sensible way to use the application is to consistently save different versions of the document, so that if I find something got 'oopsed' I can hopefully go back and see what happened and then by hand fix the necessary parts of the new document to match what they should have been (as shown in the old one).
 
What advice do you have about how best to avoid that sort of trouble?
AnswerRe: Verifying undo/redo consistencymemberzdf3-Feb-09 10:24 
The wrong behavior of the applications you are referring may be for a number of reasons; most of them derived form ad-hoc handling of undo/redo. The most likely one is someone forgot to push the document’s state into the undo/redo stack. From the user point of view”undo” will step back two steps instead of one, and redo will never redo both steps. I have tried to protect the user against this kind of mistakes by hiding action’s execute method (see the “Questions?” section): the action is executed through the action-executor (the undo/redo stack). The executor knows which methods to call and in which order.
 
I do not know which solution is the best. I can only recommend you to try my solution and see if it suits you.
 
Kind regards,
ZDF Smile | :)

What is good is twice as good if it's simple.

GeneralComment For reportmember12324r32353214-Jun-06 18:43 
Comment For report
GeneralGoodmember12324r32353214-Jun-06 18:30 
Good
 
Hiu
GeneralProject defaults [modified]memberTodd Smith14-Jun-06 15:08 
Your project defaults have a few hardcoded values that make it difficult to download and build without first reconfiguring.
 
just an fyi
 
I was also able to get the undo to stop working. I drew about 7 lines, a rectangle, an ellipse, then hit clear. After a few undo's (undo clear, undo elliplse, no more undo) it stopped working. It looks like there's an undo limitation? Draw lots and lots of line and start doing undo until it says NO MORE yet there's still lots of lines left.
 
UPDATE: ah found the undo buffer setting under Options. nvm
 
Todd Smith
 
-- modified at 21:18 Wednesday 14th June, 2006
Generalshared ptrmemberjhwurmbach24-May-06 5:57 
Great work - so far as I could see in just a short time.
But why are you using a "gome grown" smart ptr.
I know how std::auto_ptr is not usable for the case, but why are you refusing to use the shared ptr[^] from boost.org[^]?
 
Personally, I would trust this code more than any code I find elsewhere. After all, there is a rigorous perr-review scheme in place.
 
But nonetheless, your article seems to be great.

 

"We trained hard, but it seemed that every time we were beginning to form up into teams we would be reorganised. I was to learn later in life that we tend to meet any new situation by reorganising: and a wonderful method it can be for creating the illusion of progress, while producing confusion, inefficiency and demoralisation."
 
-- Caius Petronius, Roman Consul, 66 A.D.

GeneralRe: shared ptrmemberzdf24-May-06 8:23 
The main reason I did not use Boost is that I have always hated the “Battery not included” small print. Smile | :)
 
You can easily change the source code so as to use an alternate smart pointer implementation.

 
Thank you,
 
zdf
GeneralCommand PatternmemberJohn Saunders23-May-06 14:34 
Although you provide a link to the Command Pattern on wikipedia, the text doesn't mention the Command Pattern at all. You should state if you used the Command Pattern as the basis for your code or not. That allows an experienced software developer to immediately get a conceptual understanding of your design, making the details much easier to grasp.

GeneralRe: Command Pattern [modified]memberzdf23-May-06 23:12 
I think you are right: this pattern is usually known as command pattern, not action pattern. I was trying to avoid the command word because of the possible confusion with MFC terms like “Command Routing”, “Command Targets”, etc.
 
Thank you,
 
zdf

 

GeneralFeedbackmemberzdf18-May-06 7:00 
I got bad rating but no feedback explaining why. It would be nice to have some feedback. I will probably soon remove the article.
 
Regards,
 
zdf
GeneralRe: Feedbackmemberanti c18-May-06 8:24 
I like it. 5 stars from me. Best undo/redo I found on CP and seems to be portable too. Give them some time maybe they will have the curiosity to have a closer and deeper look.
 
Happy coding,
 
Anti
GeneralRe: Feedbackmemberzdf18-May-06 23:36 
Thank you!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130617.1 | Last Updated 16 Feb 2013
Article Copyright 2006 by zdf
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid