This article by Douglas Peterson.
Download source project. Zip file is 41K.
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.
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.
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.