New Term: The property of an object to be stored and loaded is persistence, which is also defined as the capability of an object to remember its state between executions.
Serialization is the way in which classes derived from CDocument store and retrieve data from an archive, which is usually a file. Figure 21.1 shows the interaction between a serialized object and an archive.
Serializing an object to and from an archive.
When an object is serialized, information about the type of object is written to the storage along with information and data about the object. When an object is deserialized, the same process happens in reverse, and the object is loaded and created from the input stream.
int nFoo = 5; fileStream << nFoo;If a file contains an int value, it can be read from the stream in the following way:
fileStream >> nFoo;A persistent object can be serialized and deserialized using a similar syntax, no matter how complicated the object's internal structure. The alternative is to create routines that understand how every object is implemented and handle the process of storing and retrieving data from files.
Using serialization to store objects is much more flexible than writing specialized functions that store data in a fixed format. Objects that are persistent are capable of storing themselves, instead of relying on an external function to read and write them to disk. This makes a persistent object much easier to reuse because the object is more self-contained.
Persistent objects also help you easily write programs that are saved to storage. An object that is serialized might be made up of many smaller objects that are also serialized. Because individual objects are often stored in a collection, serializing the collection also serializes all objects contained in the collection.
file_object << dataIn a similar way, whenever input is performed and the objects are separated by a >>, as in the following code line, a new value for the variable is retrieved from the input object:
file_object >> dataIn C++, unlike some other languages, input and output are controlled by the interaction between file and variable objects. The exact process used for input and output is controlled by the way in which the classes implement the >> and << operators.
For the topics in this section, you create a persistent class named CUser, along with the helper functions required to serialize a collection of CUser objects. Each CUser object contains a customer name and email address.
myObject.Serialize( ar );If the object isn't derived from CObject--such as a CRect object--you should use the inser-tion operator in the following way:
ar << rcWnd;This insertion operator is overloaded in the same way it is for cout, cin, and cerr, which were used in the first two sections for console mode input and output.
The most commonly used virtual function in CObject is Serialize, which is called to serialize or deserialize the object from a CArchive object. This function is declared as virtual so that any persistent object can be called through a pointer to CObject in the following way:
CObject* pObj = GetNextObject(); pObj->Serialize( ar );As discussed later in the section "Using the Serialization Macros," when you're deriving a persistent class from CObject, you must use two macros to help implement the serialization functions.
When a CArchive object is created, it is defined as used for either input or output but never both. You can use the IsStoring and IsLoading functions to determine whether a CArchive object is used for input or output, as shown in Listing 21.1.
CMyObject:Serialize( CArchive& ar )
{
if( ar.IsStoring() )
// Write object state to ar
else
// Read object state from ar
}
These operators are defined for all basic C++ types, as well as a few commonly used classes not derived from CObject, such as the CString, CRect, and CTime classes. The insertion and extraction operators return a reference to a CArchive object, enabling them to be chained together in the following way:
archive << m_nFoo << m_rcClient << m_szName;When used with classes that are derived from CObject, the insertion and extraction operators allocate the memory storage required to contain an object and then call the object's Serialize member function. If you don't need to allocate storage, you should call the Serialize member function directly.
As a rule of thumb, if you know the type of the object when it is deserialized, call the Serialize function directly. In addition, you must always call Serialize exclusively. If you use Serialize to load or store an object, you must not use the insertion and extraction operators at any other time with that object.
Time Saver: A good place to put the DECLARE_SERIAL macro is on the first line of the class declaration, where it serves as a reminder that the class can be serialized.
#ifndef CUSER
#define CUSER
class CUser : public CObject
{
DECLARE_SERIAL(CUser);
public:
// Constructors
CUser();
CUser( const CString& szName, const CString& szAddr );
// Attributes
void Set( const CString& szName, const CString& szAddr );
CString GetName() const;
CString GetAddr() const;
// Operations
virtual void Serialize( CArchive& ar );
// Implementation
private:
// The user's name
CString m_szName;
// The user's e-mail addresss
CString m_szAddr;
};
#endif CUSER
The member functions for the CUser class, including the IMPLEMENT_SERIAL macro, are provided in Listing 21.3. Save this source code in the Customers project directory as Users.cpp.
#include "stdafx.h"
#include "Users.h"
IMPLEMENT_SERIAL( CUser, CObject, 1 );
CUser::CUser() { }
CUser::CUser( const CString& szName, const CString& szAddr )
{
Set( szName, szAddr );
}
void CUser::Set( const CString& szName, const CString& szAddr )
{
m_szName = szName;
m_szAddr = szAddr;
}
CString CUser::GetName() const
{
return m_szName;
}
CString CUser::GetAddr() const
{
return m_szAddr;
}
void CUser::Serialize( CArchive& ar )
{
if( ar.IsLoading() )
{
ar >> m_szName >> m_szAddr;
}
else
{
ar << m_szName << m_szAddr;
}
}
By default, the template-based collection classes perform a bitwise write when serializing a collection and a bitwise read when deserializing an archive. This means that the data stored in the collection is literally written, bit by bit, to the archive. Bitwise serialization is a problem when you use collections to store pointers to objects. For example, the Customers project uses the CArray class to store a collection of CUser objects. The declaration of the CArray member is as follows:
CArray<CUser*, CUser*&> m_setOfUsers;Because the m_setOfUsers collection stores CUser pointers, storing the collection using a bitwise write will only store the current addresses of the contained objects. This information becomes useless when the archive is deserialized.
Most of the time, you must implement a helper function to assist in serializing a template-based collection. Helper functions don't belong to a class; they are global functions that are overloaded based on the function signature. The helper function used when serializing a template is SerializeElements. Figure 21.2 shows how you call the SerializeElements function to help serialize items stored in a collection.
The SerializeElements helper function.
A version of SerializeElements used with collections of CUser objects is provided in List- ing 21.5.
void AFXAPI SerializeElements( CArchive& ar,
CUser** pUser,
int nCount )
{
for( int i = 0; i < nCount; i++, pUser++ )
{
if( ar.IsStoring() )
{
(*pUser)->Serialize(ar);
}
else
{
CUser* pNewUser = new CUser;
pNewUser->Serialize(ar);
*pUser = pNewUser;
}
}
}
The SerializeObjects function has three parameters:
The CDocument member functions required to perform serialization in a Document/View application are mapped onto the New, Open, Save, and Save As commands available from the File menu. These member functions take care of creating or opening a document, tracking the modification status of a document, and serializing it to storage.
When documents are loaded, a CArchive object is created for reading, and the archive is deserialized into the document. When documents are saved, a CArchive object is created for writing, and the document is written to the archive. At other times, the CDocument class tracks the current modification status of the document's data. If the document has been updated, the user is prompted to save the document before closing it.
The Document/View support for serialization greatly simplifies the work required to save and load documents in a Windows program. For a typical program that uses persistent objects, you must supply only a few lines of source code to receive basic support for serialization in a Document/View program. The Customers project has about a page of Document/View source code; most of it is for handling input and output required for the example.
The routines used by CArchive for reading and writing to storage are highly optimized and have excellent performance, even when you're serializing many small data objects. In most cases, it is difficult to match both the performance and ease of use that you get from using the built-in serialization support offered for Document/View applications.
There are five phases in a document's life cycle:
Both SDI and MDI applications call the OnNewDocument function to initialize a document object. The default version of OnNewDocument calls the DeleteContents function to reset any data contained by the document. ClassWizard can be used to add a DeleteContents function to your document class. Most applications can just add code to DeleteContents instead of overriding OnNewDocument.
The major functions called when you store a document.
The default version of OnOpenDocument is sufficient for most applications. However, if your application stores data in a different way--for example, in several smaller files or in a database--you should override OnOpenDocument.
When the user selects Save As from the File menu, a Common File dialog box collects filename information. After the user selects a filename, the program calls the same CDocument functions, and the serialization process works as described previously.
The major functions called when you close a document.
If the user made changes to the document, the program displays a message box that asks the user whether the document's unsaved changes should be saved. If the user elects to save the document, the Serialize function is called. The document is then closed by calling DeleteContents and closing all views for the document.
The major functions called when you open a document.
// Attributes public: int GetCount() const; CUser* GetUser( int nUser ) const; protected: CArray<CUser*, CUser*&> m_setOfUsers;You should make two other changes to the CustomersDoc.h header file. First, because the CArray template m_setOfUsers is declared in terms of CUser pointers, you must add an #include statement for the Users.h file. Second, you use a version of the SerializeElements helper function so you need a declaration of that global function. Add the source code provided in Listing 21.7 to the top of CustomersDoc.h.
#include "Users.h" void AFXAPI SerializeElements( CArchive& ar, CUser** pUser, int nCount );Because the CCustomerDoc class contains a CArray member variable, the template collection declarations must be included in the project. Add an #include statement to the bottom of the StdAfx.h file:
#include "afxtempl.h"
The dialog box used in the Customers sample project.
Give the new dialog box a resource ID of IDD_USER_DLG. The two edit controls are used to add user names and email addresses to a document contained by the CCustomerDoc class. Use the values from Table 21.1 for the two edit controls.
| Edit Control | Resource ID |
| Name | IDC_EDIT_NAME |
| Address | IDC_EDIT_ADDR |
Using ClassWizard, add a class named CUsersDlg to handle the new dialog box. Add two CString variables to the class using the values from Table 21.2.
| Resource ID | Name | Category | Variable Type |
| IDC_EDIT_NAME | m_szName | Value | CString |
| IDC_EDIT_ADDR | m_szAddr | Value | CString |
| Menu ID | Caption | Event | Function Name |
| ID_EDIT_USER | Add User... | COMMAND | OnEditUser |
Listing 21.8 contains the complete source code for the OnEditUser function, which handles the message sent when the user selects the new menu item. If the user clicks OK, the contents of the dialog box are used to create a new CUser object, and a pointer to the new object is added to the m_setOfUsers collection. The SetModifiedFlag function is called to mark the document as changed. Add the source code provided in Listing 21.8 to the CCustomersDoc::OnEditUser member function.
void CCustomersDoc::OnEditUser()
{
CUsersDlg dlg;
if( dlg.DoModal() == IDOK )
{
CUser* pUser = new CUser( dlg.m_szName, dlg.m_szAddr );
m_setOfUsers.Add( pUser );
UpdateAllViews( NULL );
SetModifiedFlag();
}
}
Add the source code provided in Listing 21.9 to the CustomersDoc.cpp
source file. These functions provide access to the data contained by the
document. The view class, CCustomerView, calls the two CCustomersDoc
member functions provided in Listing 21.9 when updating the view window.
int CCustomersDoc::GetCount() const
{
return m_setOfUsers.GetSize();
}
CUser* CCustomersDoc::GetUser( int nUser ) const
{
CUser* pUser = 0;
if( nUser < m_setOfUsers.GetSize() )
pUser = m_setOfUsers.GetAt( nUser );
return pUser;
}
Every document needs a Serialize member function. The CCustomersDoc
class has only one data member so its Serialize function deals
only with m_setOfUsers, as shown in Listing 21.10. Add this source
code to the CCustomersDoc::Serialize member function.
void CCustomersDoc::Serialize(CArchive& ar)
{
m_setOfUsers.Serialize( ar );
}
As discussed earlier in this section, the CArray class uses the
SerializeElements helper function when the collection is serialized.
Add the SerializeElements function that was provided earlier in
Listing 21.5 to the CustomersDoc.cpp source file.
Add two #include statements to the CustomersDoc.cpp file so that the CCustomersDoc class can have access to declarations of classes used by CCustomersDoc. Add the source code from Listing 21.11 near the top of the CustomersDoc.cpp file, just after the other #include statements.
#include "Users.h" #include "UsersDlg.h"
AppWizard creates a skeleton version of the CCustomersView::OnDraw function. Add the source code from Listing 21.12 to OnDraw so that the current document contents are displayed in the view. Because this isn't a scrolling view, a limited number of items from the document can be displayed.
void CCustomersView::OnDraw(CDC* pDC)
{
CCustomersDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// Calculate the space required for a single
// line of text, including the inter-line area.
TEXTMETRIC tm;
pDC->GetTextMetrics( &tm );
int nLineHeight = tm.tmHeight + tm.tmExternalLeading;
CPoint ptText( 0, 0 );
for( int nIndex = 0; nIndex < pDoc->GetCount(); nIndex++ )
{
CString szOut;
CUser* pUser = pDoc->GetUser( nIndex );
szOut.Format( "User = %s, email = %s",
pUser->GetName(),
pUser->GetAddr() );
pDC->TextOut( ptText.x, ptText.y, szOut );
ptText.y += nLineHeight;
}
}
As with most documents, the CCustomersDoc class calls UpdateAllViews
when it is updated. The MFC framework then calls the OnUpdate
function for each view connected to the document.
Use ClassWizard to add a message-handling function for CCustomersView::OnUpdate and add the source code from Listing 21.13 to it. The OnUpdate function invalidates the view; as a result, the view is redrawn with the updated contents.
void CCustomersView::OnUpdate( CView* pSender,
LPARAM lHint,
CObject* pHint)
{
InvalidateRect( NULL );
}
Add an #include statement to the CustomersView.cpp file
so that the view can use the CUser class. Add the include statement
beneath the other include statements in CustomersView.cpp.
#include "Users.h"Compile and run the Customers project. Add names to the document by selecting Add User from the Edit menu. Figure 21.7 shows an example of the Customers project running with a few email addresses.
The Customers example with some email addresses.
Serialize the contents of the document by saving it to a file, and close the document. You can reload the document by opening the file.
A The MFC serialization process is incompatible with abstract base classes. You can never have an instance of an abstract class; because each serialized object is created as it is read from an instance of CArchive, MFC will attempt to create an abstract class. This isn't allowed by the C++ language definition.
Q Does it matter where I put the DECLARE_SERIAL macro in my class declaration? I added the macro to my source file, and now I receive compiler errors.
A The serialization macros can go anywhere, but you must be sure to specify the access allowed for the class declaration after the macro. Place a public, private, or protected label immediately after the macro and your code should be fine.
2. What is serialization?
3. What is the difference between serialization and deserialization?
4. What MFC class is used to represent a storage object?
5. What virtual function is implemented by all persistent classes?
6. What is the name of the helper function that assists in serializing a template collection that contains pointers?
2. Modify the Customers project so that the number of items stored in a document is displayed when the application starts or a file is opened.
Download Sample Programs - Customers.zip
|
|
|
|