
Introduction
This is an article written by a beginner for beginners. Or at least, by a beginner for his own benefit, but he is posting it up on CodeProject because he is hoping that it will be of interest to others too. The style is intended to be useful to someone who is still relatively new (but not completely new) to setting up projects using the Visual Studio 6 IDE. The content of the article may be of interest to someone who is familiar with the technique of serializing an array of simple objects contained in a CObArray container, but who has wondered how this method can be extended to more complex data structures.
Background
I wrote this program to find a way of storing and recalling information about relationships between objects using MFC serialization techniques. From the names of my custom classes I have used (CNode and CSubNode), it may be apparent to the reader that I am interested in investigating ways of modeling networks. The project discussed in this article represents a possible method for storing and recalling various network structures.
Using the code
The zip file that is available with this article contains the entire code for the project described below. The code pertinent to the serialization of a collection of collections is in the Serialize functions of the CSerializeExpDoc, CNode, and CSubNode classes.
In the example project, I have made the first level of objects being collected of type 'CNode' and the objects those objects collect of type 'CSubNode'.
The CNode class inherits from CSubNode which, in turn, inherits from CObject. CNode, therefore, inherits the CObject properties via CSubNode as well as a few other members.
For the serialization to work, the CNode class doesn't have to be derived from CSubNode, but if it wasn't, CNode would explicitly have to inherit from CObject using the following declaration in the file 'Node.h':
class CNode : public CObject
instead of:
class CNode : public CSubNode
Also, the GetString, SetString, and m_strString members would have to be replicated in CNode.
The code for this project has been made available for you to download, peruse, compile, and run as you like.
Here follows a description of creating the project from beginning to end...
Create the skeleton application
Create a Single Document Interface program with Document/View architecture support. The project here is called 'SerializeExp'. De-select ActiveX. You don't specifically need ActiveX here. On stage four of the Application Wizard, click 'Advanced' and choose the file extension to be used by your application's data files. On step 6, change the base class to 'CFormView'. You can now click 'Finish' to build your skeleton project.
Make up the form and prepare the control variables
The necessary controls to be placed on the form can be divided into two groups: those associated with nodes and those associated with sub nodes.
Here are the controls with the View class member variables to be associated with them via the Class Wizard:
| Control Type |
Control ID |
View Class Member Variable |
Other Information |
| group box |
IDC_STATIC |
- |
Caption: Node |
| static text |
IDC_SNPOSITION |
CString m_sNPosition |
Caption: Node information\n\nRecord 0 of 0 |
| group box |
IDC_STATIC |
- |
- |
| edit box |
IDC_ENOD |
CString m_sNod |
- |
| button |
IDC_BNODLEFT |
- |
Caption: < |
| button |
IDC_BNODRIGHT |
- |
Caption: > |
| group box |
IDC_STATIC |
- |
Caption: Sub Node |
| static text |
IDC_SSPOSITION |
CString m_sSPosition |
Caption: Sub Node information\n\nRecord 0 of 0 |
| group box |
IDC_STATIC |
- |
- |
| edit box |
IDC_ESUBNOD |
CString m_sSubNod |
- |
| static text |
IDC_STATIC |
- |
- |
| edit box |
IDC_ESNWEIGHT |
short m_shortSubNodeWeight |
Styles: Number |
| button |
IDC_BSUBNODLEFT |
- |
Caption: < |
| button |
IDC_BSUBNODRIGHT |
- |
Caption: > |
Create the custom classes
Right-click over where it says 'SerializeExp' in the ClassView workspace, and click on 'New Class...'. Change the class type to 'Generic Class', call it 'CSubNode', and derive it from 'CObject' as public.
Repeat the above instructions to create another class called 'CNode', but this time, derive it from 'CSubNode' as public instead of 'CObject'.
Add the class variables and methods for reading and writing variables
To the class CNode, add a public variable called m_oaSubNodes of type CObArray, and a private variable called m_iSubNodePosition of type int.
To the class CSubNode, add a private variable called m_shortWeight of type short, and a protected variable called m_strString of type CString.
One of the sources I based this project on (Chapman) uses inline functions for getting and setting class variables. Inline functions are faster to execute than non-inline functions, but can make executable files physically larger, since, when the program is compiled, the entire function is copied to each point in the program where it is called, instead of causing a jump in the execution of the program to a single instance of the code.
An inline function is created simply by putting the implementation of the function immediately after the declaration in the class' header file. So for the subnode class, you should add inline functions for getting and setting m_strString and m_shortWeight, by putting the following lines with the 'public' declarations in the header file.
void SetString(CString inPath) {m_strString = inPath;}
CString GetString() {return m_strString;}
void SetWeight(short inWeight) {m_shortWeight = inWeight;}
short GetWeight() {return m_shortWeight;}
Implement constructors
Here, the initial values are set for the members of the new custom class object members.
For CSubNode, implement the constructor as follows:
CSubNode::CSubNode()
{
m_shortWeight=1;
m_strString="";
}
and for CNode...
CNode::CNode()
{
m_strString = "";
m_iSubNodePosition = 0;
CSubNode *pSubNode = new CSubNode();
try
{
m_iSubNodePosition = (m_oaSubNodes.GetSize() - 1);
}
catch (CMemoryException* perr)
{}
}
Add the member variables for the document class
Using the ClassView, add the following private variables of type 'int'...
m_iCurPosition
m_iSubNodePosition
Then, add a public variable of type 'CObArray', called m_oaNodes.
Make the custom classes serializable
Making the custom classes serializable involves the following steps:
- Add '
DECLARE_SERIAL' to the declaration file macros.
- Add '
IMPLEMENT_SERIAL' to the implementation file macros.
- Add '
Serialize' functions to the classes to deal with the CArchive object, which is where the C++ save and restore streams are handled.
- The
DECLARE_SERIAL macro is placed at the beginning of the class declarations of the classes you want to serialize. Use the name of the class as the argument for the macro.
- The
IMPLEMENT_SERIAL macro goes at the end of the macros at the top of the implementation files for each class you want to make serializable. This macro takes three arguments, namely, the name of the class, its base class, and a version number which should be changed if you change the data structures that you serialize when you update your application.
The contents of SubNode.h should now look something like this...
#if !defined(AFX_SUBNODE_H__3C84564E_BBEE_4B26_99B2_9996E51F4457
__INCLUDED_)
#define AFX_SUBNODE_H__3C84564E_BBEE_4B26_99B2_9996E51F4457
__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif
class CSubNode : public CObject
{
DECLARE_SERIAL (CSubNode)
public:
CSubNode();
virtual ~CSubNode();
private:
short m_shortWeight;
protected:
CString m_strString;
};
#endif _9996E51F4457__INCLUDED_)
...and the contents of Node.h should look like this...
#if !defined(AFX_NODE_H__A68ACB18_C472_487F_BACF_BADC4304203D
__INCLUDED_)
#define AFX_NODE_H__A68ACB18_C472_487F_BACF_BADC4304203D
__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif
#include "SubNode.h"
class CNode : public CSubNode
{
DECLARE_SERIAL (CNode)
public:
CObArray m_oaSubNodes;
CNode();
virtual ~CNode();
private:
int m_iSubNodePosition;
};
#endif _BADC4304203D__INCLUDED_)
The contents of the CSubNode implementation file should look like this...
#include "stdafx.h"
#include "SerializeExp.h"
#include "SubNode.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
IMPLEMENT_SERIAL (CSubNode, CObject, 1)
CSubNode::CSubNode()
{
m_shortWeight=1;
m_strString="";
}
CSubNode::~CSubNode()
{
}
... and the contents of the CNode implementation file should look like this...
#include "stdafx.h"
#include "SerializeExp.h"
#include "Node.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
IMPLEMENT_SERIAL (CNode, CSubNode, 1)
CNode::CNode()
{
m_iSubNodePosition = 0;
CSubNode *pSubNode = new CSubNode();
try
{
m_iSubNodePosition = (m_oaSubNodes.GetSize() - 1);
}
catch (CMemoryException* perr)
{}
}
CNode::~CNode()
{
}
- Now it is time to create the functionality for saving and restoring data for the custom classes via '
Serialize' functions. Using the ClassView tab in the workspace, add public functions of type 'void' to both CSubNode and CNode, both with the declaration 'Serialize(CArchive &ar)'.
In this application, the part of the serialization process we are concerned with starts in the document object. The document class object's Serialize function passes a reference of the archive object to its CObArray collection of CNode objects via CObArray's own serialization function. This causes each CNode object to receive a reference to the archive object in turn, repeating the process internally - passing a reference of the archive object to its collection of CSubNode objects, causing them to become serialized in turn.
Then, for the CSubNode function, it is just a matter of streaming the object's data to or from the archive object, depending on whether or not the data is being saved or retrieved...
To kick off, edit the document class function so that an archive object reference gets passed to its collection of CNode objects...
void CSerializeExpDoc::Serialize(CArchive& ar)
{
m_oaNodes.Serialize(ar);
}
Now, edit the CNode class Serialize function. This function serializes its own non-collection data before calling the Serialize function of its CSubNode object collection.
The base class for each serializable object must be given the opportunity to serialize its data before the derived class object serializes. Therefore, every 'Serialize' function must start with a call to the Serialize function of the base class. This is 'CObject' for 'CSubNode' objects and 'CSubNode' for 'CNode' objects.
void CNode::Serialize(CArchive &ar)
{
CSubNode::Serialize(ar);
int iNoSubNodes = 0;
if (ar.IsStoring())
{
iNoSubNodes = m_oaSubNodes.GetSize();
ar << m_iSubNodePosition << m_strString << iNoSubNodes;
}
else
ar >> m_iSubNodePosition >> m_strString >> iNoSubNodes;
m_oaSubNodes.Serialize(ar);
}
At the end of the serialization chain, each CSubNode object performs its own serialization...
void CSubNode::Serialize(CArchive &ar)
{
CObject::Serialize(ar);
if (ar.IsStoring())
ar << m_shortWeight << m_strString;
else
ar >> m_shortWeight >> m_strString;
}
Add the document class navigation and record handler functions
When the application is running, the Document Class navigates through the data as per instructions from the view class, and is responsible for creating new records and deleting old ones.
The code which goes into the project next refers to the CNode and CSubNode classes from within the Document class. In order to get the application to compile, you need to put in 'forward declarations' for the custom classes in the document class header file. This consists of placing the following lines...
class CNode;
class CSubNode;
... under the macros in the header file. The top of the header file for the document class should now look something like this...
#if !defined(AFX_SERIALIZEEXPDOC_H__90309A3B_DF70_4EFE_A06A_D15E6DC1FB45
__INCLUDED_)
#define AFX_SERIALIZEEXPDOC_H__90309A3B_DF70_4EFE_A06A_D15E6DC1FB45
__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif
class CNode;
class CSubNode;
class CSerializeExpDoc : public CDocument
{
You will also need to put the following #include lines in the document class implementation file...
#include "Node.h"
#include "SubNode.h"
...so that the top of the implementation file looks something like this...
#include "stdafx.h"
#include "SerializeExp.h"
#include "Node.h"
#include "SubNode.h"
#include "SerializeExpDoc.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
Now, create the following private functions in the document class...
AddNewRecord() of type CNode *
AddNewSubNode() of type CSubNode *
These can then be implemented as follows...
CNode * CSerializeExpDoc::AddNewRecord()
{
CNode *pNode = new CNode();
CSubNode *pSubNode = new CSubNode();
try
{
pNode->m_oaSubNodes.Add(pSubNode);
m_oaNodes.Add(pNode);
SetModifiedFlag();
m_iCurPosition = (m_oaNodes.GetSize()-1);
}
catch (CMemoryException* perr)
{
AfxMessageBox("Out of Memory",MB_ICONSTOP | MB_OK);
if (pNode)
{
delete pNode;
pNode = NULL;
}
if (pSubNode)
{
delete pSubNode;
pSubNode = NULL;
}
perr->Delete ();
}
return pNode;
}
and:
CSubNode * CSerializeExpDoc::AddNewSubNode()
{
CNode * curNode = (CNode *) m_oaNodes[m_iCurPosition];
CSubNode *pSubNode = new CSubNode();
try
{
curNode->m_oaSubNodes.Add(pSubNode);
SetModifiedFlag();
m_iSubNodePosition = (curNode->m_oaSubNodes.GetSize()-1);
}
catch (CMemoryException* perr)
{
AfxMessageBox("Out of Memory",MB_ICONSTOP | MB_OK);
if (pSubNode)
{
delete pSubNode;
pSubNode = NULL;
}
perr->Delete ();
}
return pSubNode;
}
To save time, I am going to simply copy all of the functions here. You can add them to your project using the ClassView. They are all public. The type and declaration for each function can be gleaned from its implementation, as written out below. For instance, for the function 'GetCurRecord', the type is 'CNode *' and the declaration is 'GetCurRecord()'.
CNode* CSerializeExpDoc::GetCurRecord()
{
if (m_iCurPosition >=0)
return (CNode*)m_oaNodes[m_iCurPosition];
else
return NULL;
}
CNode* CSerializeExpDoc::GetFirstRecord()
{
if (m_oaNodes.GetSize() > 0)
{
m_iCurPosition =0;
m_iSubNodePosition = 0;
GetFirstSubNode();
return (CNode*)m_oaNodes[0];
}
else
return NULL;
}
CNode* CSerializeExpDoc::GetNextRecord()
{
m_iSubNodePosition = 0;
if (++m_iCurPosition < m_oaNodes.GetSize()){
return (CNode*)m_oaNodes[m_iCurPosition];
}
else
return AddNewRecord();
}
CNode* CSerializeExpDoc::GetPrevRecord()
{
if (m_oaNodes.GetSize() > 0)
{
m_iSubNodePosition = 0;
if (--m_iCurPosition < 0){
m_iCurPosition = 0;
}
return (CNode*)m_oaNodes[m_iCurPosition];
}
else
return NULL;
}
CNode * CSerializeExpDoc::GetLastRecord()
{
if (m_oaNodes.GetSize() > 0)
{
m_iSubNodePosition = 0;
m_iCurPosition = (m_oaNodes.GetSize() - 1);
m_iSubNodePosition = 0;
return (CNode*)m_oaNodes[m_iCurPosition];
}
else
return NULL;
}
int CSerializeExpDoc::GetCurRecordNbr()
{
return (m_iCurPosition +1);
}
CSubNode* CSerializeExpDoc::GetFirstSubNode()
{
CNode * curNode = (CNode *) m_oaNodes[m_iCurPosition];
if (curNode->m_oaSubNodes.GetSize() > 0)
{
m_iSubNodePosition =0;
return (CSubNode*)curNode->m_oaSubNodes[0];
}
else
return NULL;
}
CSubNode* CSerializeExpDoc::GetNextSubNode()
{
CNode * curNode = (CNode *) m_oaNodes[m_iCurPosition];
if (++m_iSubNodePosition < curNode->m_oaSubNodes.GetSize())
return (CSubNode*)curNode->m_oaSubNodes[m_iSubNodePosition];
else
return AddNewSubNode();
}
CSubNode* CSerializeExpDoc::GetPrevSubNode()
{
CNode * curNode = (CNode *) m_oaNodes[m_iCurPosition];
if (curNode->m_oaSubNodes.GetSize() > 0)
{
if (--m_iSubNodePosition < 0)
m_iSubNodePosition = 0;
return (CSubNode*)curNode->m_oaSubNodes[m_iSubNodePosition];
}
else
return NULL;
}
int CSerializeExpDoc::GetTotalSubNodes()
{
CNode * curNode = (CNode *) m_oaNodes[m_iCurPosition];
return curNode->m_oaSubNodes.GetSize();
}
int CSerializeExpDoc::GetCurSubNodeNbr()
{
return (m_iSubNodePosition + 1);
}
Cleaning up the document
In C++, you need to make sure that all of the objects you create are properly deleted when you don't need them any more, or your application may cause a problem known as 'memory leak'. The following function is an event handler which is called when the document is closed or a new document is opened. It is a pair of nested loops, one for looping through the nodes and deleting them, and an inner loop for looping through the sub nodes contained within each node object and deleting them. You need to use the Class Wizard for creating this function. Click on the 'Message Maps' tab. Make sure the document class' class name is selected in the drop-down list, then double click on 'DeleteContents' in the 'Messages' list to generate the function.
Fill out the body of the function with the following code...
void CSerializeExpDoc::DeleteContents()
{
int noCount = m_oaNodes.GetSize();
int noPos;
if (noCount)
{
for (noPos = 0; noPos < noCount; noPos++){
CNode * pCNode = (CNode *)m_oaNodes[noPos];
int snCount = pCNode->m_oaSubNodes.GetSize();
int snPos;
if (snCount)
{
for (snPos = 0; snPos < snCount; snPos++)
delete pCNode->m_oaSubNodes[snPos];
pCNode->m_oaSubNodes.RemoveAll();
}
delete m_oaNodes[noPos];
}
m_oaNodes.RemoveAll();
}
CDocument::DeleteContents();
}
Before leaving the document class implementation file, scroll up to the top and add a '#include' line for the view class so that the top of the file now looks something like this...
#include "stdafx.h"
#include "SerializeExp.h"
#include "Node.h"
#include "SubNode.h"
#include "SerializeExpDoc.h"
#include "SerializeExpView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
Displaying a new record set
At this point, create a private function in the View class of type void, declared as PopulateView(). We'll put in the code later.
Create two new private member variables for the View class. One of type CNode*, called m_nCurNode, and another of type CSubNode*, called m_snCurSubNode.
Now, make another public function in the View class of type void, declared as ShowFirst(). This function is executed when a document is opened.
Put the following code in it...
void CSerializeExpView::ShowFirst()
{
CSerializeExpDoc* pDoc = GetDocument();
if (pDoc)
{
m_nCurNode = pDoc->GetFirstRecord();
m_snCurSubNode = pDoc->GetFirstSubNode();
if (m_nCurNode)
{
PopulateView();
}
}
}
What to do when a new document is opened
Instead of the document class calling ShowFirst directly, it calls the NewDataSet function in the view class. You need to create this function. It is another public function of type void.
Implement NewDataSet as follows...
void CSerializeExpView::NewDataSet()
{
ShowFirst();
}
The OnNewDocument function creates the new document, creates fresh, empty records ready for new information, and clears the current view. The function should already exist in the document class. You need to add code to it as follows...
BOOL CSerializeExpDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
if (!AddNewRecord())
return FALSE;
POSITION pos = GetFirstViewPosition();
CSerializeExpView* pView = (CSerializeExpView*) GetNextView(pos);
if (pView)
pView->NewDataSet();
return TRUE;
}
Displaying the current record
This function displays data from the document in the view. Go back to the function CSerializeExpDoc::PopulateView you created earlier, and fill it with the following code...
void CSerializeExpView::PopulateView()
{
CSerializeExpDoc* pDoc = GetDocument();
if (pDoc)
{
m_sNPosition.Format("Node information\n\nRecord %d of %d",
pDoc->GetCurRecordNbr(),pDoc->GetTotalRecords());
m_sSPosition.Format("Sub Node information\n\nRecord %d of %d",
pDoc->GetCurSubNodeNbr(),pDoc->GetTotalSubNodes());
if (m_nCurNode)
{
m_sNod = m_nCurNode->GetString();
if (m_snCurSubNode){
if (pDoc->GetTotalSubNodes()>0){
m_sSubNod = m_snCurSubNode->GetString();
m_shortSubNodeWeight =
m_snCurSubNode->GetWeight();
}
}
}
}
UpdateData (FALSE);
}
Before the project will compile, you need to add:
- the
GetTotalRecords() function to the document class, and
#include "Node.h" to the view class implementation file
Add the function GetTotalRecords() to the document class. It is of type int and is public. Implement it as follows...
int CSerializeExpDoc::GetTotalRecords()
{
return m_oaNodes.GetSize();
}
Add the line #include "Node.h" near the top of the view class implementation file so that the include section now looks like...
#include "stdafx.h"
#include "SerializeExp.h"
#include "Node.h"
#include "SerializeExpDoc.h"
#include "SerializeExpView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
Navigating the record set
The application needs to have functions added which respond to user input in the View class. These are called 'event handlers', and are most easily created using the ClassWizard. Open the ClassWizard, click on the Message Maps tab, make sure the View class is selected in the 'Class name:' drop down list, and then for each of the Object IDs IDC_BNODLEFT, IDC_BNODRIGHT, IDC_BSUBNODLEFT, and IDC_BSUBNODRIGHT, double click on the 'BN_CLICKED' message and change the suggested member function name to the following in turn...
OnBNodeLeft
OnBNodeRight
OnBSubNodeLeft
OnBSubNodeRight
Now implement the functions as follows...
void CSerializeExpView::OnBNodeLeft()
{
CSerializeExpDoc * pDoc = GetDocument();
if (pDoc)
{
m_nCurNode = pDoc->GetPrevRecord();
pDoc->GetFirstSubNode();
OnBSubNodeLeft();
if (m_nCurNode)
{
PopulateView();
}
}
}
void CSerializeExpView::OnBNodeRight()
{
CSerializeExpDoc * pDoc = GetDocument();
if (pDoc)
{
m_nCurNode = pDoc->GetNextRecord();
pDoc->GetFirstSubNode();
OnBSubNodeLeft();
if (m_nCurNode)
{
PopulateView();
}
}
}
void CSerializeExpView::OnBSubNodeLeft()
{
CSerializeExpDoc * pDoc = GetDocument();
if (pDoc)
{
m_snCurSubNode = pDoc->GetPrevSubNode();
if (m_snCurSubNode)
{
PopulateView();
}
}
}
void CSerializeExpView::OnBSubNodeRight()
{
CSerializeExpDoc * pDoc = GetDocument();
if (pDoc)
{
m_snCurSubNode = pDoc->GetNextSubNode();
if (m_snCurSubNode)
{
PopulateView();
}
}
}
What to do when opening a saved document
You need to create an event handler function for the OnOpenDocument message in the Document class. Implement this function as follows...
BOOL CSerializeExpDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
POSITION pos = GetFirstViewPosition();
CSerializeExpView* pView =
(CSerializeExpView*) GetNextView(pos);
if (pView)
pView->NewDataSet();
return TRUE;
}
Saving edits and changes
All that is left to do now is to place event handlers for the edit box controls. Using the ClassWizard for the View class, select the corresponding Object IDs for each of the edit boxes, then double click on the EN_CHANGE message for each of the edit boxes. The ClassWizard then suggests a function name for each event handler. You can change this into something more meaningful if need be.
For IDC_ENOD, the ClassWizard suggests OnChangeEnod as an event handler name. I suggest using OnChangeENode instead. For IDC_ESNWEIGHT, instead of using OnChangeWsnweight, I suggest using OnChangeWSNWeight, and for IDC_ESUBNOD, instead of using OnChangeEsubnod, I suggest using OnChangeESubNode.
Implement these functions as follows...
void CSerializeExpView::OnChangeENode()
{
UpdateData(TRUE);
if (m_nCurNode)
m_nCurNode->SetString(m_sNod);
}
void CSerializeExpView::OnChangeESNWeight()
{
UpdateData(TRUE);
if (m_snCurSubNode)
m_snCurSubNode->SetWeight(m_shortSubNodeWeight);
}
void CSerializeExpView::OnChangeESubNode()
{
UpdateData(TRUE);
if (m_snCurSubNode)
m_snCurSubNode->SetString(m_sSubNod);
}
Memory management note...
For better memory management and efficiency, use CObArray's SetSize function. This can prevent fragmentation and unnecessary copying.
References
- Much of this article is inspired by and based on 'Teach Yourself Visual C++ 6 in 21 Days', Chapter 13: 'Saving and Restoring Work--File Access' by Davis Chapman.
- I got plenty more inspiration and pleasure reading A serialization primer - Part 3, by Ravi Bhavnani.