Pass the MCAD/MCSD: Learning to Access and Manipulate XML Data

Date: May 9, 2003

Return to the article

XML is pervasive in .NET. In order to pass the MCAD/MCSD Exam(70-320): Developing XML Web Services and Server Components with Visual C# .NET and the .NET Framework, you need to know XML, starting with this section on accessing and manipulating XML data.

Objectives

This chapter covers the following Microsoft-specified objective for the "Consuming and Manipulating Data" section of the "Developing XML Web Services and Server Components with Microsoft Visual C# .NET and the Microsoft .NET Framework" exam:

Extensible Markup Language (far better known as XML) is pervasive in .NET. It's used as the format for configuration files, as the transmission format for SOAP messages, and in many other places. It's also rapidly becoming the most widespread common language for many development platforms.

This objective tests your ability to perform many XML development tasks. To pass this section of the exam, you need to know how to read an XML file from disk, and how to create your own XML from a DataSet object in your application. You also need to be familiar with the XPath query language, and with the creation and use of XSD schema files.

You'll also need to understand the connections that Microsoft SQL Server has with the XML universe. You need to be able to extract SQL Server data in XML format, and to be able to update a SQL Server database by sending it properly formatted XML.

Finally, the exam tests your ability to validate XML to confirm that it conforms to a proper format. The .NET Framework includes several means of validating XML that you should be familiar with.

Outline

Introduction

Accessing an XML File

Synchronizing DataSet Objects with XML

Understanding XPath

Generating and Using XSD Schemas

Using XML with SQL Server

Chapter Summary

Apply Your Knowledge


Introduction

You can't use the .NET Framework effectively unless you're familiar with XML. That's true even if you're working only with desktop applications, but if you want to write XML Web Services and other distributed applications, XML knowledge is even more important. The .NET Framework uses XML for many purposes itself, but it also makes it very easy for you to use XML in your own applications.

The FCL's support for XML is mainly contained in the System.Xml namespace. This namespace contains objects to parse, validate, and manipulate XML. You can read and write XML, use XPath to navigate through an XML document, or check to see whether a particular document is valid XML by using the objects in this namespace.

NOTE

XML Basics In this chapter, I've assumed that you're already familiar with the basics of XML, such as elements and attributes. If you need a refresher course on XML Basics, refer to Appendix B, "XML Standards and Syntax."

As you're learning about XML, you'll become familiar with some other standards as well. These include the XPath query language and the XSD schema language. You'll see in this chapter how these other standards are integrated into the .NET Framework's XML support.

Accessing an XML File

Access and Manipulate XML Data: Access an XML file by using the Document Object Model (DOM) and an XmlReader.

The most basic thing you can do with an XML file is open it and read it to find out what the file contains. The .NET Framework offers both unstructured and structured ways to access the data within an XML file. That is, you can treat the XML file either as a simple stream of information or as a hierarchical structure composed of different entities, such as elements and attributes.

In this section of the chapter you'll learn how to extract information from an XML file. I'll start by showing you how you can use the XmlReader object to move through an XML file, extracting information as you go. Then you'll see how other objects, including the XmlNode and XmlDocument objects, provide a more structured view of an XML file.

I'll work with a very simple XML file named Books.xml that represents three books that a computer bookstore might stock. Here's the raw XML file:

<?xml version="1.0" encoding="UTF-8"?>
<Books>
  <Book Pages="1088">
    <Author>Delaney, Kalen</Author>
    <Title>Inside Microsoft SQL Server 2000</Title>
    <Publisher>Microsoft Press</Publisher>
  </Book>
  <Book Pages="997">
    <Author>Burton, Kevin</Author>
    <Title>.NET Common Language Runtime</Title>
    <Publisher>Sams</Publisher>
  </Book>
  <Book Pages="392">
    <Author>Cooper, James W.</Author>
    <Title>C# Design Patterns</Title>
    <Publisher>Addison Wesley</Publisher>
  </Book>
</Books>

Understanding the DOM

The Document Object Model, or DOM, is an Internet standard for representing the information contained in an HTML or XML document as a tree of nodes. Like many other Internet standards, the DOM is an official standard of the World Wide Web Consortium, better known as the W3C.

Even though there is a DOM standard, not all vendors implement the DOM in exactly the same way. The major issue is that there are actually several different standards grouped together under the general name of DOM. Also, vendors pick and choose which parts of these standards to implement. The .NET Framework includes support for the DOM Level 1 Core and DOM Level 2 Core specifications, but it also extends the DOM by adding additional objects, methods, and properties to the specification.

NOTE

DOM Background You can find the official DOM specifications at http://www.w3.org/DOM. For details of Microsoft's implementation in the .NET Framework, see the "XML Document Object Model (DOM)" topic in the .NET Framework Developer's Guide.

Structurally, an XML document is a series of nested items, including elements and attributes. Any nested structure can be transformed to an equivalent tree structure if the outermost nested item is made the root of the tree, the next-in items the children of the root, and so on. The DOM provides the standard for constructing this tree, including a classification for individual nodes and rules for which nodes can have children. Figure 2.1 shows how the Books.xml file might be represented as a tree.

Figure 2.1 You can represent an XML file as a tree of nodes.

In its simplest form, the DOM defines an XML document as a tree of nodes. The root element in the XML file becomes the root node of the tree, and other elements become child nodes.

TIP

Attributes in the DOM In the DOM, attributes are not represented as nodes within the tree. Rather, attributes are considered to be properties of their parent elements. You'll see later in the chapter that this is reflected in the classes provided by the .NET Framework for reading XML files.

Using an XmlReader Object

The XmlReader class is designed to provide forward-only, read-only access to an XML file. This class treats an XML file similarly to the way that a cursor treats a resultset from a database. At any given time, there is one current node within the XML file, represented by a pointer that you can move around within the file. The class implements a Read() method that returns the next XML node to the calling application. There are also many other members in the XmlReader class; I've listed some of these in Table 2.1.

Table 2.1 Important Members of the XmlReader Class

Member

Type

Description

Depth

Property

Specifies the depth of the current node in the XML document

EOF

Property

Represents a Boolean property that is true when the current node pointer is at the end of the XML file

GetAttribute()

Method

Gets the value of an attribute

HasAttributes

Property

Returns true when the current node contains attributes

HasValue

Property

Returns true when the current node can have a Value property

IsEmptyElement

Property

Returns true when the current node represents an empty XML element

IsStartElement()

Method

Determines whether the current node is a start tag

MoveToElement()

Method

Moves to the element containing the current attribute

MoveToFirstAttribute()

Method

Moves to the first attribute of the current element

MoveToNextAttribute()

Method

Moves to the next attribute

Name

Property

Specifies a qualified name of the current node

NodeType

Property

Specifies the type of the current node

Read()

Method

Reads the next node from the XML file

Skip()

Method

Skips the children of the current element

Value

Property

Specifies the value of the current node


The XmlReader class is a purely abstract class. You cannot create an instance of XmlReader in your own application. Generally, you'll use the XmlTextReader class instead. The XmlTextReader class implements XmlReader for use with text streams. Step-by-Step 2.1 shows you how to use the XmlTextReader class.

STEP BY STEP 2.1 - Using the XmlTextReader Class

  1. Create a new Visual C# .NET Windows application. Name the application 320C02.

  2. Right-click on the project node in Solution Explorer and select Add, Add New Item.

  3. Select the Local Project Items node in the Categories tree view. Select the XML File template. Name the new file Books.xml and click OK.

  4. Modify the code for the Books.xml file as follows:

    <?xml version="1.0" encoding="UTF-8"?>
    <Books>
      <Book Pages="1088">
        <Author>Delaney, Kalen</Author>
        <Title>Inside Microsoft SQL Server 2000</Title>
        <Publisher>Microsoft Press</Publisher>
      </Book>
      <Book Pages="997">
        <Author>Burton, Kevin</Author>
        <Title>.NET Common Language Runtime</Title>
        <Publisher>Sams</Publisher>
      </Book>
      <Book Pages="392">
        <Author>Cooper, James W.</Author>
        <Title>C# Design Patterns</Title>
        <Publisher>Addison Wesley</Publisher>
      </Book>
    </Books>
  5. Add a new form to the project. Name the new form StepByStep2_1.cs.

  6. Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.

  7. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Text;
  8. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnReadXML_Click(
      object sender, System.EventArgs e)
    {
      StringBuilder sbNode = new StringBuilder();
    
      // Create a new XmlTextReader on the file
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      // Walk through the entire XML file
      while(xtr.Read())
      {
        sbNode.Length = 0;
        for(int intI=1; intI <= xtr.Depth ; intI++)
        {
          sbNode.Append(" ");
        }
        sbNode.Append(xtr.Name + " ");
        sbNode.Append(xtr.NodeType.ToString());
    
        if (xtr.HasValue)
        {
          sbNode.Append(": " + xtr.Value);
        }
        lbNodes.Items.Add(sbNode.ToString());
      }
      // Clean up
      xtr.Close();
    }
  9. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  10. Run the project. Click the button. You'll see a schematic representation of the XML file, as shown in Figure 2.2.

Figure 2.2 An XML file translated into schematic form by an XmlTextReader object.

As you can see in Step-by-Step 2.1, the output has everything in the XML file, including the XML declaration and any whitespace (such as the line feeds and carriage returns that separate lines of the files). On the other hand, the output doesn't include XML attributes. But the XmlTextReader is flexible enough that you can customize its behavior as you like.

Step-by-Step 2.2 shows an example where the code displays only elements, text, and attributes.

STEP BY STEP 2.2 - Using the XmlTextReader Class to Read Selected XML Entities

  1. Add a new form to the project. Name the new form StepByStep2_2.cs.

  2. Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Text;
  4. Double-click the button and add the following code to handle the button's Click event:

    private void btnReadXML_Click(
      object sender, System.EventArgs e)
    {
      StringBuilder sbNode = new StringBuilder();
    
      // Create a new XmlTextReader on the file
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      // Walk through the entire XML file
      while(xtr.Read())
      {
        if((xtr.NodeType == XmlNodeType.Element) ||
          (xtr.NodeType == XmlNodeType.Text) )
        {
          sbNode.Length = 0;
          for(int intI=1;
            intI <= xtr.Depth ; intI++)
          {
            sbNode.Append(" ");
          }
          sbNode.Append(xtr.Name + " ");
          sbNode.Append(xtr.NodeType.ToString());
    
          if (xtr.HasValue)
          {
            sbNode.Append(": " + xtr.Value);
          }
          lbNodes.Items.Add(sbNode.ToString());
          // Now add the attributes, if any
          if (xtr.HasAttributes)
          {
            while(xtr.MoveToNextAttribute())
            {
              sbNode.Length=0;
              for(int intI=1;
                intI <= xtr.Depth;intI++)
              {
                sbNode.Append(" ");
              }
              sbNode.Append(xtr.Name + " ");
              sbNode.Append(
                xtr.NodeType.ToString());
              if (xtr.HasValue)
              {
                sbNode.Append(": " +
                  xtr.Value);
              }
              lbNodes.Items.Add(
                sbNode.ToString());
            }
          }
        }
      }
      // Clean up
      xtr.Close();
    }
  5. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  6. Run the project. Click the button. You'll see a schematic representation of the elements and attributes in the XML file, as shown in Figure 2.3.

Figure 2.3 Selected entities from an XML file translated into schematic form by an XmlTextReader object.

Note that XmlTextReader does not consider attributes to be nodes; however, XmlTextReader provides the MoveToNextAtttibute() method to treat them as nodes. Alternatively, you can retrieve attributes by using indexers on the XmlTextReader object. If the current node represents an element in the XML file, then this code retrieves the value of the first attribute of the element:

xtr[0]

This code retrieves the value of an attribute named Pages:

xtr["Pages"]

The XmlNode Class

The individual items in the tree representation of an XML file are called nodes. As you've seen in Step-by-Steps 2.1 and 2.2, many different entities within the XML file can be represented by nodes: elements, attributes, whitespace, end tags, and so on. The DOM distinguishes these different types of nodes by assigning a node type to each one. In the .NET Framework, the possible node types are listed by the XmlNodeType enumeration. Table 2.2 lists the members of this enumeration.

Table 2.2 Members of the XmlNodeType Enumeration

Member

Represents

Attribute

An XML attribute

CDATA

An XML CDATA section

Comment

An XML comment

Document

The outermost element of the XML document (that is, the root of the tree representation of the XML)

DocumentFragment

The outermost element of an XML document's subsection

DocumentType

A Document Type Description (DTD) reference

Element

An XML element

EndElement

The closing tag of an XML element

EndEntity

The end of an included entity

Entity

An XML entity declaration

EntityReference

A reference to an entity

None

An XmlReader object that has not been initialized

Notation

An XML notation

ProcessingInstruction

An XML processing instruction

SignificantWhitespace

Whitespace that must be preserved to re-create the original XML document

Text

The text content of an attribute, element, or other node

Whitespace

Space between actual XML markup items

XmlDeclaration

The XML declaration


The code you've seen so far in this chapter deals with nodes as part of a stream of information returned by the XmlTextReader object. But the .NET Framework also includes another class, XmlNode, which can be used to represent an individual node from the DOM representation of an XML document. If you instantiate an XmlNode object to represent a particular portion of an XML document, you can alter the properties of the object and then write the changes back to the original file. The DOM provides two-way access to the underlying XML in this case.

NOTE

Specialized Node Classes In addition to XmlNode, the System.Xml namespace also contains a set of classes that represent particular types of nodes: XmlAttribute, XmlComment, XmlElement, and so on. These classes all inherit from the XmlNode class.

The XmlNode class has a rich interface of properties and methods. You can retrieve or set information about the entity represented by an XmlNode object, or you can use its methods to navigate the DOM. Table 2.3 shows the important members of the XmlNode class.

Table 2.3 - Important Members of the XmlNode Class

Member

Type

Description

AppendChild()

Method

Adds a new child node to the end of this node's list of children

Attributes

Property

Returns the attributes of the node as an XmlAttributeCollection object

ChildNodes

Property

Returns all child nodes of this node

CloneNode()

Method

Creates a duplicate of this node

FirstChild

Property

Returns the first child node of this node

HasChildNodes

Property

Returns true if this node has any children

InnerText

Property

Specifies the value of the node and all its children

InnerXml

Property

Specifies the markup representing only the children of this node

InsertAfter()

Method

Inserts a new node after this node

InsertBefore()

Method

Inserts a new node before this node

LastChild

Property

Returns the last child node of this node

Name

Property

Specifies the node's name

NextSibling

Property

Returns the next child of this node's parent node

NodeType

Property

Specifies this node's type

OuterXml

Property

Specifies the markup representing this node and its children

OwnerDocument

Property

Specifies the XmlDocument object that contains this node

ParentNode

Property

Returns this node's parent

PrependChild()

Method

Adds a new child node to the beginning of this node's list of children

PreviousSibling

Property

Returns the previous child of this node's parent node

RemoveAll()

Method

Removes all children of this node

RemoveChild()

Method

Removes a specified child of this node

ReplaceChild()

Method

Replaces a child of this node with a new node

SelectNodes()

Method

Selects a group of nodes matching an XPath expression

SelectSingleNode()

Method

Selects the first node matching an XPath expression

WriteContentTo()

Method

Writes all children of this node to an XmlWriter object

WriteTo()

Method

Writes this node to an XmlWriter object


The XmlDocument Class

There's no direct way to create an XmlNode object that represents an entity from a particular XML document. Instead, you can retrieve XmlNode objects from an XmlDocument object. The XmlDocument object represents an entire XML document. Step-by-Step 2.3 shows how you can use the XmlNode and XmlDocument objects to navigate through the DOM representation of an XML document.

