Environment: VC7, VC6, WinXP, WinME, Win98
I am pleased to submit a tutorial to CodeGuru; I have benefited very much from the generous and helpful resources here.
This tutorial tries to explain how I developed Undo/Redo capability for my own applications. It works very well for me. Perhaps others here might find it useful. I'll refer to it as the "DocVars" method.
The basic philosophy of the DocVars method is enumerated in the statements below:
- Packaging—Place all of a document's undo-able and redo-able variables into a separate class (CDocVars, for example).
- Handling and modifying the DocVars as a complete package—If one of the variables in the document's current DocVars needs to be changed, a new instance of the DocVars is created reflecting the change.
- Serving—Manage a list of CDocVars as the modification of the document proceeds. This tutorial uses CUpdateMgr for this job. Management responsibilities include:
- Receiving updates of new DocVars from the document and adding them to the list.
- Sending out listed DocVars to the document in response to Undo and Redo requests from the document.
- Custodial Duties —A single class must take the responsibility of supporting the CURRENT and LIVE DocVars. This tutorial uses the document as the DocVars custodian. In this capacity, the document is the central clearinghouse for the application's Undo-able/Redo-able variables. All important variables are maintained by the document. And to gain access to them, all other classes must consult the document.
The following conversation should help to explain how the system works.
|CView:||Hey, CDocument! What you got?|
|CDocument:||That all depends on what you want it for.|
|CView:||I need to make a change to one of your variables.|
|CDocument:||Okay, I'm gonna send you a copy of my current DocVars package. Make whatever changes you need. When you're done, send it back to me.|
|CView:||The whole package?! Geeze! I just want to make one teensy weensy change. Can't you just send me the one variable I want?|
|CDocument:||DUDE! P-A-C-K-A-G-E. OK?|
|CView:||Okay, but it seems like the hard way to do things.|
|CDocument:||Quit yer complainin' and get back to work.|
|CView:||Right, I've made the change to my DocVars copy. You want it?|
|CDocument:||Yea, send it back to me.|
|CView:||What happens now?|
|CDocument:||I make a unique copy. If the DocVars has pointer variables, I have to allocate memory for new pointers and then fill them up with the parameters from the DocVars you just sent me. Afterwards, I send it along to CUpdateMgr. He adds it to his list and sends a copy back to me when he's done.|
|CView:||You gotta be kiddin' me!! What a tangled bureaucracy!! You're as bad as the Department of Motor Vehicles.|
|CDocument:||Please just shut yer yapper and let me do my job.|
|CDocument:||Hello, CUpdateMgr? I got a new DocVars package for ya. Please update your list.|
|CUpdateMgr:||Got it. I'm adding it to the end of the list. Done. Now I'm sending a copy back to you.|
|CDocument:||Got it. Thanks. The package you returned to me is now my current DocVars.|
|LATER THAT DAY...|
|CMainFrame:||Hey, CDocument! Some geek with a mouse just clicked the "UNDO" button.|
|CDocument:||Thanks for the message. I'll tell CUpdateMgr.|
|CDocument:||Hey, CUpdateMgr! I just got an Undo request.|
|CUpdateMgr:||Okeeday. Let me scroll back one place on the list of DocVars. There it is. Okay, I'm sending you the older DocVars.|
|CDocument:||Got it, and it is now my current DocVars.|
|MUCH LATER THAT DAY...|
|CView:||Hey, CDocument! What you got?|
|CDocument:||That all depends. What do you need?|
|CView:||I just need to check the value of one of your DocVars variables.|
|CDocument:||Okay, I'm gonna send you a pointer to my current DocVars.|
|CDocument:||But you leave them variables alone. NO CHANGES! Ya Hear, BOY!?|
|CDocument:||Because you will completely disintegrate the good thang me and CUpdateMgr got goin' here, and you'll ruin the whole Undo/Redo feature.|
|CView:||Roger! No changes. Just lookin'.|
CDocument or Not—Although the example app uses the MFC Document/View setup, the Doc/View setup is not a requirement. The trick is to set a single class as the custodian. It doesn't need to be a CDocument. For example, a dialog-based app could use the main dialog class as the DocVars custodian.
Serialization—The example app uses MFC serialization. To make this possible, CDocVars and all of the DocVars's class-based variables must support serialization. Because CObject supports serialization, I derived CDocVars from CObject. If you need DocVars variables based on custom classes, consider deriving them from CObject. Just remember that when you derive a new class from CObject, you will need to write a Copy Constructor for it. In addition, you will need to prepare an Assignment Operator for (=) and possibly others depending on your needs. See DocVars.h in the example app's source to see how I did it.
Templates—CUpdateMgr uses a custom list class, CtObjectList. This list class is a template class derived from CList. For me, templates are a bit of a mystery. But one thing is certain. They are extremely convenient. In this example, I had to prepare an (=) assignment operator, and I overrode only the Serialize() function. Please note that CtObjectList expects to handle serializable types. In the present case, it handles CDocVars types that are derived from the serializable CObject class.
Infinite Undo—The Serialize() override function in the CUndo_Redo_DemoDoc class is set up so you can preserve all Undo states to the origination of the document. That is, no matter how many editing sessions you have, you will still be able to Undo to the very beginning. The down side of this feature is that you can end up with pretty hefty files. If this is a problem, I recommend that you place a limit on the number of DocVars in the UpdateMgr's list and make this a user-configurable value. Or, you could simply disable this intersession feature by commenting-out the #define INFINITE_UNDO statement in Undo_Redo_DemoDoc.cpp.
Pointers—Don't forget that if you are using pointer variables in your DocVars, you have to associate the pointers in each DocVars with unique spaces in memory. Otherwise, the DocVars pointer variables throughout the list of DocVars all will be pointing to the same memory address—which kinda defeats the purpose. I'll leave it to you to figure out the best way to do that.
DocVarsGetByValue ()—This function is intended to be used mainly as a way for the document to deliver a copy of its current DocVars. This is so the calling function can modify its copy of the DocVars set without directly affecting the document's current DocVars.
DocVarsGetPointer()—Call this function when you need to find out the value of a variable in the document's current DocVars. DO NO MODIFICATIONS of DocVars variables while in possession of this pointer. Otherwise, total chaos.
Example MSVS Project —I am supplying the example source code in the form of a Microsoft Visual C++.NET Solution. Users of MSVC++ 6 might find it easier to create a new SDI/DocView Workspace and then simply add the files from the example.
Best of luck.
DownloadsDownload demo project -137 Kb
Download source - 54 Kb