Is there anything worse than losing work? Programs hang before you get a chance to save. Some other program hangs your machine even though there's nothing wrong with the program you're using. And it seems the more important the work you're doing, the less often you remember to click that Save button. Walter Meerschaert has a solution-your program should save all by itself. He shows how an autosave feature should work and implements autosave for the MFC sample application, Scribble.
You're sitting at your computer early in the morning on January 1, 2000, and you notice that your computer has crashed. In your haste to get to the huge smash party last night, you forgot to save what you were working on. You boot back up, and everything seems to go okay. You try to start the program you were working in and . . . . If the programmer included an autosave feature, your document will be there, right where you left off. If not . . .How should autosave work?
What do we want autosave to do? Well, ideally, autosave should work in the background to make sure that the user doesn't lose any work if the program stops before they have a chance to save their work. Autosave shouldn't impose itself on the user for any reason. When the application starts, autosave should check to see whether there are any documents to present to the user, and when they're opened, the application interface should reflect the fact that you're viewing a recovered file. All of this should ideally happen automatically, without any user interaction.
The save in autosave
Let's first take a look at the saving part of autosave. I'm using the Scribble example from the MFC tutorial as a starting point (if you're following along at home, I started with Step 6). Ideally, autosave should kick in whenever the document has changed from the last saved version. How will you know that? It might sound like a tough thing to notice, but in every case that changes the document, the developer will call the CDocument virtual function SetModifiedFlag(). I overrode this function in the CScribbleDoc class to do the saving part of autosave.
If your documents are slow to save to disk, or if you're very sensitive to the user interface pausing to perform the autosave, you might want to put the entire autosave functionality in a second thread to keep the UI from slowing down. That's the approach I'll take: SetModifiedFlag() will quickly save the document to a memory file, and another thread will write it out to disk. Another option would be to use a timer to autosave at user-defined intervals.
Let's first create a place to keep autosave information in our document class. We'll need to know where the file is being autosaved, whether we're currently performing an autosave, and whether the file is in a normal state or is being recovered from an autosave file. Add the following items to the header file for CScribbleDoc (scribdoc.h):
struct Autosave
{
CMemFile* file;
CScribbleDoc* pDoc;
BOOL bModified;
};
class CScribbleDoc : public CDocument
{
. . .
protected:
CString m_strAutosave;
BOOL m_bRecovered;
CSemaphore m_semAutosave;
CSingleLock m_lockAutosave;
public:
virtual void
SetModifiedFlag(BOOL bModified = TRUE);
void SetRecovered(BOOL bRecovered)
{m_bRecovered = bRecovered;}
friend UINT
ManageAutosave(LPVOID lParam);
. . .
};
The m_strAutosave variable contains the path name of the file containing the current autosave file. You create and destroy this file in the overridden SetModifiedFlag(). The m_bRecovered flag is the state flag for opening a recovered file. I'll discuss this variable in more detail when I address recovering files later in the article. For now, just initialize m_bRecovered to FALSE in the document constructor. The m_semAutosave and m_lockAutosave handle synchronization while an autosave is in progress.
The Autosave structure is for passing to the worker thread to let it know what to save. I'll discuss that more later on.
Now, we'll add code to the implementation file for the document (scribdoc.cpp). You'll need direct.h, and I've already mentioned the constructor change:
#include <direct.h> // for standard directory access
//. . .
CScribbleDoc::CScribbleDoc()
{
m_bRecovered = FALSE;
}
SetModifiedFlag() should still call the base version. If you're working with a recovered file already, there's no point autosaving it. If you're autosaving, you create an Autosave structure and fill it in. Then use a CArchive to serialize the document to the memory file. Finally, hand off control to a global function called ManageAutoSave() to take care of business in a separate thread. Here's SetModifiedFlag():
void CScribbleDoc::SetModifiedFlag(BOOL bModified)
{
CDocument::SetModifiedFlag(bModified);
// don't autosaverecovered files until
// they're set tonormal
if(m_bRecovered ||m_lockAutosave.IsLocked())
return;
Autosave* as = new Autosave;
as->pDoc = this;
as->bModified =bModified;
as->file = NULL;
if(bModified)
{
as->file = new CMemFile();
TRY
{
CArchivear(as->file, CArchive::store);
Serialize(ar);
ar.Close();
}
CATCH_ALL(e)
{
return;
}
END_CATCH_ALL
}
m_lockAutosave.Lock();
AfxBeginThread(ManageAutosave, (LPVOID)as);
}
ManageAutoSave() first establishes the filename to which the memory file should be autosaved. The autosave file is placed in the same directory as the file being worked on, or the current directory if the file has never been saved. Alter the extension slightly to avoid overwriting the current document file: Change the last character of the extension to a tilde (~). Then it's just a matter of writing the memory file to the autosave file. There's a fair amount of error catching but not much error reporting, since the user didn't ask us to write this file. The last step is to delete the autosave file if the document is being saved or closed normally.
Here's ManageAutoSave():
UINT ManageAutosave(LPVOID lParam)
{
Autosave* as = (Autosave*)lParam;
if(as == NULL)
return 1;
CScribbleDoc* pDoc = as->pDoc;
// autosave to m_strAutosave
if(as->bModified)
{
// if filename is empty,that means we've
// never auto-saved
if(pDoc->m_strAutosave.IsEmpty())
{
pDoc->m_strAutosave = pDoc->GetPathName();
int index =pDoc->m_strAutosave.Find('.');
if(index == -1)
{
// in the case where we have no pathname,
// we haven't saved our file for the
// first time,so we create an autosave
// file from the doc title in the
// current directory
_getcwd(
pDoc->m_strAutosave.GetBufferSetLength(_MAX_PATH),
_MAX_PATH);
pDoc->m_strAutosave.ReleaseBuffer();
if(pDoc->m_strAutosave[
pDoc->m_strAutosave.GetLength()-1]
!='\\')
pDoc->m_strAutosave+= '\\';
pDoc->m_strAutosave += '~';
pDoc->m_strAutosave += pDoc->GetTitle();
}
else
pDoc->m_strAutosave =
pDoc->m_strAutosave.Left(index);
CString ext;
VERIFY(pDoc->GetDocTemplate()->GetDocString(
ext, CDocTemplate::filterExt));
// assumethree-character extension; if
// you have ashorter or longer extension,
// reactaccordingly
VERIFY(ext.GetLength()== 4);
ext.SetAt(3,'~');
pDoc->m_strAutosave += ext;
}
// save contents toautosave file
// handle errorssilently
CFile f;
if(f.Open(pDoc->m_strAutosave,
CFile::modeReadWrite
|CFile::modeCreate|CFile::shareExclusive))
{
TRY
{
char sz[1024];
as->file->SeekToBegin();
DWORD nread;
do
{
nread =as->file->Read sz, 1024);
f.Write(sz, nread);
}
while(nread ==1024);
f.Close();
deleteas->file;
// put autosave name in the Registry
Scribble()->AddAutosave(
pDoc->m_strAutosave);
}
CATCH_ALL(e)
{
// forgeteverything - we might set an
// app-levelflag for autosaving
// and turn it off here and alert the user
remove(pDoc->m_strAutosave);
Scribble()->RemoveAutosave(pDoc->m_strAutosave);
pDoc->m_strAutosave.Empty();
}
END_CATCH_ALL
}
else
pDoc->m_strAutosave.Empty();
}
else
{
// if there's an
autosave file
if(!pDoc->m_strAutosave.IsEmpty())
{
// delete
autosave file
remove(pDoc->m_strAutosave);
// remove
autosave file from the Registry
Scribble()->RemoveAutosave(
pDoc->m_strAutosave);
// forget name of
file
pDoc->m_strAutosave.Empty();
}
}
pDoc->m_lockAutosave.Unlock();
delete as;
return 0;
}
Notice the
Scribble() function. Scribble() is a global function that simply returns a
pointer to the one and only CScribbleApp object. This is directly analagous to
calling AfxGetApp(), except I don't ever have to cast the result to the desired
type, CScribbleApp*. The AddAutosave()
and RemoveAutosave() functions in CScribbleApp save and remove the filenames of
the autosave files in the Registry so that Scribble will remember the autosaves
between invocations. They use a static string to hold the Registry entry:
static char szAutosaveSection[] = "Autosave";
void CScribbleApp::AddAutosave(LPCSTR szAutosave)
{
WriteProfileString(szAutosaveSection,
szAutosave, "1");
}
void CScribbleApp::RemoveAutosave(LPCSTR szAutosave)
{
WriteProfileString(szAutosaveSection,
szAutosave, NULL);
}
Time for a few words about document state. During an autosave, you need to be sure that you've completely finished the current change operation so that the document in memory is in a complete state, ready for archiving. For example, in Scribble, the view class calls on the doc class to create a new stroke using the NewStroke() function. The NewStroke() function creates a new stroke and returns its pointer. Unfortunately, it also calls SetModifiedFlag(). This is a problem because the stroke isn't in a completed state upon construction. It's basically invalid until it has points. In other words, if we were to archive the file at that point (do an autosave), the stroke would be saved in an incomplete state and the program would likely crash on running if that file were reopened and drawn. The solution is to only call SetModifiedFlag() when the document and all of its members are in a state fit for serialization. I did this to CScribbleDoc by removing the call to SetModifiedFlag() from NewStroke() and placing it in a new function called EndNewStroke(), and calling EndNewStroke() from the view class override of OnLButtonUp(), which is when the stroke is "finished."
If it's your habit
to call SetModifiedFlag() whenever you so much as breathe on the document,
whether it's
in a valid (savable) state or not, you might want to think about that a bit
before implementing this autosave technique. It's better to call
SetModifiedFlag() when the modifications are complete.
Recovering autosave files
Now let's turn to the act of opening the autosave files and proudly displaying them for the user after an abnormal program termination.
The application class is responsible for remembering and forgetting where the autosave files are, as you've seen, and also for opening them when the app starts. The CheckAutosave() function enumerates the Registry entries looking for files that still exist and can be opened, and opens any that pass that test. It looks like this:
BOOL CScribbleApp::CheckAutosave()
{
HKEY hSecKey =
GetSectionKey(szAutosaveSection);
if (hSecKey == NULL)
return FALSE;
BOOL bRet = FALSE;
char
szAutosave[_MAX_PATH];
DWORD Size = _MAX_PATH;
DWORD i = 0;
while(::RegEnumValue(
hSecKey, i++, szAutosave,
&Size, NULL, NULL, NULL, NULL)
!=
ERROR_NO_MORE_ITEMS)
{
if(Size > 0
&& DoesFileExist(szAutosave))
{
bRet = TRUE;
OpenDocumentFile(szAutosave);
remove(szAutosave);
}
Size = _MAX_PATH;
}
WriteProfileString(szAutosaveSection, NULL, NULL);
::RegCloseKey(hSecKey);
return bRet;
}
This code uses a simple helper function called DoesFileExist(). Here's that function:
BOOL DoesFileExist(LPCSTR path)
{
BOOL bFound = FALSE;
long handle;
struct _finddata_t
info;
if((handle =
_findfirst(path, &info)) != -1)
{
bFound = TRUE;
_findclose(handle);
}
return bFound;
}
If the file
exists, CheckAutoSave() will call OpenDocumentFile(). The default
OpenDocumentFile() function implementation is a simple redirection to
the CDocManager function of the same name. CDocManager's OpenDocumentFile()
finds an appropriate CDocTemplate (as registered in InitInstance) and then
calls the CDocTemplate::OpenDocumentFile().
The default
implementation of CMultiDocTemplate::OpenDocumentFile() creates
a new document, creates a frame for it to be viewed in, and tells the document
to open itself. It then sets the document path and title, and tells the frame
to update itself based on the contents of the document.
Armed with this knowledge, you can do several things. First, override OpenDocumentFile() in CScribbleDocTemplate, your own implementation of CMultiDocTemplate. Second, call CheckAutosave() function in InitInstance(), and react accordingly if you find autosave files.
You'll need a header file for CScribbleDocTemplate:
class CScribbleDocTemplate : public CMultiDocTemplate
{
public:
CScribbleDocTemplate(UINT nIDResource,
CRuntimeClass* pDocClass,
CRuntimeClass* pFrameClass,
CRuntimeClass* pViewClass):
CMultiDocTemplate(nIDResource, pDocClass,
pFrameClass, pViewClass)
{}
virtual CDocument*
OpenDocumentFile(
LPCTSTR
lpszPathName, BOOL bMakeVisible = TRUE);
};
Next, write the
override of OpenDocumentFile(). I started with the MFC code from
CMultiDocTemplate-if you wish to implement an autosave for an SDI app, you'd
derive from CSingleDocTemplate and copy and alter the implementation from that
class. The code for these is in docmulti.cpp or docsingle.cpp in the MFC\source
directory where you installed the product that came
with MFC.
The main change here is to see whether the filename ends with a tilde and alter the display to reflect the fact. We do this by adding "(recovered)" to the document title. We could also have marked the file as dirty at this point, so that the user would be prompted to save the file when the file is closed. Here's the function:
CDocument*
CScribbleDocTemplate::OpenDocumentFile(
LPCTSTR lpszPathName,
BOOL bMakeVisible)
{
CScribbleDoc* pDocument =
(CScribbleDoc*)CreateNewDocument();
if (pDocument == NULL)
{
TRACE0("CDocTemplate::CreateNewDocument _
returned NULL.\n");
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);
return NULL;
}
ASSERT_VALID(pDocument);
BOOL bAutoDelete = pDocument->m_bAutoDelete;
pDocument->m_bAutoDelete = FALSE;
// don't destroy if something goes wrong
CFrameWnd* pFrame = CreateNewFrame(pDocument, NULL);
pDocument->m_bAutoDelete = bAutoDelete;
if (pFrame == NULL)
{
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);
delete pDocument;
// explicit delete on error
return NULL;
}
ASSERT_VALID(pFrame);
if (lpszPathName == NULL)
{
// create a new document - with default name
SetDefaultTitle(pDocument);
// avoid creating temporary compound file when
// starting up invisible
if (!bMakeVisible)
pDocument->m_bEmbedded = TRUE;
if (!pDocument->OnNewDocument())
{
// user has been alerted to what failed in
// OnNewDocument
TRACE0("CDocument::OnNewDocument returned _
FALSE.\n");
pFrame->DestroyWindow();
return NULL;
}
// it worked, now bump untitled count
m_nUntitledCount++;
}
else
{
// open an existing document
CWaitCursor wait;
BOOL bRecovered = FALSE;
if(lpszPathName[strlen(lpszPathName) - 1]
== '~')
//recovered file
{
bRecovered = TRUE;
pDocument->SetRecovered(bRecovered);
}
if (!pDocument->OnOpenDocument(lpszPathName))
{
// user has been alerted to what failed in
// OnOpenDocument
TRACE0("CDocument::OnOpenDocument returned _
FALSE.\n");
pFrame->DestroyWindow();
return NULL;
}
pDocument->SetPathName(lpszPathName);
if(bRecovered)
{
// put a good extension on the document
CString s = lpszPathName;
int index = s.Find('.');
VERIFY(index > 0);
s = s.Left(index);
CString ext;
VERIFY(GetDocString(ext,
CDocTemplate::filterExt));
s += ext;
pDocument->SetPathName(s);
s = pDocument->GetTitle();
s += " (recovered)";
pDocument->SetTitle(s);
pDocument->SetRecovered(FALSE);
}
}
InitialUpdateFrame(pFrame, pDocument, bMakeVisible);
return pDocument;
}
Now, to make sure
that code gets called, change InitInstance() in CScribbleApp. You want to use
the
new class when creating the template, and call CheckAutosave() to see whether
there are any files left over to recover. If there are, it will open the
autosave files and override the default action of making a new file or opening
a file. You might alternatively want to recover files only after checking with
the user. That's also easily handled in the CheckAutosave() function.
InitInstance() ends up looking like this:
BOOL
CScribbleApp::InitInstance()
{
// Standard initialization
// If you're not using these features and wish to
// reduce the size of your final executable, you
// should remove from the following the specific
// initialization routines you don't need.
#ifdef _AFXDLL
Enable3dControls();
// call this when using MFC in a shared DLL
#else
Enable3dControlsStatic();
// call this when linking to MFC statically
#endif
LoadStdProfileSettings();
// load standard INI file options (including MRU)
// we need to make sure we're using the Registry
SetRegistryKey("ScribbleIncorporated");
// Register the application's document templates.
// Document templates serve as the connection
// between documents, frame windows, and views.
// we need to make sure we're creating a
// CScribbleDocTemplate
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CScribbleDocTemplate(
IDR_SCRIBBTYPE,
RUNTIME_CLASS(CScribbleDoc),
RUNTIME_CLASS(CChildFrame),
// custom MDI child frame
RUNTIME_CLASS(CScribbleView));
AddDocTemplate(pDocTemplate);
// create main MDI Frame window
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
m_pMainWnd = pMainFrame;
// Enable drag/drop open. We don't call this in
// Win32, since a document file extension wasn't
// chosen while running AppWizard.
m_pMainWnd->DragAcceptFiles();
// enable DDE Execute open
EnableShellOpen();
RegisterShellFileTypes(TRUE);
// parse command line for standard shell commands,
// DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// check autosave and don't open any files or create
// a new file if we're recovering files
if(CheckAutosave())
cmdInfo.m_nShellCommand =
CCommandLineInfo::FileNothing;
// dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// the main window has been initialized, so show
// and update it
pMainFrame->ShowWindow(m_nCmdShow);
pMainFrame->UpdateWindow();
return TRUE;
}
If you use MFC Document/View architecture in your applications, you should be able to copy this functionality to add autosave to your own programs. Then your program is not only Y2K compliant, it's Y2K safe! And it's safe against all of those other nasties, like tripping over the power cord, that happen on any day of the year.
Download AUTOSAVE.exe.
Walter Meerschaert has been a programmer with Callan Associates Inc. for the past 11 years. He has written numerous programs, including PEP for Windows and EdWin-Electronic Documents for Windows. walterm@callan.com.