STEP BY STEP 2.3 - Using the XmlDocument and XmlNode Classes

  1. Add a new form to the project. Name the new form StepByStep2_3.cs.

  2. Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Text;
  4. Double-click the button and add the following code to handle the button's Click event:

    private void btnReadXML_Click(
      object sender, System.EventArgs e)
    {
      StringBuilder sbNode = new StringBuilder();
    
      // Create a new XmlTextReader on the file
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      // Load the XML file to an XmlDocument
      xtr.WhitespaceHandling = WhitespaceHandling.None;
      XmlDocument xd = new XmlDocument();
      xd.Load(xtr);
      // Get the document root
      XmlNode xnodRoot = xd.DocumentElement;
      // Walk the tree and display it
      XmlNode xnodWorking;
      if (xnodRoot.HasChildNodes)
      {
        xnodWorking = xnodRoot.FirstChild;
        while (xnodWorking != null)
        {
          AddChildren(xnodWorking, 0);
          xnodWorking = xnodWorking.NextSibling;
        }
      }
      // Clean up
      xtr.Close();
    }
    
    private void AddChildren(XmlNode xnod, Int32 intDepth)
    {
      // Adds a node to the ListBox,
      // together with its children.
      // intDepth controls the depth of indenting
    
      StringBuilder sbNode = new StringBuilder();
      // Process only Text and Element nodes
      if((xnod.NodeType == XmlNodeType.Element) ||
        (xnod.NodeType == XmlNodeType.Text) )
      {
        sbNode.Length = 0;
        for(int intI=1;
          intI <= intDepth ; intI++)
        {
          sbNode.Append(" ");
        }
        sbNode.Append(xnod.Name + " ");
        sbNode.Append(xnod.NodeType.ToString());
        sbNode.Append(": " + xnod.Value);
        lbNodes.Items.Add(sbNode.ToString());
    
        // Now add the attributes, if any
        XmlAttributeCollection atts = xnod.Attributes;
        if(atts != null)
        {
          for(int intI = 0;
            intI < atts.Count; intI++)
          {
            sbNode.Length = 0;
            for (int intJ = 1;
               intJ <= intDepth + 1; intJ++)
            {
              sbNode.Append(" ");
            }
            sbNode.Append(atts[intI].Name + " ");
            sbNode.Append(
              atts[intI].NodeType.ToString());
            sbNode.Append(": " +
              atts[intI].Value);
            lbNodes.Items.Add(sbNode);
          }
        }
        // And recursively walk
        // the children of this node
        XmlNode xnodworking;
        if (xnod.HasChildNodes)
        {
          xnodworking = xnod.FirstChild;
          while (xnodworking != null)
          {
            AddChildren(
              xnodworking, intDepth + 1);
            xnodworking = xnodworking.NextSibling;
          }
        }
      }
    }
  5. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  6. Run the project. Click the button. You'll see a schematic representation of the elements and attributes in the XML file.

Step-by-Step 2.3 uses recursion to visit all the nodes in the XML file. That is, it starts at the document's root node (returned by the DocumentElement property of the XmlDocument object) and visits each child of that node in turn. For each child, it displays the desired information, and then visits each child of that node in turn, and so on.

In addition to the properties used in Step-by-Step 2.3, the XmlDocument class includes a number of other useful members. Table 2.4 lists the most important of these.

Table 2.4 Important Members of the XmlDocument Class

Member

Type

Description

CreateAttribute()

Method

Creates an attribute node

CreateElement()

Method

Creates an element node

CreateNode()

Method

Creates an XmlNode object

DocumentElement

Property

Returns the root XmlNode object for this document

DocumentType

Property

Returns the node containing the DTD declaration for this document, if it has one

ImportNode()

Method

Imports a node from another XML document

Load()

Method

Loads an XML document into the XmlDocument object

LoadXml()

Method

Loads the XmlDocument object from a string of XML data

NodeChanged

Event

Occurs after the value of a node has been changed

NodeChanging

Event

Occurs when the value of a node is about to be changed

NodeInserted

Event

Occurs when a new node has been inserted

NodeInserting

Event

Occurs when a new node is about to be inserted

NodeRemoved

Event

Occurs when a node has been removed

NodeRemoving

Event

Occurs when a node is about to be removed

PreserveWhitespace

Property

Returns true if whitespace in the document should be preserved when loading or saving the XML

Save()

Method

Saves the XmlDocument object as a file or stream

WriteTo()

Method

Saves the XmlDocument object to an XmlWriter object


REVIEW BREAK

Synchronizing DataSet Objects with XML

Access and Manipulate XML Data: Transform DataSet data into XML data.

One area in which the .NET Framework's use of XML is especially innovative is in connecting databases with XML. You already know that ADO.NET provides a complete in-memory representation of the structure and data of a relational database through its DataSet object. What the System.Xml namespace adds to this picture is the capability to automatically synchronize a DataSet object with an equivalent XML file. In this section of the chapter, you'll learn about the classes and techniques that make this synchronization possible.

The XmlDataDocument Class

The XmlDocument class is useful for working with XML via the DOM, but it's not a data-enabled class. To bring the DataSet class into the picture, you need to use an XmlDataDocument class, which inherits from the XmlDocument class. Table 2.5 shows the additional members that the XmlDataDocument class adds to the XmlDocument class.

Table 2.5 Additional Members of the XmlDataDocument Class

Member

Type

Description

DataSet

Property

Retrieves a DataSet object representing the data in the XmlDataDocument object

GetElementFromRow()

Method

Retrieves an XmlElement object representing a specified DataRow object

GetRowFromElement()

Method

Retrieves a DataRow object representing a specified XmlElement object

Load()

Method

Loads the XmlDataDocument object and synchronizes it with a DataSet object


Synchronizing a DataSet Object with an XmlDataDocument Object

The reason the XmlDataDocument class exists is to allow you to exploit the connections between XML documents and DataSet objects. You can do this by synchronizing the XmlDataDocument object (and hence the XML document that it represents) with a particular DataSet object. When you synchronize an XmlDataDocument object with DataSet object, any changes made in one are automatically reflected in the other. You can start the synchronization process with any of these objects:

I'll demonstrate these three options in the remainder of this section.

Starting with an XmlDataDocument Object

One way to synchronize a DataSet object and an XmlDataDocument object is to start with the XmlDataDocument object and to retrieve the DataSet object from its DataSet property. Step-by-Step 2.4 demonstrates this technique.

STEP BY STEP 2.4 - Retrieving a DataSet Object from an XmlDataDocument Object

  1. Add a new form to the project. Name the new form StepByStep2_4.cs.

  2. Add a Button control (btnLoadXml) and a DataGrid control (dgXML) to the form.

  3. Switch to the code view and add the following using directives:

    using System.Data;
    using System.Xml;
  4. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnLoadXml_Click(
      object sender, System.EventArgs e)
    {
      // Create a new XmlTextReader on the file
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      // Create an object to synchronize
      XmlDataDocument xdd = new XmlDataDocument();
      // Retrieve the associated DataSet
      DataSet ds = xdd.DataSet;
      // Initialize the DataSet by reading the schema
      // from the XML document
      ds.ReadXmlSchema(xtr);
      // Reset the XmlTextReader
      xtr.Close();
      xtr = new XmlTextReader(@"..\..\Books.xml");
      // Tell it to ignore whitespace
      xtr.WhitespaceHandling = WhitespaceHandling.None;
      // Load the synchronized object
      xdd.Load(xtr);
      // Display the resulting DataSet
      dgXML.DataSource = ds;
      dgXML.DataMember = "Book";
      // Clean up
      xtr.Close();
    }
  5. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  6. Run the project. Click the button. The code will load the XML file and then display the corresponding DataSet object on the DataGrid control, as shown in Figure 2.4.

Figure 2.4 You can synchronize a DataSet object with an XmlDataDocument object.

The code in Step-by-Step 2.4 performs some extra setup to make sure that the DataSet object can hold the data from the XmlDataDocument object. Even when you're creating the DataSet object from the XmlDataDocument object, you must still explicitly create the schema of the DataSet object before it will contain data. That's necessary because in this technique you can also use a DataSet object that represents only a portion of the XmlDataDocument object. In this case, the code takes advantage of the ReadXmlSchema() method of the DataSet object to automatically construct a schema that matches the XML document. Because the XmlTextReader object is designed for forward-only use, the code closes and reopens this object after reading the schema so that it can also be used to read the data.

TIP

Automatic Schema Contents When you use the ReadXmlSchema() method of the DataSet object to construct an XML schema for the DataSet, both elements and attributes within the XML document become DataColumn objects in the DataSet object.

Starting with a Full DataSet Object

A second way to end up with a DataSet object synchronized to an XmlDataDocument object is to start with a DataSet object. Step-by-Step 2.5 demonstrates this technique.

STEP BY STEP 2.5 - Creating an XmlDataDocument Object from a DataSet Object

  1. Add a new form to the project. Name the new form StepByStep2_5.cs.

  2. Add a Button control (btnLoadDataSet) and a ListBox control (lbNodes) to the form.

  3. Open Server Explorer.

  4. Expand the tree under Data Connections to show a SQL Server data connection that points to the Northwind sample database. Expand the Tables node of this database. Drag and drop the Employees table to the form to create a SqlConnection object and a SqlDataAdapter object on the form.

  5. Select the SqlDataAdapter object. Click the Create DataSet hyperlink beneath the Properties Window. Name the new DataSet dsEmployees and click OK.

  6. Add a new class to the project. Name the new class Utility.cs. Alter the code in Utility.cs as follows:

    using System;
    using System.Windows.Forms;
    using System.Xml;
    using System.Text;
    
    namespace _320C02
    {
      public class Utility
      {
        public Utility()
        {
        }
        public void XmlToListBox(
          XmlDocument xd, ListBox lb)
        {
          // Get the document root
          XmlNode xnodRoot = xd.DocumentElement;
          // Walk the tree and display it
          XmlNode xnodWorking;
          if(xnodRoot.HasChildNodes)
          {
            xnodWorking = xnodRoot.FirstChild;
            while(xnodWorking != null)
            {
              AddChildren(xnodWorking, lb, 0);
              xnodWorking =
                xnodWorking.NextSibling;
            }
          }
        }
    
        public void AddChildren(XmlNode xnod,
          ListBox lb, Int32 intDepth)
        {
          // Adds a node to the ListBox,
          // together with its children. intDepth
          // controls the depth of indenting
    
          StringBuilder sbNode =
            new StringBuilder();
          // Only process Text and Element nodes
          if((xnod.NodeType == XmlNodeType.Element)
            ||(xnod.NodeType == XmlNodeType.Text))
          {
            sbNode.Length = 0;
            for(int intI=1;
              intI <= intDepth ; intI++)
            {
              sbNode.Append(" ");
            }
            sbNode.Append(xnod.Name + " ");
            sbNode.Append(
              xnod.NodeType.ToString());
            sbNode.Append(": " + xnod.Value);
            lb.Items.Add(sbNode.ToString());
    
            // Now add the attributes, if any
            XmlAttributeCollection atts =
              xnod.Attributes;
            if(atts != null)
            {
              for(int intI = 0;
                intI < atts.Count; intI++)
              {
                sbNode.Length = 0;
                for (int intJ = 1;
                  intJ <= intDepth + 1;
                  intJ++)
                {
                  sbNode.Append(" ");
                }
                sbNode.Append(
                  atts[intI].Name + " ");
                sbNode.Append(atts[
                  intI].NodeType.ToString());
                sbNode.Append(": " +
                  atts[intI].Value);
                lb.Items.Add(sbNode);
              }
            }
            // And recursively walk
            // the children of this node
            XmlNode xnodworking;
            if (xnod.HasChildNodes)
            {
              xnodworking = xnod.FirstChild;
              while (xnodworking != null)
              {
                AddChildren(xnodworking, lb,
                  intDepth + 1);
                xnodworking =
                  xnodworking.NextSibling;
              }
            }
          }
        }
      }
    }
  7. Switch to the code view of the form StepByStep2_5.cs and add the following using directives:

    using System.Data;
    using System.Xml;
  8. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnLoadDataSet_Click(
      object sender, System.EventArgs e)
    {
      // Fill the DataSet
      sqlDataAdapter1.Fill(dsEmployees1, "Employees");
      // Retrieve the associated document
      XmlDataDocument xd =
        new XmlDataDocument(dsEmployees1);
      // Display it in the ListBox
      Utility util = new Utility();
      util.XmlToListBox(xd, lbNodes);
    }
  9. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  10. Run the project. Click the button. The code loads the DataSet object from the Employees table in the SQL Server database. It then converts the DataSet object to XML and displays the results in the ListBox, as shown in Figure 2.5.

Figure 2.5 The XmlDataDocument object allows structured data to be retrieved and manipulated through a DataSet object.

Starting with an XML Schema

The third method to synchronize the two objects is to follow a three-step process:

  1. Create a new DataSet object with the proper schema to match an XML document, but no data.

  2. Create the XmlDataDocument object from the DataSet object.

  3. Load the XML document into the XmlDataDocument object.

Step-by-Step 2.6 demonstrates this technique.

STEP BY STEP 2.6 - Synchronization Starting with an XML Schema

  1. Add a new form to the project. Name the new form StepByStep2_6.cs.

  2. Add a Button control (btnLoadXml), a DataGrid control (dgXML), and a ListBox control (lbNodes) to the form.

  3. Add a new XML Schema file to the project from the Add New Item dialog box. Name the new file Books.xsd.

  4. Switch to the XML view of the schema file and add this code:

    <?xml version="1.0" encoding="utf-8" ?>
    <xs:schema id="Books" xmlns=""
     xmlns:xs="http://www.w3.org/2001/XMLSchema">
      <xs:element name="Books">
        <xs:complexType>
          <xs:choice maxOccurs="unbounded">
            <xs:element name="Book">
              <xs:complexType>
                <xs:sequence>
                  <xs:element name="Author"
                   type="xs:string" />
                  <xs:element name="Title"
                   type="xs:string" />
                </xs:sequence>
                <xs:attribute name="Pages"
                 type="xs:string" />
              </xs:complexType>
            </xs:element>
          </xs:choice>
        </xs:complexType>
      </xs:element>
    </xs:schema>
  5. Switch to the code view of the form StepByStep2_5.cs and add the following using directives:

    using System.Data;
    using System.Xml;
  6. You need the Utility.cs class created in Step-by-Step 2.5, so create it now if you didn't already create it.

  7. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnLoadXml_Click(
      object sender, System.EventArgs e)
    {
      // Create a dataset with the desired schema
      DataSet ds = new DataSet();
      ds.ReadXmlSchema(@"..\..\Books.xsd");
      // Create a matching document
      XmlDataDocument xdd = new XmlDataDocument(ds);
      // Load the XML
      xdd.Load(@"..\..\Books.xml");
      // Display the XML via the DOM
      Utility util = new Utility();
      util.XmlToListBox(xdd, lbNodes);
      // Display the resulting DataSet
      dgXML.DataSource = ds;
      dgXML.DataMember = "Book";
    }
  8. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  9. Run the project. Click the button. The code loads the DataSet object and the XmlDataDocument object. It then displays the DataSet object in the DataGrid control and the XML document content in the ListBox control, as shown in Figure 2.6.

Figure 2.6 You can start synchronization with a schema file to hold only the desired data in the DataSet object.

