Chapter 17: XML 789
The Model.Horsepower property is associated with the [XmlIgnore]
attribute, which specifies that the XmlSerializer not include the property in the
XML document.
Deserializing XML
The XmlSerializer.Deserialize( ) method converts an XML document into an
object of the appropriate type:
//:c17:CarFromFile.cs
//Compile with:
//csc /reference:CarStructure2.exe CarFromFile.cs
//Demonstrates XML Deserialization
using System;
using System.IO;
using System.Xml.Serialization;
public class CarFromFile {
public static void Main(){
XmlSerializer xs = new XmlSerializer(typeof(Car));
FileStream str =
new FileStream("car.xml", FileMode.Open);
Car c = (Car) xs.Deserialize(str);
str.Close();
Console.WriteLine("{0} {1} {2}, {3} {4}",
c.Model.Year, c.Model.Maker, c.Model.Make,
c.Mileage.Quantity, c.Mileage.Units);
}
}///:~
This example requires a reference to the Car and other domain objects defined in
the CarStructure example, but so long as the public properties of the object
suffice to put it into a viable state, the XmlSerializer can both write and read
objects to any kind of Stream.
Can’t serialize cycles
This is a perfectly legitimate object-oriented relationship:
Yang Yin
1111
Figure 17-2: Cyclical relationships can be hard to express in XML
790 Thinking in C# www.ThinkingIn.NET
In this relationship, a Yang contains a reference to a Yin and a Yin to a Yang. It
is possible for both objects to contain references to each other; the data structure
may have a cycle.
XML does not use references to relate objects, it uses containment. One object
contains another, which in turn contains its subobjects. There are no references
native to XML. If the XmlSerializer detects a cycle, it throws an
InvalidOperationException, as this example demonstrates:
//:c17:YinYang.cs
//Throws an exception due to cyclical reference
using System;
using System.Xml.Serialization;
public class Yin {
Yang yang;
public Yang Yang{
get{ return yang;}
set { yang = value;}
}
public Yin(){
}
}
public class Yang {
Yin yin;
public Yin Yin{
get{ return yin;}
set { yin = value;}
}
public Yang(){
}
public static void Main(){
Yin yin = new Yin();
Yang yang = new Yang();
//Set up cycle
yin.Yang = yang;
yang.Yin = yin;
XmlSerializer xs = new XmlSerializer(typeof(Yin));
//Throws InvalidOperationException
Chapter 17: XML 791
xs.Serialize(Console.Out, yin);
}
}///:~
If you wish to use XML to serialize an object structure that might contain cycles,
you will have to create your own proxy for references. This will always require the
use of unique text-based ids in lieu of references, the use of [XmlIgnore] and
the dynamic “reattachment” of references based on the XML Ids.
Throughout this book, we’ve often used the phrase “This is an example of the X
design pattern.” Here, we have what seems to be the opposite case, a situation
where we see a common problem (XML serialization of cyclical references) and
can identify a path towards a general solution. There’s a certain temptation to
design something and present it as “the Mock Reference pattern” (or whatever).
However, probably the most distinctive feature of the seminal books in the
patterns movement (Design Patterns and Pattern-Oriented Software
Architecture) is that they were based on software archaeology; patterns were
recognized in existing, proven software solutions. There are no .NET patterns yet
and very few XML patterns; there simply has not been enough time for a variety
of design templates to prove themselves in the field.
Having said that, let’s take a crack at a serializable Yin-Yang object structure:
//:c17:SerializedYinYang.cs
// Can serialize cycles
using System;
using System.IO;
using System.Collections;
using System.Xml.Serialization;
public class Yin {
static Hashtable allYins = new Hashtable();
public static Yin YinForId(Guid g){
return(Yin) allYins[g];
}
Yang yang;
public Yang Yang{
get{ return yang;}
set {
yang = value;
}
792 Thinking in C# www.MindView.net
}
Guid guid = Guid.NewGuid();
[XmlAttribute]
public Guid Id{
get { return guid;}
set{
lock(typeof(Yin)){
allYins[guid] = null;
allYins[value] = this;
guid = value;
}
}
}
public Yin(){
allYins[guid] = this;
}
}
public class Yang {
Yin yin;
[XmlIgnore]
public Yin Yin{
get{ return yin;}
set { yin = value;}
}
public Guid YinId{
get { return yin.Id;}
set {
yin = Yin.YinForId(value);
if (yin == null) {
yin = new Yin();
yin.Id = value;
}
}
}
Guid guid = Guid.NewGuid();
Chapter 17: XML 793
[XmlAttribute]
public Guid Id{
get { return guid;}
set { guid = value;}
}
public Yang(){
}
public static void Main(){
Yin yin = new Yin();
Yang yang = new Yang();
yin.Yang = yang;
yang.Yin = yin;
XmlSerializer xs = new XmlSerializer(typeof(Yin));
MemoryStream memStream = new MemoryStream();
xs.Serialize(memStream, yin);
memStream.Position = 0;
StreamReader reader = new StreamReader(memStream);
string xmlSerial = reader.ReadToEnd();
Console.WriteLine(xmlSerial);
Console.WriteLine("Creating new objects");
memStream.Position = 0;
Yin newYin = (Yin) xs.Deserialize(memStream);
xs.Serialize(Console.Out, newYin);
Yang newYang = newYin.Yang;
Yin refToNewYin = newYang.Yin;
if (refToNewYin == newYin) {
Console.WriteLine("\nCycle re-established");
}
if (newYin == yin) {
Console.WriteLine("Objects are the same");
} else {
Console.WriteLine("Objects are different");
}
}
}///:~
794 Thinking in C# www.ThinkingIn.NET
This program relies on the Guid structure, which is a “globally unique identifier”
value; both classes have Id properties associated with the
XmlAttributeAttribute that can serve to uniquely identify the objects over
time.
2
The Yin class additionally has a static Hashtable allYins that returns
the Yin for a particular Guid. The Yin( ) constructor and the Yin.Id.set
method update the allYins keys so that allYins and YinForId( ) properly
return the Yin for the particular Guid.
The Yang class property Yin is marked with [XmlIgnore] so that the
XmlSerializer won’t attempt to do a cycle. Instead, Yang.YinId is serialized.
When Yang.YinId.set is called, the reference to Yang.Yin is reestablished by
calling Yin.YinForId( ).
The Yang.Main( ) method creates a Yin and a Yang, establishes their cyclical
relationship, and serializes them to a MemoryStream. The MemoryStream
is printed to the console, gets its Position reset, and is then passed to
XmlSerializer.Deserialize( ), creating new Yin and Yang objects. Although
newYin and newYang have the same Id values and the same cyclical
relationships that the original yin and yang had, they are new objects, as Figure
17-3 illustrates.
2
It’s theoretically possible for GUIDs generated at different times to have the same value,
but it’s exceedingly rare. That’s why GUIDs are only “globally” and not “universally”
unique.
Chapter 17: XML 795
yang : YangYang.Main yin : Yin allYins xs :
XmlSerializer
new
new
[yinGuid] = yin
Yang = yang
Yin = yin
Serialize(yin)
Deserialize(memStream)
newYang :
Yang
new
Id.set(yinGuid)
[yinGuid] = null
changes "official"
Yin for yinGuid
from yin to newYin
[yinGuid] = newYin
new
YinId.set(yinGuid)
Get(yinGuid)
newYin
newYin : Yin
newYin
796 Thinking in C# www.MindView.net
Figure 17-3: Reconstructing cycles from an XML document
The output of the program looks like this, although the Guids will be different:
<?xml version="1.0"?>
<Yin xmlns:xsd="
xmlns:xsi="
Id="8342aaa3-31e4-4d56-95fc-0959301a7ccf">
<Yang Id="531ba739-673e-4840-a2c1-3027f9e60d9f">
<YinId>8342aaa3-31e4-4d56-95fc-0959301a7ccf</YinId>
</Yang>
</Yin>
Creating new objects
<?xml version="1.0" encoding="IBM437"?>
<Yin xmlns:xsd="
xmlns:xsi="
Id="8342aaa3-31e4-4d56-95fc-0959301a7ccf">
<Yang Id="531ba739-673e-4840-a2c1-3027f9e60d9f">
<YinId>8342aaa3-31e4-4d56-95fc-0959301a7ccf</YinId>
</Yang>
</Yin>
Cycle re-established
Schemas
So far, the only restriction that we’ve placed on the XML is that it be well formed.
But XML has an additional capability to specify that only elements of certain
types and with certain data be added to the document. Such documents not only
are well formed, they are valid. XML documents can be validated in two ways,
via Document Type Definition (DTD) files and via W3C XML Schema (XSD) files.
The XML Schema definition is still quite new, but is significantly more powerful
than DTDs. Most significantly, DTDs are not themselves XML documents, so you
can’t create, edit, and reason about DTDs with the same tools and code that you
use to work with XML documents.
An XML Schema, on the other hand, is itself an XML document, so working with
XML Schemas can be done with the same classes and methods that you use to
work with any XML document. Another advantage of XML Schemas is that they
provide for validity checking of the XML data; if the XML Schema specifies that
an element must be a positiveInteger, then a variety of tools can validate the
data flowing in or out of your program to confirm the element’s values.
This XML Schema validates the data of the CarStructure example:
Chapter 17: XML 797
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified"
xmlns:xs="
<xs:element name="Car" nillable="true" type="Car" />
<xs:complexType name="Car">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="1"
name="Model" type="Model" />
<xs:element minOccurs="0" maxOccurs="1"
name="Mileage" type="Mileage" />
<xs:element minOccurs="1" maxOccurs="1"
name="AirConditioning" nillable="true"
type="AirConditioning" />
</xs:sequence>
<xs:attribute name="VIN" type="xs:string" />
</xs:complexType>
<xs:complexType name="Model">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1"
name="Year" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1"
name="Manufacturer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1"
name="Make" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Mileage">
<xs:simpleContent>
<xs:extension base="xs:int">
<xs:attribute name="Units" type="xs:string" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="AirConditioning" />
<xs:element name="Model" nillable="true"
type="Model" />
<xs:element name="Mileage" nillable="true"
type="Mileage" />
<xs:element name="AirConditioning" nillable="true"
type="AirConditioning" />
</xs:schema>
798 Thinking in C# www.ThinkingIn.NET
We aren’t going to go over the details, because you’ll never have to handcraft an
XML Schema until you’re working at a quite advanced level. The .NET
Framework SDK comes with a tool (xsd.exe) that can generate an XML Schema
from an already compiled .NET assembly. This example was generated with this
command line:
xsd CarStructure.exe
More commonly, you’ll be working in a domain with a standards group that
produces the XML Schema as one of its key technical tasks. If given an XML
Schema, the xsd tool can generate classes with public properties that conform to
the XML Schema’s specification. In practice, this rarely works without
modification of the schema; whether the fault lies in the xsd tool or the
widespread lack of experience with XML Schema in vertical industries is difficult
to say.
You can also use the xsd tool to generate a class descended from type DataSet.
As discussed in Chapter 10, a DataSet is an in-memory, disconnected
representation of relational data. So with the xsd tool, you can automatically
bridge the three worlds of objects, XML, and relational data.
ADO and XML
From the discussion of ADO in Chapter 10, you’ll remember that a DataSet is an
in-memory representation of a relational model. An XmlDataDocument is an
XML Doocument whose contents are synchronized with a DataSet, changes to
the XML data are reflected in the DataSet’s data, changes to the DataSet’s data
are reflected in the XmlDataDocument (as always, committing these changes
to the database requires a call to IDataAdapter.Update( )).
This example quickly revisits the Northwind database and is essentially a rehash
of our first ADO.NET program (you’ll need a copy of NWind.mdb in the current
directory):
//:c17:NwindXML.cs
//Demonstrates ADO to XML bridge
using System;
using System.Xml;
using System.Text;
using System.Data;
using System.Data.OleDb;
class NWindXML {
Chapter 17: XML 799
public static void Main(string[] args){
DataSet ds = Employees("Nwind.mdb");
Console.WriteLine(
"DS filled with {0} rows",
ds.Tables[0].Rows.Count);
//New lines begin here
ds.WriteXml(Console.OpenStandardOutput());
Console.WriteLine();
ds.WriteXmlSchema(Console.OpenStandardOutput());
//End new stuff
}
private static DataSet Employees(string fileName){
OleDbConnection cnctn = new OleDbConnection();
cnctn.ConnectionString =
"Provider=Microsoft.JET.OLEDB.4.0;" +
"data source=" + fileName;
DataSet ds = null;
try {
cnctn.Open();
string selStr =
"SELECT FirstName, LastName FROM EMPLOYEES";
IDataAdapter adapter =
new OleDbDataAdapter(selStr, cnctn);
ds = new DataSet("Employees");
adapter.Fill(ds);
} finally {
cnctn.Close();
}
return ds;
}
}///:~
After retrieving a DataSet filled from the Employees table using the same
ADO.NET code shown in Chapter 10,
3
we create a new XmlDataDocument
that is linked to the DataSet ds. We then use DataSet.WriteXml( ) and
DataSet.WriteXmlSchema( ) to present the XML view of the DataSet (an
3
Well, almost the same. Instead of “SELECT * FROM EMPLOYEES” we limit the data to
first and last names because the EMPLOYEES table contains photographs that make the
returned XML unwieldy.
800 Thinking in C# www.MindView.net
alternative would be to use an XmlTextWriter). When run, the output ends like
this:
…
<Table>
<FirstName>Anne</FirstName>
<LastName>Dodsworth</LastName>
</Table>
<Table>
<FirstName>Bob</FirstName>
<LastName>Dobbs</LastName>
</Table>
</Employees>
<?xml version="1.0"?>
<xs:schema id="Employees" xmlns=""
xmlns:xs="
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xs:element name="Employees" msdata:IsDataSet="true">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="Table">
<xs:complexType>
<xs:sequence>
<xs:element name="FirstName" type="xs:string"
minOccurs="0" />
<xs:element name="LastName" type="xs:string"
minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
A DataSet can also directly read an XML stream, either validating it against an
existing schema or inferring a schema from the data. Here, the car.xml becomes
the source of a DataSet and its inferred schema written to the screen:
//:c17:CarDataSet.cs
//Demonstrates DataSet from XML
using System;
Chapter 17: XML 801
using System.Xml;
using System.Text;
using System.IO;
using System.Data;
using System.Data.OleDb;
class CarDataSet {
public static void Main(){
DataSet carDataSet = new DataSet();
carDataSet.ReadXml(
"car.xml", XmlReadMode.InferSchema);
Stream stdout = Console.OpenStandardOutput();
carDataSet.WriteXmlSchema(stdout);
}
}///:~
Combined with the BindingContext class discussed in Chapter 14, .NET’s
powerful infrastructure for XML, data, and display is beginning to fall into place.
The final piece of the puzzle will have to wait until the next chapter, with our
discussion of Web Services that use XML as the native “language” of Web-based
conversations.
XPath navigation
While SQL is the native means of navigating relational data, XPath is the native
means of navigating XML data. Since XML has a treelike structure, the natural
way to get from one place to another is to specify a “path” up-or-down the
branches from either the current Node or from the root Element. XPath is not
itself expressed as an XML Document, rather it is reminiscent of file-system
navigation commands.
The simplest XPaths are those that specify an absolute path to an object. Given
this XML:
<!DOCTYPE set SYSTEM "docbookx.dtd">
<set>
<book>
<bookinfo>
<title>Thinking in C#</title>
<author>
<personname>
<firstname>Larry</firstname>
802 Thinking in C# www.ThinkingIn.NET
<surname>O'Brien</surname>
</personname>
</author>
<author>
<personname>
<firstname>Bruce</firstname>
<surname>Eckel</surname>
</personname>
</author>
<copyright>
<year>2002</year>
<holder>Larry O'Brien & Bruce
Eckel</holder>
</copyright>
</bookinfo>
<preface>
<title>Introduction</title>
</preface>
<chapter>
<title>Those Who Can, Code</title>
</chapter>
<chapter>
<title>Introduction To Objects</title>
</chapter>
<chapter>
<title>Hello, Objects</title>
</chapter>
</book>
</set>
The XPath statement /set/chapter/title selects the titles of all the chapters.
The command //title on the other hand, selects all title elements, including the
title element that is contained below the bookinfo node.
The asterisk (*) command selects all elements contained within the specified
path; //bookinfo/* selects the title, author, and copyright elements.
You move about an XmlDocument (or any other object that implements
IXPathNavigable) via an XPathNavigator. This is the same philosophy that
gives rise to the use of IEnumerators on collection classes – separating the
issues of traversal from the issues of the data structure. However, XPath takes
this one step further: The XPathNavigator is responsible for selecting a
Chapter 17: XML 803
particular node, the XPathNavigator then produces an XPathNodeIterator
to actually move about relative to the position selected by the XPathNavigator.
These relationships are illustrated in Figure 17-4, which illustrates the behavior
of this example:
//:c17:CarNavigator.cs
//Demonstrates XPathNavigator
using System;
using System.Xml;
using System.Xml.XPath;
class CarNavigator {
CarNavigator(string fName){
XmlDocument myDocument = new XmlDocument();
myDocument.Load(fName);
XPathNavigator myNavigator =
myDocument.CreateNavigator();
XPathNodeIterator myIterator =
myNavigator.Select("//Model/*");
while (myIterator.MoveNext()) {
Console.WriteLine(
"Node {0} Value {1}",
myIterator.Current.Name,
myIterator.Current.Value);
}
}
public static void Main(){
new CarNavigator("car.xml");
}
}///:~
804 Thinking in C# www.MindView.net
Car
Vin = “12345678”
Model
CruiseControlMileage
Units = “Miles”
Data = 80000
Make
Data = “Civic”
Manufacturer
Data = “Honda”
Year
Data = “1992”
myNavigator.Select( )
myIterator :
XPathNodeIterator
myDocument :
XmlDocument
Figure 17-4: Navigating with XPathNodeIterator
The CarNavigator( ) constructor loads the data structure from the car.xml
file. XmlDocument.CreateNavigator( ) is a factory method that generates
the XPathNavigator. The XPathNavigator.Select( ) is given an argument
that translates as “select all the children nodes of all the Model nodes.”
There’s an overloaded version of XmlDocument.CreateNavigator( ) that
takes a reference to an XmlNode as an argument; it creates an
XPathNavigator whose context node is not the root node (as was the case in
the previous example) but the passed-in XmlNode. This allows you to work with
subsets of a large XML document. The next example uses this method to create
an XPathNavigator that navigates across a specific sale from the Northwind
database. Additionally, the XPath selection statement in this example uses an
argument to qualify the nodes returned by XPath.Select( ). When an XPath
expression is qualified with square bracket notation, the contents of the square
brackets are logically evaluated. In addition to logical expressions, 1-based index
values can be used. Thus //chapter[3] or //chapter[title=‘Hello, Objects!’] both
select the third chapter of this book’s Docbook representation. In this example,
this type of XPath qualifier is used to select only the sales of a particular
employee:
//:c17:NWindNavigator.cs
//Demonstrates XPathNavigator subselection
using System;
using System.Xml;
using System.Xml.XPath;
using System.Data;
using System.Data.OleDb;
Chapter 17: XML 805
class NWindNavigator {
public static void Main(string[] args){
DataSet ds = EmployeesOrders("Nwind.mdb");
Console.WriteLine(
"DS filled with {0} rows",
ds.Tables[0].Rows.Count);
XmlDataDocument doc = new XmlDataDocument(ds);
SelectSalesByLastName(doc, "Callahan");
}
private static void SelectSalesByLastName(
XmlDataDocument doc, string lastName){
XPathNavigator nav = doc.CreateNavigator();
string xPathSel =
"//Table[LastName='" + lastName + "']";
XPathNodeIterator iter = nav.Select(xPathSel);
while (iter.MoveNext()) {
XPathNavigator saleNav = iter.Current.Clone();
saleNav.MoveToFirstChild();
string fName = saleNav.Value;
saleNav.MoveToNext();
string lName = saleNav.Value;
saleNav.MoveToNext();
string delDate = saleNav.Value;
Console.WriteLine(
"{0} {1} sold for delivery on {2}",
fName, lName, delDate);
}
}
private static DataSet EmployeesOrders(
string fileName){
OleDbConnection cnctn = new OleDbConnection();
cnctn.ConnectionString =
"Provider=Microsoft.JET.OLEDB.4.0;" +
"data source=" + fileName;
DataSet ds = null;
try {
cnctn.Open();
string selStr =
806 Thinking in C# www.ThinkingIn.NET
"SELECT FirstName, LastName, OrderDate FROM"
+ " EMPLOYEES, ORDERS where "
+ " Employees.EmployeeId = Orders.EmployeeId";
IDataAdapter adapter =
new OleDbDataAdapter(selStr, cnctn);
ds = new DataSet("Employees");
adapter.Fill(ds);
} finally {
cnctn.Close();
}
return ds;
}
}///:~
This example starts similarly to the previous Northwind examples, except instead
of just loading data from the Employees table, this time the SQL SELECT
statement joins the employees and their orders. The resulting dataset is
approximately 800 lines long.
After the DataSet is returned to the NWindNavigator( ) constructor, the
constructor creates an XmlDataDocument synchronized with the DataSet.
This XmlDataDocument and the last name of one salesperson become
arguments to NWindNavigator.SelectSalesByLastName( ).
The NWindNavigator.SelectSalesByLastName( ) method constructs an
XPath selector of the form:
//Table[LastName=’Callahan’]
which creates an XPathNodeIterator for “Every node that is a Table and which
in turn has a LastName child node whose value is ‘Callahan.’”
We are not interested in the Table node, of course, we are interested in its
children nodes: the FirstName, LastName, and DeliveryDate nodes. To
navigate to them, we clone XPathNodeIterator.Current and call the resulting
XPathNavigator saleNav. We use XPathNavigator.MoveToFirstChild( )
and XPathNavigator.MoveToNext( ) to traverse the instance of the Table
node. We write to the screen the values of the various nodes in this sub-tree.
This combination of XPathNavigator.Select( ),
XPathNodeIterator.MoveNext( ), and XPathNavigator.MoveToXxx( )
methods is typical of use. The XPathNavigator.MoveToXxx( ) methods are
Chapter 17: XML 807
significantly faster than XPathNavigator.Select( ), but not as flexible for
complex navigation.
If you will be using a single XPathNavigator.Select( ) statement repeatedly,
you can use XPathNavigator.Compile( ) to get a reference to an
XPathExpression, which you can pass in to an overloaded version of
XPathNavigator.Select( ).
XPath syntax has a few other tricks:
♦ The at symbol (@) is used to specify an attribute. Thus, you might select
a specific car by using /Car[@VIN=“12345678”] or a specific yin node
with /yin[@Id=“8342aaa3-31e4-4d56-95fc-0959301a7ccf”>
♦ You can combine paths with the vertical bar operator (|) . //chapter |
//preface selects all elements that are either chapters or prefaces .
♦ The XPath axes child, descendant, parent, ancestor, following-
sibling, and preceding-sibling can be used to select nodes that have
the appropriate structural relationship with the specified node.
/set/book/chapter[1]/following-sibling selects the second chapter of the
book (remember that XPath indices are
1-based, so chapter[1] selects the first chapter). The child axis is the
default axis and need not be specified.
An XPath explorer
The best way to understand XPath is to experiment. This program loads XML
documents into a TreeView control, and highlights element nodes
corresponding to the XPath selection statement you type into the provided
TextBox.
//:c17:XmlTreeView.cs
//Provides a graphical XPath navigator
using System;
using System.Text;
using System.Drawing;
using System.Xml;
using System.Xml.XPath;
using System.Windows.Forms;
class XmlTreeView : TreeView {
XmlDocument doc;
readonly Color DEFAULT_FORECOLOR = Color.Black;
808 Thinking in C# www.MindView.net
readonly Color DEFAULT_BACKCOLOR = Color.White;
readonly Color HIGHLIGHT_FORECOLOR = Color.Blue;
readonly Color HIGHLIGHT_BACKCOLOR = Color.Red;
internal XmlTreeView(XmlDocument src){
Init(src);
}
private void Init(XmlDocument src){
doc = new XmlDocument();
PopulateTreeFromNodeSet(
src.CreateNavigator(), doc, this.Nodes);
}
void PopulateTreeFromNodeSet(
XPathNavigator nav, XmlNode buildNode,
TreeNodeCollection pNodeCol){
do {
string eType = nav.Name;
string eVal = nav.Value;
string nodeText = eType + ": " + eVal;
XmlTreeNode node = null;
switch (nav.NodeType) {
case (XPathNodeType.Element) :
node = new XmlTreeNode(eType, doc);
break;
case(XPathNodeType.Text) :
node = new XmlTreeNode(eVal, doc);
break;
default:
node = new XmlTreeNode(
nav.NodeType.ToString(), doc);
break;
}
pNodeCol.Add(node.TreeNode);
buildNode.AppendChild(node);
if (nav.HasChildren) {
XPathNavigator clone = nav.Clone();
clone.MoveToFirstChild();
PopulateTreeFromNodeSet(
clone, node, node.TreeNode.Nodes);
Chapter 17: XML 809
}
}while (nav.MoveToNext());
}
internal void Highlight(string xPath){
ResetFormatting();
try {
XPathNavigator nav = doc.CreateNavigator();
XPathNodeIterator iter = nav.Select(xPath);
while (iter.MoveNext()) {
XmlNode node =
((IHasXmlNode)iter.Current).GetNode();
if (node is XmlTreeNode) {
XmlTreeNode tNode = (XmlTreeNode) node;
tNode.TreeNode.BackColor = Color.Red;
tNode.TreeNode.ForeColor = Color.Blue;
Invalidate();
}
}
} catch (Exception e) {
Console.WriteLine(e);
}
}
internal void ResetFormatting(){
foreach(TreeNode node in Nodes){
ResetNodeFormatting(node);
}
Invalidate();
}
private void ResetNodeFormatting(TreeNode node){
node.BackColor = DEFAULT_BACKCOLOR;
node.ForeColor = DEFAULT_FORECOLOR;
foreach(TreeNode child in node.Nodes){
ResetNodeFormatting(child);
}
}
internal void LoadFile(string fName){
try {
810 Thinking in C# www.ThinkingIn.NET
XmlDocument doc = new XmlDocument();
doc.Load(fName);
Nodes.Clear();
Init(doc);
} catch (Exception e) {
Console.WriteLine(e);
}
}
}
class XmlTreeNode : XmlElement {
internal XmlTreeNode(string val, XmlDocument doc)
: base("",val,"",doc){
tn = new TreeNode(val);
}
TreeNode tn;
internal TreeNode TreeNode{
get { return tn;}
set { tn = value;}
}
}
class XmlTreeViewForm : Form {
XmlTreeView tl;
TextBox xPathText;
public XmlTreeViewForm(){
Text = "XPath Explorer";
XmlDocument doc = new XmlDocument();
doc.Load("car.xml");
tl = new XmlTreeView(doc);
tl.Dock = DockStyle.Fill;
Controls.Add(tl);
Panel cPanel = new Panel();
cPanel.Dock = DockStyle.Top;
cPanel.Height = 25;
Button xPathSel = new Button();
Chapter 17: XML 811
xPathSel.Text = "Highlight";
xPathSel.Dock = DockStyle.Left;
xPathSel.Click += new EventHandler(NewXPath);
cPanel.Controls.Add(xPathSel);
xPathText = new TextBox();
xPathText.Dock = DockStyle.Left;
xPathText.Width = 150;
cPanel.Controls.Add(xPathText);
Label lbl = new Label();
lbl.Text = "XPath: ";
lbl.Dock = DockStyle.Left;
lbl.Width = 60;
cPanel.Controls.Add(lbl);
Controls.Add(cPanel);
MainMenu mainMenu = new MainMenu();
MenuItem fMenu = new MenuItem("&File");
mainMenu.MenuItems.Add(fMenu);
MenuItem open = new MenuItem("&Open");
fMenu.MenuItems.Add(open);
open.Click += new EventHandler(FileOpen);
fMenu.MenuItems.Add(new MenuItem("-"));
MenuItem exit = new MenuItem("E&xit");
exit.Click += new EventHandler(AppClose);
fMenu.MenuItems.Add(exit);
Menu = mainMenu;
}
void NewXPath(object src, EventArgs e){
string xPath = xPathText.Text;
tl.Highlight(xPath);
}
void FileOpen(object src, EventArgs e){
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "XML files (*.xml)|*.xml";
ofd.FilterIndex = 1;
812 Thinking in C# www.MindView.net
DialogResult fChosen = ofd.ShowDialog();
if (fChosen == DialogResult.OK) {
string fName = ofd.FileName;
tl.LoadFile(fName);
}
}
void AppClose(object src, EventArgs e){
Application.Exit();
}
public static void Main(){
Application.Run(new XmlTreeViewForm());
}
}///:~
The concept of this program is that a TreeView with its TreeNodes is
conceptually similar to an XmlDocument with its XmlNodes. To bridge the
gap between the two classes, we create XmlTreeView, a type of TreeView that
contains a reference to an XmlDocument, and a bunch of XmlTreeNodes, a
type of XmlElement that contains a reference to a TreeNode. An
XPathNavigator selects the XmlNodes in the XmlDocument, selected
XmlNodes that are of type XmlTreeNode change the appearance of their
corresponding TreeNode:
Chapter 17: XML 813
Figure 17-5: The class structure of XmlTreeView
The first task is populating a TreeView with the elements from an
XmlDocument. XmlTreeView.Init( ), which the XmlTreeView( )
constructor calls, creates a new XmlDocument called doc. The passed-in
XmlDocument src is used as the basis for an XPathNavigator that will walk
over all of its nodes, while the XmlTreeView’s inherited TreeNodeCollection
nodes is the source of the TreeNodes. Init( ) calls
XmlTreeView.PopulateNodeFromTreeSet with hthese three arguments,
XPathNavigator nav, XmlNode doc (upcast from XmlDocument), and
TreeNodeCollection Nodes.
XmlTreeView.PopulateNodeFromTreeSet( ) iterates over each XmlNode
in the XPathNavigator nav nodeset. A new XmlTreeNode is created for each
node in the nodeset. The XmlTreeNode( ) constructor in turn creates a new
TreeNode. The TreeNode is appended to the passed-in TreeNodeCollection
and the XmlTreeNode is appended to the XmlDocument doc. If the node
has children, the method clones the XPathNavigator and recursively calls
itself, this time with arguments set to the cloned XPathNavigator (pointing to a
child node of the original XmlDocument), the just-created XmlTreeNode,
and a reference to the just-created TreeNode. The end result is that
TreeView
XmlTreeView
Sets
appearance
Selects
Nodes
XPathNavigator
XmlNode
TreeNode
*
*
*
*
Nodes
Nodes
XmlDocument
1
1
doc
XmlTreeNode
tn
*
*