Pass the MCAD/MCSD: Learning to Access and Manipulate XML Data
Date: May 9, 2003
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:
Access and manipulate XML Data.
Access an XML file by using the Document Object Model (DOM) and an XmlReader.
Transform DataSet data into XML data.
Use XPath to query XML data.
Generate and use an XSD schema.
Write a SQL statement that retrieves XML data from a SQL Server database.
Update a SQL Server database by using XML.
Validate an XML document.
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
- Understanding the DOM
- Using an XMLReader Object
- The XmlNode Class
- The XmlDocument Class
Synchronizing DataSet Objects with XML
- The XmlDataDocument Class
- Synchronizing a DataSet Object with an XmlDataDocument Object
- Starting with an XmlDataDocument Object
- Starting with a Full DataSet Object
- Starting with an XML Schema
Understanding XPath
- The XPath Language
- Using the XPathNavigator Class
- Selecting Nodes with XPath
- Navigating Nodes with XPath
Generating and Using XSD Schemas
- Generating an XSD Schema
- Using an XSD Schema
- Validating Against XSD
- Validating Against a DTD
Using XML with SQL Server
- Generating XML with SQL Statements
- Understanding the FOR XML Clause
- Using ExecuteXmlReader() Method
- Updating SQL Data by Using XML
- Installing SQLXML
- Using DiffGrams
Chapter Summary
Apply Your Knowledge
-
Use the XmlDocument and XmlNode objects to navigate through some XML files. Inspect the node types that you find and understand how they relate to the original XML.
-
Use the XmlDataDocument class to synchronize a DataSet object with an XML file. Save the XML file to disk and inspect its contents. Understand how the generated XML relates to the original DataSet object.
-
Use an XPath processor to run XPath queries against an XML file. Make sure you know the XPath syntax to select portions of the XML.
-
Use the methods of the DataSet object to create XSD files. Inspect the generated XSD and understand how it relates to the original objects.
-
Use XML to read and write SQL Server data. You can install the MSDE version of SQL Server from your Visual Studio .NET CD-ROMs if you don't have a full SQL Server to work with.
-
Use the XmlValidatingReader class to validate an XML file. Make a change to the file that makes it invalid and examine the results when you try to validate the file.
-
Review the XML Data section of the Common Tasks QuickStart Tutorials that ship as part of the .NET Framework SDK.
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
-
Create a new Visual C# .NET Windows application. Name the application 320C02.
-
Right-click on the project node in Solution Explorer and select Add, Add New Item.
-
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.
-
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>
-
Add a new form to the project. Name the new form StepByStep2_1.cs.
-
Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Text;
-
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(); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
-
Add a new form to the project. Name the new form StepByStep2_2.cs.
-
Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Text;
-
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(); }
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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
-
Add a new form to the project. Name the new form StepByStep2_3.cs.
-
Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Text;
-
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; } } } }
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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
The Document Object Model (DOM) is a W3C standard for representing the information contained in an HTML or XML document as a tree of nodes.
The XmlReader class defines an interface for reading XML documents. The XmlTextReader class inherits from the XmlReader class to read XML documents from streams.
The XmlNode object can be used to represent a single node in the DOM.
The XmlDocument object represents an entire XML document.
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:
An XmlDataDocument object
A full DataSet object
A schema-only DataSet object
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
-
Add a new form to the project. Name the new form StepByStep2_4.cs.
-
Add a Button control (btnLoadXml) and a DataGrid control (dgXML) to the form.
-
Switch to the code view and add the following using directives:
using System.Data; using System.Xml;
-
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(); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
-
Add a new form to the project. Name the new form StepByStep2_5.cs.
-
Add a Button control (btnLoadDataSet) and a ListBox control (lbNodes) to the form.
-
Open Server Explorer.
-
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.
-
Select the SqlDataAdapter object. Click the Create DataSet hyperlink beneath the Properties Window. Name the new DataSet dsEmployees and click OK.
-
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; } } } } } }
-
Switch to the code view of the form StepByStep2_5.cs and add the following using directives:
using System.Data; using System.Xml;
-
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); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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:
Create a new DataSet object with the proper schema to match an XML document, but no data.
Create the XmlDataDocument object from the DataSet object.
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
-
Add a new form to the project. Name the new form StepByStep2_6.cs.
-
Add a Button control (btnLoadXml), a DataGrid control (dgXML), and a ListBox control (lbNodes) to the form.
-
Add a new XML Schema file to the project from the Add New Item dialog box. Name the new file Books.xsd.
-
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>
-
Switch to the code view of the form StepByStep2_5.cs and add the following using directives:
using System.Data; using System.Xml;
-
You need the Utility.cs class created in Step-by-Step 2.5, so create it now if you didn't already create it.
-
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"; }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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:
-
Add a new form GuidedPracticeExercise2_1.cs to your Visual C# .NET project.
-
Add two Button controls (btnLoadXml and btnSaveXml) and a DataGrid control (dgXML) to the form.
-
Switch to the code view and add the following using directives:
using System.Data; using System.Xml;
-
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; }
-
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(); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
The XmlDataDocument class is a subclass of the XmlDocument class that can be synchronized with a DataSet object.
You can start the synchronization process with the XmlDataDocument object or with the DataSet object, or you can use a schema file to construct both objects.
Changes to one synchronized object are automatically reflected in the other.
You can use an XmlTextWriter object to persist an XmlDocument object back to disk.
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:
./ uses the current node as the current context.
/ uses the root of the XML document as the current context.
.// uses the entire XML hierarchy starting with the current node as the current context.
// uses the entire XML document as the current context.
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
-
Add a new form to the project. Name the new form StepByStep2_7.cs.
-
Add a Label control, a TextBox control (txtXPath), a Button control (btnEvaluate), and a ListBox control (lbNodes) to the form.
-
Switch to the code view and add the following using directive:
using System.Xml;
-
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(); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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:
Selecting a set of nodes with an XPath expression
Navigating the DOM representation of the XML document
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
-
Add a new form to the project. Name the new form StepByStep2_8.cs.
-
Add a Label control, a TextBox control (txtXPath), a Button control (btnEvaluate), and a ListBox control (lbNodes) to the form.
-
Switch to the code view and add the following using directive:
using System.Xml.XPath;
-
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); } }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
-
Add a new form to the project. Name the new form StepByStep2_9.cs.
-
Add four Button controls (btnParent, btnPrevious, btnNext, and btnChild), and a ListBox control (lbNodes) to the form.
-
Switch to the code view and add the following using directive:
using System.Xml.XPath;
-
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); }
-
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"); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
XPath is a language for specifying or selecting parts of an XML document. XPath is a query language for XML.
An XPath expression returns a set of zero or more nodes from the DOM representation of an XML document.
The SelectNodes() method of the XmlNode object returns a set of nodes selected by an XPath expression.
The XPathDocument and XPathNavigator objects are optimized for fast execution of XPath queries.
The XPathNavigator object allows random-access navigation of the structure of an XML document.
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
-
Add a new form to the project. Name the new form StepByStep2_10.cs.
-
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.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Data; using System.IO;
-
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(); }
-
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.
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
-
Add a new form to the project. Name the new form StepByStep2_11.cs.
-
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.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Data; using System.IO;
-
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();}
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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:
You can use a file generated by an external application such as Microsoft SQL Server or Microsoft Access.
You can create your own schema files from scratch, using the techniques that you learned in Chapter 1.
You can extract inline schema information from an XML file by using the DataSet.ReadXmlSchema() method.
You can infer schema information from an XML file by using the DataSet.InferXmlSchema() method.
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
-
Add a new form to the project. Name the new form StepByStep2_12.cs.
-
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.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Xml.Schema;
-
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"); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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.
-
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.
-
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
-
Add a new form to the project. Name the new form StepByStep2_13.cs.
-
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.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Xml.Schema;
-
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"); }
-
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>
-
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>
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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.
-
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.
-
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
-
Add a new form to the project. Name the new form StepByStep2_14.cs.
-
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.
-
Switch to the code view and add the following using directives:
using System.Xml; using System.Xml.Schema;
-
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"); }
-
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.
-
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)>
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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.
-
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.
-
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
You can extract an inline schema from an XML file by using the ReadXmlSchema() method of the DataSet class.
You can infer a schema from the structure of an XML file by using the InferXmlSchema() method of the DataSet class.
You can validate an XML document for conformance with an inline schema, an external schema, a DTD, or an XDR file by using the XmlValidatingReader class.
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
-
Add a new form to the project. Name the new form StepByStep2_15.cs.
-
Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.
-
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.
-
Switch to code view and add the following using directives:
using System.Text; using System.Xml; using System.Data; using System.Data.SqlClient;
-
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.
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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:
A direct SOAP interface so that SQL Server can work with Web services without intervening components.
XML views via XSD schemas.
Client-side FOR XML support.
An OLE DB provider for SQL XML data.
Managed classes to expose SQLXML functionality in the .NET environment.
Support for DiffGrams generated by .NET.
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:
Windows Installer 2.0
Microsoft SOAP Toolkit 2.0 SP2
SQL Server 2000 Client Tools
MDAC 2.6 or later
.NET Framework 1.0 or later.
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
-
Add a new form to the project. Name the new form StepByStep2_16.cs.
-
Add a Button control (btnUpdate) to the form.
-
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.
-
Switch to code view and add the following using directives:
using Microsoft.Data.SqlXml; using System.IO;
-
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(); } }
-
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>
-
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>
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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
The FOR XML clause in the SQL Server SELECT statement lets you generate XML documents directly from SQL Server data.
By choosing appropriate options in FOR XML, you can map SQL Server columns as either attributes or elements in the generated XML. You can also choose whether to use Base64 encoding in binary columns, and whether to embed schema information.
You can use the ExecuteXmlReader() method of the SqlCommand object to retrieve XML from a SQL Server database and assign it to classes within the .NET Framework.
The SQLXML package contains XML-related updates for SQL Server 2000.
You can use DiffGrams to package updates to SQL Server tables as XML files. The SqlXmlCommand object can apply DiffGrams to a SQL Server database.
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.
-
Add a new form to the project. Name the new form GuidedPracticeExercise2_2.cs.
-
Add a Button control (btnReadXml) and a ListBox control (lbNodes) to the form.
-
Switch to code view and add the following using directives:
using Microsoft.Data.SqlXml; using System.Xml;
-
You need the Utility.cs class created in Step-by-Step 2.5, so create it now if you didn't already create it.
-
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(); }
-
Insert the Main() method to launch the form. Set the form as the startup object for the project.
-
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
DiffGram
DOM (Document Object Model)
DTD
valid XML document
W3C (World Wide Web Consortium)
well-formed XML document
XPath
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.
-
Create a new Visual C# .NET project to use for the exercises in this chapter.
-
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>
-
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>
-
Add a new form to the project. Name the new form Exercise2_1.cs.
-
Add a Label control, a TextBox control (txtXPath), a Button control (btnEvaluate), and a ListBox control (lbNodes) to the form.
-
Switch to code view and add the following using directive:
-
using System.Xml.XPath;
-
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); } }
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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.
-
Add a new form to the project. Name the new form Exercise2_2.cs.
-
Add a DataGrid control (dgProducts) and a Button control (btnWriteDiffGram) to the form.
-
Expand the Server Explorer tree view to show a Data Connection to the Northwind sample database. Drag and drop the connection to the form.
-
Drag a SaveFileDialog component from the Toolbox and drop it on the form. Set the FileName property of the component to diffgram.xml.
-
Switch to code view and add the following using directive:
using System.Data; using System.Data.SqlClient;
-
Add the following DataSet declaration to the class definition:
DataSet dsProducts = new DataSet();
-
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"; }
-
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); }
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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.
-
Add a new form to the project. Name the new form Exercise2_3.cs.
-
Add two Label controls, two TextBox controls (txtSQLStatement and txtXPath), two Button controls (btnCreateDataSet and btnSelectNodes), and a ListBox control (lbNodes) to the form.
-
Expand the Server Explorer tree view to show a data connection to the Northwind sample database. Drag and drop the connection to the form.
-
Switch to code view and add the following using directives:
using System.Data; using System.Data.SqlClient; using System.Xml; using System.Xml.XPath;
-
Add the following code to the class definition:
DataSet ds; XmlDataDocument xdd;
-
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); }
-
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); } }
-
Insert the Main() method to launch the form. Set the form as the startup form for the project.
-
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
How are XML elements and attributes represented in the DOM?
When should you use an XmlTextReader object by itself rather than with an XmlDocument object?
Explain three ways to synchronize an XmlDataDocument object with a DataSet object.
Why should you use an explicit path rather than a documentwide wildcard in an XPath expression?
What is the difference between the XPath expressions /Customers/Customer/Order[1] and (/Customers/Customer/Order)[1]?
How can you instantiate an XPathNavigator object?
What options are there for validating an XML file with the XmlValidatingReader object?
What are the three main variants of the FOR XML clause in T-SQL?
How can you generate schema information with the FOR XML clause in T-SQL?
What data operations can be represented in a DiffGram?
Exam Questions
-
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/2003 2 1/2/2003 3 1/3/2003
-
1 2 3
-
Orders Order 1/1/2003 Order 1/2/2003 Order 1/3/2003
-
1/1/2003 1/2/2003 1/3/2003
-
-
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?
-
Orders and Order will become DataTable objects. OrderID will become a DataColumn object. OrderDate will not be mapped.
-
Orders and Order will become DataTable objects. OrderID and OrderDate will become DataColumn objects.
-
Orders and Order will become DataTable objects. OrderDate will become a DataColumn. OrderID will not be mapped.
-
Orders will become a DataTable. Order and OrderDate will become DataColumn objects.
-
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?
-
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.
-
Load the XML file into an XmlDataDocument object. Retrieve the DataSet object from the XmlDataDocument object's DataSet property.
-
Load the XML file into a DataSet object by calling the DataSet object's ReadXml() method.
-
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.
-
-
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?
-
The CompanyName in the database remains "Biggs Industries".
-
The CompanyName in the database is changed to "Biggs Limited".
-
The CompanyName in the database is changed to "Biggs Co."
-
A record locking error is thrown.
-
-
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?
-
/Customers/Customer/Orders/Order/@OrderID[1]
-
(/Customers/Customer/Orders/Order)[1]/@OrderID
-
/Customers/Customer/Orders/Order[1]/@OrderID
-
(/Customers/Customer/Orders/Order/@OrderID)[1]
-
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?
-
/Customers/Customer[./Orders/Order/OrderDate="1/3/2003"]/CustomerName
-
/Customers/Customer[/Orders/Order/OrderDate="1/3/2003"]/CustomerName
-
/Customers/Customer[//Orders/Order/OrderDate="1/3/2003"]/CustomerName
-
/Customers/Customer[Orders/Order/OrderDate="1/3/2003"]/CustomerName
-
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?
-
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.
-
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.
-
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.
-
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.
-
-
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?
-
XPathNavigator
-
XmlReader
-
XmlTextReader
-
XPathExpression
-
-
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?
-
0
-
1
-
2
-
3
-
-
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?
-
FOR XML AUTO
-
FOR XML RAW
-
FOR XML EXPLICIT
-
FOR XML AUTO, XMLDATA
-
-
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?
-
Use FOR XML RAW instead of FOR XML AUTO in the SQL statement.
-
Replace the XmlDocument object with an XmlDataDocument object.
-
Replace the SqlCommand object with a SqlXmlCommand object.
-
Replace the XmlReader object with an XmlTextReader object.
-
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?
-
Use a SqlCommand object in place of the SqlXmlCommand object.
-
Supply an appropriate schema mapping file for the DiffGram.
-
Store the text of the DiffGram in the CommandText property of the SqlXmlCommand object.
-
Use a SqlConnection object to make the initial connection to the database.
-
Which of these operations can be carried out in a SQL Server database by sending a properly-formatted DiffGram to the database? (Select two.)
-
Adding a row to a table
-
Adding a primary key to a table
-
Deleting a row from a table
-
Changing the data type of a column
-
-
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?
-
The current node remains unchanged and there is no error.
-
The current node remains unchanged and a runtime error is thrown.
-
The next sibling of the current node becomes the current node and there is no error.
-
The next sibling of the current node becomes the current node and a runtime error is thrown.
-
-
Which of these operations requires you to have an XML schema file?
-
Updating a SQL Server database with a DiffGram through the SQLXML Managed classes
-
Validating an XML file with the XmlValidatingReader class
-
Performing an XPath query with the XPathNavigator class
-
Reading an XML file with the XmlTextReader class
-
Answers to Review Questions
-
XML elements are represented as nodes within the DOM. XML attributes are represented as properties of their parent nodes.
-
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.
-
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.
-
XPath expressions containing an explicit path, such as /Customers/Customer/Order/OrderID, are faster to evaluate than documentwide wildcard expressions such as //OrderID.
-
/Customers/Customer/Order[1] selects the first order for each customer, whereas (/Customers/Customer/Order)[1] selects the first order in the entire document.
-
You can instantiate an XPathNavigator object by calling the CreateNavigator() method of the XmlDocument, XmlDataDocument, or XPathDocument classes.
-
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.
-
The three main variants of FOR XML are FOR XML RAW, FOR XML AUTO, and FOR XML EXPLICIT.
-
To include schema information with a FOR XML query, specify the XMLDATA option.
-
DiffGrams can represent insertions, deletions, and modifications of the data in a DataSet object or SQL Server database.
Answers to Exam Questions
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
D. The XPathDocument class is optimized for read-only XPath queries. For more information, see the section "Understanding XPath" in this chapter.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
-
Visual Studio .NET Combined Help Collection
-
Employing XML in the .NET Framework
-
XML and the DataSet
-
-
Microsoft SQL Server Books Online
-
Retrieving XML Documents Using FOR XML
-
SELECT Statement
-
-
.NET Framework QuickStart Tutorials, Common Tasks QuickStart, XML section.
-
Mike Gunderloy. ADO and ADO.NET Programming. Sybex, 2002.
-
John E. Simpson. XPath and XPointer. O'Reilly, 2002.
-
John Griffin. XML and SQL Server. New Riders, 2001.