Serialization is the method used by MFC programs to read and write application data to files. In this hour, you will learn about
You will also create an example that uses serialization in a Document/View application.
New Term: Serialization is the process of storing the state of an object for the purpose of loading it at another time.
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 22.1 shows the interaction between a serialized object and an archive.
Figure 22.1.
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.
The goal behind serialization is to make the storage of complex objects as simple and reliable as the storage of the basic data types available in C++. You can store a basic type, such as an int, in a file in the following way:
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.
Using AppWizard, create an MDI project named Customers. This project uses serialization to store a very simple list of customer names and email addresses, using a persistent class named CUser. This project will serve as the basis for examples and source code used in the remainder of this hour.
In Hour 1, "Introducing Visual C++ 5," you used the insertion operator, or <<, to output a value to the screen. This operator is actually the C++ left-shift operator, but it is overloaded so that whenever an output object and variable are separated by a <<, as in the following code line, the variable is written to the output object:
file_object << data
In 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 >> data
In 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 hour, 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.
You use two MFC classes to serialize objects:
Objects are serialized in one of two ways. As a rule of thumb, if an object is derived from CObject, that object's Serialize member function is called in the following way:
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 hours for console mode input and output.
You must use the CObject class for all classes that use the MFC class library's built-in support for serialization. The CObject class contains virtual functions that are used during serialization. In addition, the CArchive class is declared as a "friend" class for CObject, providing it access to private and protected member variables.
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.
The CArchive class is used to model a generic storage object. In most cases, a CArchive object is attached to a disk file. In some cases, however, the object might be connected to an object that only seems to be a file, like a memory location or another type of storage.
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 22.1.
CMyObject:Serialize( CArchive& ar ) { if( ar.IsStoring() ) // Write object state to ar else // Read object state from ar }
The MFC class library overloads the insertion and extraction operators for many commonly used classes and basic types. You often use the insertion operator, <<, to serialize--or store--data to the CArchive object. You use the extraction operator, >>, to deserialize--or load--data from a CArchive object.
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.
There are two macros that you must use when creating a persistent class based on CObject. Use the DECLARE_SERIAL macro in the class declaration file and the IMPLEMENT_SERIAL macro in the class implementation file.
The DECLARE_SERIAL macro takes a single parameter: the name of the class to be serialized. An example of a class that can be serialized is provided in Listing 22.2. Save this source code in the Customers project directory in a file named Users.h.
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 IMPLEMENT_SERIAL macro takes three parameters and is usually placed before any member functions are defined for a persistent class. The parameters for IMPLEMENT_SERIAL are the following:
The schema number is a version number for the class layout used when you're serializing and deserializing objects. If the schema number of the data being loaded doesn't match the schema number of the object reading the file, the program throws an exception. The schema number should be incremented when changes are made that affect serialization, such as adding a class member or changing the serialization order.
The member functions for the CUser class, including the IMPLEMENT_SERIAL macro, are provided in Listing 22.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; }
Every persistent class must implement a Serialize member function, which is called in order to serialize or deserialize an object. The single parameter for Serialize is the CArchive object used to load or store the object. The version of Serialize used by the CUser class is shown in Listing 22.4; add this function to the Users.cpp source file.
void CUser::Serialize( CArchive& ar ) { if( ar.IsLoading() ) { ar >> m_szName >> m_szAddr; } else { ar << m_szName << m_szAddr; } }
You can serialize most MFC collection classes, enabling large amounts of information to be stored and retrieved easily. For example, you can serialize a CArray collection by calling its Serialize member function. As with the other MFC template-based collection classes, you cannot use the insertion and extraction operators with CArray.
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 22.2 shows how you call the SerializeElements function to help serialize items stored in a collection.
Figure 22.2.
The SerializeElements helper function.
A version of SerializeElements used with collections of CUser objects is provided in List- ing 22.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:
In this example, when you're serializing objects to the archive, each CUser object is individually written to the archive. When you're deserializing objects, a new CUser object is created, and that object is deserialized from the archive. The collection stores a pointer to the new object.
The Document/View architecture uses serialization to save or open documents. When a document is saved or loaded, the MFC framework in cooperation with the application's document class creates a CArchive object and serializes the document to or from storage.
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.
As discussed in Hour 9, "The Document/View Architecture," data stored in a Document/View application is contained by a class derived from CDocument. This class also is responsible for controlling the serialization of all data contained by the document class. This includes tracking modifications to the document so that the program can display a warning before the user closes an unsaved document.
There are five phases in a document's life cycle:
You learned about most of these phases in earlier hours. The following sections discuss how each phase affects document serialization.
As discussed in Hour 9, you create MDI and SDI documents differently. An MDI application creates a new CDocument class for every open document, whereas an SDI program reuses a single document.
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.
When the user saves a document by selecting File | Save, the CWinApp::OnFileSave function is called. This function is almost never overridden; it's a good idea to leave it alone because it calls the CDocument::OnOpenDocument function to serialize the document's data. The default version of OnOpenDocument creates a CArchive object and passes it to the document's Serialize member function. Usually, you serialize the data contained in the document in the same way that other member data was serialized earlier this hour. After the document's data has been serialized, the dirty bit is cleared, marking the document as unmodified. The steps involved in storing a document are shown in Figure 22.3.
Figure 22.3.
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.
When the user closes a document, the MFC Document/View framework calls the document object's OnCloseDocument member function, as shown in Figure 22.4. The default version of this function checks the document to make sure that no unsaved changes are lost by calling the IsModified function. If the user did not modify the document object, DeleteContents is called to free the data stored by the document, and all views for the document are closed.
Figure 22.4.
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.
When you're loading a document, the MFC framework calls the document object's OnOpenDocument function. The default version of this function calls the DeleteContents member function and then calls Serialize to load, or deserialize, the archive. The default version of OnOpenDocument, shown in Figure 22.5, is sufficient for almost any application.
The document class used in the Customers project has one new data member, a CArray object that stores a collection of CUser pointers representing a customer list. The document class also has two member functions used to access the array of CUser pointers. Add declarations for m_setOfUsers and two member functions to the CCustomersDoc class, as shown in List- ing 22.6.
Figure 22.5.
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 22.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 to enter data for the Customers example is similar to dialog boxes you created for previous examples. Create a dialog box that contains two edit controls, as shown in Figure 22.6.
Figure 22.6.
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 22.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 22.2.
Resource ID | Name | Category | Variable Type |
IDC_EDIT_NAME | m_szName | Value | CString |
IDC_EDIT_ADDR | m_szAddr | Value | CString |
Use the values from Table 22.3 to add a menu item and message-handling function to the CCustomersDoc class. Add the new menu item, labeled New User..., to the Edit menu in the IDR_CUSTOMTYPE menu resource. To reduce the amount of source code required for this example, handle the menu item directly with the document class. However, the dialog box can also be handled by a view class or CMainFrame.
Menu ID | Caption | Event | Function Name |
ID_EDIT_USER | Add User... | COMMAND | OnEditUser |
Listing 22.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 22.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 22.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 22.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 22.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 hour, the CArray class uses the SerializeElements helper function when the collection is serialized. Add the SerializeElements function that was provided earlier in Listing 22.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 22.11 near the top of the CustomersDoc.cpp file, just after the other #include statements.
#include "Users.h" #include "UsersDlg.h"
The view class, CCustomersView, displays the current contents of the document. When the document is updated, the view is repainted and displays the updated contents. You must update two functions in the CCustomersView class: OnDraw and OnUpdate.
AppWizard creates a skeleton version of the CCustomersView::OnDraw function. Add the source code from Listing 22.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 22.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 22.7 shows an example of the Customers project running with a few email addresses.
Figure 22.7.
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.
In this hour, you learned about serialization and persistence and how they are used in a Document/View application. You also learned about the CDocument functions used for serialization and created a small Document/View serialization example.
The Workshop is designed to help you anticipate possible questions, review what you've learned, and begin thinking ahead to putting your knowledge into practice. The answers to the quiz are in Appendix B, "Quiz Answers."
© Copyright, Macmillan Computer Publishing. All rights reserved.