By: Kenn Scribner for Visual C++ Developer
Background: I had written the previous articles that discussed XML in general, and I had even created a few example applications (in MFC) that used the MSXML parser to work with the XML data. But this article (finally) deals with the XML parser itself, where you'll see how to add nodes, delete nodes, and otherwise wreak havoc with your XML document. |
A Brief MSXML XML Primer
If you had a chance to read, or review, my recent articles regarding XML and the MSXML DOM parser, you probably noted that I exposed a very limited set of the parser's capabilities. My goal in those articles was to introduce XML as a technology rather than to exercise the XML parser itself. In this brief article I'd like to revisit the MSXML parser and cover some of the basics you need to work with XML documents and nodes. The three major areas I'll cover will be searching for specific nodes, inserting nodes, and retrieving nodal values. Other related actions, such as deleting or moving existing nodes, are very similar to the operations I'll describe here.
Mechanics of the MSXML ParserThe MSXML parser is based upon the XML document object model, and it's important to review the various document objects. I've recreated the table I presented previously to help with the review. You see this shown in Table 1.
Table 1--XML DOM Objects and Their Uses
DOM Object | Purpose |
DOMImplementation | A query object to determine the level of DOM support |
DocumentFragment | Represents a portion of the tree (good for cut/paste operations) |
Document | Represents the top node in the tree |
NodeList | Iterator object to access XML nodes |
Node | Extends the core XML tagged element |
NamedNodeMap | Namespace support and iteration through the collection of attribute nodes |
CharacterData | Text manipulation object |
Attr | Represents the element's attribute(s) |
Element | Nodes that represent XML elements (good for accessing attributes) |
Text | Represents the textual content of a given element or attribute object |
CDATASection | Used to mask sections of XML from parsing and validation |
Notation | Contains a notation based within the DTD or schema |
Entity | Represents a parsed or unparsed entity |
EntityReference | Represents an entity reference node |
ProcessingInstruction | Represents a processing instruction |
These objects come right from the XML specifications themselves. MSXML takes the additional step of rolling the XML DOM objects into COM. Because of this it's often rather easy to decipher which XML DOM object goes with which MSXML COM interface. IXMLDOMNode represents the DOM Node, for example.
One thing that often confuses people I meet is that a given XML document object may be (and usually is) polymorphic. That is, a Node is also an Element. This confuses things from time to time when you try to decide which DOM object is required to perform what action. You create DOM Nodes using the Document object, but if you want to add an attribute to the newly created node, you have to access it via its Element personality. If there is a magic pattern relating objects and actions, I haven't divined it from my daily work. I find myself continuously referring to the MSDN documentation to see which COM interface provides the methods I need to perform the tasks I'm trying to accomplish. The various object methods do appear to be logically grouped, which is my theory as to how the DOM was developed (by grouping logical operations).
The trick is then to retrieve the appropriate DOM object from the MSXML parser, the concrete implementation of which is a COM object. The basic mode of operations is then first to instantiate a copy of the MSXML COM object itself, and from it request or otherwise obtain pointers to additional XML DOM objects (which are themselves COM objects).
The MSXML DOM Exerciser ApplicationIt would be easy to create a fancy application to demonstrate many MSXML features, but the truth is the additional code would just add clutter. Instead, I elected to develop a simple console-based application that performs four basic actions:
To simplify further, I hard-coded the names of the XML document files and the XML nodes themselves. Naturally, had this been a real application, you'd rarely (if ever) resort to such tactics. But in this case these tradeoffs make sense to simplify the code surrounding the MSXML work.
As I often do, I've resorted to using ATL to wrap many of the COM-related activities in the sample application. You'll certainly see me using CComPtr and CComQIPtr objects, but I also mixed in a few CComBSTR and CComVariant objects for good measure. If you're unfamiliar with them, just remember that they're templates that take care of many details that, while important in a larger sense, aren't critical to understand for our purposes here. What's important here is to see how to search for XML nodes, add a new node (with an attribute), and display text contained within a node.
The entire console-based application's source code is shown in Listing 1.
Listing 1. Contents of XMLNodeExerciser.cpp
// XMLNodeExerciser.cpp
//
// Locates a specific XML node in an XML document and inserts
// a new child node, with an attribute, therein.
//
#include <atlbase.h>
#include <msxml.h>
#include <iostream>
void main()
{
// Start COM
CoInitialize(NULL);
// Open the file...note this simplistic executable
// assumes the file is named "xmldata.xml" and is
// in the same directory as the executable. It
// further assumes the file looks like this:
//
// <?xml version="1.0"?>
// <xmldata>
// <xmlnode />
// <xmltext>Hello, World!</xmltext>
// </xmldata>
//
// The executable will find the node "xmlnode"
// and insert a new node "xmlchildnode" (with the
// attribute 'xml="fun"') if everything
// works as expected. It then finds the node
// "xmltext" and retrieves the text contained
// within the node and displays that. Finally, it
// saves the XML document into a new file named
// "updatedxml.xml".
try {
// Create an instance of the parser
CComPtr<IXMLDOMDocument> spXMLDOM;
HRESULT hr = spXMLDOM.CoCreateInstance(__uuidof(DOMDocument));
if ( FAILED(hr) ) throw "Unable to create XML parser object";
if ( spXMLDOM.p == NULL ) throw "Unable to create XML parser object";
// Load the XML document file...
VARIANT_BOOL bSuccess = false;
hr = spXMLDOM->load(CComVariant(L"xmldata.xml"),&bSuccess);
if ( FAILED(hr) ) throw "Unable to load XML document into the parser";
if ( !bSuccess ) throw "Unable to load XML document into the parser";
// Check for <xmldata>
// <xmlnode>...
//
// Construct search string
// "xmldata/xmlnode"
CComBSTR bstrSS(L"xmldata/xmlnode");
CComPtr<IXMLDOMNode> spXMLNode;
hr = spXMLDOM->selectSingleNode(bstrSS,&spXMLNode);
if ( FAILED(hr) ) throw "Unable to locate 'xmlnode' XML node";
if ( spXMLNode.p == NULL ) throw "Unable to locate 'xmlnode' XML node";
// The COM object "spXMLNode" now contains the XML
// node <xmlnode>, so we create a child node at
// this point... The DOM creates the node itself,
// then we place it using the XML node we located as
// the parent.
CComPtr<IXMLDOMNode> spXMLChildNode;
hr = spXMLDOM->createNode(CComVariant(NODE_ELEMENT),
CComBSTR("xmlchildnode"),
NULL,
&spXMLChildNode);
if ( FAILED(hr) ) throw "Unable to create 'xmlchildnode' XML node";
if ( spXMLChildNode.p == NULL ) throw "Unable to create 'xmlchildnode' XML node";
// Place the node...if all goes well, the two nodal
// parameters provided to appendChild will be the
// same node. If there is a problem, the "inserted"
// node's pointer will be NULL.
CComPtr<IXMLDOMNode> spInsertedNode;
hr = spXMLNode->appendChild(spXMLChildNode,&spInsertedNode);
if ( FAILED(hr) ) throw "Unable to move 'xmlchildnode' XML node";
if ( spInsertedNode.p == NULL ) throw "Unable to move 'xmlchildnode' XML node";
// Add the attribute. Note we do this through the IXMLDOMElement
// interface, so we need to do the QI().
CComQIPtr<IXMLDOMElement> spXMLChildElement;
spXMLChildElement = spInsertedNode;
if ( spXMLChildElement.p == NULL ) throw "Unable to query for 'xmlchildnode' XML element interface";
hr = spXMLChildElement->setAttribute(CComBSTR(L"xml"),CComVariant(L"fun"));
if ( FAILED(hr) ) throw "Unable to insert new attribute";
// Check for <xmldata>
// <xmltext>...
//
// Construct search string
// "xmldata/xmltext"
spXMLNode = NULL; // release previous node...
bstrSS = L"xmldata/xmltext";
hr = spXMLDOM->selectSingleNode(bstrSS,&spXMLNode);
if ( FAILED(hr) ) throw "Unable to locate 'xmltext' XML node";
if ( spXMLNode.p == NULL ) throw "Unable to locate 'xmltext' XML node";
// Pull the enclosed text and display
CComVariant varValue(VT_EMPTY);
hr = spXMLNode->get_nodeTypedValue(&varValue);
if ( FAILED(hr) ) throw "Unable to retrieve 'xmltext' text";
if ( varValue.vt == VT_BSTR ) {
// Display the results...since we're not using the
// wide version of the STL, we need to convert the
// BSTR to ANSI text for display...
USES_CONVERSION;
LPTSTR lpstrMsg = W2T(varValue.bstrVal);
std::cout << lpstrMsg << std::endl;
} // if
else {
// Some error
throw "Unable to retrieve 'xmltext' text";
} // else
// If we got this far, we've inserted the new node, so
// save the resulting XML document...
hr = spXMLDOM->save(CComVariant("updatedxml.xml"));
if ( FAILED(hr) ) throw "Unable to save updated XML document";
// Write a note to the screen...
std::cout << "Processing complete..." << std::endl << std::endl;
} // try
catch(char* lpstrErr) {
// Some error...
std::cout << lpstrErr << std::endl << std::endl;
} // catch
catch(...) {
// Unknown error...
std::cout << "Unknown error..." << std::endl << std::endl;
} // catch
// Stop COM
CoUninitialize();
}
The application will load an XML document file called xmldata.xml (assumed to be in the same directory as the executable), which it assumes contains this XML data:
<?xml version="1.0"?>
<xmldata>
<xmlnode />
<xmltext>Hello, World!</xmltext>
</xmldata>
We'll first search for the xmlnode node, and if we find it, we'll insert a new node (with an attribute) as a child. The resulting XML document will then be:
<?xml version="1.0"?>
<xmldata>
<xmlnode>
<xmlchildnode xml="fun" />
</xmlnode>
<xmltext>Hello, World!</xmltext>
</xmldata>
After printing the message contained within the <xmltext /> node ("Hello, World!"), we'll save this new XML document to a file called updatedxml.xml. You can then look at the results using a text editor or Internet Explorer 5.x. Let's now turn to the code.
The application begins by initializing the COM runtime, after which it creates an instance of the MSXML parser:
// Create an instance of the parser
CComPtr<IXMLDOMDocument> spXMLDOM;
HRESULT hr = spXMLDOM.CoCreateInstance(__uuidof(DOMDocument));
if ( FAILED(hr) ) throw "Unable to create XML parser object";
if ( spXMLDOM.p == NULL ) throw "Unable to create XML parser object";
If we were able to create an instance of the parser, we next load the XML document into the parser:
// Load the XML document file...
VARIANT_BOOL bSuccess = false;
hr = spXMLDOM->load(CComVariant(L"xmldata.xml"),&bSuccess);
if ( FAILED(hr) ) throw "Unable to load XML document into the parser";
if ( !bSuccess ) throw "Unable to load XML document into the parser";
Searching for nodes involves the document object, so we use IXMLDOMDocument::selectSingleNode() to find a specific XML node based upon its name. There are other techniques, but this is most straightforward if you know precisely which node you're interested in locating:
// Check for <xmldata>
// <xmlnode>...
//
// Construct search string
// "xmldata/xmlnode"
CComBSTR bstrSS(L"xmldata/xmlnode");
CComPtr<IXMLDOMNode> spXMLNode;
hr = spXMLDOM->selectSingleNode(bstrSS,&spXMLNode);
if ( FAILED(hr) ) throw "Unable to locate 'xmlnode' XML node";
if ( spXMLNode.p == NULL ) throw "Unable to locate 'xmlnode' XML node";
Some of the other techniques you might use are to obtain a list of the nodes in the document (using IXMLDOMDocument::nodeFromID() or IXMLDOMElement::getElementsByTagName(), for example) or by accessing the document as a tree and traversing the tree in a typical tree traversal fashion (get child nodes, get sibling nodes, and so on).
In any case, the result of the search is a MSXML node object, IXMLDOMNode. This node must exist in the document or the search will fail. Given this node, the exerciser application uses it as a parent node for a brand new XML node, which is created by the XML document object:
// The COM object "spXMLNode" now contains the XML
// node <xmlnode>, so we create a child node at
// this point... The DOM creates the node itself,
// then we place it using the XML node we located as
// the parent.
CComPtr<IXMLDOMNode> spXMLChildNode;
hr = spXMLDOM->createNode(CComVariant(NODE_ELEMENT),
CComBSTR("xmlchildnode"),
NULL,
&spXMLChildNode);
if ( FAILED(hr) ) throw "Unable to create 'xmlchildnode' XML node";
if ( spXMLChildNode.p == NULL ) throw "Unable to create 'xmlchildnode' XML node";
If the parser could create the node, we now have to place it in the XML tree. To do this, we use the node we found during our search as the parent of the new node. IXMLDOMNode::appendChild() is just the method we need:
// Place the node...if all goes well, the two nodal
// parameters provided to appendChild will be the
// same node. If there is a problem, the "inserted"
// node's pointer will be NULL.
CComPtr<IXMLDOMNode> spInsertedNode;
hr = spXMLNode->appendChild(spXMLChildNode,&spInsertedNode);
if ( FAILED(hr) ) throw "Unable to move 'xmlchildnode' XML node";
if ( spInsertedNode.p == NULL ) throw "Unable to move 'xmlchildnode' XML node";
If the parent node did insert the newly created node as a child, it returns another instance of IXMLDOMNode that represents the new child node. In fact, this new child node and the node you passed to appendChild() are the same XML node. Checking the pointer of the appended child node is useful, however, because the pointer will be NULL if there was a problem.
At this time you have located a specific node and created a new child node for it, so let's see how to work with attributes. Imagine you want to add this attribute to the new child node:
xml="fun"
This isn't hard to do, but you will have to defer using
IXMLDOMNode in lieu of IXMLDOMElement. In this case you need to access the element nature of the child node. In practice, this means you must query the IXMLDOMNode interface for its associated IXMLDOMElement interface, and given that, call IXMLDOMElement::setAttribute():// Add the attribute. Note we do this through the IXMLDOMElement
// interface, so we need to do the QI().
CComQIPtr<IXMLDOMElement> spXMLChildElement;
spXMLChildElement = spInsertedNode;
if ( spXMLChildElement.p == NULL ) throw "Unable to query for 'xmlchildnode' XML element interface";
hr = spXMLChildElement->setAttribute(CComBSTR(L"xml"),CComVariant(L"fun"));
if ( FAILED(hr) ) throw "Unable to insert new attribute";
It is at this point the XML tree has been modified and the desired tree has been created. We could save the document to disk at this time, or perform additional work. For now, let's search for another node and display the value (text) the node contains. You've seen how to search for a node, so let's jump right to the data extraction.
The trick to extracting node data lies with IXMLDOMNode::get_nodeTypedValue(). The data a node contains may be identified using the Microsoft data types schema, so you could easily store floating point values, integers, strings, or anything the schema supports. You specify the data type using the dt:type attribute like so:
<model dt:type="string">SL-2</model>
<year dt:type="int">1992</year>
If a particular node has a specified data type, you can extract the data in that format using get_nodeTypedValue(). If no data type is specified, the data is assumed to be textual and the parser will return to you a VARIANT with BSTR data. In this case, that's fine, as the node we're searching for is a text node that actually contains a string. We could always convert the string to another form, using things like atoi() and so forth if we wanted. In this case we simply extract the string data and display it:
// Pull the enclosed text and display
CComVariant varValue(VT_EMPTY);
hr = spXMLNode->get_nodeTypedValue(&varValue);
if ( FAILED(hr) ) throw "Unable to retrieve 'xmltext' text";
if ( varValue.vt == VT_BSTR ) {
// Display the results...since we're not using the
// wide version of the STL, we need to convert the
// BSTR to ANSI text for display...
USES_CONVERSION;
LPTSTR lpstrMsg = W2T(varValue.bstrVal);
std::cout << lpstrMsg << std::endl;
} // if
else {
// Some error
throw "Unable to retrieve 'xmltext' text";
} // else
If we could retrieve the value associated with the node, and if that value was a BSTR (the data type we expected), we display the text on the screen. If not, we display an error message, but you could easily take other actions depending upon the situation.
Our final XML-related action is to save the updated XML tree to the disk, which we do using IXMLDOMDocument::save():
// If we got this far, we've inserted the new node, so
// save the resulting XML document...
hr = spXMLDOM->save(CComVariant("updatedxml.xml"));
if ( FAILED(hr) ) throw "Unable to save updated XML document";
After the save completes, we write a brief note to the screen and quit.
This sample application is by no means a fancy one. There is a lot more you could do, but hopefully this brief application gives you a better idea how you might use the MSXML parser from your C++ programs. The parser itself is a complex piece of software, and I can't recommend highly enough that you use the MSDN library as a reference. The parser exposes many interfaces, and those interfaces typically expose many methods. Even so, I use the parser extensively in my own projects and find it well implemented and easy to use, once I had written some code and experimented. Hopefully you'll also find many uses for both the parser, and for XML in general.
Comments? Questions? Find a bug? Please send me a note! |