Stingray® Foundation : Chapter 15 XML Serialization Architecture : XML Framework Tutorial
XML Framework Tutorial
This tutorial will walk you through the steps required to add XML serialization support to an existing MFC Document-View application. (A starter application XMLTutorial is available from the Knowledge Base on the Rogue Wave web site. For details on additional samples, see “Location of Sample Code” in the Getting Started part.) All tutorial steps will be based on adding to and changing the code in the XMLSerTut_Base application. You can follow along and make the changes to the code as you go, or refer to the completed application provided in the same directory.
The starter application
XMLSerTut_Base was generated using the MFC application wizard. It is a very simple drawing program. The user can select shapes from the toolbar and drop them onto the view window
Figure 19 – The XML Starter Application.
The starter application classes
CXShape—The base class for the drawing shapes. This class contains an STL vector of POINT structures.
CXTriangle, CXCircle, and CXRectangleCXShape-derived classes used to render their respective shapes.
CXDiagram— Custom class that contains an array of CXShape objects.
CDesignDoc — The application's CDocument class. The document data consists of a title and a diagram (CXDiagram object).
Modifying application data classes
In our starter application the CDesignDoc, CXDiagram, and CXShape classes are all serializable classes. Each object is responsible for serializing its internal data. For XML serialization, we will be using helper classes called formatters to write each object's data as XML tags. For this to succeed we need to ensure that the internal data is publicly accessible.
For example, the CXShape class member m_vecPoints is protected. To allow us to read the shape's data, we can add two accessor methods to the class.
We can also add similar logic to the CXDiagram object to give us access to the title and array of shape objects.
We do not need to add public accessors to our document class as we will not be using a formatter class for the document. However, the internal data should be either protected or have protected accessors since we will be creating a new class derived from the existing document class. Read more on this in “XML-enabling the document class.”
 
class CXShape : public CObject
{
...
public:
// Added public accessor to determine how many points in the vector
int GetNumPoints() const;
// Added public accessor to fetch each point from the vector (zero-based)
const POINT& GetPoint(int idx) const;
protected:
std::vector<POINT> m_vecPoints;
};
class CXDiagram : public CObject
{
...
public:
// Added public accessors for title and shape object array
CString GetTitle() const;
CTypedPtrArray<CObArray,CXShape*>* GetShapesArray();
protected:
CTypedPtrArray<CObArray,CXShape*> m_arrShapes;
CString m_strTitle;
};
Adding SFL XML Support
 
stdafx.h
To link in the SFL libraries, we need to make some modifications to our project's stdafx.h (precompiled header file).
 
// SFL-XML
// XML framework requires ATL support
#include <atlbase.h>
CComModule _Module;
#include <atlcom.h>
// If you want to link statically to the SFL library
// remove the following line
#define _SFLDLL
// The main header for XML serialization support
// We can alternately use sflall.h
#include <foundation/xmlserialize.h>
Resource includes
1. Open up the resource includes dialog via the View | Resource includes... menu option.
2. Add sflres.h to the read-only symbol directives.
3. Add sfl.rc at the bottom of the compile-time directives.
Figure 20 – Resource Includes Dialog
 
XML-enabling the document class
 
Using the SECXMLDocAdapter_T wrapper class
The SFL library provides a very handy wrapper class, SECXMLDocAdapter_T, for adding XML serialization to your existing CDocument class. To use this template, simply create a new class that publicly inherits from the template. The template argument is your existing document class.
 
class CDesignDocXML : public sfl::SECXMLDocAdapter_T<CDesignDoc>
{
...
// Our required override of XMLSerialize
void XMLSerialize(sfl::SECXMLArchive &ar);
// This method is invoked by the framework to determine
// what the XML tag name will be for our document
virtual void GetElementType(LPTSTR str)
{
_tcscpy(str, _T("DiagramDocument"));
}
};
NOTE >> The sfl:: scope declaration is a typedef for the stingray::foundation namespace. You can just as easily add the directive using namespace stingray::foundation; to your header files (or stdafx.h). However, we have chosen to use the sfl:: notation to clearly document where SFL framework classes are being used.
We must provide an implementation of the pure virtual SECXMLDocAdapter_T::XMLSerialize() method. For now, we'll simply provide the boilerplate code.
 
