Safe Document Handling
Written by Stephane Bessette
[NOTE: Here is a link to a zip of the source code for this article. Ed]
Implementing safe document handling is not difficult, but requires some thought. Here are some guidelines, a class that encapsulates this functionality, and an application framework that puts it to use.
What's This All About?
It's simple. If a program allows a user to manipulate some data, then that program should ensure the safety of the data. For example, if a user is working on a document and then decides to create a new document, he/she should be warned if the work has not been saved and offered the opportunity of saving or discarding that work, or of cancelling the action. This same principle applies to opening another document, closing the application, and OS/2's shutdown notification.
Behind The Scenes
At the heart of safe document handling lies a simple boolean variable: bModifiedDocument. If there is modified data, unsaved data, then this variable is TRUE. If there have been no changes since the last save, or if this is still a new, unchanged document, then this variable is FALSE. Initially this variable will be set to FALSE.
A second critical aspect of save document handling is provided via the boolean function CanClose(). Whenever a document management action is issued (Open, New, Close), a call to CanClose() will determine whether the action will be permitted or not.
The CanClose() function is fairly straightforward. If there are no modified data, if bModifiedDocument is FALSE, then it will return TRUE. But if there are modified data, if bModifiedDocument is TRUE, then the user will be consulted.
The user may elect to save the work, discard the work, or cancel the current action (Open, New, Close). If the work is discarded or successfully saved, then the function will return TRUE. But if the user selects to cancel the current action, then the function will return FALSE.
Let's take a look at the save action. In this framework, the FileSave() function returns a boolean. If there were no errors while writing the work to disk, then bModifiedDocument is set to FALSE and the function returns TRUE. Otherwise, bModifiedDocument is unchanged, and the function returns FALSE.
That was simple enough. Now comes a brain teaser. Should the data be saved if bModifiedDocument is FALSE? There would be no apparent reason, since the contents in the application are the same as in the file. So we may optimize the function by returning immediately. But here's a special situation. A user is working on a document, deletes the file, and then issues the save command. If the save is not performed and the user quits the application, the work will be lost. So, it may be preferable to perform some unnecessary saves than to potentially lose a piece of work.
And here's a second special situation. A user starts two instances of your program to work on the same document. He or she doesn't like the changes made with one program and decides to issue a save command from the other instance of the application in order to restore the contents of the file. This wouldn't work if the application did not really save the work. And before anyone starts thinking of a scheme where an application could keep tabs on what every possible instance is doing, consider the situation where two different applications are used to work on the same document. In conclusion, the FileSave() function always saves, regardless of whether bModifiedDocument is TRUE or FALSE.
That was simple. But too simple. If the current document is a new document, we do not know its name, nor do we know where to save it (which drive and directory). So, first check if we have a filename for the current document. If we do, save the work. But if we don't, then obtain a filename from the file Save as dialog.
When the Save as command is issued, a file Save as dialog should present the user with the current filename, along with the drive and directory where this file resides. If there is no current filename, then the current directory should suffice, and you may provide a default filename, such as Untitled.
Once the new filename has been obtained, a check should be made to verify that there is not currently a file with that same name. If there is, then the user should be asked if the file should be overwritten or not. If the user answers YES, then the file should be overwritten, otherwise the Save as command should be cancelled. Fortunately, all this work was done in the Class_FileDialog presented last month. The final step in handling the Save as command is to call the Save function to put the data to disk.
When the New command is issued, a call to CanClose() will determine whether the application should be readied for a new document or not. If we proceed with the action, we have to reset the filename of the current document, and possibly prepare the application for a new document, such as drawing an empty window.
When the open command is issued, a file open dialog should present the user with the current filename, along with the drive and directory where this file resides. If there is no current filename, then the current directory should suffice.
Once the filename has been obtained, a check should be made to verify that it indeed exists (the user may simply have typed an incorrect name in the entry field). If it does not exist then the user should be asked if the file should be created. If the user answers NO, then the open action should be cancelled. Again, the Class_FileDialog class presented last month took care of those details.
But before all of this can happen, we have to verify if we can perform the open action. A call to CanClose() will provide us with the answer. If we can proceed, then we'll call FileNew() in order to reset the state of the application, and then actually load the file from disk.
This function allows an application to notify the framework that the document is now in a modified state. Whenever the document is modified in any way, the SetModified() function should be called.
This is a bonus function that changes the text in the titlebar of the frame window. The current format is the name of the application followed by a dash enclosed in spaces, and then the full name of the file. Finally, if the document is modified, a * will be appended. And this is done automatically! When a file is loaded, its name will appear in the titlebar. When it is modified, a * will appear. And when a new file is created, Untitled will appear in lieu of a filename.
That's all it takes to implement safe document-handling. I've put all of these functions into a class (Class_SafeDocumentHandling), so that you can easily use it without having to retype or copy/paste this work. But since the framework cannot know in advanced how to save your data, I've made it into an abstract class. Class_DocumentHandling is derived from Class_SafeDocumentHandling and contains two functions: Load() and Save(). This is where you need to put your code to load and save the data. And you do not need to modify or even look at the code contained in Class_SafeDocumentHandling; just put in your code to load and save.
I've also included a sample application that demonstrates the simplicity of this framework. Main_Frame.cpp contains the application code, where you respond to events like menuitem selection. You can play with this sample application and see that this really works. However, be careful when saying yes to overwriting a file since that file will be deleted. And if you say yes to creating a new file, it will be created.