Non-Stop Autosave for MFC Documents

Walter Meerschaert

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.