Export (0) Print
Expand All

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

Visual Studio 6.0

Using the MSXML Parser to Work with XML Documents

Kenn Scribner

Kenn Scribner's recent articles on XML and the MSXML DOM parser exposed only some of the parser's capabilities. Those articles introduced XML as a technology, not the XML parser itself. Now Kenn revisits the MSXML parser and covers the basics you need to work with XML documents and nodes: searching for specific nodes, inserting nodes, and retrieving nodal values.

The MSXML parser is based upon the XML document object model, and it's important to review the various document objects, shown in Table 1. 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 easy to decipher which XML DOM object goes with which MSXML COM interface. IXMLDOMNode represents the DOM object called Node, for example.

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

It might be confusing at times, but XML document objects may be (and usually are) 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's a magic pattern relating objects and actions, I haven't divined it yet 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 that 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 then is 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 Application

It 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:

  • Loads an XML file from disk.
  • Searches for a specific node and inserts a child node to the former.
  • Searches for another node and displays the (text) value contained within the node.
  • Saves the modified XML document back to disk.

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 for our purposes here. What's important is to see how to search for XML nodes, add a new node (with an attribute), and display text contained within a node.

My console-based application, available in the accompanying Download file, 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:

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:

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 the most straightforward if you know precisely which node you're interested in locating:

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 methods you should know are IXMLDOMDocument::nodeFromID() and IXMLDOMElement::getElementsByTagName(), which you can use to obtain a list of the nodes in the document. You could also access the document as a tree and traverse it (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. My application uses it as a parent for a brand new XML node, which is created by the XML document object:

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, the next step is to place it in the XML tree. IXMLDOMNode::appendChild() is just the method for the job:

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.

By now, I've located a specific node and created a new child node for it, so let's see how to work with attributes. Imagine that you want to add this attribute to the new child node:

xml="fun"

This isn't hard to do, but you'll have to switch from IXMLDOMNode to IXMLDOMElement, 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():

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";

At this point, the XML tree has been modified and the desired tree has been created. The application 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 already seen how to search for a node, so let's jump right to the data extraction.

The trick to extracting node data lies with using IXMLDOMNode::get_nodeTypedValue(). The data a node contains may be identified using the Microsoft data types schema, so you can 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 with 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:

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;
} 
else {
   // Some error
   throw "Unable to retrieve 'xmltext' text";
} 

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 accomplish by using IXMLDOMDocument::save():

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's a lot more you could do, but I hope this brief sample gives you an idea of how to 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, now that I've written some code and experimented. I hope you'll also find many uses for both the parser, and for XML in general.

Download XMLNODE.ZIP

To find out more about Visual C++ Developer and Pinnacle Publishing, visit their website at http://www.pinpub.com/

Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.

This article is reproduced from the November 2000 issue of Visual C++ Developer. Copyright 2000, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. Visual C++ Developer is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-788-1900.

Show:
© 2014 Microsoft