Introduction
How to autosave files and then recover these files after a program crashes, is something that is seldom discussed, but adds power and flexibility to a program. This article will describe how to implement an autosave and autorecover method similar to that used by MS Office.
How it works
Autosaving is quite simple. After a specific amount of time, your program must serialize the loaded documents to the disk at a specific location. These serializations must not overwrite the files that are currently in use by the user, because this would eliminate the user's choice of whether or not they would like to save the file on exit from your program. Each time the program loads up, it must search for autosaved files and restore them as needed.
As with Office, this implementation saves these temporary files to a temporary directory inside your Windows directory and searches this directory at load time for autosaved files. If any are found, it will then begin its recovery process.
Assigning an Autosave Directory
First, you must choose where you are going to save the files. This directory must remain constant, or else you would have to search the entire hard disk for autosaved files... and that is out of the question. For practical purposes, your program should place them inside a folder in the most constant directory on the HD, the Windows directory. This directory should be stored in the environment variable WINDIR
. If for some reason, the user's system does not contain a WINDIR
environment variable, or the environment variable is incorrectly set, your program needs to detect this and choose another directory. You can easily check to see if the stored directory exists, by using the CFileFind
class to search for it. The following code will do just that (gm_autosave
directory should be a string, either global, as with this example, or accessible through a member function in your application). It is best preformed within the InitInstance
function of your application.
::GetEnvironmentVariable("WINDIR",buffer,512);
CFileFind CFF;
if(CFF.FindFile(buffer,0))
{
base=buffer;
}
else base = "C:\\TEMP";
gm_autosaveDirectory = base+"\\TEMP";
Optionally, you could use GetTempPath(DWORD nBufferLength, LPTSTR lpBuffer);
to retrieve a temporary path. However, the above implementation will allow you to specify a specific directory for the files to occupy, such as "Temporary Autosaved Files".
Autosaving files
Now that the path is determined, your program will know where to save its files. The next part is, therefore, implementing the save routine in your application. To avoid soaking up system resources, use the OnTimer
function in your Main Frame window. On initialization, your program needs to call the SetTimer(...)
function with the appropriate values. SetTimer(0,m_autosavetime * 60 * 1000,NULL)
will install the timer for you (assuming m_autosavetime
is in minutes). The OnTimer()
function of your Main Frame will now be called each time the timer has expired. Within this function, you will need to send autosave messages to all the views contained by your app. Use EnumChildWindows
to call a procedure with each child window.
::EnumChildWindows(m_hWnd,AutosaveTimerChildProc,NULL);
AutosaveTimerChildProc
needs to verify that the given child is a view for the document before sending the message to the window. DYNAMIC_DOWNCAST
the pointer passed to your procedure to verify that it is a view, and then use PostMessage
to notify your view of the change:
BOOL CALLBACK AutosaveTimerChildProc( HWND hwnd, LPARAM lParam)
{
if(DYNAMIC_DOWNCAST(CMyView,CWnd::FromHandle(hwnd)) != NULL)
{
::PostMessage(hwnd,WM_MYVIEW_AUTOSAVETIMER,0,0);
}
return(TRUE);
}
Note: WM_MYVIEW_AUTOSAVETIMER
should be defined as an Application Message, which is based of WM_APP
.
Now, your view must process the message and save the file in a restorable way. Add a message map such as this:
ON_MESSAGE(WM_MYVIEW_AUTOSAVETIMER,OnAutosaveTimer)
and prototype the function in your view's class definition as a void accepting the standard WPARAM
and LPARAM
messages. This function can get a little tricky. The problem is that if the user saves the file in another location, then the restore path for the file will change also. Since the autosave name is based off the actual filename (in case the user needs to manually access the file in some strange incident) and you don't want more than one copy of the autosaved file, your program must always store the name of the last autosave backup and delete the autosaved backup, before it writes its copy of the file. This implementation uses a vector
of CString
s to store the backup filenames (this vector will contain either the name of the backup or nothing at all). Additionally, your program must make sure that the directory for your autosave has been created before saving to it! If an error occurs during the process, the choice is up to you how you will recover or handle it.
void CMyView::OnAutosaveTimer(WPARAM w, LPARAM l)
{
CFileFind CFF;
if(CFF.FindFile(gm_autosaveDirectory.GetBuffer(1))==FALSE)
{
if(CreateDirectory(gm_autosaveDirectory.GetBuffer(1),
NULL) ==0)
{
}
}
CString fname = (gm_autosaveDirectory +
"\\"+GetDocument()->GetTitle()+".MBK");
if(m_autosave_names.size() > 0) //delete old file
{
if(CFF.FindFile((*m_autosave_names.begin()))==TRUE)
{
if(::DeleteFile(((*m_autosave_names.begin()))) == 0)
{
//an error has occured, process the error here if you want
//however, if the file simply does not exist,
//that is fine, ignore the error and continue
}
}
// remove the old filename from the vector
m_autosave_names.erase(m_autosave_names.begin());
}
m_autosave_names.push_back(fname); //add the new filename to the vector
//call store function
GetDocument()->StoreAutoRecoveryInformation(fname);
}
The StoreAutoRecoveryInformation
function is implementation dependant. An easy implementation would simply serialize the document's true path to an archive, and then call the Serialize
function to save the file after the path. For our example, this will suffice:
CMyDoc::StoreAutoRecoveryInformation(CString path)
{
CFile f;
if(f.Open(path,CFile::modeWrite | CFile::modeCreate)!=0)
{
CArchive ar(f,CArchive::store);
ar.WriteString(path);
try
{
Serialize(ar);
}
catch(CException *e)
{
}
ar.Close();
f.Close();
}
else
{
}
}
Autosave Recovery
Rather than writing an entirely new document and view class for recovered files, you can simply modify your Document's Serialize(...)
function to check the file extension on serialization. If the extension is that of the autosave file type, perform the appropriate actions.
CString MakeExt(CString fname)
{
for(int i = fname.GetLength()-1; i >0; i--)
{
if(fname.GetAt(i)=='.') return fname.Mid(i);
if(fname.GetAt(i)=="\\") break;
}
return "";
}
.
.
.
CMyDoc::Serialize(CArchive ar)
{
CString ext=MakeExt(ar.GetFile()->GetFileName());
ext.MakeLower();
if(ext == ".mbk" && ar.IsLoading())
{
CString s;
//read path to restore path member variable
m_restorepath = ar.ReadString(s)
// read old path to member variable
m_oldpath = ar.GetFile()->GetFileName();
CMyBaseDocument::Serialize();
//call the base document's serialize function,
// or perform your default serialization here
}
// put your default serialization code here
else CMyBaseDocument::Serialize();
}
Because of the way that the MFC code for document serialization works, the document's view will have to handle changing the document's path to the correct location. (By default, the path will be to the autorestored copy, but you want to restore the original path, so that the user can continue where they left off with as little hassle as possible.) In the OnInitialUpdate
function of your view, check to see if the document's restore path has been set. If it has, then you know that the program has just loaded an autorecovered file, and the path and title of the document and view must be changed to the proper location.
CMyDoc *doc=GetDocument();
if(doc->m_restorepath.GetLength() > 0)
{
doc->SetPathName(doc->m_restorepath,TRUE);
doc->SetModifiedFlag(TRUE);
doc->UpdateAllViews(NULL);
m_autosave_names.push_back(doc->m_oldpath);
}
Your program must autorecover the files each time it loads. This is done by searching the autosave directory for all autosaved files and loading as needed from your InitInstance
function. If you allow multiple instances of your program, make sure that the current instance is the only instance before recovering autosaved files.
BOOL bFound = FALSE;
HANDLE hMutexOneInstance = NULL;
#ifdef _WIN32
hMutexOneInstance =
CreateMutex(NULL,TRUE,_T("PreventSecondInstanceMutex"));
if(GetLastError() == ERROR_ALREADY_EXISTS)
bFound = TRUE;
#else
if(m_hPrevInstance != NULL)
bFound = TRUE;
#endif
#ifdef _WIN32
if(hMutexOneInstance) ReleaseMutex(hMutexOneInstance);
#endif
.
.
.
if(bFound)
{
if(CFF.FindFile((gm_autosaveDirectory+"\\"+
"*.MBK").GetBuffer(1),0)==TRUE)
{
if(::MessageBox(NULL,
"Autosaved files found. Would you like to recover them?\n"
"(WARNING: IF YOU PRESS NO, YOU WILL NOT BE ABLE TO RECOVER"
" THE FILES IN THE FUTURE).", "MYDOC", MB_YESNO) != IDNO)
{
while(CFF.FindNextFile() !=0)
{
CMyDoc *doc=
(CMyDoc *)OpenDocumentFile(CFF.GetFilePath().GetBuffer(1));
}
OpenDocumentFile(CFF.GetFilePath().GetBuffer(1));
}
else
{
while(CFF.FindNextFile() !=0)
{
DeleteFile(CFF.GetFilePath().GetBuffer(1));
}
DeleteFile(CFF.GetFilePath().GetBuffer(1));
}
}
}
Use class wizard to add the PostNcDestroy
member to your view class, and then delete the autosaved file there (it will only be deleted if the window closes normally). The following chunk of code will do that for you.
CFileFind CFF;
if(m_autosave_names.size() > 0)
{
if(CFF.FindFile(((*m_autosave_names.begin())).c_str())==TRUE)
{
::DeleteFile(((*m_autosave_names.begin())).c_str());
}
m_autosave_names.erase(m_autosave_names.begin());
}
Finally, at program close, these files should be deleted and the directory removed. In your ExitInstance
function, delete the directory.
CFileFind CFF;
if(CFF.FindFile(gm_autosaveDirectory.GetBuffer(1))==TRUE)
RemoveDirectory(gm_autosaveDirectory.GetBuffer(1));