Introduction
In Part 1, I showed how to implement MFC-style serialization in .NET using an Archive
class similar to MFC's CArchive
. In Part 2, I will show how Archive
can serve as a base class for MfcArchive
- a .NET class that allows reading MFC-serialized objects into .NET applications, with proper conversion of CString
, COleDateTime
, and COleCurrency
.
The MfcArchive Class
In Part 1, I introduced the Archive
class which allowed reading and writing values to/from a System.IO.Stream
object. From that base class, I derive the MfcArchive
class. The MfcArchive
class supports reading only (I did not implement any functionality to write serialization in MFC compatible format). I override the Read
function for any types that are implemented differently in MFC including strings, dates, Boolean
, and Decimal
(currency).
public enum OleDateTimeStatus
{
Valid = 0,
Invalid = 1,
Null = 2
};
public enum OleCurrencyStatus
{
Valid = 0,
Invalid = 1,
Null = 2
}
public class MfcArchive : Archive
{
public MfcArchive(Stream _stream, ArchiveOp _op)
: base(_stream, _op)
{
if (_op == ArchiveOp.store)
{
throw new NotImplementedException(
"Writing to MFC compatible serialization is not supported.");
}
}
new public void Read(out Decimal d)
{
Int32 status, high;
UInt32 low;
base.Read(out status);
base.Read(out high);
base.Read(out low);
if (status != (int)OleCurrencyStatus.Valid)
{
d = 0;
}
else
{
Int64 final = MakeInt64((int)low, high);
d = Decimal.FromOACurrency(final);
}
}
new public void Read(out Boolean b)
{
Int32 l;
base.Read(out l);
if (l == 0) b = false;
else b = true;
}
new public void Read(out DateTime dt)
{
UInt32 status;
base.Read(out status);
Double l;
base.Read(out l);
dt = DateTime.FromOADate(l);
if (status == (UInt32)OleDateTimeStatus.Null ||
status == (UInt32)OleDateTimeStatus.Invalid)
{
dt = DateTime.FromOADate(0.0);
}
}
public void Read(out DateTime? dt)
{
UInt32 status;
base.Read(out status);
Double l;
base.Read(out l);
dt = DateTime.FromOADate(l);
if (status == (UInt32)OleDateTimeStatus.Null ||
status == (UInt32)OleDateTimeStatus.Invalid)
{
dt = null;
}
}
new public void Read(out string s)
{
s = MFCStringReader.ReadCString(this.reader);
}
static public Int64 MakeInt64(Int32 l1, Int32 l2)
{
return ((UInt32)(((UInt32)(l1)) | ((UInt32)((UInt32)(l2))) << 32));
}
}
Status enumerations are the same as from MFC.
DateTime
and Decimal
(COleCurrency
in MFC) pose particular problems for handling invalid values. For DateTime
, I decided to set the DateTime
value to the COleDateTime
value of 0.0 (Midnight, December 30, 1899) if the date is invalid. You could set this to DateTime.MinValue
if you prefer. Also, I implemented a function to read nullable DateTime
values and set the value to null if the DateTime
is not valid. The same method could be used for currency values.
The MfcStringReader Class
The most complicated code is required to read and convert CString
values. CStrings in MFC could be serialized in either ANSI or Unicode and strings. The string content is preceded by the length of the string. To read and convert strings, I use the MfcStringReader
helper class. This is a slight modification of code which I believe was originally written by Luis Barreira and posted to bytes.com. It is basically a C# conversion of the internal C++ MFC code for serializing a CString
.
static public class MFCStringReader
{
static public string ReadCString(BinaryReader reader)
{
string str = "";
int nConvert = 1;
UInt32 nNewLen = ReadStringLength(reader);
if (nNewLen == unchecked((UInt32)(-1)))
{
nConvert = 1 - nConvert;
nNewLen = ReadStringLength(reader);
if (nNewLen == unchecked((UInt32)(-1)))
return str;
}
UInt32 nByteLen = nNewLen;
nByteLen += (UInt32)(nByteLen * (1 - nConvert));
if (nNewLen != 0)
{
byte[] byteBuf = reader.ReadBytes((int)nByteLen);
StringBuilder sb = new StringBuilder();
if (nConvert != 0)
{
for (int i = 0; i < nNewLen; i++)
sb.Append((char)byteBuf[i]);
}
else
{
for (int i = 0; i < nNewLen; i++)
sb.Append((char)(byteBuf[i * 2] + byteBuf[i * 2 + 1] * 256));
}
str = sb.ToString();
}
return str;
}
static private UInt32 ReadStringLength(BinaryReader reader)
{
UInt32 nNewLen;
byte bLen = reader.ReadByte();
if (bLen < 0xff)
return bLen;
UInt16 wLen = reader.ReadUInt16();
if (wLen == 0xfffe)
{
return unchecked((UInt32)(-1));
}
else if (wLen == 0xffff)
{
nNewLen = reader.ReadUInt32();
return nNewLen;
}
else
return wLen;
}
}
Implementing The Code
To demonstrate and test the code, I implementing a simple MFC dialog-based project using Visual C++ 6.0. The project writes a simple hierarchy of CPerson
objects to a file. I then read the same data into a simple hierarchy of Person
objects in a C# application. It is important to see how the process works with hierarchical objects - something that regular .NET serialization takes care of automatically.
The Example Project in Visual C++ 6.0
The CPerson Header File
As in Part 1, I created a Person
class to demonstrate serialization. The Person
class includes a list of CPerson
objects - children of the Person
- to demonstrate serializing a hierarchy.
#include "PersonList.h"
class CPerson : public CObject
{
public:
CPerson();
~CPerson();
DECLARE_SERIAL(CPerson);
public:
int m_nAge;
double m_fWeight;
COleDateTime m_dtBirthday;
CString m_strName;
BOOL m_bDeceased;
COleCurrency m_curAccountBalance;
CPersonList m_Children;
public:
virtual void Serialize(CArchive& ar);
};
The CPerson Implementation File
#include "stdafx.h"
#include "Person.h"
IMPLEMENT_SERIAL(CPerson, CObject, 1)
CPerson::CPerson()
{
m_nAge = 0;
}
CPerson::~CPerson()
{
}
void CPerson::Serialize(CArchive& ar)
{
DWORD dwVersion = 0x00000000;
if (ar.IsStoring())
{
ar<<dwVersion;
ar<<m_nAge;
ar<<m_fWeight;
ar<<m_dtBirthday;
ar<<m_strName;
ar<<m_bDeceased;
ar<<m_curAccountBalance;
m_Children.Serialize(ar);
}
else
{
ar>>dwVersion;
ar>>m_nAge;
ar>>m_fWeight;
ar>>m_dtBirthday;
ar>>m_strName;
ar>>m_bDeceased;
ar>>m_curAccountBalance;
m_Children.Serialize(ar);
}
}
The CPersonList Header File
.NET has nice classes for collections, including the generics, and with garbage collection, managing collections is much easier. In MFC, I had to use the CObList
class. It is best to created a derived class for each type of collection.
class CPersonList : public CObList
{
public:
CPersonList();
~CPersonList();
DECLARE_SERIAL(CPersonList);
public:
virtual void Serialize(CArchive& ar);
};
The CPersonList Implementation
#include "stdafx.h"
#include "PersonList.h"
#include "Person.h"
IMPLEMENT_SERIAL(CPersonList, CObList, 0)
CPersonList::CPersonList()
{
}
CPersonList::~CPersonList()
{
Clear();
}
void CPersonList::Clear()
{
while (GetHeadPosition())
{
CPerson* pPerson = (CPerson*)RemoveHead();
delete pPerson;
}
}
void CPersonList::Serialize(CArchive &ar)
{
DWORD dwVersion = 0x00000000;
int nMax = 0;
POSITION Pos;
CPerson* pPerson;
if (ar.IsStoring())
{
ar<<dwVersion;
nMax = this->GetCount();
ar<<nMax;
Pos = GetHeadPosition();
while (Pos != NULL)
{
pPerson = (CPerson*)GetNext(Pos);
pPerson->Serialize(ar);
}
}
else
{
ar>>dwVersion;
ar>>nMax;
Clear();
int n;
for (n = 0; n < nMax; ++n)
{
pPerson = new CPerson();
pPerson->Serialize(ar);
AddTail(pPerson);
}
}
}
Writing a File Using C++ Serialization
Serialization in MFC is usually done from the CDocument
class which creates the CArchive
automatically. This code shows how to create a CArchive
manually and serialize directly to a file.
CFile f;
CFileException fe;
CString s;
if (!f.Open(strFileName, CFile::modeWrite | CFile::modeCreate, &fe))
{
s.Format(_T("Failed to open file %s for writing."), strFileName);
AfxMessageBox(s, MB_OK | MB_ICONHAND, 0);
return;
}
try
{
CArchive ar(&f, CArchive::store);
person.Serialize(ar);
}
catch (CException* e)
{
s.Format(_T("Failed to write file %s."), strFileName);
AfxMessageBox(s, MB_OK | MB_ICONHAND, 0);
e->Delete();
}
The .NET Implementation of Person and Person List
To read the object into my .NET application, I have to create Person
objects with the same properties.
public class Person
{
public string Name;
public int Age;
public double Weight;
public float Height;
public DateTime Birthday;
public Char Sex;
public bool Deceased;
public decimal AccountBalance;
public PersonList Children;
public Person()
{
Children = new PersonList();
}
public void WriteToConsole()
{
Console.WriteLine("Name: " + Name);
Console.WriteLine("Age: " + Age);
Console.WriteLine("Weight: " + Weight);
Console.WriteLine("Height: " + Height);
Console.WriteLine("Birthday: " + Birthday);
Console.WriteLine("Sex: " + Sex);
Console.WriteLine("Deceased: " + Deceased);
Console.WriteLine("Account Balance: " + AccountBalance);
Console.WriteLine("{0} has {1} children.", Name, Children.Count);
foreach (Person child in Children)
{
child.WriteToConsole();
Console.WriteLine();
}
}
virtual public void Serialize(MfcArchive ar)
{
UInt32 version = 0x00000000;
if (ar.IsStoring())
{
throw new NotImplementedException("MfcArchive can't store");
}
else
{
ar.Read(out version);
if (version > 0x00000000)
{
throw new VersionException();
}
ar.Read(out Age);
ar.Read(out Weight);
ar.Read(out Birthday);
ar.Read(out Name);
ar.Read(out Deceased);
ar.Read(out AccountBalance);
Children.Serialize(ar);
}
}
}
The PersonList Class
For the list of children, it is best to derive a class from the generic List<>
.
public class PersonList : List<Person>
{
public void Serialize(MfcArchive ar)
{
UInt32 version = 0x00000000;
Int32 max;
if (ar.IsStoring())
{
throw new NotImplementedException("MfcArchive can't store");
}
else
{
ar.Read(out version);
if (version > 0x00000000)
{
throw new VersionException();
}
ar.Read(out max);
int n;
for (n = 0; n < max; ++n)
{
Person person = new Person();
person.Serialize(ar);
Add(person);
}
}
}
}
The Demo Project
The demo project includes a Visual C++ dialog application that creates a small hierarchy of CPerson
objects and writes them to a file using standard MFC serialization.
The .NET project is a Visual Studio 2005 project with a console application that reads the file created from the C++/MFC application, and creates a .NET hierarchy of Person
objects with the same properties.
Potential Pitfalls in Real-World Applications
In my example, I bypassed the CDocument
serialization and tested only by serializing directly to a file. In the standard MFC Document/View architecture, serialization is handled by CDocument
and derived classes. I quickly tested CDocument
and no bytes were prefixed to the file. But I did not test COleDocument
. Be aware of situations where CDocument
or related classes add bytes to the file where you don't expect them.
Also, COleDocument
classes can contain embedded and linked OLE objects. OLE objects don't have an equivalent in .NET so reading those files are problematic, not only in the format, but in what you do with the data once you read it.
Conclusions
This project shows how you can read into a .NET application data objects created using Microsoft Foundation Class (MFC) serialization. This project should be beneficial to anyone converting an MFC application to .NET. Unfortunately, this is code that Microsoft should have provided back in about 2002.