void CDesignDocXML::XMLSerialize(sfl::SECXMLArchive &ar)
{
if(ar.IsStoring())
{
// Write out XML
}
else
{
// Read in XML
}
}
We provide an override of the GetElementType() method so that the XML framework will know what to call the top-level tag in the XML document. Our resulting XML will look like this:
 
<?xml version="1.0" standalone="yes"?>
<DiagramDocument>
<!-- document data will be child nodes of this top-level node -->
</DiagramDocument>
 
Modifying the base application
Now that we have a document class capable of participating in the SFL XML framework, we need to make some small modifications to the application.
In the application object's ::InitInstance() we'll add logic to initialize the OLE libraries and the SFL framework:
 
BOOL CXMLSerTutApp::InitInstance()
{
// SFL-XML
// Required SFL framework initialization
AfxOleInit();
sfl::SECXMLInitFTRFactory();
...
Change the application's CDocTemplate to use the new XML-enabled document class:
 
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_XMLSERTYPE,
RUNTIME_CLASS(CDesignDocXML), // new XML-enabled doc class
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CDesignView));
AddDocTemplate(pDocTemplate);
...
}
Adding menu commands
We'll edit the menu resource to add some entries for loading and saving XML files. This is not absolutely required since the framework will actually parse the file extension when you load or save your document. If the .xml extension is found, the XML framework will call your document's XMLSerialize() override. Otherwise, your base class Serialize(CArchive& ar) method will be invoked.
Figure 21 – Menu Commands
Menu command handlers
To handle the menu commands, we make some MESSAGE_MAP entries in our new document class (the one we created from the template and the original document class). You can choose any id value you want for the menu commands, as they aren't predefined in the SFL headers. You do not need to write any message handlers, because they have been provided by the template base class.
 
BEGIN_MESSAGE_MAP(CDesignDocXML, CDocument)
//{{AFX_MSG_MAP(CDesignDocXML)
// Our custom menu commands mapped to the template
// base class handlers
ON_COMMAND(ID_FILE_OPENXML, OnSECFileOpenXML)
ON_COMMAND(ID_FILE_SAVEXML, OnSECFileSaveXML)
ON_COMMAND(ID_FILE_SAVEXMLAS, OnSECFileSaveXMLAs)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Creating XML formatters
We added public accessors for serializable data to our application classes so that we can create XML formatters to save our objects as XML. A formatter is a simple class that implements the IXMLSerialize interface. The framework contains a base class, CXMLSerializeImp, which can be used as the base class for our formatter classes.
Our formatter has to do three things:
Write class data as XML.
Read class data from XML.
Create a new instance of the class when reading XML.
We'll first concentrate on the first two requirements.
The CXShape base class formatter
We will create a class hierarchy of formatters that parallels our CXShape class hierarchy. We derive a class from CXMLSerializeImp and provide an override of the XMLSerialize() method.
 
class CXShapeFMT : public sfl::CXMLSerializeImp
{
public:
// All our CXShape derived classes do their serialization in
// the base class, so we only need one implmentation of
// XMLSerialize, and we can take a base class pointer
CXShapeFMT(CXShape* pShape, LPCTSTR strElementType = _T("Shape"))
: sfl::CXMLSerializeImp(strElementType),m_pShape(pShape)
{
}
virtual ~CXShapeFMT(){}
virtual void XMLSerialize(sfl::SECXMLArchive& ar);
protected:
// Pointer to the shape we're serializing
CXShape* m_pShape;
};
Implementing CXShape::XMLSerialize()
To write out the shape data we need to do the following:
1. Write out the number of points.
2. Loop through our POINT vector and write each point's x and y value.
This is the same logic that exists in the standard CXShape::Serialize() method. We use the SECXMLArchive::Read() and ::Write() methods to read and write XML tags. Our first call is to read or write the point count, which will be read from and written to the <PointCount> XML tag. This is the first method parameter.
To ensure that our points can be read back into the object in the correct order, we separate each point as a unique XML child element. Here we have used the format PTxxxxxx to name the XML tags, so that the first point is <PT000001>. Each PTxxxxxx will have two child elements, <XValue> and <YValue>. Even though an STL collection is zero-based, we're using a 1-based naming convention for demonstration purposes.
 
