Click here to Skip to main content
Click here to Skip to main content

Implementing MFC-Style Serialization in .NET - Part 2 - Reading MFC Serialized Objects into .NET

, 21 Jan 2009
Rate this:
Please Sign up or sign in to vote.
This article shows how to read into a .NET application binary files created using MFC serialization.

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
}

/// <summary>
/// Class allows reading objects serialize using MFC CArchive
/// </summary>
    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)
        {
            // MFC stores decimal as 32-bit status value, 32-bit high value,
            // and 32-bit low value
            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)
        {
            // MFC stores bools as 32-bit "long"

            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); // status is a 32-bit "long" in C++

            // MFC stores dates as 8-byte double
            Double l;
            base.Read(out l);
            dt = DateTime.FromOADate(l);

            if (status == (UInt32)OleDateTimeStatus.Null || 
                status == (UInt32)OleDateTimeStatus.Invalid)
            {
                // in this situation, the date is not valid.  
                // One option is to set to the initialized OLE Date value of 0.0
                dt = DateTime.FromOADate(0.0);
            }
        }

        public void Read(out DateTime? dt)
        {
            UInt32 status;
            base.Read(out status); // status is a 32-bit "long" in C++

            Double l;
            base.Read(out l);
            dt = DateTime.FromOADate(l);

            // read in nullable type
            if (status == (UInt32)OleDateTimeStatus.Null || 
                status == (UInt32)OleDateTimeStatus.Invalid)
            {
                dt = null;
            }
        }

        new public void Read(out string s)
        {
            s = MFCStringReader.ReadCString(this.reader);
        }

        // Convert current low and high to 8-Byte C++ CURRENCY structure 
        static public Int64 MakeInt64(Int32 l1, Int32 l2)
        {
            return ((UInt32)(((UInt32)(l1)) | ((UInt32)((UInt32)(l2))) << 32));
        }

    } // end of class

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; // if we get ANSI, convert

        UInt32 nNewLen = ReadStringLength(reader);
        if (nNewLen == unchecked((UInt32)(-1)))
        {
            nConvert = 1 - nConvert;
            nNewLen = ReadStringLength(reader);
            if (nNewLen == unchecked((UInt32)(-1)))
                return str;
        }

        // set length of string to new length
        UInt32 nByteLen = nNewLen;
        nByteLen += (UInt32)(nByteLen * (1 - nConvert)); // bytes to read

        // read in the characters
        if (nNewLen != 0)
        {
            // read new data
            byte[] byteBuf = reader.ReadBytes((int)nByteLen);

            // convert the data if as necessary
            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;

        // attempt BYTE length first
        byte bLen = reader.ReadByte();

        if (bLen < 0xff)
            return bLen;

        // attempt WORD length
        UInt16 wLen = reader.ReadUInt16();
        if (wLen == 0xfffe)
        {
            // UNICODE string prefix (length will follow)
            return unchecked((UInt32)(-1));
        }
        else if (wLen == 0xffff)
        {
            // read DWORD of length
            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
{

// Construction
public:
	CPerson();
	~CPerson();
	DECLARE_SERIAL(CPerson);

// Attributes
public:
	int m_nAge;
	double m_fWeight;
	COleDateTime m_dtBirthday;
	CString m_strName;
	BOOL m_bDeceased;
	COleCurrency m_curAccountBalance;

	CPersonList m_Children;

// operations
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;

		// write the list of children
		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;

		// write the list of children
		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
{
// Construction
public:
	CPersonList();
	~CPersonList();
	DECLARE_SERIAL(CPersonList);

// operations
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; // store the number of objects

		Pos = GetHeadPosition();
		while (Pos != NULL)
		{
			pPerson = (CPerson*)GetNext(Pos);
			pPerson->Serialize(ar);			
		}

	}
	else
	{
		ar>>dwVersion;

		ar>>nMax;

		Clear();  // need to clear the current list

		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)
    {
        // with each change in version, increment this value
        UInt32 version = 0x00000000;

        if (ar.IsStoring())
        {
            throw new NotImplementedException("MfcArchive can't store");
        }
        else
        {
            ar.Read(out version);

            if (version > 0x00000000)
            {
                throw new VersionException();
            }

            // be sure to read in the order in which they were stored
            ar.Read(out Age);
            ar.Read(out Weight);
            ar.Read(out Birthday);
            ar.Read(out Name);
            ar.Read(out Deceased);
            ar.Read(out AccountBalance);

            // read in the list of children
            Children.Serialize(ar);
        }
    } // end of serialize

} // end of class

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); // read the version flag

                if (version > 0x00000000)
                {
                    throw new VersionException();
                }

                ar.Read(out max); // read the number of objects

                int n;
                for (n = 0; n < max; ++n)
                {
                    Person person = new Person();
                    person.Serialize(ar);
                    Add(person);
                }
            }

        } // end serializing
    }

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Robert Pittenger, MCPD-EAD
President Starpoint Software Inc.
United States United States
Bob Pittenger is founder and President of Starpoint Software Inc. He holds a B.A. degree from Miami University, M.S. and Ph.D. degrees from Purdue University, and an MBA from Xavier University. He has been programming since 1993, starting with Windows application development in C++/MFC and moving to C# and .NET around 2005 and is a .NET Microsoft Certified Professional Developer.
 
Bob is the author of two books:
Billionaire: How the Ultra-Rich Built Their Fortunes Through Good and Evil and What You Can Learn from Them
and
Wealthonomics: The Most Important Economic and Financial Concepts that Can Make You Rich Fast.
Visit http://www.billionairebook.net for more information.

Comments and Discussions

 
QuestionWhat if MFC application uses CTime types instead of COleDateTime? Pinmemberzolitamasi19-Feb-10 6:31 
GeneralType information and object graphs PinmemberDanielEarwicker23-Jan-09 0:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140814.1 | Last Updated 21 Jan 2009
Article Copyright 2009 by Robert Pittenger, MCPD-EAD
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid