Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C++

A Simple, Action Based, Undo/Redo Framework

Rate me:
Please Sign up or sign in to vote.
4.51/5 (38 votes)
16 Feb 2013CPOL5 min read 96.9K   1.9K   104   31
How to use a simple, action based, undo/redo framework
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.
    C++
    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.
    C++
    //
    // 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:
    C++
    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:
    C++
    // ...
    m_spActionExecutor->Unexecute( 1 ); // un-executes last executed action
    // ...
  3. Re-executing the last un-executed actions:
    C++
    // ...
    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.

C++
/*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.

C++
/*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:

C++
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.

C++
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)


Written By
zdf
Romania Romania
Just a humble programmer.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Michael Haephrati20-Feb-13 7:51
professionalMichael Haephrati20-Feb-13 7:51 
GeneralRe: My vote of 5 Pin
zdf21-Feb-13 0:06
zdf21-Feb-13 0:06 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.