void CXShapeFMT::XMLSerialize(sfl::SECXMLArchive &ar)
{
// Shared XML serialization routine for all CXShape classes
int nPointCount = 0;
CString strPointTag;
if (ar.IsLoading()) // Read from XML
{
// Read in the point count
ar.Read(_T("PointCount"),nPointCount);
 
// Read in each point from the XML document
for(int idx = 0; idx < nPointCount; idx++)
{
// Format the string to create our unique PTxxxxxx tag name
strPointTag.Format(_T("%s%06d"),_T("PT"),(idx+1));
// Open the point tag and read its X and Y values
ar.OpenElement(strPointTag);
POINT ptTemp;
//Add the point to the shape object's vector if successful read
if ((ar.Read(_T("XValue"),ptTemp.x)) && (ar.Read(_T("YValue"),ptTemp.y)))
m_pShape->AddPoint(ptTemp);
// Close the tag
ar.CloseElement(strPointTag);
}
}
else // Write to XML
{
// Write out the point count to the <PointCount> tag
nPointCount = m_pShape->GetNumPoints();
ar.Write(_T("PointCount"),nPointCount);
// Write out each point in the vector
for(int idx = 0; idx < nPointCount; idx++)
{
// Format the string to create our unique tag name
strPointTag.Format(_T("%s%06d"),_T("PT"),(idx+1));
// Open/Create the tag
ar.OpenElement(strPointTag);
// Write the X and Y values
const POINT& ptTemp = m_pShape->GetPoint(idx);
ar.Write(_T("XValue"),ptTemp.x);
ar.Write(_T("YValue"),ptTemp.y);
// Close the tag
ar.CloseElement(strPointTag);
}
}
}
Creating formatters for derived CXShape classes
We've met our first two requirements for reading and writing our shape class data as XML. So how do we satisfy the third? How can we create an instance of our object from simple XML text?
The SFL framework uses a set of macros that are similar to the MFC FOUNDATION_DECLARE_SERIAL / IMPLEMENT_SERIAL macros. These SFL macros define a lookup map that determines what XML tag corresponds to your domain object classes.
To make our concrete shape classes creatable from XML, we derive three new classes that inherit from the CXShapeFMT class we just created. We're only showing one class, but the procedure is the same for all three.
Our concrete formatter class is doing two things:
1. Mapping a specific formatter to a specific class.
2. Describing what the class XML tag will be (via the constructor).
 
class CXCircleFMT : public CXShapeFMT
{
// Add our class to the XML serialization map
BEGIN_SEC_XMLFORMATTERMAP(CXCircleFMT)
// First parameter is our domain class,
// second is this formatter class
XMLFORMATTERMAP_ADDENTRY(CXCircle, CXCircleFMT)
END_SEC_XMLFORMATTERMAP()
public:
// The second parameter to the constructor determines what
// the name of the XML tag will be when writing this object.
CXCircleFMT(CXCircle* pShape, LPCTSTR strElementType = _T("Circle"))
: CXShapeFMT((CXShape*)pShape, strElementType)
{
}
virtual ~CXCircleFMT() {}
};
In our implementation (.cpp) file, we use another SFL macro to create an instance of the XML initilization information. The macros we put in the class header declare a static nested class, and we initialize this static instance.
 
// Everywhere we have declared a BEGIN_SEC_XMLFORMATTERMAP in a
// formatter class requires a matching DEFINE_SEC_XMLFORMATTERMAP
DEFINE_SEC_XMLFORMATTERMAP(CXCircleFMT)
The CXDiagram formatter
The final class for which we need a formatter is our CXDiagram class. The process of declaring the class and initializing the lookup map is the same as it is for the shape classes.
 
