INSTRUCTIONS
These are the steps used in the project file to add multi-level undo/redo capability to the Scribble application.
Copy Files
Edit Resources
CStroke Changes
CScribbleDoc Changes
CScribbleView Changes
DESCRIPTION
There are many ways you could implement multi-level undo/redo in your
applications. This mechanism and the provided classes represent my personal
preference. Perhaps you will take what you learn here and build something
more suited to your own taste.
The first change we made to Scribble was to derive the CStroke objects from
CRefObject rather than CObject. If you look at CRefObject you will notice
that it’s brutally simple, and works much like COM objects do. The advantage
is it’s companion class CRefObList. It is identical to CObList except that
it automatically implements reference counting on objects it stores.
The reason we implement reference counting on our stroke objects is that
they need to remain in memory even after they have been deleted. In order to
undo a stroke, we need to remember what that stroke was. So each of the
commands maintains a pointer (and a reference count) to the original stroke
object.
We also added a new member to CStroke, GetBoundingRect. GetBoundingRect
exists so that we can remove a stroke by invalidating the region where it
used to exist.
Next we added a base class to CScribbleDoc, ICommandHistory.
ICommandHistory is our undo/redo buffer (and interface to it). It’s what
turns the ordinary CDocument into a multi-level undo/redo document.
Let’s talk about commands. The commands are what give us the undo/redo
capability. The basic rule for using commands is: Any time you would call
SetModifiedFlag(TRUE) on the document, do the action through a command. Once
you commit to multi-level undo/redo, you cannot cheat. Any action that
modifies the document must have a command (and subsequent undo command), and
must be done via the command mechanism.
Note that in some cases, you can reuse commands for both undo and redo. Take
for instance a move object command:
CCmdMoveObject::CCmdMoveObject(CObject* pObject, CSize sizeOffset); CCmdMoveObject::GetUndoCommand() { return new CCmdMoveObject(m_pObject, -m_sizeOffset); } CCmdMoveObject::GetRedoCommand() { return new CCmdMoveObject(m_pObject, m_sizeOffset); }
Originally in the CScribbleView::OnLButtonUp, we asked the document to
create a new Stroke and add it to it’s list before the stroke was even
defined (CScribbleDoc::NewStroke). We then defined the stroke as the user
moved the mouse around. Now the view creates a stroke object, defines it as
the user moves the mouse and then creates an “add stroke” command which it
passes to the document (via it’s new Do member) for the document to execute.
One benefit of this is that we can elect not to add a stroke (or whatever
objects we are creating) at this point. One reason may be to prevent
duplicate strokes from being entered into the document’s list, or perhaps
your application has some form of rules that can be checked here and validated
before the command is even issued.
At this point it’s your choice to either let the command manipulate the
document’s contents itself, or to do it via some interface to the document.
I choose the latter because it better encapsulates a document’s functionality,
and keeps my command objects fairly simple. The document’s Do member calls
the command’s Execute member which in turn asks the document to add the stroke
that is has a reference to.
The Do member also logs this command (if it’s Execute member returns TRUE),
and now it can be undone simply by calling CScribbleDoc::Undo.
The last change we made to scribble was to add an OnUpdate handler to the
view. After a command executes, the virtual OnCommandExecute member is called.
We overrode this member to display some text on the status bar and to call
UpdateAllViews, passing the command as the hint. OnUpdate then updates the view
to reflect the document change that occured by the execution of the command.
Using this override is simpler than having everyone of your commands call
UpdateAllViews on successfull execution.
The commands are given a static m_nID member through the use of the
DECLARE_COMMAND and IMPLEMENT_COMMAND macro’s. This ID can be accessed with the
COMMAND_ID macro. The use of the static ID allows us to use a case statement
to determine what command it is, rather than a long list of if (IsKindOf)s.
You can even have a base class for a group of commands and give the base class
a command ID, but not the derived classes (use DECLARE_COMMAND_NOID and
IMPLEMENT_COMMAND_NOID). This is handy if you have a bunch of commands that
are updated in the same way and so you only need one update case that can
update any of the commands in this group.
USING THE COMMAND HISTORY “CLEAN” STATE
Normally you use CDocument’s m_bModified member (through SetModifiedFlag
and IsModified) to maintain the “clean/dirty” state of the document. When using
multi-level undo/redo, a document can be returned to a “clean” state by use of
the undo or redo commands.
There are two steps you need to take to use the command history’s “clean”
state to reflect the state of the document:
As a precaution, I override CDocument::SetModifiedFlag to do nothing at
all. You don’t have to do this as long as your app never calls
SetModifiedFlag.
Note that you do not manually dirty the document. The command history handles
it’s “dirty” state. You only have to tell it when the document has been saved.
NOTIFYING THE USER OF THE DOCUMENT’S “CLEAN” STATE
You can show the user a “dirty flag” in the document’s title by following
these steps:
Note that if CDocument::GetTitle had been virtual, the m_strTitleOrg and
the messy manipulation of the title would have been unnessassary, but I don’t
know another way to handle it.