The advantage to using this technique is that you don't have to represent the entire XML document in the DataSet schema; the schema needs to include only the XML elements that you want to work with. For example, in this case the DataSet object does not contain the Publisher column, even though the XmlDataDocument includes that column (as you can verify by inspecting the information in the ListBox control).

Guided Practice Exercise 2.1

As you might guess, the XmlTextReader is not the only class that provides a connection between the DOM and XML documents stored on disk. There's a corresponding XmlTextWriter class that is designed to take an XmlDocument object and write it back to a disk file.

For this exercise, you should write a form that enables the user to open an XML file and edit the contents of the XML file on a DataGrid control. Users who are finished editing should be able to click a button and save the edited file back to disk. You can use the WriteTo() method of an XmlDocument or XmlDataDocument object to write that object to disk through an XmlTextWriter object.

How would you design such a form?

You should try working through this problem on your own first. If you get stuck, or if you'd like to see one possible solution, follow these steps:

  1. Add a new form GuidedPracticeExercise2_1.cs to your Visual C# .NET project.

  2. Add two Button controls (btnLoadXml and btnSaveXml) and a DataGrid control (dgXML) to the form.

  3. Switch to the code view and add the following using directives:

    using System.Data;
    using System.Xml;
  4. Double-click the form and add the following code in the class definition:

    // Define XmlDataDocument object and DataSet
    // object at the class level
    XmlDataDocument xdd = new XmlDataDocument();
    DataSet ds;
    
    private void GuidedPracticeExercise2_1_Load(
      object sender, System.EventArgs e)
    {
      // Retrieve the associated DataSet and store it
      ds = xdd.DataSet;
    }
  5. Double-click both the Button controls and add the following code in their Click event handlers:

    private void btnLoadXml_Click(
      object sender, System.EventArgs e)
    {
      // Create a new XmlTextReader on the file
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      // Initialize the DataSet by reading the schema
      // from the XML document
      ds.ReadXmlSchema(xtr);
      // Reset the XmlTextReader
      xtr.Close();
      xtr = new XmlTextReader(@"..\..\Books.xml");
      // Tell it to ignore whitespace
      xtr.WhitespaceHandling = WhitespaceHandling.None;
      // Load the synchronized object
      xdd.Load(xtr);
      // Display the resulting DataSet
      dgXML.DataSource = ds;
      dgXML.DataMember = "Book";
      // Clean up
      xtr.Close();
    }
    
    private void btnSaveXml_Click(
      object sender, System.EventArgs e)
    {
      // Create a new XmlTextWriter on the file
      XmlTextWriter xtw = new XmlTextWriter(
        @"..\..\Books.xml",
        System.Text.Encoding.UTF8);
      // And write the document to it
      xdd.WriteTo(xtw);
      xtw.Close();
    }
  6. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  7. Run the project. Click the Load XML button to load the Books.xml file to the DataGrid control. Change some data on the DataGrid control, and then click the Save XML button. Close the form. Open the Books.xml file to see the changed data.

The code in this exercise keeps the XmlDataDocument and DataSet objects at the class level so that they remain in scope while the user is editing data. To make the data available for editing, a DataSet object is synchronized to an XmlDataDocument object and then that DataSet object is bound to a DataGrid control. As the user makes changes on the DataGrid control, those changes are automatically saved back to the DataSet object, which in turn keeps the XmlDataDocument object synchronized to reflect the changes. When the user clicks the Save XML button, the contents of the XmlDataDocument object are written back to disk.

If you have difficulty following this exercise, review the section "Synchronizing DataSet Objects with XML." After doing that review, try this exercise again.

REVIEW BREAK

Understanding XPath

Access and Manipulate XML Data: Use XPath to query XML data.

To pass the exam, you should also have basic knowledge of XPath. XPath is another W3C standard, formally known as the XML Path Language. XPath is described by the W3C as "a language for addressing parts of an XML document." The .NET implementation of XPath supports the Version 1.0 Recommendation standard for XPath, which you can find at http://www.w3.org/TR/xpath.

You can think of XPath as being a query language, conceptually similar to SQL. Just as SQL allows you to select a set of information from a table or group of tables, XPath allows you to select a set of nodes from the DOM representation of an XML document. In this section, I'll introduce you to the basic syntax of XPath, and then show you how you can use XPath in the .NET System.Xml namespace.

The XPath Language

XPath is not itself an XML standard. XPath expressions are not valid XML documents. Rather, XPath is a language for talking about XML. By writing an appropriate XPath expression, you can select particular elements or attributes within an XML document.

XPath starts with the notion of current context. The current context defines the set of nodes that an XPath query will inspect. In general, there are four choices for specifying the current context for an XPath query:

To use XPath to identify a set of elements, you use the path down the tree structure to those elements, separating elements by forward slashes. For example, this XPath expression selects all the Author elements in the Books.xml file:

/Books/Book/Author

You can also select all the Author elements without worrying about the full path to get to them by using this expression:

//Author

You can use * as a wildcard at any level of the tree. So, for example, this expression selects all the Author nodes that are grandchildren of the Books node:

