Introduction
Serialization is the standard way MFC proposes to save objects into files. By the use of versionable schemas, your objects can grow and be extended and the application will still be capable of reading older files with lower schema versions.
But if you want to be able to save a file in a way that it might be readable with an older application version, you run into the limits of what MFC provides...
Situation
#define BLOCK_SCHEMA 4
IMPLEMENT_SERIAL(CBlock, CObject, BLOCK_SCHEMA|VERSIONABLE_SCHEMA)
Each class you define as DECLARE_SERIAL
has a static const member of the type CRuntimeClass
, and the schema version you choose for your class is hard-coded into its instantiation.
Whenever you serialize an instance of your class, its CRuntimeClass
member (called classCBlock
in the above example) is serialized into the archive:
void CArchive::WriteClass(const CRuntimeClass* pClassRef)
{
...
this << wNewClassTag;
ClassRef->Store(*this);
..
}
The CRuntimeClass
implementation finally writes the schema version into the archive:
void CRuntimeClass::Store(CArchive& ar) const
{
WORD nLen = (WORD)lstrlenA(m_lpszClassName);
ar << (WORD)m_wSchema << nLen;
ar.Write(m_lpszClassName, nLen*sizeof(char));
}
Now you say: "Aha, I know what he's trying to do…". Exactly. We have to find a way to change the schema version used when writing the file, in order to create a file that is compatible with an older application. But then we have to change the serialize methods of our objects as well. This is actually easier than you think…
Solution
Let's start with the Serialize
method. It consists normally of two parts, the Storing part and the Loading part. The Storing part usually reads the schema version on the first line and then loads the object's members considering the schema:
void CBlock::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
…
}
else
{
UINT nSchema = ar.GetObjectSchema();
ar >> m_iType;
ar >> m_iLines;
if (nSchema > 3)
ar >> m_strDisplayName;
}
}
Now we have to prepare a similar code for the Storing part, which takes the schema version into consideration. We know that the schema is stored in a member of the CRuntimeClass
. Fortunately this member is public
, so we can simply get its value (masked with VERSIONABLE_SCHEMA
):
void CBlock::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
UINT nSchema = classCBlock.m_wSchema & ~VERSIONABLE_SCHEMA;
ar << m_iType;
ar << m_iLines;
if (nSchema > 3)
ar << m_strDisplayName;
}
else
{
UINT nSchema = ar. GetObjectSchema();
ar >> m_iType;
ar >> m_iLines;
if (nSchema > 3)
ar >> m_strDisplayName;
}
}
Isn't this simple? And as you see, the Storing and Loading parts are symmetric, so it's very easy from now on to maintain this code for further schema version changes.
But now comes the more tricky part. We have to tell the CRuntimeClass
that it should use a version number which is different to the one hard-coded. Since the member classCBlock
is static const
, we have to cast it in order to change its members. To make it even easier for me, I defined a macro which does the whole thing for me:
#define SETSCHEMA(name, schema) \
((CRuntimeClass*)&name::class##name)->m_wSchema =
schema | VERSIONABLE_SCHEMA
The code to change the version can now look something like this:
voidPrepareObjectSchema(int nVersion)
{
switch (nVersion)
{
case VERSION3:
{
SETSCHEMA(CBlock, 3);
SETSCHEMA(CLinkedBlock, 1);
SETSCHEMA(CParameter, 3);
break;
}
case VERSION4:
default:
{
SETSCHEMA(CBlock, BLOCK_SCHEMA);
SETSCHEMA(CLinkedBlock, LINKEDBLOCK_SCHEMA);
SETSCHEMA(CParameter, PARAMETER_SCHEMA);
break;
}
}
}
Attention
It is very important that you change the schema versions back to the original values once your file is saved.
Enjoy…