Multiple Level Undo/Redo

INSTRUCTIONS
These are the steps used in the project file to add multi-level undo/redo capability to the Scribble application.

Copy Files

  • Copy scribble tutorial files (I used step 3) into new folder.
  • Copy "command" and "ref object" files into the folder and add them to the project. You could also put them in a library which would be more convenient. These files are: Command.hCommand.cppCommandHistory.hCommandHistory.cppRefObject.hRefObject.cpp
  • Add includes for RefObject.h, Command.h, and CommandHistory.h to stdafx.h
  • Edit Resources

  • Add undo/redo buttons to the IDR_MAINFRAME toolbar. Make sure their id's are ID_EDIT_UNDO and ID_EDIT_REDO (there are string table entries for both in MFC). You can grab the images from this project.
  • Add "&Redo" to the IDR_SCRIBBTYPE edit menu and use ID_EDIT_REDO command id.
  • CStroke Changes

  • Derive CStroke from CRefObject instead of CObject
  • Change CStroke's IMPLEMENT_SERIAL macro to use new base class
  • Under "Operations" add GetBoundingRect declaration to CStroke: CRect GetBoundingRect() const;
  • Add CStroke::GetBoundingRect definition to ScribDoc.cpp
  • Immediately after class definition, add CStrokeList typedef for easy use: typedef CTypedRefObList< CStroke* > CStrokeList;
  • CScribbleDoc Changes
  • Add class ICustomCommandHistory to override OnCommandExecute
  • Multiply derive CScribbleDoc from ICustomCommandHistory
  • Change m_strokeList's type from CTypedPtrList< CObList,CStroke* > to CStrokeList
  • Add GetPenWidth member
  • Delete NewStroke declaration and definition
  • Add AddStroke, AddStrokes, RemoveStroke, and RemoveStrokes declarations and definitions
  • Modify CScribbleDoc::DeleteContents definition as shown. Notice that we do not delete the strokes in the list as this is handled by the CRefObList class
  • Add command handlers for ID_EDIT_UNDO and ID_EDIT_REDO (both Command and update handlers).
  • Modified CScribbleDoc::OnEditClearAll to use a command (so that the clear all event can be undone).
  • Add include for ScribVw.h (to see command declarations). This is not nessassary if you move the ID_EDIT_CLEARALL handler to the view class. It is my personal preference to have the view generate commands for the doc rather than letting the doc make commands on it's own.
  • CScribbleView Changes

  • Add command enumeration
  • Add command declarations/definitions
  • Make changes to CScribbleView::OnLButtonDown and CScribbleView::OnLButtonUp
  • Add OnUpdate override to the view
  • CScribbleView::OnDraw strokeList variable's type changed to const CStrokeList&

  • 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:

  • Override CDocument::IsModified to return IsDirty();
  • Override CDocument::OnSaveDocument to call SetClean(); if the document was properly saved.
  • 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:

  • Override ICommandHistory::OnStatusChange. Have ICustomCommandHistory::OnStatusChange call an UpdateTitle member of the document.
  • Add m_strTitleOrg member to CScribbleDoc.
  • Add UpdateTitle member to CScribbleDoc.
  • Override CDocument::SetTitle to set m_strTitleOrg.
  • 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.

    Download demo project - 45KB



    Comments

    • Many Many Thanks to you!

      Posted by Legacy on 05/16/2000 12:00am

      Originally posted by: jack zhang

      Your article is so useful,just like stingsoft's mvc thanks!

      Reply
    Leave a Comment
    • Your email address will not be published. All fields are required.

    Top White Papers and Webcasts

    • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

    • CentreCorp is a fully integrated and diversified property management and real estate service company, specializing in the "shopping center" segment, and is one of the premier retail service providers in North America. Company executives travel a great deal, carrying a number of traveling laptops with critical current business data, and no easy way to back up to the network outside the office. Read this case study to learn how CentreCorp implemented a suite of business continuity services that included …

    Most Popular Programming Stories

    More for Developers

    RSS Feeds