Introduction
Object serialization is one of the most powerful features of MFC. With it
you can store your objects into a file and let MFC recreate your objects
from that file. Unfortunately there are some pitfalls in using it correctly. So it happened
that yesterday I spent a lot of time in understanding what I did wrong while
programming a routine that wrapped up some objects for a drag and drop
operation. After a long debugging session I understood what happened and so I think
that it might be a good idea to share my newly acquired knowledge with you.
The Wrong Way
Let's start with my wrong implementation of object serialization.
A very convenient way to implement drag and drop and clipboard operations
is to wrap up your objects in a COleDataSource
object. This
object can hold any data with a defined format or with a format that you can
define and then it can be passed either to the clipboard by calling
COleDataSource::SetClipboard
or to the drag and drop handler
by calling COleDataSource::DoDragDrop
.
If you want to transfer a set of known objects between different parts
of your applications, it is convenient to serialize them in a memory file
and to wrap them up in a COleDataSource
.
Let's start: This is my object:
class CLeopoldoOb : public CObject
{
DECLARE_SERIAL(CLeopoldoOb)
public:
CLeopoldoOb();
protected:
};
And this is my first version of the routine that wrapped up a set
of those objects in the COleDataSource
:
...
COleDataSource* pDataSource = new COleDataSource;
if ( !pDataSource ) {
return NULL;
}
CSharedFile sf (GMEM_MOVEABLE|GMEM_DDESHARE|GMEM_ZEROINIT);
CArchive ar (&sf, CArchive::store);
ar << m_nObjectCount;
for ( int i = 0; i < m_nObjectCount; i++ ) {
CLeopoldoOb leo;
...
ar << &leo
}
ar.Flush ();
ar.Close ();
pDataSource->CacheGlobalData (g_cfLeopoldo, sf.Detach ());
pDataSource->DoDragDrop ();
if ( pDataSource->m_dwRef <= 1 ) {
delete pDataSource;
}
else {
pDataSource->ExternalRelease ();
}
...
And let's see the routine that unwraps the objects when they are
dropped:
...
BOOL CMainFrame::OnDrop (COleDataObject* pDataObject,
DROPEFFECT dropEffect, CPoint point)
{
if ( pDataObject->IsDataAvailable (g_cfLeopoldo) ) {
CLeopoldoOb *pOb;
HGLOBAL hMem = pDataObject->GetGlobalData (g_cfLeopoldo);
CMemFile mf;
UINT nCount;
mf.Attach ((BYTE *)::GlobalLock (hMem), ::GlobalSize (hMem));
CArchive ar (&mf, CArchive::load);
ar >> nCount;
for ( UINT n = 0; n < nCount; n++ ) {
try {
ar >> pOb;
....
delete pOb;
}
catch { CException *pEx ) {
pEx->Delete ();
}
}
ar.Close ();
mf.Detach ();
::GlobalUnlock (hMem);
::GlobalFree (hMem);
return nCount > 0;
}
return FALSE;
}
What happened? Apparently this stuff worked, if you had only one object
wrapped up. OK. Sometimes the application crashed but basically it worked.
If instead more than one object was wrapped up, two strange things happened:
- Only the first object was unwrapped
- A
CArchiveException
with badIndex
sometimes occurred
When tested under the debugger, I saw that the creation of the object
from the serialization failed starting from the second time. Since there was
definitively no error in the load routine, I reached the conclusion that the
archive data must be messed up.
What Went Wrong?
After debugging in the profundities of CArchive
I discovered that
the process of storing dynamic objects is not as simple as I imagined it
should be.
Obviously my error was during the creation of the archive. There are two
important rules:
- The objects must be dynamically created (with
CRuntimeClass::CreateObject
). - The objects must remain valid until the serialization has been finished.
If you are asking why, take a look into the source of CArchive
and you will see that the archive stores additional information about those
objects during its life. Furthermore the entire MFC seems to have knowledge
about all dynamically created CObject
s.
The Right Way
First of all we need a little helper class that will make life simpler:
class CAllocatedObArray : public CObArray
{
public:
CAllocatedObArray () { }
virtual ~CAllocatedObArray () { RemoveAll (); }
public:
void RemoveAll ();
};
void CAllocatedObArray::RemoveAll ()
{
for ( int i = 0; i < GetSize (); i++ ) {
CObject *pObject = GetAt (i);
if ( pObject ) {
delete pObject;
}
}
}
The following is the modified wrapper routine. You will see that all
objects are created dynamically and stored into this array. After the
serialization has finished, the array goes out of scope and our allocated
objects will be automatically be destroyed.
...
COleDataSource* pDataSource = new COleDataSource;
if ( !pDataSource ) {
return NULL;
}
CSharedFile sf (GMEM_MOVEABLE|GMEM_DDESHARE|GMEM_ZEROINIT);
CArchive ar (&sf, CArchive::store);
CAllocatedObArray tmpArray;
CLeopoldoOb leo;
ar << m_nObjectCount;
for ( int i = 0; i < m_nObjectCount; i++ ) {
CLeopoldoOb *pLeo = (CLeopoldoOb *) leo.GetRuntimeClass ()->CreateObject ();
tmpArr.Add (pLeo);
...
ar << pLeo
}
ar.Flush ();
ar.Close ();
pDataSource->CacheGlobalData (g_cfLeopoldo, sf.Detach ());
pDataSource->DoDragDrop ();
if ( pDataSource->m_dwRef <= 1 ) {
delete pDataSource;
}
else {
pDataSource->ExternalRelease ();
}
...
This works. Probably this will be nothing new for all experts among you,
but since I did make this error after years of programming MFC, I hope
that this article will help some beginner.