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:
- An action interface,
C_Action
- An action executor interface,
C_ActionExecutor
- A default action executor creator,
CreateActionExecutor_Default
- 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:
- Set up the framework.
- Write the actions.
- Use the actions.
Setting up the Framework
- Unpack the KisWinBin.zip file. The default path I am using is d:\. This is also the path used by the demo application.
- Prepare your project:
- Add the
kis
path to your project. - Add the kis.lib library to your project.
- Add the kis.dll library to your project output directory.
- Add the
#include “kis.h”
directive to your STDAFX.H file.
- 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
- 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. - 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.
- Use exceptions to let the client know something went wrong.
- Make sure the execution is atomic in the sense that it never leaves the target in an invalid state.
- Use action creators to create action objects; do not expose the derived action class.
SP_Action CreateAction_Clear ( C_Document* a_pTarget );
namespace
{
class C_Action_Clear : public C_Action
{
public:
C_Action_Clear ( C_Document* a_pTarget )
};
}
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.
- Executing an action:
kis::SP_Action spAction = CreateAction_Clear( this );
try
{
m_spActionExecutor->Execute( spAction );
}
catch ( )
{
}
- Un-executing the last executed actions:
m_spActionExecutor->Unexecute( 1 );
- Re-executing the last un-executed actions:
m_spActionExecutor->Reexecute( 2 );
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.
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.
unsigned int C_DemoAction_ClearDrawing::GetBytesCount() const
{
return sizeof( *this ); }
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()
{
kis::SP_ActionExecutor spActionExecutor = kis::CreateActionExecutor_Default();
try
{
spActionExecutor->Execute( CreateAction_Clear( this ) );
spActionExecutor->Execute( CreateAction_Insert( this, "abc" ) );
}
catch ( ... )
{
spActionExecutor->Unexecute( spActionExecutor->GetUnexecuteCount() ); }
}
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