class CXDiagramFMT : public sfl::CXMLSerializeImp
{
// MACROS for initializing the XML formatter map
BEGIN_SEC_XMLFORMATTERMAP(CXDiagramFMT)
XMLFORMATTERMAP_ADDENTRY(CXDiagram, CXDiagramFMT)
END_SEC_XMLFORMATTERMAP()
public:
CXDiagramFMT(CXDiagram* pDiagram, LPCTSTR strElementType = _T("Diagram"))
: sfl::CXMLSerializeImp(strElementType), m_pDiagram(pDiagram)
{
}
virtual ~CXDiagramFMT(){}
virtual void XMLSerialize(sfl::SECXMLArchive& ar);
protected:
// Pointer to the diagram we are serializing.
CXDiagram* m_pDiagram;
};
Implementation of CXDiagramFMT::XMLSerialize()
Our diagram class needs to write out its title and list of shape objects. In the standard MFC serialization we can write the list of objects with one line of code since we are using a CTypedPtrArray, which provides a Serialize() method.
The SFL framework provides a set of prebuilt formatter classes that can accomplish the same one-line serialization for MFC collection classes. This makes our implementation of XMLSerialize() quite simple.
Here we will use the SFL-provided CTypedPtrArrayFTR formatter class. This is a template class. The two template parameters are the same as the template parameters for the CTypedPtrArray declared in the CXDiagram class.
When we call the SECXMLArchive::Read() method, we pass NULL as the first argument. For the second parameter, we create an instance of the formatter inline. The first parameter to the constructor is a pointer to the diagram's array. The second parameter determines the name of the XML tag representing the collection of objects.
The resulting XML will have this structure:
 
<Title>Untitled</Title>
<Shapes>
<!-- all the shape nodes here -->
</Shapes>
void CXDiagramFMT::XMLSerialize(sfl::SECXMLArchive &ar)
{
// We will serialize our title and then the list of child objects
CString strTitle;
CTypedPtrArray<CObArray,CXShape*>* pShapes =
m_pDiagram->GetShapesArray();
 
if (ar.IsLoading()) // Reading in from XML
{
ar.Read(_T("Title"),strTitle);
m_pDiagram->SetTitle(strTitle);
// Use the SFL- provided formatter to read the collection
ar.Read(NULL,sfl::CTypedPtrArrayFTR<CObArray,
CXShape*>(pShapes,_T("Shapes")));
}
else // Storing to XML
{
strTitle = m_pDiagram->GetTitle();
ar.Write(_T("Title"),strTitle);
// Use the SFL- provided formatter to write the collection
ar.Write(NULL,sfl::CTypedPtrArrayFTR<CObArray,
CXShape*>(pShapes,_T("Shapes")));
}
}
Finishing up
Now that we have all our formatter classes written, the only item left is to add serialization code to our new document class.
 
void CDesignDocXML::XMLSerialize(sfl::SECXMLArchive &ar)
{
// Use our diagram formatter object to handle the XML
// serialization. This parrallels the logic in the standard
// DocView serialization
if(ar.IsStoring())
{
// Write out XML
ar.Write(NULL,CXDiagramFMT(&m_Diagram));
}
else
{
// Read in XML
ar.Read(NULL,CXDiagramFMT(&m_Diagram));
}
}
Our results: We can now run our application, create a new drawing, and save it as XML. If we've done everything correctly, our XML will look like this:
 
<?xml version="1.0" standalone="yes"?>
<DiagramDocument>
<Diagram>
<Title>Untitled</Title>
<Shapes>
<Size>1</Size>
<Element0>
<Type>Circle</Type>
<Circle>
<PointCount>2</PointCount>
<PT000001>
<XValue>4</XValue>
<YValue>2</YValue>
</PT000001>
<PT000002>
<XValue>104</XValue>
<YValue>102</YValue>
</PT000002>
</Circle>
</Element0>
</Shapes>
</Diagram>
</DiagramDocument>