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.
Collapse
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)
{
if(CFF.FindFile((*m_autosave_names.begin()))==TRUE)
{
if(::DeleteFile(((*m_autosave_names.begin()))) == 0)
{
}
}
m_autosave_names.erase(m_autosave_names.begin());
}
m_autosave_names.push_back(fname);
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.
Collapse
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;
m_restorepath = ar.ReadString(s)
m_oldpath = ar.GetFile()->GetFileName();
CMyBaseDocument::Serialize();
}
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.
Collapse
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));