Fundamentals of Serialization
|
|
|
When performing file processing, the values are
typically of primitive types (char, short,
int, float, or double).
In the same way, we can individually save many values, one at a time. This
technique doesn't include an object created from (as a variable of) a class.
By contrast, serialization is the ability to save an object, such as a
variable of a class, at once. As done for file processing, serialization is
used to save an object to a stream, such as a disk.
|
There are usually two catedories of serialization. One
consists of saving one object to a stream. Another type of serialization
is used to save a collection or list of objects.
As you may be aware already, the primary class used to
perform file processing in MFC is called CArchive. On one
hand, this parent-less class is used to transmit a value to a medium, such
as a disk, where the value would be stored. If the item to be saved is an
object made of disparate values, the CArchive object
would write each of those values in a consequentive manner for later
retrieval. On the other hand, a CArchive class can be
used to get a value from a stream. If the value is actually an object made
of various values, the CArchive object would get those
values and put them together to reconstruct the object that was saved.
To handle a CArchive object, you must
create a file that identifies the process by which the object will be
saved or retrieved. This is done by declaring a CFile or
a CFile-based object. After creating the CFile
object, call its Open() member function and initialize
it. Pass the first argument as the name of the file and the second
argument to specify what operation you want to perform (to read to or
write from a stream). Here is an example:
void CHouseDlg::OnFileProcessing()
{
CFile fleHouse;
fleHouse.Open(L"House2.hs", CFile::modeCreate | CFile::modeWrite);
}
MFC Support for Serialization
|
|
The MFC library has a high level of support for
serialization. It starts with the CObject class that is
the ancestor to most MFC classes. This CObject class is
equipped with a member function named Serialize. Its
syntax is:
virtual void Serialize(CArchive& ar);
As you can see, this member function takes a
CArchive object as argument. This is the object that holds the
value(s) to be be saved. The actual serialization needs more information
than that.
The Process of Serializing an Object
|
|
There are a few steps you should (must) follow to
implement serialization in an MFC application. As mentioned already,
serialization consists of saving an object. This means that you must
either get an object or create one. This also means that you can use an
existing class to serialize or you must first create a class.
To create a class whose objects would be serialized,
derive it from CObject. Here is an example:
class CHouse : public CObject
{
};
Since this is primarily a normal C++ class, you can
(in fact should, even must) add an argument-less constructor (the default
constructor) and an optional destructor to it. Here is an example:
class CHouse : public CObject
{
public:
CHouse();
~CHouse();
};
In C/C++, a macro is an action that the compiler must
perform without checking whether it is right. The MFC provides many types
of macros.
One of the macros of the MFC is named
DECLARE_SERIAL. This allows you to indicate that a class can be
serialized. To use, type it followed by parentheses in the source file of
the class and in a public section. The DECLARE_SERIAL
macro takes one constructor. In the parentheses of the macro, type the
name of the class. This would be done as follows:
class CHouse : public CObject
{
public:
CHouse(void);
~CHouse(void);
DECLARE_SERIAL(CHouse);
};
Implementing a Serial Macro
|
|
The counterpart of the DECLARE_SERIAL
macro is used in the implementation of the class and it is named
IMPLEMENT_SERIAL. To use it, in the body of the implementation of
the class, type it followed by parentheses.
The IMPLEMENT_SERIAL macro takes
three arguments. In its parentheses, type the first argument as the name
of your class and the second argument as its base class. The third
argument is called a schema number and it is a positive integer. This
means that you can specify it as 0, 1, or up. Here is an example:
#include "House.h"
IMPLEMENT_SERIAL(CHouse, CObject, 0)
CHouse::CHouse(void)
{
}
CHouse::~CHouse(void)
{
}
When an object is being saved, that is, during
serialization, the schema number is saved too. When an object is
deserialized, the compiler is given a schema number. During that
serialization, the compiler checks whether both numbers (the number used
to serialize and the number used to deserialize) are the same. There are
two primary options and an alternative:
- If the numbers are the same, the object gets opened and read
- If the number passed for deserialization is not found (at all) or
is different than the number that was used to serialize the same
object, the compiler throws a CArchiveException exception
- As an alternative, you can save an object in more than one version
(more than one schema number), in which case each schema number is
considered its own version, and pass a different number when it
becomes time to deserialize one of them
Preparing for Serialization
|
|
Remember that the CObject class is
equiped with the Serialize() member function. In your
CObject-derived class, you should override that member
function. The CObject::Serialize() member function takes a CArchive
reference as argument. Here is an example of creating it in a header file:
class CHouse : public CObject
{
public:
CHouse();
~CHouse();
public:
void Serialize(CArchive& ar);
DECLARE_SERIAL(CHouse);
};
When overriding the CObject::Serialize()
member function, implement the necessary mechanism that will indicate how
an object of that class is serialized. In the MFC library (unlike some
libraries, such as the .NET Framework), only one member function is used
for both serialization and deserialization. Remember that the
CArchive class is equipped with the IsStoring()
member function that specifies the serialization and deserialization
option. The C++ equivalent operations are << and >> respectively.
When implementing the Serialize()
member function, you can use an if conditional statement
to find out whether the object is being serialized or deserialized. This
can be done as follows:
Header File:
#pragma once
#include "afx.h"
class CHouse : public CObject
{
public:
CHouse(void);
~CHouse(void);
private:
CString HouseNumber;
CString HouseType;
CString Condition;
double MarketValue;
public:
void Serialize(CArchive& ar);
DECLARE_SERIAL(CHouse);
};
Source File:
#include "StdAfx.h"
#include "House.h"
IMPLEMENT_SERIAL(CHouse, CObject, 0)
CHouse::CHouse(void)
{
}
CHouse::~CHouse(void)
{
}
void CHouse::Serialize(CArchive& ar)
{
CObject::Serialize(ar);
if( ar.IsStoring() )
ar << HouseNumber << HouseType << Condition << MarketValue;
else
ar >> HouseNumber >> HouseType >> Condition >> MarketValue;
}
After creating the class, you can serialize. This is
done by calling its Serialize() member function. As you
may know already, this member function takes a CArchive argument. This
means that, first build a CArchive object before calling the
Serialize() member function. The CArchive object takes a
CFile or CFile-based argument that holds the
name of the file to open, and the option that specifies to save the file,
such as CArchive::store. Hee is an example of how this
can be done.
void CHouseDlg::OnBnClickedSave()
{
UpdateData(TRUE);
CFile fleHouse;
fleHouse.Open(L"House1.hse", CFile::modeCreate | CFile::modeWrite);
CArchive ar(&fleHouse, CArchive::store);
house.HouseNumber = "882-100"; // m_HouseNumber;
house.HouseType = "Single Family"; // m_HouseType;
house.Condition = "Excellent"; // m_Condition;
house.MarketValue = 685440; // m_MarketValue;
house.Serialize(ar);
ar.Close();
}
Deserializating an Object
|
|
After saving an object, to deserialize it, once again
you can call its Serialize member function. First create
a CArchive object that holds the name of the file and the
type of operation; in this case, this would be CArchive::load.
Once the object has been deserialized, it holds the values you can then
individually retrieve. Here is an example of how this can be done:
void CHouseDlg::OnBnClickedOpen()
{
UpdateData(TRUE);
CFile fleHouse;
fleHouse.Open(L"House1.hse", CFile::modeRead);
CArchive ar($fleHouse, CArchive::load);
house.Serialize(ar);
m_HouseNumber = house.HouseNumber;
m_HouseType = house.HouseType;
m_Condition = house.Condition;
m_MarketValue = house.MarketValue;
ar.Close();
fleHouse.Close();
UpdateData(FALSE);
}
Serialization and Collections
|
|
Although serialization can be performed on an object,
it becomes redundant if you know the values of the object; you can simply
save them individually. The main importance of serialization is in its
ability to save a list of objects where each item is made of internal
individual values.
As you may know already, the MFC provides two
categories of collection classes: non-templates and template-based. In the
same way, the MFC provides two broad types of serialization. One is meant
for non-template classes and the other for template-based colleciton
classes.
Non-Template Serialization
|
|
The MFC library provides the following classes that
don't use templates: CObArray, CByteArray,
CDWordArray, CPtrArray,
CStringArray, CWordArray, CUIntArray,
CObList, CPtrList, CStringList,
CMapPtrToWord, CMapPtrToPtr,
CMapStringToOb, CMapStringToPtr,
CMapStringToString, CMapWordToOb, and
CMapWordToPtr.
We start with serialization of non-template collection
classes because their objects are really easy to do.
Introduction to an Array of Objects
|
|
One of the collection classes of the MFC library is
the CObArray class. The particularity of this class is that it can
be used to create a list of CObject values. This doesn't mean that
the objects have to fit in any particular MFC scenario. Of course, there
are a few rules you must follow but the main theory is that the class that
would constitute the object must be derived from CObject. Normally,
this is an advantage because such a class would be ready for serialization
and MFC's exception handling.
The CObArray class is part of MFC's collection
classes defined in the afxcoll.h library. Therefore, if you plan to
use it to create and manage your list, make sure you include this header
file where necessary.
Practical
Learning: Introducing the List of Objects
|
|
- Start Microsoft Visual Studio
- To create a new application, on the main menu, click File -> New
Project...
- In the middle list, click MFC Application
- Set the Name to CarRental1
- Click OK
- In the second page of the wizard, click Next
- In the second page, click Dialog Based and click Next
- In the third page, set the Title to Bethesda Car Rental - Cars
Inventory
- Click Finish
- Design the dialog box box as follows:
|
Control |
ID |
Caption |
Other Properties |
Static Text |
|
|
Tag Number: |
|
Edit Control |
|
IDC_TAG_NUMBER |
|
|
Static Text |
|
|
Make: |
|
Edit Control |
|
IDC_MAKE |
|
|
Static Text |
|
|
Model |
|
Edit Control |
|
IDC_MODEL |
|
|
Static Control |
|
|
Year |
|
Edit Control |
|
IDC_YEAR |
|
Align Text: Right |
Button |
|
IDC_FIRST |
First |
|
Button |
|
IDC_PREVIOUS |
Previous |
|
Button |
|
IDC_NEXT |
Next |
|
Button |
|
IDC_LAST |
Last |
|
Button |
|
IDC_NEW_CAR |
New Car |
|
Button |
|
IDC_DELETE_CAR |
Delete Car |
|
Button |
|
IDCANCEL |
Close |
|
|
- Right-click the dialog box and click Class Wizard...
- In the Class Name, make sure CCarRental1Dlg is selected.
Click
the Member Variables tab
- Double-click each edit control and click Add Variable
- Create the Value variables as follows:
ID |
Category |
Type |
Name |
IDC_MAKE |
Value |
CString |
m_Make |
IDC_MODEL |
Value |
CString |
m_Model |
IDC_TAG_NUMBER |
Value |
CString |
m_TagNumber |
IDC_YEAR |
Value |
int |
m_Year |
- Click OK to close the MFC Class Wizard
Setting Up an Array of Objects
|
|
To create an object that you want to handle using a
CObArray collection, the first rule you must follow is that your class
must be based on CObject. If you only plan to create an object and
use it temporarily, this is all you need to do. If you want to be able to
save the list, there are a few more steps you must follow. For example,
you must include the DECLARE_SERIAL macro in the header file of
your class and you must include the IMPLEMENT_SERIAL macro in the
source file of your class. Also, if you plan to save your class, you must
override the derived Serialize() member function. This is necessary
because you must specify what member variable(s) would need to be
persisted.
Practical
Learning: Setting Up a List
|
|
- In the Class View, right-click the name of the project -> Add ->
Class
- In the middle section, click MFC Class
- Click Add
- Set the Class Name to CCar and, in the Base Class, select
CObject
- Click Finish
- Change the header file as follows:
#pragma once
// CCar command target
class CCar : public CObject
{
DECLARE_SERIAL(CCar)
public:
CCar();
CCar(CString tagnbr, CString mk, CString mdl, int yr);
virtual ~CCar();
private:
CString TagNumber;
CString Make;
CString Model;
int Year;
public:
CString getTagNumber(void);
void setTagNumber(CString tag);
CString getMake(void);
void setMake(CString mk);
CString getModel(void);
void setModel(CString mdl);
int getYear(void);
void setYear(int yr);
virtual void Serialize(CArchive& ar);
};
- Access the Car.cpp source file and change it as follows:
// Car.cpp : implementation file
//
#include "stdafx.h"
#include "CarRental1.h"
#include "Car.h"
// CCar
IMPLEMENT_SERIAL(CCar, CObject, 1)
CCar::CCar()
: TagNumber(_T("")), Make(_T("")), Model(_T("")), Year(0)
{
}
CCar::CCar(CString tagnbr, CString mk, CString mdl, int yr)
: TagNumber(tagnbr), Make(mk), Model(mdl), Year(yr)
{
}
CCar::~CCar()
{
}
// CCar member functions
CString CCar::getTagNumber(void)
{
return TagNumber;
}
void CCar::setTagNumber(CString tag)
{
TagNumber = tag;
}
CString CCar::getMake(void)
{
return Make;
}
void CCar::setMake(CString mk)
{
Make = mk;
}
CString CCar::getModel(void)
{
return Model;
}
void CCar::setModel(CString mdl)
{
Model = mdl;
}
int CCar::getYear(void)
{
return Year;
}
void CCar::setYear(int yr)
{
Year = yr;
}
void CCar::Serialize(CArchive& ar)
{
CObject::Serialize(ar);
if (ar.IsStoring())
{
ar << TagNumber << Make << Model << Year;
}
else
{
ar >> TagNumber >> Make >> Model >> Year;
}
}
When you are ready to start the list, like any other
class, you must first declare a variable of type CObArray. To
support this, the CObArray class is equipped with a simple default
constructor. If you are planning to create and manage the list from one
member function or message, you can declare the variable locally. On the
other hand, if you are planning to access the list from various sections
of your code, you should declare the variable globally, for example, in
the header file.
Practical
Learning: Setting Up a List
|
|