/Books/*/Author

XPath expressions select a set of elements, not a single element. Of course, the set might have only a single member, or no members at all. In the context of the XmlDocument object, an XPath expression can be used to select a set of XmlNode objects to operate on later.

TIP

Be Explicit when Possible In general, the expression with the explicit path (/Books/Book/@Pages) can be evaluated more rapidly than the expression that searches the entire document (//@Pages). The former has to search only a limited number of nodes to return results, whereas the latter needs to look through the entire document.

To identify a set of attributes, you trace the path down the tree to the attributes, just as you do with elements. The only difference is that attribute names must be prefixed with an @ character. For example, this XPath expression selects all the Pages attributes from Book elements in the Books.xml file:

/Books/Book/@Pages

Of course, in the Books.xml file, only Book elements have a Pages attribute. So in this particular context, this XPath expression is equivalent to the previous one:

//@Pages

You can select multiple attributes with the @* operator. To select all attributes of Book elements anywhere in the XML, use this expression:

//Book/@*

XPath also offers a predicate language to enable you to specify smaller groups of nodes or even individual nodes in the XML tree. You might think of this as a filtering capability similar to a SQL WHERE clause. One thing you can do is specify the exact value of the node that you'd like to work with. To find all Publisher nodes with the value Addison Wesley you could use the XPath expression

/Books/Book/Publisher[.="Addison Wesley"]

Here the [] operator specifies a filter pattern and the dot operator stands for the current node. Filters are always evaluated with respect to the current context. Alternatively, you can find all Book elements published by Addison Wesley:

/Books/Book[./Publisher="Addison Wesley"]

Note that there is no forward slash between an element and a filtering expression in XPath.

Of course, you can filter on attributes as well as elements. You can also use operators and Boolean expressions within filtering specifications. For example, you might want to find Books that have a thousand or more pages:

/Books/Book[./@Pages>=1000]

Because the current node is the default context, you can simplify this expression a little bit:

/Books/Book[@Pages>=1000]

XPath also supports a selection of filtering functions. For example, to find books whose title starts with A you could use this XPath expression:

/Books/Book[starts-with(Title,"A")]

Table 2.6 lists some additional XPath functions.

Table 2.6 - Selected XPath Functions

Function

Description

concat()

Concatenates strings

contains()

Determines whether one string contains another

count()

Returns the number of nodes in an expression

last()

Specifies the last element in a collection

normalize-space()

Removes whitespace from a string

not()

Negates its argument

number()

Converts its argument to a number

position()

Specifies the ordinal of a node within its parent

starts-with()

Determines whether one string starts with another

string-length()

Returns the number of characters in a string

substring()

Returns a substring from a string


Square brackets are also used to indicate indexing. Collections are indexed starting at one. To return the first Book node, you'd use this expression:

/Books/Book[1]

To return the first title of the second book:

/Books/Book[2]/Title[1]

To return the first author in the XML file, regardless of the book:

(/Books/Book/Author)[1]

The parentheses are necessary because the square brackets have a higher operator precedence than the path operators. Without the brackets, the expression would return the first author of every book in the file. There's also a last() function that you can use to return the last element in a collection, no matter how many elements are in the collection:

/Books/Book[last()]

Another useful operator is the vertical bar, which is used to form the union of two sets of nodes. This expression returns all the authors for books published by Addison Wesley or Microsoft Press:

/Books/Book[./Publisher="Addison Wesley"]/Author |
_ /Books/Book[./Publisher="Microsoft Press"]/Author

One way to see XPath in action is to use the SelectNodes() method of the XmlNode object, as shown in Step-by-Step 2.7.

STEP BY STEP 2.7 - Selecting Nodes with XPath

  1. Add a new form to the project. Name the new form StepByStep2_7.cs.

  2. Add a Label control, a TextBox control (txtXPath), a Button control (btnEvaluate), and a ListBox control (lbNodes) to the form.

  3. Switch to the code view and add the following using directive:

    using System.Xml;
  4. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnEvaluate_Click(
      object sender, System.EventArgs e)
    {
      // Load the Books.xml file
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      xtr.WhitespaceHandling = WhitespaceHandling.None;
      XmlDocument xd = new XmlDocument();
      xd.Load(xtr);
      // Retrieve nodes to match the expression
      XmlNodeList xnl = xd.DocumentElement.SelectNodes(
        txtXPath.Text);
      // And dump the results
      lbNodes.Items.Clear();
      foreach (XmlNode xnod in xnl)
      {
        // For elements, display the corresponding
        // Text entity
        if (xnod.NodeType == XmlNodeType.Element)
        {
          lbNodes.Items.Add(
            xnod.NodeType.ToString() + ": " +
            xnod.Name + " = " +
            xnod.FirstChild.Value);
        }
        else
        {
          lbNodes.Items.Add(
            xnod.NodeType.ToString()+ ": " +
            xnod.Name + " = " + xnod.Value);
        }
      }
      // Clean up
      xtr.Close();
    }
  5. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  6. Run the project. Enter an XPath expression in the TextBox control. Click the button to see the nodes that the expression selects from the Books.xml file, as shown in Figure 2.7.

Figure 2.7 You can use XPath expressions to query an XML document.

The SelectNodes() method of the XmlNode object takes an XPath expression and evaluates that expression over the document. The resulting nodes are returned in an XmlNodeList object, which is just a collection of XML nodes.

Using the XPathNavigator Class

You've seen how you can use the XmlReader class to move through an XML document. But the XmlReader allows only forward-only, read-only access to the document. There is another set of navigation classes in the System.Xml.XPath namespace. In particular, the XPathNavigator class provides you with read-only, random access to XML documents.

You can perform two distinct tasks with an XPathNavigator object:

In the remainder of this section, I'll show you how to use the XPathNavigator class for these tasks.

Selecting Nodes with XPath

To use the XPathNavigator class, you should start with an XmlDocument, XmlDataDocument, or XPathDocument object. In particular, if you're mainly interested in XPath operations, you should use the XPathDocument class. The XPathDocument class provides a representation of the structure of an XML document that is optimized for query operations. You can construct an XPathDocument object from a URI (including a local file name), a stream, or a reader containing XML.

The XPathDocument object has a single method of interest, CreateNavigator(). (You'll also find this method on the XmlDocument and XmlDataDocument objects.) As you've probably guessed, the CreateNavigator() method returns an XPathNavigator object that can perform operations with the XML document represented by the XPathDocument object. Table 2.7 lists the important members of the XPathNavigator object.

Table 2.7 - Important Members of the XPathNavigator Class

Member

Type

Description

Clone()

Method

Creates a duplicate of this object with the current state

ComparePosition()

Method

Compares two XPathNavigator objects to determine whether they have the same current node

Compile()

Method

Compiles an XPath expression for faster execution

Evaluate()

Method

Evaluates an XPath expression

HasAttributes

Property

Indicates whether the current node has any attributes

HasChildren

Property

Indicates whether the current node has any children

IsEmptyElement

Property

Indicates whether the current node is an empty element

Matches()

Method

Determines whether the current node matches an XSLT pattern

MoveToFirst()

Method

Moves to the first sibling of the current node

MoveToFirstAttribute()

Method

Moves to the first attribute of the current node

MoveToFirstChild()

Method

Moves to the first child of the current node

MoveToNext()

Method

Moves to the next sibling of the current node

MoveToNextAttribute()

Method

Moves to the next attribute of the current node

MoveToParent()

Method

Moves to the parent of the current node

MoveToPrevious()

Method

Moves to the previous sibling of the current node

MoveToRoot()

Method

Moves to the root node of the DOM

Name

Property

Specifies the qualified name of the current node

Select()

Method

Uses an XPath expression to select a set of nodes

Value

Property

Specifies the value of the current node


Like the XmlReader class, the XPathNavigator class maintains a pointer to a current node in the DOM at all times. But the XPathNavigator brings additional capabilities to working with the DOM. For example, you can use this class to execute an XPath query, as shown in Step-by-Step 2.8.

TIP

XPathNavigator Object Can Move Backward Note that unlike the XmlReader class, the XPathNavigator class implements methods such as MoveToPrevious() and MoveToParent() that can move backward in the DOM. The XPathNavigator class provides random access to the entire XML document.

STEP BY STEP 2.8 - Selecting Nodes with an XPathNavigator Object

  1. Add a new form to the project. Name the new form StepByStep2_8.cs.

  2. Add a Label control, a TextBox control (txtXPath), a Button control (btnEvaluate), and a ListBox control (lbNodes) to the form.

  3. Switch to the code view and add the following using directive:

    using System.Xml.XPath;
  4. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnEvaluate_Click(
      object sender, System.EventArgs e)
    {
      // Load the Books.xml file
      XPathDocument xpd = new
        XPathDocument(@"..\..\Books.xml");
      // Get the associated navigator
      XPathNavigator xpn = xpd.CreateNavigator();
      // Retrieve nodes to match the expression
      XPathNodeIterator xpni =
        xpn.Select(txtXPath.Text);
      // And dump the results
      lbNodes.Items.Clear();
      while (xpni.MoveNext())
      {
        lbNodes.Items.Add(
          xpni.Current.NodeType.ToString() + ": "
          + xpni.Current.Name + " = " +
          xpni.Current.Value);
      }
    }
  5. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  6. Run the project. Enter an XPath expression in the TextBox control. Click the button to see the nodes that the expression selects from the Books.xml file.

The Select() method of the XPathNavigator class returns an XPathNodeIterator object, which lets you visit each member of the selected set of nodes in turn. It has Count and Current properties, and (as you saw in the code for Step-by-Step 2.8) a MoveNext() method that advances it through the set of nodes.

Navigating Nodes with XPath

You can also use the XPathNavigator object to move around in the DOM. Step-by-Step 2.9 demonstrates the MoveTo methods of this class.

STEP BY STEP 2.9 - Navigating with an XPathNavigator Object

  1. Add a new form to the project. Name the new form StepByStep2_9.cs.

  2. Add four Button controls (btnParent, btnPrevious, btnNext, and btnChild), and a ListBox control (lbNodes) to the form.

  3. Switch to the code view and add the following using directive:

    using System.Xml.XPath;
  4. Double-click the form and add the following code to the class definition:

    XPathDocument xpd;
    XPathNavigator xpn;
    
    private void StepByStep2_9_Load(
      object sender, System.EventArgs e)
    {
      // Load the Books.xml file
      xpd = new XPathDocument(@"..\..\Books.xml");
      // Get the associated navigator
      xpn = xpd.CreateNavigator();
      xpn.MoveToRoot();
      ListNode();
    }
    
    private void ListNode()
    {
      // Dump the current node to the listbox
      lbNodes.Items.Add(xpn.NodeType.ToString() +
        ": " + xpn.Name + " = " + xpn.Value);
    }
  5. Double-click the Button controls and add the following code to their Click event handlers:

    private void btnParent_Click(
      object sender, System.EventArgs e)
    {
      // Move to the parent of the current node
      if(xpn.MoveToParent())
        ListNode();
      else
        lbNodes.Items.Add("No parent node");
    }
    
    private void btnChild_Click(
      object sender, System.EventArgs e)
    {
      // Move to the first child of the current node
      if(xpn.MoveToFirstChild())
        ListNode();
      else
        lbNodes.Items.Add("No child node");
    }
    
    private void btnPrevious_Click(
      object sender, System.EventArgs e)
    {
      // Move to the previous sibling
      // of the current node
      if(xpn.MoveToPrevious())
        ListNode();
      else
        lbNodes.Items.Add("No previous node");
    }
    
    private void btnNext_Click(
      object sender, System.EventArgs e)
    {
      // Move to the next sibling of the current node
      if(xpn.MoveToNext())
        ListNode();
      else
        lbNodes.Items.Add("No next node");
    }
  6. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  7. Run the project. Experiment with the buttons. You'll find that you can move around in the DOM, as shown in Figure 2.8.

Figure 2.8 You can navigate an XML Document by using an XpathNavigator object.

Step-by-Step 2.9 demonstrates two important things about the XPathNavigator class. First, the value of a node is the concatenated text of all the nodes beneath that node. Second, the MoveTo methods of the XPathNavigator will never throw an error, whether or not there is an appropriate node to move to. Instead, they simply return false when the requested navigation cannot be performed.

REVIEW BREAK

Generating and Using XSD Schemas

Access and Manipulate XML Data: Generate and use an XSD schema.

In Chapter 1, "Creating and Manipulating DataSets," you learned how to create an XSD schema in the Visual Studio .NET user interface by dragging and dropping XML elements from the toolbox. This method is useful when you need to create a schema from scratch. But there will be times when you want to create a schema to match an existing object. In this section, you'll learn about the methods that are available to programmatically generate XSD schemas.

Generating an XSD Schema

One obvious source for an XML Schema is an XML file. An XML file can contain explicit schema information (in the form of an embedded schema) or it can contain implicit schema information in its structure. If the file contains explicit schema information, you can use the DataSet object to read that information and create the corresponding schema as a separate file, as shown in Step-by-Step 2.10.

STEP BY STEP 2.10 - Extracting an XML Schema

  1. Add a new form to the project. Name the new form StepByStep2_10.cs.

  2. Add a Button control (btnGetSchema) and a TextBox control (txtSchema) to the form. Set the MultiLine property of the TextBox to true and set its ScrollBars property to Vertical.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Data;
    using System.IO;
  4. Double-click the Button controls and add code to process an XML document when you click the Button control:

    private void btnGetSchema_Click(
      object sender, System.EventArgs e)
    {
      // Load the XML file with inline schema info
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Products.xml");
      // Read the schema (only) into a DataSet
      DataSet ds = new DataSet();
      ds.ReadXmlSchema(xtr);
      // Write the schema out as a separate stream
      StringWriter sw = new StringWriter();
      ds.WriteXmlSchema(sw);
      txtSchema.Text = sw.ToString();
      // Clean up
      xtr.Close();
    }
  5. Add a new XML file to the project. Name the new file Products.xml. Add this XML to the new file:

    <?xml version="1.0" encoding="UTF-8"?>
    <root xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     xmlns:od="urn:schemas-microsoft-com:officedata">
    <xsd:schema>
     <xsd:element name="dataroot">
     <xsd:complexType>
      <xsd:choice maxOccurs="unbounded">
      <xsd:element ref="Products"/>
      </xsd:choice>
     </xsd:complexType>
     </xsd:element>
     <xsd:element name="Products">
     <xsd:annotation>
      <xsd:appinfo/>
     </xsd:annotation>
     <xsd:complexType>
      <xsd:sequence>
       <xsd:element name="ProductID"
        od:jetType="autonumber"
        od:sqlSType="int" od:autoUnique="yes"
        od:nonNullable="yes">
       <xsd:simpleType>
        <xsd:restriction base="xsd:integer"/>
       </xsd:simpleType>
       </xsd:element>
       <xsd:element name="ProductName" minOccurs="0"
         od:jetType="text" od:sqlSType="nvarchar">
       <xsd:simpleType>
        <xsd:restriction base="xsd:string">
        <xsd:maxLength value="40"/>
        </xsd:restriction>
       </xsd:simpleType>
       </xsd:element>
       <xsd:element name="SupplierID" minOccurs="0"
         od:jetType="longinteger" od:sqlSType="int">
       <xsd:simpleType>
        <xsd:restriction base="xsd:integer"/>
       </xsd:simpleType>
       </xsd:element>
       <xsd:element name="CategoryID" minOccurs="0"
         od:jetType="longinteger" od:sqlSType="int">
       <xsd:simpleType>
        <xsd:restriction base="xsd:integer"/>
       </xsd:simpleType>
       </xsd:element>
       <xsd:element name="QuantityPerUnit" minOccurs="0"
        od:jetType="text" od:sqlSType="nvarchar">
       <xsd:simpleType>
        <xsd:restriction base="xsd:string">
        <xsd:maxLength value="20"/>
        </xsd:restriction>
       </xsd:simpleType>
       </xsd:element>
       <xsd:element name="UnitPrice" minOccurs="0"
         od:jetType="currency"
         od:sqlSType="money" type="xsd:double"/>
       <xsd:element name="UnitsInStock" minOccurs="0"
         od:jetType="integer"
         od:sqlSType="smallint" type="xsd:short"/>
       <xsd:element name="UnitsOnOrder" minOccurs="0"
         od:jetType="integer"
         od:sqlSType="smallint" type="xsd:short"/>
       <xsd:element name="ReorderLevel" minOccurs="0"
         od:jetType="integer"
         od:sqlSType="smallint" type="xsd:short"/>
       <xsd:element name="Discontinued"
         od:jetType="yesno"
         od:sqlSType="bit" od:nonNullable="yes"
         type="xsd:byte"/>
      </xsd:sequence>
      </xsd:complexType>
     </xsd:element>
    </xsd:schema>
    <dataroot xmlns:xsi=
      "http://www.w3.org/2000/10/XMLSchema-instance">
     <Products>
      <ProductID>1</ProductID>
      <ProductName>Chai</ProductName>
      <SupplierID>1</SupplierID>
      <CategoryID>1</CategoryID>
      <QuantityPerUnit>10 boxes x 20 bags
      </QuantityPerUnit>
      <UnitPrice>18</UnitPrice>
      <UnitsInStock>39</UnitsInStock>
      <UnitsOnOrder>0</UnitsOnOrder>
      <ReorderLevel>10</ReorderLevel>
      <Discontinued>0</Discontinued>
     </Products>
     <Products>
      <ProductID>2</ProductID>
      <ProductName>Chang</ProductName>
      <SupplierID>1</SupplierID>
      <CategoryID>1</CategoryID>
      <QuantityPerUnit>24 - 12 oz bottles
      </QuantityPerUnit>
      <UnitPrice>19</UnitPrice>
      <UnitsInStock>17</UnitsInStock>
      <UnitsOnOrder>40</UnitsOnOrder>
      <ReorderLevel>25</ReorderLevel>
      <Discontinued>0</Discontinued>
     </Products>
     <Products>
      <ProductID>3</ProductID>
      <ProductName>Aniseed Syrup</ProductName>
      <SupplierID>1</SupplierID>
      <CategoryID>2</CategoryID>
      <QuantityPerUnit>12 - 550 ml bottles
      </QuantityPerUnit>
      <UnitPrice>10</UnitPrice>
      <UnitsInStock>13</UnitsInStock>
      <UnitsOnOrder>70</UnitsOnOrder>
      <ReorderLevel>25</ReorderLevel>
     <Discontinued>0</Discontinued>
     </Products>
     <Products>
      <ProductID>4</ProductID>
      <ProductName>
       <![CDATA[Chef Anton's Cajun Seasoning]]>
      </ProductName>
      <SupplierID>2</SupplierID>
      <CategoryID>2</CategoryID>
      <QuantityPerUnit>48 - 6 oz jars</QuantityPerUnit>
      <UnitPrice>22</UnitPrice>
      <UnitsInStock>53</UnitsInStock>
      <UnitsOnOrder>0</UnitsOnOrder>
      <ReorderLevel>0</ReorderLevel>
      <Discontinued>0</Discontinued>
     </Products>
     <Products>
      <ProductID>5</ProductID>
      <ProductName><![CDATA[Chef Anton's Gumbo Mix]]>
      </ProductName>
      <SupplierID>2</SupplierID>
      <CategoryID>2</CategoryID>
      <QuantityPerUnit>36 boxes</QuantityPerUnit>
      <UnitPrice>21.35</UnitPrice>
      <UnitsInStock>0</UnitsInStock>
      <UnitsOnOrder>0</UnitsOnOrder>
      <ReorderLevel>0</ReorderLevel>
      <Discontinued>1</Discontinued>
     </Products>
     <Products>
      <ProductID>6</ProductID>
      <ProductName>
      <![CDATA[Grandma's Boysenberry Spread]]>
      </ProductName>
      <SupplierID>3</SupplierID>
      <CategoryID>2</CategoryID>
      <QuantityPerUnit>12 - 8 oz jars</QuantityPerUnit>
      <UnitPrice>25</UnitPrice>
      <UnitsInStock>120</UnitsInStock>
      <UnitsOnOrder>0</UnitsOnOrder>
      <ReorderLevel>25</ReorderLevel>
      <Discontinued>0</Discontinued>
     </Products>
    </dataroot>
    </root>

NOTE

Generating an Inline Schema The Products.xml file is generated by exporting a portion of the Products table from the Northwind sample database in Microsoft Access 2002.

  1. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  2. Run the project. Click the button to load the XML file and extract the inline schema information to the text box, as shown in Figure 2.9.

Figure 2.9 You can extract XSD schema information from an XML file by using the DataSet.ReadXmlSchema() method.

The DataSet object must have the capability to read an XML schema so that it can construct a matching data structure in memory. The .NET Framework designers thoughtfully exposed this capability to you through the ReadXmlSchema() and WriteXmlSchema() methods of the DataSet object. But what if the file does not contain explicit schema information? It turns out that you can still use the DataSet object, because this object also can infer an XML schema based on the data in an XML file. Step-by-Step 2.11 demonstrates this technique.

STEP BY STEP 2.11 - Inferring an XML Schema

  1. Add a new form to the project. Name the new form StepByStep2_11.cs.

  2. Add a Button control (btnInferSchema) and a TextBox control (txtSchema) to the form. Set the MultiLine property of the TextBox to true and set its ScrollBars property to Vertical.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Data;
    using System.IO;
  4. Double-click the Button controls and add code to process an XML document when you click the Button control:

    private void btnInferSchema_Click(
      object sender, System.EventArgs e)
    {
      // Load an XML file with no schema information
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Books.xml");
      // Read the schema (only) into a DataSet
      DataSet ds = new DataSet();
      String[] ns = {};
      ds.InferXmlSchema(xtr, ns);
      // Write the schema out as a separate stream
      StringWriter sw = new StringWriter();
      ds.WriteXmlSchema(sw);
      txtSchema.Text = sw.ToString();
      // Clean up
      xtr.Close();}
  5. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  6. Run the project. Click the button to load the XML file and infer the schema information to the text box, as shown in Figure 2.10.

Figure 2.10 You can infer schema information from an XML file by using the DataSet.InferXmlSchema() method.

To summarize, there are at least four ways that you can obtain XSD files for your applications:

NOTE

XML Schema Definition Tool (xsd.exe) You can use the .NET Framework XML Schema Definition Tool (xsd.exe) to:

Generate an XML schema document from a DLL or EXE.

Generate a C# class file that conforms to the given XML schema file.

Generate an XML schema from an XML Data Reduced (XDR) schema file.

Generate an XML schema from an XML file.

Generate DataSet classes from an XML schema file.

Using an XSD Schema

Access and Manipulate XML Data: Validate an XML Document.

The prime use of a schema file is to validate the corresponding XML file. Although any XML file that conforms to the syntactical rules for XML is well-formed, this does not automatically make the file valid. A valid XML file is one whose structure conforms to a specification. This specification can be in the form of an XML schema or a Document Type Description (DTD), for example. Any valid XML file is well-formed, but not every well-formed XML file is valid.

In this section, you'll see the programmatic support that the .NET Framework provides for validating XML files.

Validating Against XSD

To validate an XML document, you can use the XmlValidatingReader class. This class implements the XmlReader class and provides support for validating XML documents. It can also validate the XML document as it is read in to the XmlDocument object. Step-by-Step 2.12 shows how you can use the XmlValidatingReader object to validate an XML document with an inline schema.

STEP BY STEP 2.12 - Validating an XML Document Against an Inline Schema

  1. Add a new form to the project. Name the new form StepByStep2_12.cs.

  2. Add a Button control (btnValidate) and a TextBox control (txtErrors) to the form. Set the MultiLine property of the TextBox to true and set its ScrollBars property to Vertical.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Xml.Schema;
  4. Double-click the Button controls and add code to validate an XML document when you click the Button control:

    private void btnValidate_Click(
      object sender, System.EventArgs e)
    {
      // Load a document with an inline schema
      XmlTextReader xtr =
        new XmlTextReader(@"..\..\Products.xml");
      // Prepare to validate it
      XmlValidatingReader xvr =
        new XmlValidatingReader(xtr);
      xvr.ValidationType = ValidationType.Schema;
      // Tell the validator what to do with errors
      xvr.ValidationEventHandler +=
        new ValidationEventHandler(ValidationHandler);
      // Load the document, thus validating
      XmlDocument xd = new XmlDocument();
      xd.Load(xvr);
      // Clean up
      xvr.Close();
    }
    
    public void ValidationHandler(
      object sender, ValidationEventArgs e)
    {
      // Dump any validation errors to the UI
      txtErrors.AppendText(e.Message + "\n");
    }
  5. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  6. Run the project. Click the button to load and simultaneously validate the XML file, as shown in Figure 2.11.

Figure 2.11 You can validate an XML document by using XmlValidatingReader class.

  1. Stop the project. Open the Products.xml file and make a change. For example, change the name of a child element from SupplierID to SupplierIdentifier.

  2. Run the project. Click the button to load and simultaneously validate the XML file. You'll see additional validation errors, as shown in Figure 2.12.

Figure 2.12 While the XML document is validated, the XmlValidatingReader object raises a ValidationEventHandler event for each error in the XML document.

An inline schema cannot contain an entry for the root element of the document, so even when the document is otherwise valid, you'll get an error from that node. As you can see, the XmlValidatingReader object is constructed so that it does not stop on validation errors. Rather, it continues processing the file, but raises an event for each error. This lets your code decide how to handle errors while still filling the XmlDocument object.

When you change the name of an element in the XML, the XML remains well-formed. But because that name doesn't match the name in the schema file, that portion of the XML document becomes invalid. The XmlValidatingReader object responds by raising additional events.

You can also validate an XML file against an external schema. Step-by-Step 2.13 shows this technique in action.

STEP BY STEP 2.13 - Validating an XML Document Against an External Schema

  1. Add a new form to the project. Name the new form StepByStep2_13.cs.

  2. Add a Button control (btnValidate) and a TextBox control (txtErrors) to the form. Set the MultiLine property of the TextBox to true and set its ScrollBars property to Vertical.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Xml.Schema;
  4. Double-click the Button controls and add code to validate an XML document when you click the Button control:

    private void btnValidate_Click(
      object sender, System.EventArgs e)
    {
      // Load a document with an external schema
      XmlTextReader xtr =
        new XmlTextReader(@"..\..\Books2.xml");
      // Prepare to validate it
      XmlValidatingReader xvr =
        new XmlValidatingReader(xtr);
      xvr.ValidationType = ValidationType.Schema;
      // Tell the validator what to do with errors
      xvr.ValidationEventHandler +=
        new ValidationEventHandler(ValidationHandler);
      // Load the schema
      XmlSchemaCollection xsc =
        new XmlSchemaCollection();
      xsc.Add("xsdBooks", @"..\..\Books2.xsd");
      // Tell the validator which schema to use
      xvr.Schemas.Add(xsc);
      // Load the document, thus validating
      XmlDocument xd = new XmlDocument();
      xd.Load(xvr);
      // Clean up
      xvr.Close();
    }
    public void ValidationHandler(
      object sender, ValidationEventArgs e)
    {
      // Dump any validation errors to the UI
      txtErrors.AppendText(e.Message + "\n");
    }
  5. Add a new XML file to the project. Name the new file Books2.xml. Enter the following text for Books2.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <Books xmlns="xsdBooks">
      <Book Pages="1088">
        <Author>Delaney, Kalen</Author>
        <Title>Inside Microsoft SQL Server 2000</Title>
        <Publisher>Microsoft Press</Publisher>
      </Book>
      <Book Pages="997">
        <Author>Burton, Kevin</Author>
        <Title>.NET Common Language Runtime</Title>
        <Publisher>Sams</Publisher>
      </Book>
      <Book Pages="392">
        <Author>Cooper, James W.</Author>
        <Title>C# Design Patterns</Title>
        <Publisher>Addison Wesley</Publisher>
      </Book>
    </Books>
  6. Add a new schema file to the project. Name the new schema file Books2.xsd. Enter the following text for Books2.xsd:

    <?xml version="1.0" encoding="utf-8" ?>
    <xs:schema id="Books" xmlns="xsdBooks"
     elementFormDefault="qualified"
     targetNamespace="xsdBooks"
     xmlns:xs="http://www.w3.org/2001/XMLSchema">
      <xs:element name="Books">
        <xs:complexType>
          <xs:choice maxOccurs="unbounded">
            <xs:element name="Book">
              <xs:complexType>
                <xs:sequence>
                 <xs:element name="Author"
                   type="xs:string" />
                 <xs:element name="Title"
                   type="xs:string" />
                 <xs:element name="Publisher"
                   type="xs:string" />
                </xs:sequence>
                <xs:attribute name="Pages"
                 type="xs:string" />
              </xs:complexType>
            </xs:element>
          </xs:choice>
        </xs:complexType>
      </xs:element>
    </xs:schema>
  7. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  8. Run the project. Click the button to load and simultaneously validate the XML file. Because the file exactly matches the schema, you won't see any errors.

  9. Stop the project. Open the Books2.xml file and make a change. For example, change the name of a child element from Author to Writer.

  10. Run the project. Click the button to load and simultaneously validate the XML file. You'll see validation errors, as shown in Figure 2.13.

Figure 2.13 To validate against an external schema, you need to add the schema to the XmlValidatingReader.Schemas collection.

Validating Against a DTD

Using schema files is not the only way to describe the structure of an XML file. An older standard for specifying structure is the Document Type Definition, or DTD. DTDs are part of the Standardized Generalized Markup Language (SGML) standard, from which both HTML and XML derive. The XmlValidatingReader class can also validate an XML document for conformance with a DTD, as you'll see in Step-by-Step 2.14.

NOTE

DTD Tutorial A good source for more information on DTDs is the XMLFiles.com DTD Tutorial, located at http://www.xmlfiles.com/dtd.

STEP BY STEP 2.14 - Validating an XML Document Against a DTD

  1. Add a new form to the project. Name the new form StepByStep2_14.cs.

  2. Add a Button control (btnValidate) and a TextBox control (txtErrors) to the form. Set the MultiLine property of the TextBox to true and set its ScrollBars property to Vertical.

  3. Switch to the code view and add the following using directives:

    using System.Xml;
    using System.Xml.Schema;
  4. Double-click the Button controls and add code to validate an XML document when you click the Button control:

    private void btnValidate_Click(
      object sender, System.EventArgs e)
    {
      // Load a document with a DTD
      XmlTextReader xtr =
        new XmlTextReader(@"..\..\Books3.xml");
      // Prepare to validate it
      XmlValidatingReader xvr =
        new XmlValidatingReader(xtr);
      xvr.ValidationType = ValidationType.DTD;
      // Tell the validator what to do with errors
      xvr.ValidationEventHandler +=
        new ValidationEventHandler(ValidationHandler);
      // Load the document, thus validating
      XmlDocument xd = new XmlDocument();
      xd.Load(xvr);
      // Clean up
      xvr.Close();
    }
    
    public void ValidationHandler(
      object sender, ValidationEventArgs e)
    {
      // Dump any validation errors to the UI
      txtErrors.AppendText(e.Message + "\n");
    }
  5. Add a new XML file to the project. Name the new file Books3.xml. Enter the following text for Books3.xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE Books SYSTEM "books.dtd">
    <Books>
      <Book Pages="1088">
        <Author>Delaney, Kalen</Author>
        <Title>Inside Microsoft SQL Server 2000</Title>
        <Publisher>Microsoft Press</Publisher>
      </Book>
      <Book Pages="997">
        <Author>Burton, Kevin</Author>
        <Title>.NET Common Language Runtime</Title>
        <Publisher>Sams</Publisher>
      </Book>
      <Book Pages="392">
        <Author>Cooper, James W.</Author>
        <Title>C# Design Patterns</Title>
        <Publisher>Addison Wesley</Publisher>
      </Book>
    </Books>

NOTE

XDR Validation The XmlValidatingReader can also validate an XML file for conformance with an XML Data Reduced (XDR) specification. XDR is a standard that Microsoft briefly embraced for describing XML files before they settled on the more standard XSD. You're not likely to find many XDR files in common use.

  1. Add a new text file to the project. Name the new schema file Books.dtd. Enter the following text for Books.dtd:

    <!ELEMENT Books (Book)* >
    <!ELEMENT Book (Author, Title, Publisher) >
    <!ATTLIST Book Pages CDATA #REQUIRED>
    <!ELEMENT Author (#PCDATA)>
    <!ELEMENT Title (#PCDATA)>
    <!ELEMENT Publisher (#PCDATA)>
  2. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  3. Run the project. Click the button to load and simultaneously validate the XML file. Because the file exactly matches the schema, you won't see any errors.

  4. Stop the project. Open the Books3.xml file and make a change. For example, change the name of a child element from Author to Writer.

  5. Run the project. Click the button to load and simultaneously validate the XML file. You'll see validation errors, as shown in Figure 2.14.

Figure 2.14 You can use the XmlValidatingReader class to validate an XML document against a DTD by setting its ValidationType property to ValidationType.DTD.

If you inspect the code, you'll see that the only difference between validating against a schema file and validating against a DTD is in the constant chosen for the ValidationType property of the XmlValidatingReader object.

REVIEW BREAK

Using XML with SQL Server

The .NET team is not the only group at Microsoft that's been working with XML. Over the past several releases, Microsoft SQL Server has become increasingly integrated with XML. In the current release, you can generate XML with SQL statements, using Microsoft T-SQL extensions to the SQL standard query language. You can also update SQL Server tables by sending properly formed XML messages, called DiffGrams, to a SQL Server database. In this section, you'll learn the basics of interacting with SQL Server via XML.

Generating XML with SQL Statements

Access and Manipulate XML Data: Write a SQL statement that retrieves XML data from a SQL Server database.

Understanding the FOR XML Clause

SQL Server enables you to retrieve the results of any query as XML rather than as a SQL resultset. To do this, you use the Microsoft-specific FOR XML clause. You can use a variety of options in the FOR XML clause to customize the XML that SQL Server generates.

The first option is FOR XML RAW. When you use raw mode with FOR XML, SQL Server returns one element (always named row) for each row of the resultset, with the individual columns represented as attributes. For example, consider this query:

SELECT Customers.CustomerID, Customers.CompanyName,
 Orders.OrderID, Orders.OrderDate
 FROM Customers INNER JOIN Orders
 ON Customers.CustomerID = Orders.CustomerID
 WHERE Country = 'Brazil' AND
  OrderDate BETWEEN '1997-03-15' AND '1997-04-15'
 FOR XML RAW

If you execute this query (for example, using SQL Query Analyzer) in the Northwind sample database, you'll get back these results:

<row CustomerID="RICAR"
 CompanyName="Ricardo Adocicados"
 OrderID="10481" OrderDate="1997-03-20T00:00:00"/>
<row CustomerID="QUEEN" CompanyName="Queen Cozinha"
 OrderID="10487" OrderDate="1997-03-26T00:00:00"/>
<row CustomerID="COMMI" CompanyName="Comércio Mineiro"
 OrderID="10494" OrderDate="1997-04-02T00:00:00"/>
<row CustomerID="TRADH"
 CompanyName="Tradiça[dd]o Hipermercados"
 OrderID="10496" OrderDate="1997-04-04T00:00:00"/>

NOTE

Result Formatting SQL Query Analyzer returns XML results as one long string. I've reformatted these results for easier display on the printed page. If you have trouble seeing all the results in SQL Query Analyzer, select Tools, Options, Results, and increase the Maximum Character Width setting.

If the query output contains binary columns, you must include the BINARY BASE64 option after the FOR XML clause to avoid a runtime error:

SELECT EmployeeID, Photo
 FROM Employees
 FOR XML RAW, BINARY BASE64

With this option, standard Base64 coding is used to encode any binary columns in the output XML.

The second variant of the FOR XML clause is FOR XML AUTO. When you use auto mode with FOR XML, nested tables in the resultset are represented as nested elements in the XML. Columns are still represented as attributes. For example, here's a query that uses FOR XML AUTO:

SELECT Customers.CustomerID, Customers.CompanyName,
 Orders.OrderID, Orders.OrderDate
 FROM Customers INNER JOIN Orders
 ON Customers.CustomerID = Orders.CustomerID
 WHERE Country = 'Brazil' AND
  OrderDate BETWEEN '1997-03-15' AND '1997-04-15'
 FOR XML AUTO

Here's the corresponding resultset:

 <Customers CustomerID="RICAR"
 CompanyName="Ricardo Adocicados">
 <Orders OrderID="10481"
   OrderDate="1997-03-20T00:00:00"/>
</Customers>
<Customers CustomerID="QUEEN"
   CompanyName="Queen Cozinha">
 <Orders OrderID="10487"
   OrderDate="1997-03-26T00:00:00"/>
</Customers>
<Customers CustomerID="COMMI"
 CompanyName="Comércio Mineiro">
 <Orders OrderID="10494"
   OrderDate="1997-04-02T00:00:00"/>
</Customers>
<Customers CustomerID="TRADH"
 CompanyName="Tradição Hipermercados">
 <Orders OrderID="10496"
   OrderDate="1997-04-04T00:00:00"/>
</Customers>

Note that in this resultset, the Orders element is nested within the Customers element for each order. If there were multiple orders for a single customer, the Orders element would repeat as many times as necessary.

There's a second variant of FOR XML AUTO. You can include the ELEMENTS option to represent columns as elements rather than as attributes. Here's query that uses this option:

SELECT Customers.CustomerID, Customers.CompanyName,
 Orders.OrderID, Orders.OrderDate
 FROM Customers INNER JOIN Orders
 ON Customers.CustomerID = Orders.CustomerID
 WHERE Country = 'Brazil' AND
  OrderDate BETWEEN '1997-03-15' AND '1997-04-15'
 FOR XML AUTO, ELEMENTS

Here's the corresponding resultset:

<Customers>
 <CustomerID>RICAR</CustomerID>
 <CompanyName>Ricardo Adocicados</CompanyName>
 <Orders>
  <OrderID>10481</OrderID>
  <OrderDate>1997-03-20T00:00:00</OrderDate>
 </Orders>
</Customers>
<Customers>
 <CustomerID>QUEEN</CustomerID>
 <CompanyName>Queen Cozinha</CompanyName>
 <Orders>
  <OrderID>10487</OrderID>
  <OrderDate>1997-03-26T00:00:00</OrderDate>
 </Orders>
</Customers>
<Customers>
 <CustomerID>COMMI</CustomerID>
 <CompanyName>Comércio Mineiro</CompanyName>
 <Orders>
  <OrderID>10494</OrderID>
  <OrderDate>1997-04-02T00:00:00</OrderDate>
 </Orders>
</Customers>
<Customers>
 <CustomerID>TRADH</CustomerID>
 <CompanyName>Tradição Hipermercados</CompanyName>
 <Orders>
  <OrderID>10496</OrderID>
  <OrderDate>1997-04-04T00:00:00</OrderDate>
 </Orders>
</Customers>

The final variant of FOR XML is FOR XML EXPLICIT. In explicit mode, you must construct your query so as to create a resultset with the first column named Tag and the second column named Parent. These columns create a self-join in the resultset that is used to determine the hierarchy of the created XML file. Here's a relatively simple query in explicit mode:

SELECT 1 AS Tag, NULL AS Parent,
 Customers.CustomerID AS [Customer!1!CustomerID],
 Customers.CompanyName AS [Customer!1!CompanyName],
 NULL AS [Order!2!OrderID],
 NULL AS [Order!2!OrderDate]
 FROM Customers WHERE COUNTRY = 'Brazil'
UNION ALL
SELECT 2, 1,
 Customers.CustomerID, Customers.CompanyName,
 Orders.OrderID, Orders.OrderDate
 FROM Customers INNER JOIN Orders
 ON Customers.CustomerID = Orders.CustomerID
 WHERE Country = 'Brazil' AND
  OrderDate BETWEEN '1997-03-15' AND '1997-04-15'
ORDER BY [Customer!1!CustomerID], [Order!2!OrderID]
FOR XML EXPLICIT

The resulting XML from this query is as follows:

<Customer CustomerID="COMMI"
 CompanyName="Comércio Mineiro">
 <Order OrderID="10494"
   OrderDate="1997-04-02T00:00:00"/>
</Customer>
<Customer CustomerID="FAMIA"
 CompanyName="Familia Arquibaldo"/>
<Customer CustomerID="GOURL"
 CompanyName="Gourmet Lanchonetes"/>
<Customer CustomerID="HANAR"
  CompanyName="Hanari Carnes"/>
<Customer CustomerID="QUEDE"
  CompanyName="Que Delícia"/>
<Customer CustomerID="QUEEN"
  CompanyName="Queen Cozinha">
 <Order OrderID="10487"
   OrderDate="1997-03-26T00:00:00"/>
</Customer>
<Customer CustomerID="RICAR"
 CompanyName="Ricardo Adocicados">
 <Order OrderID="10481"
   OrderDate="1997-03-20T00:00:00"/>
</Customer>
<Customer CustomerID="TRADH"
 CompanyName="Tradição Hipermercados">
 <Order OrderID="10496"
   OrderDate="1997-04-04T00:00:00"/>
</Customer><Customer CustomerID="WELLI"
 CompanyName="Wellington Importadora"/>

Note that in this case even customers without orders in the specified time period are included, because the first half of the query retrieves all customers from Brazil. Explicit mode allows you the finest control over the generated XML, but it's also the most complex mode to use in practice. You should stick to raw or auto mode whenever possible.

Finally, you can generate schema information as part of a SQL Server query by including the XMLDATA option in the query. You can do this in any of the FOR XML modes. For example, here's a query you saw earlier in this section with the XMLDATA option added:

SELECT Customers.CustomerID, Customers.CompanyName,
 Orders.OrderID, Orders.OrderDate
 FROM Customers INNER JOIN Orders
 ON Customers.CustomerID = Orders.CustomerID
 WHERE Country = 'Brazil' AND
  OrderDate BETWEEN '1997-03-15' AND '1997-04-15'
 FOR XML AUTO, ELEMENTS, XMLDATA

The resulting XML is as follows:

<Schema name="Schema1"
 xmlns="urn:schemas-microsoft-com:xml-data"
 xmlns:dt="urn:schemas-microsoft-com:datatypes">
 <ElementType name="Customers" content="eltOnly"
  model="closed" order="many">
  <element type="Orders" maxOccurs="*"/>
  <element type="CustomerID"/>
  <element type="CompanyName"/>
 </ElementType>
 <ElementType name="CustomerID" content="textOnly"
  model="closed" dt:type="string"/>
 <ElementType name="CompanyName" content="textOnly"
  model="closed" dt:type="string"/>
 <ElementType name="Orders" content="eltOnly"
  model="closed" order="many">
  <element type="OrderID"/>
  <element type="OrderDate"/>
 </ElementType>
 <ElementType name="OrderID" content="textOnly"
  model="closed" dt:type="i4"/>
 <ElementType name="OrderDate" content="textOnly"
  model="closed" dt:type="dateTime"/>
</Schema>
<Customers xmlns="x-schema:#Schema1">
 <CustomerID>RICAR</CustomerID>
 <CompanyName>Ricardo Adocicados</CompanyName>
 <Orders>
  <OrderID>10481</OrderID>
  <OrderDate>1997-03-20T00:00:00</OrderDate>
 </Orders>
</Customers>
<Customers xmlns="x-schema:#Schema1">
 <CustomerID>QUEEN</CustomerID>
 <CompanyName>Queen Cozinha</CompanyName>
 <Orders>
  <OrderID>10487</OrderID>
  <OrderDate>1997-03-26T00:00:00</OrderDate>
 </Orders>
</Customers>
<Customers xmlns="x-schema:#Schema1">
 <CustomerID>COMMI</CustomerID>
 <CompanyName>Comércio Mineiro</CompanyName>
 <Orders>
  <OrderID>10494</OrderID>
  <OrderDate>1997-04-02T00:00:00</OrderDate>
 </Orders>
</Customers>
<Customers xmlns="x-schema:#Schema1">
 <CustomerID>TRADH</CustomerID>
 <CompanyName>Tradição Hipermercados</CompanyName>
 <Orders>
  <OrderID>10496</OrderID>
  <OrderDate>1997-04-04T00:00:00</OrderDate>
 </Orders>
</Customers>

Using ExecuteXmlReader() Method

ADO.NET provides a means to integrate SQL Server's XML capabilities with the .NET Framework classes. The ExecuteXmlReader() method of the SqlCommand object enables you to retrieve an XmlReader directly from a SQL statement, provided that the SQL statement uses the FOR XML clause. Step-by-Step 2.15 shows you how.

STEP BY STEP 2.15 - Using the ExecuteXmlReader() Method

  1. Add a new form to the project. Name the new form StepByStep2_15.cs.

  2. Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.

  3. Open Server explorer. Expand the Data Connections node and locate a node that represents the Northwind sample database from a SQL Server. Drag and drop the node to the form to create a SqlConnection object.

  4. Switch to code view and add the following using directives:

    using System.Text;
    using System.Xml;
    using System.Data;
    using System.Data.SqlClient;
  5. Double-click the Button control and add the following code to handle the button's Click event:

    private void btnReadXML_Click(
      object sender, System.EventArgs e)
    {
      SqlCommand cmd = sqlConnection1.CreateCommand();
      // Create a command to retrieve XML
      cmd.CommandType = CommandType.Text;
      cmd.CommandText = "SELECT Customers.CustomerID, "
      + "Customers.CompanyName," +
      "Orders.OrderID, Orders.OrderDate " +
      "FROM Customers INNER JOIN Orders " +
      "ON Customers.CustomerID = Orders.CustomerID " +
      "WHERE Country = 'Brazil' AND " +
      "OrderDate BETWEEN '1997-03-15' AND '1997-04-15' "
      + "FOR XML AUTO, ELEMENTS";
      sqlConnection1.Open();
    
      // Read the XML into an XmlReader
      XmlReader xr = cmd.ExecuteXmlReader();
      StringBuilder sbNode = new StringBuilder();
    
      // Dump the contents of the reader
      while(xr.Read())
      {
        if((xr.NodeType == XmlNodeType.Element) ||
          (xr.NodeType == XmlNodeType.Text) )
        {
          sbNode.Length = 0;
          for(int intI=1;
            intI <= xr.Depth ; intI++)
          {
            sbNode.Append(" ");
          }
          sbNode.Append(xr.Name + " ");
          sbNode.Append(xr.NodeType.ToString());
    
          if (xr.HasValue)
          {
            sbNode.Append(": " + xr.Value);
          }
          lbNodes.Items.Add(sbNode.ToString());
    
          // Now add the attributes, if any
          if (xr.HasAttributes)
          {
            while(xr.MoveToNextAttribute())
            {
              sbNode.Length=0;
              for(int intI=1;
                intI <= xr.Depth;intI++)
              {
                sbNode.Append(" ");
              }
              sbNode.Append(xr.Name + " ");
              sbNode.Append(
                xr.NodeType.ToString());
              if (xr.HasValue)
              {
                sbNode.Append(": " +
                  xr.Value);
              }
              lbNodes.Items.Add(
                sbNode.ToString());
            }
          }
        }
      }
      // Clean up
      xr.Close();
      sqlConnection1.Close();
    }

WARNING

Close the XmlReader Object When you populate an XmlReader object with the ExecuteXmlReader() method, the XmlReader object gets exclusive use of the underlying SqlConnection object. You cannot perform any other operations through this SqlConnection object until you call the Close() method of the XmlReader object. Be sure to call the Close() method as soon as you are finished with the XmlReader object.

  1. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  2. Run the project. Click the button to run the FOR XML query and display the results in the ListBox control, as shown in Figure 2.15.

Figure 2.15 You can retrieve data as XML from a SQL Server database by using the ExecuteXmlReader() method.

Updating SQL Server Data by Using XML

Access and Manipulate XML Data: Update a SQL Server database by using XML.

You can also update SQL Server data by using special XML messages called DiffGrams. The .NET Framework uses DiffGrams internally as a means of serializing changes in a DataSet object. For example, if you pass the changes in a DataSet object from one tier to another, the .NET Framework uses a DiffGram to send the changes.

NOTE

Not a Valid Document It's tempting to think that you can read an XmlDocument object directly from the XmlReader object returned by the ExecuteXmlReader() method. Unfortunately, if you try this you'll find that it generates an error. This is because the XML returned by FOR XML queries is well-formed, but it lacks an XML declaration and a root node, and is therefore an XML fragment and not a valid XML document.

You can also use DiffGrams yourself to update data in SQL Server. However, before you can do so, you need to install some additional software. This software is the SQLXML Managed Classes, an interface between SQL Server and the .NET Framework. In this section, you learn how to install this software and then how to use DiffGrams to modify SQL Server data.

Installing SQLXML

Although SQL Server 2000 includes some XML support (for example, the FOR XML syntax is built into the product) there have been many advances in XML since that version of SQL Server was released. Microsoft has kept SQL Server in tune with these advances by issuing a series of free upgrade packages with the general name of SQLXML. As of this writing, the current release of SQLXML is SQLXML 3.0 SP1. This package includes the following features:

To install SQLXML, you need to download the current release directly from Microsoft's web site. You can always find the current release by starting at the SQLXML home page, msdn.microsoft.com/nhp/default.asp?contentid=28001300. Before you run the installation, be sure you have the following prerequisite software installed:

SQLXML 3.0 also depends on release 4.0 of the MSXML parser. If this component is not present on your computer, it will be installed as part of the SQLXML installation.

To install SQLXML, download and run the executable. You can either choose to install all components, or select specific components to install.

Using DiffGrams

After you've installed SQLXML, you can use the SqlXmlCommand object to execute a DiffGram, as shown in Step-by-Step 2.16.

STEP BY STEP 2.16 - Executing a DiffGram

  1. Add a new form to the project. Name the new form StepByStep2_16.cs.

  2. Add a Button control (btnUpdate) to the form.

  3. In Solution Explorer, right-click on the References node and select Add Reference. Select the .NET tab and click the Browse button. Browse to the SQLXML .NET library. By default this file is at c:\Program Files\SQLXML 3.0\bin\Microsoft.Data.SqlXml.dll. Click Open and then OK to add the reference.

  4. Switch to code view and add the following using directives:

    using Microsoft.Data.SqlXml;
    using System.IO;
  5. Double-click the Button control and add code to execute a DiffGram when you click the button. If your server requires you to log in with a username and password, modify the connection string accordingly:

    private void btnUpdate_Click(
      object sender, System.EventArgs e)
    {
      // Connect to the SQL Server database
      SqlXmlCommand sxc =
        new SqlXmlCommand("Provider=SQLOLEDB;" +
        "Server=(local);database=Northwind;" +
        "Integrated Security=SSPI");
      // Set up the DiffGram
      sxc.CommandType = SqlXmlCommandType.DiffGram;
      sxc.SchemaPath = @"..\..\diffgram.xsd";
      FileStream fs =
        new FileStream(@"..\..\diffgram.xml",
        FileMode.Open);
      sxc.CommandStream = fs;
    
      try
      {
        // And execute it
        sxc.ExecuteNonQuery();
        MessageBox.Show("Database was updated!");
      }
      catch (SqlXmlException ex)
      {
        ex.ErrorStream.Position = 0;
        string strErr = (new StreamReader(
          ex.ErrorStream).ReadToEnd());
        MessageBox.Show(strErr);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }
      finally
      {
        fs.Close();
      }
    }
  6. Add a new XML file to the project. Name the new file diffgram.xml. Modify the XML for diffgram.xml as follows:

    <?xml version="1.0" standalone="yes"?>
    <diffgr:diffgram
     xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
     xmlns:diffgr=
      "urn:schemas-microsoft-com:xml-diffgram-v1">
     <NewDataSet>
      <Customers diffgr:id="Customers1"
       msdata:rowOrder="0"
       diffgr:hasChanges="modified">
       <CustomerID>ALFKI</CustomerID>
       <ContactName>Maria Anderson</ContactName>
      </Customers>
     </NewDataSet>
     <diffgr:before>
      <Customers diffgr:id="Customers1"
       msdata:rowOrder="0">
       <CustomerID>ALFKI</CustomerID>
       <ContactName>Maria Anders</ContactName>
      </Customers>
     </diffgr:before>
    </diffgr:diffgram>
  7. Add a new schema file to the project. Name the new file diffgram.xsd. Switch to XML view and modify the XML for this file as follows:

    <xsd:schema xmlns:xsd=
     "http://www.w3.org/2001/XMLSchema"
     xmlns:sql="urn:schemas-microsoft-com:mapping-schema">
     <xsd:element name="Customers"
       sql:relation="Customers" >
      <xsd:complexType>
       <xsd:sequence>
        <xsd:element name="CustomerID"
          sql:field="CustomerID"
          type="xsd:string" />
        <xsd:element name="ContactName"
          sql:field="ContactName"
          type="xsd:string" />
       </xsd:sequence>
      </xsd:complexType>
     </xsd:element>
    </xsd:schema>
  8. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  9. Run the project. Click the button to update your SQL Server Northwind database. You can verify that the update worked by running a SELECT query in SQL Query Analyzer, as shown in Figure 2.16.

Figure 2.16 You can use the SqlXmlCommand object to apply DiffGrams to a SQL Server database.

You can think of a DiffGram as a before-and-after snapshot of a part of a SQL Server table. In this case, the first part of the XML file lists a row in the Customers table and indicates that it has been modified. The second part of the DiffGram contains the original data from the SQL Server table. SQL Server can use this data to find the row to be modified.

In addition to the DiffGram, this code requires a schema file that maps the element names in the DiffGram back to tables and columns in the SQL Server database. The sql:relation attribute in the schema file indicates the table mapping, whereas the sql:field attributes indicate the field mappings.

DiffGrams can insert or delete data as well as modify data. For an insertion, the DiffGram contains the data for the new row and no old data. For a deletion, the DiffGram contains the row to be deleted but no new row.

For more information on the DiffGram format, refer to the help files that are installed as a part of the SQLXML package.

REVIEW BREAK

Guided Practice Exercise 2.2

The SQLXML Managed Classes allow some additional flexibility in retrieving XML data from SQL Server to the .NET Framework classes. The key factor is that the SqlXmlCommand object includes a RootTag property. This property enables you to specify a root element to be included in the generated XML.

For this exercise, you'll use a SqlXmlCommand object to retrieve the results of a FOR XML query from a SQL Server database. You should load the results into an XmlDocument object and then display the contents of that object.

Try this on your own first. If you get stuck or would like to see one possible solution, follow the steps below.

  1. Add a new form to the project. Name the new form GuidedPracticeExercise2_2.cs.

  2. Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.

  3. Switch to code view and add the following using directives:

    using Microsoft.Data.SqlXml;
    using System.Xml;
  4. You need the Utility.cs class created in Step-by-Step 2.5, so create it now if you didn't already create it.

  5. Double-click the button control and add code to read XML from the SQL Server when you click the button:

    private void btnReadXML_Click(
      object sender, System.EventArgs e)
    {
      // Connect to the SQL Server database
      SqlXmlCommand sxc =
        new SqlXmlCommand("Provider=SQLOLEDB;" +
        "Server=(local);database=Northwind;" +
        "Integrated Security=SSPI");
      sxc.CommandType = SqlXmlCommandType.Sql;
      sxc.CommandText =
        "SELECT Customers.CustomerID, " +
        "Customers.CompanyName," +
        "Orders.OrderID, Orders.OrderDate " +
        "FROM Customers INNER JOIN Orders " +
        "ON Customers.CustomerID = Orders.CustomerID "
        + "WHERE Country = 'Brazil' AND " +
        "OrderDate BETWEEN '1997-03-15' " +
        "AND '1997-04-15' FOR XML AUTO, ELEMENTS";
      sxc.RootTag = "dataroot";
      // Read the XML into an XmlReader
      XmlReader xr = sxc.ExecuteXmlReader();
      XmlDocument xd = new XmlDocument();
      xd.Load(xr);
      Utility u = new Utility();
      u.XmlToListBox(xd, lbNodes);
      xr.Close();
    }
  6. Insert the Main() method to launch the form. Set the form as the startup object for the project.

  7. Run the project. Click the button to run the FOR XML query and display the results in the ListBox control, as shown in Figure 2.17.

Figure 2.17 You can specify a root element for retrieved XML data from the SQL Server database by using the RootTag property of the SqlXmlCommand object.

Chapter Summary

Extensible Markup Language (XML) is pervasive in the .NET Framework. It's used to pass messages between components, and as the serialization and configuration language for .NET, among other things. As you'd expect, the .NET Framework includes a rich set of classes for working with XML.

In this chapter you learned about some of the main classes in the System.Xml namespace and associated namespaces such as System.Xml.Schema and System.Xml.XPath. You learned how the Document Object Model (DOM) is used to represent an XML document as a tree of nodes. (Like many other parts of XML, the DOM is an official standard of the World Wide Web Consortium, better known as the W3C.) You saw how the XmlReader class gives you forward-only, read-only access to XML documents, and how the XmlNode and XmlDocument objects can be used to manipulate the DOM version of an XML document.

You learned about the XmlDataDocument class, which provides two-way synchronization between an XML document and the equivalent DataSet object. You saw how you could create such a synchronized hierarchical and relational view of your data by starting with either an XML document or an existing DataSet object.

This chapter also introduced the basic syntax of the XPath query language, which lets you address parts of an XML document. You learned how to use XPath in conjunction with the XPathNavigator class to select nodes of an XML document. You also learned how the XPathNavigator class provides random access to the nodes in the DOM representation of an XML document.

You explored XML schemas in a bit more depth, learning how to create a schema from an existing XML file. I also introduced the concept of XML validation, and showed you how to use the XmlValidatingReader class to validate an XML document.

Finally, you learned how to use XML to communicate with a Microsoft SQL Server database. You saw the syntax of the FOR XML clause of the T-SQL SELECT statement, which allows you to extract information from a SQL Server database directly as XML. You also learned about DiffGrams and saw how to use the SQLXML managed classes to execute a DiffGram on a SQL Server database.

KEY TERMS

Apply Your Knowledge

Exercises

2.1 - Compiling XPath Expressions

The .NET Framework developers invested a lot of effort in finding ways to make code more efficient. One performance enhancement in evaluating XPath expressions is the capability to precompile an expression for reuse. This exercise shows you how to use the Compile() method of the XPathNavigator class to create a precompiled XPath expression, represented as an XPathExpression object.

Estimated Time: 15 minutes.

  1. Create a new Visual C# .NET project to use for the exercises in this chapter.

  2. Add a new XML file to the project. Name the new file Books1.xml. Modify the XML for Books1.xml as follows:

    <?xml version="1.0" encoding="UTF-8"?>
    <Books>
      <Book Pages="1088">
        <Author>Delaney, Kalen</Author>
        <Title>
          Inside Microsoft SQL Server 2000
        </Title>
        <Publisher>Microsoft Press
        </Publisher>
      </Book>
      <Book Pages="997">
        <Author>Burton, Kevin</Author>
        <Title>.NET Common Language Runtime
        </Title>
        <Publisher>Sams</Publisher>
      </Book>
      <Book Pages="392">
        <Author>Cooper, James W.</Author>
        <Title>C# Design Patterns</Title>
        <Publisher>Addison Wesley
        </Publisher>
      </Book>
    </Books>
  3. Add a new XML file to the project. Name the new file Books2.xml. Modify the XML for Books2.xml as follows:

    <?xml version="1.0" encoding="UTF-8" ?>
    <Books>
      <Book Pages="792">
        <Author>LaMacchia, Brian A.
        </Author>
        <Title>.NET Framework Security
        </Title>
        <Publisher>Addison Wesley
        </Publisher>
      </Book>
      <Book Pages="383">
        <Author>Bischof Brian</Author>
        <Title>The .NET Languages:
         A Quick Translation Guide</Title>
        <Publisher>Apress</Publisher>
      </Book>
      <Book Pages="196">
        <Author>Simpson, John E.</Author>
        <Title>XPath and XPointer</Title>
        <Publisher>O'Reilly</Publisher>
      </Book>
    </Books>
  4. Add a new form to the project. Name the new form Exercise2_1.cs.

  5. Add a Label control, a TextBox control (txtXPath), a Button control (btnEvaluate), and a ListBox control (lbNodes) to the form.

  6. Switch to code view and add the following using directive:

  7. using System.Xml.XPath;

  8. Double-click the Button control and add code to handle the button's Click event:

    private void btnEvaluate_Click(
      object sender, System.EventArgs e)
    {
      // Load the Books1.xml file
      XPathDocument xpd1 =
        new XPathDocument(
        @"..\..\Books1.xml");
      // Get the associated navigator
      XPathNavigator xpn1 =
        xpd1.CreateNavigator();
      // Precompile an XPath expression
      XPathExpression xpe =
        xpn1.Compile(txtXPath.Text);
      // Retrieve nodes to match
      // the expression
      XPathNodeIterator xpni =
        xpn1.Select(xpe);
      // And dump the results
      lbNodes.Items.Clear();
      lbNodes.Items.Add(
        "Results from Books1.xml:");
      // Load the Books1.xml file
    
      while(xpni.MoveNext())
      {
        lbNodes.Items.Add(" " +
         xpni.Current.NodeType.ToString() +
         ": " + xpni.Current.Name + " = " +
         xpni.Current.Value);
      }
    
      // Now get the second document
      XPathDocument xpd2 =
        new XPathDocument(
        @"..\..\Books2.xml");
      // Get the associated navigator
      XPathNavigator xpn2 =
        xpd2.CreateNavigator();
      // Retrieve nodes to match
      // the expression
      // Reuse the precompiled expression
      xpni = xpn2.Select(xpe);
      // And dump the results
      lbNodes.Items.Add(
        "Results from Books2.xml:");
      while (xpni.MoveNext())
      {
        lbNodes.Items.Add(" " +
        xpni.Current.NodeType.ToString() +
        ": " + xpni.Current.Name + " = " +
          xpni.Current.Value);
      }
    }
  9. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  10. Run the project. Enter an XPath expression and click the button. The code precompiles the expression and then uses the precompiled version to select nodes from each of the two XML files, as shown in Figure 2.18.

Figure 2.18 The Compile() method of the XPathNavigator class allows you to create a precompiled XPath expression.

2.2 - Creating DiffGrams

If you'd like to experiment with the DiffGram format, you don't have to create DiffGrams by hand. You can use the WriteXml() method of the DataSet object to create a DiffGram containing all the changes made since the DataSet object was initialized. This exercise walks you through the process of creating a DiffGram.

Estimated Time: 15 minutes.

  1. Add a new form to the project. Name the new form Exercise2_2.cs.

  2. Add a DataGrid control (dgProducts) and a Button control (btnWriteDiffGram) to the form.

  3. Expand the Server Explorer tree view to show a Data Connection to the Northwind sample database. Drag and drop the connection to the form.

  4. Drag a SaveFileDialog component from the Toolbox and drop it on the form. Set the FileName property of the component to diffgram.xml.

  5. Switch to code view and add the following using directive:

    using System.Data;
    using System.Data.SqlClient;
  6. Add the following DataSet declaration to the class definition:

    DataSet dsProducts = new DataSet();
  7. Double-click the form and add the following code to handle the form's Load event:

    private void Exercise2_2_Load(
      object sender, System.EventArgs e)
    {
      // Create a command to retrieve data
      SqlCommand cmd =
        sqlConnection1.CreateCommand();
      cmd.CommandType = CommandType.Text;
      cmd.CommandText =
        "SELECT * FROM Products " +
        "WHERE CategoryID = 6";
      // Retrieve the data to the DataSet
      SqlDataAdapter da =
        new SqlDataAdapter();
      da.SelectCommand = cmd;
      da.Fill(dsProducts, "Products");
      // Bind it to the user interface
      dgProducts.DataSource = dsProducts;
      dgProducts.DataMember = "Products";
    }
  8. Double-click the button and add the following code to handle the button's Click event:

    private void btnWriteDiffGram_Click(
      object sender, System.EventArgs e)
    {
      // Prompt for a filename
      saveFileDialog1.ShowDialog();
      // Write out the DiffGram
      dsProducts.WriteXml(
        saveFileDialog1.FileName,
        XmlWriteMode.DiffGram);
    }
  9. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  10. Run the project. The form displays records from the Northwind Products table for the selected category ID. Make some changes to the data; you can use the DataGrid to add, edit, or delete records. Click the button and select a name for the DiffGram. Click the Write DiffGram button to create the DiffGram. Open the new DiffGram in a text editor and inspect it to see how your changes are represented.

2.3 - Using XPath with Relational Data

By combining an XML-synchronized DataSet object with an XPath expression, you can use XPath to select information from a relational database. This exercise shows you how you can use a DataSet object, an XmlDataDocument object, and an XPathNavigator object together.

Estimated Time: 25 minutes.

  1. Add a new form to the project. Name the new form Exercise2_3.cs.

  2. Add two Label controls, two TextBox controls (txtSQLStatement and txtXPath), two Button controls (btnCreateDataSet and btnSelectNodes), and a ListBox control (lbNodes) to the form.

  3. Expand the Server Explorer tree view to show a data connection to the Northwind sample database. Drag and drop the connection to the form.

  4. Switch to code view and add the following using directives:

    using System.Data;
    using System.Data.SqlClient;
    using System.Xml;
    using System.Xml.XPath;
  5. Add the following code to the class definition:

    DataSet ds;
    XmlDataDocument xdd;
  6. Double-click the Create DataSet button and enter code to load a DataSet object and an XmlDataDocument object:

    private void btnCreateDataSet_Click(
      object sender, System.EventArgs e)
    {
      // Create a command to retrieve data
      SqlCommand cmd =
        sqlConnection1.CreateCommand();
      cmd.CommandType = CommandType.Text;
      cmd.CommandText = txtSQLStatement.Text;
      // Retrieve the data to the DataSet
      SqlDataAdapter da =
        new SqlDataAdapter();
      da.SelectCommand = cmd;
      ds = new DataSet();
      da.Fill(ds, "Table1");
      // Retrieve the XML form of the Dataset
      xdd = new XmlDataDocument(ds);
    }
  7. Double-click the Select Nodes button and enter code to evaluate the XPath expression:

    private void btnSelectNodes_Click(
      object sender, System.EventArgs e)
    {
      // Get the associated navigator
      XPathNavigator xpn =
        xdd.CreateNavigator();
      // Retrieve nodes to match
      // the expression
      XPathNodeIterator xpni =
        xpn.Select(txtXPath.Text);
      // And dump the results
      lbNodes.Items.Clear();
      while (xpni.MoveNext())
      {
        lbNodes.Items.Add(
          xpni.Current.NodeType.ToString()
          + ": " + xpni.Current.Name +
          " = " + xpni.Current.Value);
      }
    }
  8. Insert the Main() method to launch the form. Set the form as the startup form for the project.

  9. Run the project. Enter a SQL Statement that retrieves rows and click the first button. Then enter an XPath expression and click the second button. You'll see the XPath query results as a set of XML nodes, as shown in Figure 2.19.

Figure 2.19 You can use XPath with an XmlDataDocument object.

Review Questions

  1. How are XML elements and attributes represented in the DOM?

  2. When should you use an XmlTextReader object by itself rather than with an XmlDocument object?

  3. Explain three ways to synchronize an XmlDataDocument object with a DataSet object.

  4. Why should you use an explicit path rather than a documentwide wildcard in an XPath expression?

  5. What is the difference between the XPath expressions /Customers/Customer/Order[1] and (/Customers/Customer/Order)[1]?

  6. How can you instantiate an XPathNavigator object?

  7. What options are there for validating an XML file with the XmlValidatingReader object?

  8. What are the three main variants of the FOR XML clause in T-SQL?

  9. How can you generate schema information with the FOR XML clause in T-SQL?

  10. What data operations can be represented in a DiffGram?

Exam Questions

  1. Your application contains an XML file, Orders.xml, with the following content:

    <?xml version="1.0" encoding="utf-8" ?>
    <Orders>
     <Order OrderID="1">
      <OrderDate>1/1/2003</OrderDate>
     </Order>
     <Order OrderID="2">
      <OrderDate>1/2/2003</OrderDate>
     </Order>
     <Order OrderID="3">
      <OrderDate>1/3/2003</OrderDate>
     </Order>
    </Orders>

    Your application also contains a form with a Button control named btnProcess and a ListBox control named lbNodes. The Click event handler for the Button control has this code:

    private void btnProcess_Click(
      object sender, System.EventArgs e)
    {
      XmlTextReader xtr = new
        XmlTextReader(@"..\..\Orders.xml");
      while(xtr.Read())
      {
        if ((xtr.NodeType ==
            XmlNodeType.Attribute)
          || (xtr.NodeType ==
            XmlNodeType.Element)
          || (xtr.NodeType ==
            XmlNodeType.Text))
        {
          if(xtr.HasValue)
           lbNodes.Items.Add(xtr.Value);
        }
      }
      xtr.Close();
    }

    What will be the contents of the ListBox control after you click the button?

    1. 1
      1/1/2003
      2
      1/2/2003
      3
      1/3/2003
    2. 1
      2
      3
    3. Orders
      Order
      1/1/2003
      Order
      1/2/2003
      Order
      1/3/2003
    4. 1/1/2003
      1/2/2003
      1/3/2003
  2. Your application contains the following XML file:

    <?xml version="1.0" encoding="utf-8" ?>
    <Orders>
     <Order OrderID="1">
      <OrderDate>1/1/2003</OrderDate>
     </Order>
     <Order OrderID="2">
      <OrderDate>1/2/2003</OrderDate>
     </Order>
     <Order OrderID="3">
      <OrderDate>1/3/2003</OrderDate>
     </Order>
    </Orders>

    Your application uses the ReadXmlSchema() method of the DataSet object to create a DataSet object with an appropriate schema for this XML file. Which of the following mappings between XML entities and DataSet object entities will this method create?

    1. Orders and Order will become DataTable objects. OrderID will become a DataColumn object. OrderDate will not be mapped.

    2. Orders and Order will become DataTable objects. OrderID and OrderDate will become DataColumn objects.

    3. Orders and Order will become DataTable objects. OrderDate will become a DataColumn. OrderID will not be mapped.

    4. Orders will become a DataTable. Order and OrderDate will become DataColumn objects.

  3. Your application includes an XML file that represents inventory in a warehouse. Each inventory item is identified by 50 different elements. You need to work with 4 of these elements on a DataGrid control. You plan to create a DataSet object containing the appropriate data that can be bound to the DataGrid control. How should you proceed?

    1. Load the XML file into an XmlDocument object. Create a DataSet object containing a single DataTable object with the desired column. Write code that loops through the nodes in the XmlDocument object and that transfers the data from the appropriate nodes to the DataSet object.

    2. Load the XML file into an XmlDataDocument object. Retrieve the DataSet object from the XmlDataDocument object's DataSet property.

    3. Load the XML file into a DataSet object by calling the DataSet object's ReadXml() method.

    4. Create a schema that includes the four required elements. Create a DataSet object from this schema. Create an XmlDataDocument object from the DataSet object. Load the XML document into the XmlDataDocument object.

  4. You use a SqlDataAdapter object to fill a DataSet object with the contents of the Customers table in your database. The CompanyName of the first customer is "Biggs Industries". You synchronize an XmlDataDocument object with the DataSet object. In the DataSet object, you change the CompanyName of the first customer to "Biggs Limited". After that, in the XmlDataDocument, you change the value of the corresponding node to "Biggs Co." When you call the Update() method of the SqlDataAdapter object, what is the effect?

    1. The CompanyName in the database remains "Biggs Industries".

    2. The CompanyName in the database is changed to "Biggs Limited".

    3. The CompanyName in the database is changed to "Biggs Co."

    4. A record locking error is thrown.

  5. Your application contains the following XML file:

    <?xml version="1.0" encoding="utf-8" ?>
    <Customers>
     <Customer>
      <CustomerName>A Company</CustomerName>
      <Orders>
       <Order OrderID="1">
        <OrderDate>1/1/2003</OrderDate>
       </Order>
       <Order OrderID="2">
        <OrderDate>1/2/2003</OrderDate>
       </Order>
      </Orders>
     </Customer>
     <Customer>
      <CustomerName>B Company</CustomerName>
      <Orders>
       <Order OrderID="3">
        <OrderDate>1/2/2003</OrderDate>
       </Order>
       <Order OrderID="4">
        <OrderDate>1/3/2003</OrderDate>
       </Order>
      </Orders>
     </Customer>
     <Customer>
      <CustomerName>C Company</CustomerName>
      <Orders>
       <Order OrderID="5">
        <OrderDate>1/4/2003</OrderDate>
       </Order>
       <Order OrderID="6">
        <OrderDate>1/5/2003</OrderDate>
       </Order>
      </Orders>
     </Customer>
    </Customers>

    Which XPath expression will return the first OrderID for each customer?

    1. /Customers/Customer/Orders/Order/@OrderID[1]

    2. (/Customers/Customer/Orders/Order)[1]/@OrderID

    3. /Customers/Customer/Orders/Order[1]/@OrderID

    4. (/Customers/Customer/Orders/Order/@OrderID)[1]

  6. Your application contains the following XML file:

    <?xml version="1.0" encoding="utf-8" ?>
    <Customers>
     <Customer>
      <CustomerName>A Company</CustomerName>
      <Orders>
       <Order OrderID="1">
        <OrderDate>1/1/2003</OrderDate>
       </Order>
       <Order OrderID="2">
        <OrderDate>1/2/2003</OrderDate>
       </Order>
      </Orders>
     </Customer>
     <Customer>
      <CustomerName>B Company</CustomerName>
      <Orders>
       <Order OrderID="3">
        <OrderDate>1/2/2003</OrderDate>
       </Order>
       <Order OrderID="4">
        <OrderDate>1/3/2003</OrderDate>
       </Order>
      </Orders>
     </Customer>
     <Customer>
      <CustomerName>C Company</CustomerName>
      <Orders>
       <Order OrderID="5">
        <OrderDate>1/4/2003</OrderDate>
       </Order>
       <Order OrderID="6">
        <OrderDate>1/5/2003</OrderDate>
       </Order>
      </Orders>
     </Customer>
    </Customers>

    Which XPath expression will return the CustomerName for all customers who placed an order on 1/3/2003?

    1. /Customers/Customer[./Orders/Order/OrderDate="1/3/2003"]/CustomerName

    2. /Customers/Customer[/Orders/Order/OrderDate="1/3/2003"]/CustomerName

    3. /Customers/Customer[//Orders/Order/OrderDate="1/3/2003"]/CustomerName

    4. /Customers/Customer[Orders/Order/OrderDate="1/3/2003"]/CustomerName

  7. Your application allows the user to perform arbitrary XPath queries on an XML document. The user does not need to be able to alter the document. Which approach will give you the maximum performance for these requirements?

    1. Read the document into an XmlDocument object. Use the CreateNavigator() method of the XmlDocument object to return an XPathNavigator object. Perform your queries by using the XPathNavigator object.

    2. Read the document into an XmlDataDocument object. Use the DataSet property of the XmlDataDocument object to return a DataSet object. Perform your queries by using the DataSet object.

    3. Read the document into an XmlDataDocument object. Use the CreateNavigator() method of the XmlDataDocument object to return an XPathNavigator object. Perform your queries by using the XPathNavigator object.

    4. Read the document into an XPathDocument object. Use the CreateNavigator() method of the XPathDocument object to return an XPathNavigator object. Perform your queries by using the XPathNavigator object.

  8. You are designing an application that will enable the user to explore the structure of an XML file. You need to allow the user to move to the parent node, next node, or first child node from the current node. Which object should you use to implement this requirement?

    1. XPathNavigator

    2. XmlReader

    3. XmlTextReader

    4. XPathExpression

  9. Your XML document has an inline schema. What is the minimum number of errors that this document will generate if you validate it with the XmlValidatingReader class?

    1. 0

    2. 1

    3. 2

    4. 3

  10. You are retrieving customer data from a SQL Server database into an XML document. You want the CustomerName and ContactName columns to be translated into XML elements. Which clause should you use in your SQL statement?

    1. FOR XML AUTO

    2. FOR XML RAW

    3. FOR XML EXPLICIT

    4. FOR XML AUTO, XMLDATA

  11. Your application contains the following code:

    private void btnReadXML_Click(
      object sender, System.EventArgs e)
    {
      // Create a command to retrieve XML
      SqlCommand sc =
        SqlConnection1.CreateCommand();
      sc.CommandType = SqlCommandType.Text;
      sc.CommandText =
       "SELECT Customers.CustomerID, " +
       "Customers.CompanyName," +
       "Orders.OrderID, Orders.OrderDate " +
       "FROM Customers INNER JOIN Orders " +
       "ON Customers.CustomerID = " +
       "Orders.CustomerID "
       + "WHERE Country = 'Brazil' AND " +
       "OrderDate BETWEEN '1997-03-15' " +
       "AND '1997-04-15' " +
       "FOR XML AUTO, ELEMENTS";
      // Read the XML into an XmlReader
      XmlReader xr = sc.ExecuteXmlReader();
      XmlDocument xd = new XmlDocument();
      xd.Load(xr);
    }

    When you run this code, you receive an error on the line of code that attempts to load the XmlDocument. What can you do to fix the problem?

    1. Use FOR XML RAW instead of FOR XML AUTO in the SQL statement.

    2. Replace the XmlDocument object with an XmlDataDocument object.

    3. Replace the SqlCommand object with a SqlXmlCommand object.

    4. Replace the XmlReader object with an XmlTextReader object.

  12. Your application contains the following code, which uses the SQLXML managed classes to apply a DiffGram to a SQL Server database:

    private void btnUpdate_Click(
      object sender, System.EventArgs e)
    {
      // Connect to the SQL Server database
      SqlXmlCommand sxc =
       new SqlXmlCommand(
       "Provider=SQLOLEDB;" +
       "Server=(local);database=Northwind;" +
       "Integrated Security=SSPI");
    
      // Set up the DiffGram
      sxc.CommandType =
        SqlXmlCommandType.DiffGram;
      FileStream fs =
       new FileStream(@"..\..\diffgram.xml",
        FileMode.Open);
      sxc.CommandStream = fs;
      // And execute it
      sxc.ExecuteNonQuery();
      MessageBox.Show(
        "Database was updated!");
    } 

    When you run the code, it does not update the database. The DiffGram is properly formatted. What should you do to correct this problem?

    1. Use a SqlCommand object in place of the SqlXmlCommand object.

    2. Supply an appropriate schema mapping file for the DiffGram.

    3. Store the text of the DiffGram in the CommandText property of the SqlXmlCommand object.

    4. Use a SqlConnection object to make the initial connection to the database.

  13. Which of these operations can be carried out in a SQL Server database by sending a properly-formatted DiffGram to the database? (Select two.)

    1. Adding a row to a table

    2. Adding a primary key to a table

    3. Deleting a row from a table

    4. Changing the data type of a column

  14. You are developing code that uses the XPathNavigator object to navigate among the nodes in the DOM representation of an XML document. The current node of the XPathNavigator is an element in the XML document that does not have any attributes or any children. You call the MoveToFirstChild() method of the XPathNavigator object. What is the result?

    1. The current node remains unchanged and there is no error.

    2. The current node remains unchanged and a runtime error is thrown.

    3. The next sibling of the current node becomes the current node and there is no error.

    4. The next sibling of the current node becomes the current node and a runtime error is thrown.

  15. Which of these operations requires you to have an XML schema file?

    1. Updating a SQL Server database with a DiffGram through the SQLXML Managed classes

    2. Validating an XML file with the XmlValidatingReader class

    3. Performing an XPath query with the XPathNavigator class

    4. Reading an XML file with the XmlTextReader class

Answers to Review Questions

  1. XML elements are represented as nodes within the DOM. XML attributes are represented as properties of their parent nodes.

  2. The XmlTextReader object provides forward-only, read-only access to XML data. For random access or for read-write access you should use the XmlDocument class or one of its derived classes.

  3. You can synchronize an XmlDataDocument object and a DataSet object by creating a DataSet object from an XmlDataDocument object, by creating an XmlDataDocument object from a DataSet object, or by creating both a DataSet object and an XmlDataDocument object from a schema.

  4. XPath expressions containing an explicit path, such as /Customers/Customer/Order/OrderID, are faster to evaluate than documentwide wildcard expressions such as //OrderID.

  5. /Customers/Customer/Order[1] selects the first order for each customer, whereas (/Customers/Customer/Order)[1] selects the first order in the entire document.

  6. You can instantiate an XPathNavigator object by calling the CreateNavigator() method of the XmlDocument, XmlDataDocument, or XPathDocument classes.

  7. You can use an XmlValidatingReader object to validate an XML file for conformance with an embedded schema, an XSD file, a DTD file, or an XDR file.

  8. The three main variants of FOR XML are FOR XML RAW, FOR XML AUTO, and FOR XML EXPLICIT.

  9. To include schema information with a FOR XML query, specify the XMLDATA option.

  10. DiffGrams can represent insertions, deletions, and modifications of the data in a DataSet object or SQL Server database.

Answers to Exam Questions

  1. D. When you read XML data with the help of XmlReader or its derived classes, nodes are not included for XML attributes, but only for XML elements and the text that they contain. XML element nodes do not have a value. The only text that will be printed out is the value of the text nodes within the OrderDate elements. For more information, see the section "Accessing an XML File" in this chapter.

  2. B. The ReadXmlSchema() method maps nested elements in the XML file to related DataTable objects in the DataSet object. At the leaf level of the DOM tree, both elements and attributes are mapped to DataColumn objects. For more information, see the section "Synchronizing DataSet Objects with XML" in this chapter.

  3. D. Looping through all the nodes in an XmlDocument object is comparatively slow. If you start with the full XML document or by calling the ReadXml() method of the DataSet object, the DataSet object will contain all 50 elements. Using a schema file enables you to limit the DataSet object to holding only the desired data. For more information, see the section "Synchronizing DataSet Objects with XML" in this chapter.

  4. C. The DataSet and the XmlDataDocument objects represent two different views of the same data structure. The last change made to either view is the change that is written back to the database. For more information, see the section "Synchronizing DataSet Objects with XML" in this chapter.

  5. C. /Customers/Customer/Orders/Order/@OrderID[1] selects the first OrderID for each order. (/Customers/Customer/Orders/Order)[1]/ @OrderID selects the OrderID for the first order in the entire file. (/Customers/Customer/Orders/Order/@OrderID)[1] selects the first OrderID in the entire file. The remaining choice is the correct one. For more information, see the section "Understanding XPath" in this chapter.

  6. A. The filtering expression needs to start with the ./ operator to indicate that it is filtering nodes under the current node at that point in the expression. For more information, see the section "Understanding XPath" in this chapter.

  7. D. The XPathDocument class is optimized for read-only XPath queries. For more information, see the section "Understanding XPath" in this chapter.

  8. A. The XPathNavigator object provides random access movement within the DOM. The XmlReader and XmlTextReader objects provide forward-only movement. The XPathExpression object is useful for retrieving a set of nodes, but not for navigating between nodes. For more information, see the section "Using the XPathNavigator Class" in this chapter.

  9. B. An inline schema cannot contain validation information for the root node of the document, so XML documents with inline schemas will always have at least one validation error. For more information, see the section "Using an XSD Schema" in this chapter.

  10. C. The raw and auto modes of the FOR XML statement always map columns to attributes, if the ELEMENTS option is not added in the FOR XML clause. Only explicit mode can map a column to an element. For more information, see the section "Understanding the FOR XML Clause" in this chapter.

  11. C. The SqlCommand.ExecuteXmlReader() method returns an XML fragment rather than an XML document. To load a complete and valid XML document, you need to use the SqlXmlCommand object (from the SQLXML Managed Classes) instead. For more information, see the section "Updating SQL Data by Using XML" in this chapter.

  12. B. When executing a DiffGram via the SQLXML managed classes, you must supply a mapping schema file. Otherwise, the code doesn't know which elements in the DiffGram map to which columns in the database. For more information, see the section "Using DiffGrams" in this chapter.

  13. A, C. DiffGrams are useful for performing data manipulation operations, but not for data definition operations. For more information, see the section "Using DiffGrams" in this chapter.

  14. A. The MoveTo methods of the XPathNavigator object always execute without error. If the requested move cannot be performed, the current node remains unchanged and the method returns false. For more information, see the section "Using the XPathNavigator Class" in this chapter.

  15. A. You cannot perform a DiffGram update without a schema file that specifies the mapping between XML elements and database columns. Validation can be performed with a DTD or XDR file instead of a schema. XPath queries and reading XML files do not require a schema. For more information, see the section "Using DiffGrams" in this chapter.

Suggested Readings and Resources

  1. Visual Studio .NET Combined Help Collection

    • Employing XML in the .NET Framework

    • XML and the DataSet

  2. Microsoft SQL Server Books Online

    • Retrieving XML Documents Using FOR XML

    • SELECT Statement

  3. .NET Framework QuickStart Tutorials, Common Tasks QuickStart, XML section.

  4. Mike Gunderloy. ADO and ADO.NET Programming. Sybex, 2002.

  5. John E. Simpson. XPath and XPointer. O'Reilly, 2002.

  6. John Griffin. XML and SQL Server. New Riders, 2001.

800 East 96th Street, Indianapolis, Indiana 46240

sale-70-410-exam    | Exam-200-125-pdf    | we-sale-70-410-exam    | hot-sale-70-410-exam    | Latest-exam-700-603-Dumps    | Dumps-98-363-exams-date    | Certs-200-125-date    | Dumps-300-075-exams-date    | hot-sale-book-C8010-726-book    | Hot-Sale-200-310-Exam    | Exam-Description-200-310-dumps?    | hot-sale-book-200-125-book    | Latest-Updated-300-209-Exam    | Dumps-210-260-exams-date    | Download-200-125-Exam-PDF    | Exam-Description-300-101-dumps    | Certs-300-101-date    | Hot-Sale-300-075-Exam    | Latest-exam-200-125-Dumps    | Exam-Description-200-125-dumps    | Latest-Updated-300-075-Exam    | hot-sale-book-210-260-book    | Dumps-200-901-exams-date    | Certs-200-901-date    | Latest-exam-1Z0-062-Dumps    | Hot-Sale-1Z0-062-Exam    | Certs-CSSLP-date    | 100%-Pass-70-383-Exams    | Latest-JN0-360-real-exam-questions    | 100%-Pass-4A0-100-Real-Exam-Questions    | Dumps-300-135-exams-date    | Passed-200-105-Tech-Exams    | Latest-Updated-200-310-Exam    | Download-300-070-Exam-PDF    | Hot-Sale-JN0-360-Exam    | 100%-Pass-JN0-360-Exams    | 100%-Pass-JN0-360-Real-Exam-Questions    | Dumps-JN0-360-exams-date    | Exam-Description-1Z0-876-dumps    | Latest-exam-1Z0-876-Dumps    | Dumps-HPE0-Y53-exams-date    | 2017-Latest-HPE0-Y53-Exam    | 100%-Pass-HPE0-Y53-Real-Exam-Questions    | Pass-4A0-100-Exam    | Latest-4A0-100-Questions    | Dumps-98-365-exams-date    | 2017-Latest-98-365-Exam    | 100%-Pass-VCS-254-Exams    | 2017-Latest-VCS-273-Exam    | Dumps-200-355-exams-date    | 2017-Latest-300-320-Exam    | Pass-300-101-Exam    | 100%-Pass-300-115-Exams    |
http://www.portvapes.co.uk/    | http://www.portvapes.co.uk/    |