Although
XmlNode
sits at the top of the inheritance tree,
XmlDocument
is the top-level node in an actual
document object tree. The XmlDocument
has child
nodes, which are accessible through the XmlNode
type’s various properties and methods. One of these
child nodes, accessible through the
DocumentElement
property, is an ordinary
XmlElement
representing the root element of the
tree. There may also be a document type node (such as
<!DOCTYPE inventory SYSTEM
"inventory.dtd">
), represented by an
XmlDocumentType
, accessible through the
DocumentType
property. Finally, some XML documents
will have an XML declaration (such as <?xml
version="1.0
" encoding="utf-8"
standalone="no">
), represented by an
XmlDeclaration
, and accessible only as an ordinary
child node of the XmlDocument
. Figure 5-3 represents a typical XML document tree
structure in memory.
You can create an
XmlDocument
in memory either by calling its
constructor or by calling XmlImplementation.CreateDocument(
)
. Both of these methods are overloaded to take an
XmlNameTable
, and the
XmlDocument
constructor is also overloaded to take
an XmlImplementation
.
XmlNameTable
is used to store atomized element and
attribute names. It provides a more efficient way to compare the
names than using strings. If you create two
XmlDocument
instances using the same
XmlImplementation
, they will share an
XmlNameTable
, making name comparisons more
efficient.
Creating a new
XmlDocument
gives you an empty document.
XmlDocument document = new XmlDocument( );
Technically, the DOM says you shouldn’t be able to
instantiate a node by calling a constructor. However, .NET has
provided an XmlDocument
constructor for
convenience.
Now that you have a
document, you’re free to start adding nodes to it.
As specified by the DOM, in addition to serving as a representation
of the XML document, XmlDocument
also acts as a
factory for the creation of new nodes. The first thing you might want
to do is to create the XML declaration.
XmlDocument
has a CreateXmlDeclaration(
)
method that does just that.
This method takes
version
,
encoding
, and
standalone
parameters. There are some
constraints on the values of these parameters: the
encoding
parameter must be
null
or the name of an encoding supported by the
System.Text.Encoding
class; the
standalone
parameter must be
null
, "yes
“, or
"no
“; and, as of this writing, the
version
parameter must be
"1.0
“. CreateXmlDeclaration( )
creates the XmlDeclaration
node, but does not
insert it into the tree; you must use AppendChild(
)
or a similar method to actually add the node to the
document:
XmlDeclaration declaration = document.CreateXmlDeclaration("1.0",Encoding.UTF8.HeaderName, null); document.AppendChild(declaration);
This code snippet sets the document’s encoding to
Encoding.UTF8
. In most cases you can safely use
the default encoding of the XmlDeclaration
.
However, if you were to save the document to an
XmlWriter
that had a different encoding, the
XmlWriter
would discard the encoding set in the
XmlDeclaration
and use its own instead. This
replacement ensures that the XML can be read back in with the correct
encoding.
Next, you might wish to specify the
document type. Example 5-2 shows a DTD named
inventory.dtd
.
<?xml version="1.0" encoding="UTF-8"?> <!ELEMENT inventory (date,items)> <!ELEMENT date EMPTY> <!ATTLIST date year CDATA #REQUIRED month (1|2|3|4|5|6|7|8|9|10|11|12) #REQUIRED day (1|2|3|4|5|6|7|8|9|10|11| 12|13|14|15|16|17|18|19| 20|21|22|23|24|25|26|27| 28|29|30|31) #REQUIRED> <!ELEMENT items (item)+> <!ELEMENT item EMPTY> <!ATTLIST item quantity CDATA #REQUIRED productCode CDATA #REQUIRED description CDATA #REQUIRED unitCost CDATA #REQUIRED>
The XmlDeclaration
and
XmlDocumentType
nodes are optional, but if you
choose to use them, they must appear in that order, and before the
document element. If you can’t add them sequentially
in your code using AppendChild( )
, you can use the
XmlDocument
type’s methods InsertAfter(
)
, InsertBefore( )
, or
PrependChild( )
to ensure that the nodes are in
the correct order. Note that PrependChild( )
is
not a DOM Level 1 or Level 2 method, but a .NET-specific extension.
XmlDocument
has a CreateDocumentType(
)
method that, predictably, creates an XML document type.
This method takes a name, a system ID, a public ID, and an internal
subset as parameters, the last three of which can be null. Again, you
must use AppendChild( )
to add the
XmlDocumentType
node to the tree:
XmlDocumentType docType = document.CreateDocumentType("inventory", null,"inventory.dtd",null); document.AppendChild(docType);
If you try to create an XmlDocumentType
node but
the specified DTD does not exist, a
FileNotFoundException
will be
thrown.
Next, you should create the
document element. CreateElement( )
creates a new
XmlElement
but, again, does not insert it into the
XML tree:
XmlElement documentElement = document.CreateElement("inventory"); document.AppendChild(documentElement);
If
you inspect the XmlDocument
instance’s DocumentElement
property, you’ll see that the new
XmlElement
has automatically become the document
element because it is the first XmlElement
added
to the tree:
XmlElement element = document.DocumentElement; Console.WriteLine("DocumentElement is " + element.Name);
At any point, you might wish to examine the document tree as it would appear if serialized to XML. The following code snippet will do that for you with nice human-readable formatting.
XmlTextWriter writer = new XmlTextWriter(Console.Out); writer.Formatting = Formatting.Indented; document.WriteTo(writer);
Next,
continue building your XmlDocument
, one element at
a time. The next required element is the date
:
XmlElement dateElement = document.CreateElement("date"); dateElement.SetAttribute("year","2002"); dateElement.SetAttribute("month","6"); dateElement.SetAttribute("day","22"); document.DocumentElement.AppendChild(dateElement);
You’ll notice that, in
this case, you call AppendChild( )
on the
document’s DocumentElement
,
rather than the document itself. Besides being the right way to build
a valid document for this DTD, this is necessary because a document
is only allowed to have one child element. Attempting to append
another child element directly to the document would cause the
following exception to be thrown:
System.InvalidOperationException: This document already has a DocumentElement node.
Continuing, create the items
and several
item
elements:
// create the items element XmlElement itemsElement = document.CreateElement("items"); document.DocumentElement.AppendChild(itemsElement); // create some item elements XmlElement itemElement = document.CreateElement("item"); itemElement.SetAttribute("quantity","15"); itemElement.SetAttribute("productCode","R-273"); itemElement.SetAttribute("description","14.4 Volt Cordless Drill"); itemsElement.AppendChild(itemElement); itemElement = document.CreateElement("item"); itemElement.SetAttribute("quantity","23"); itemElement.SetAttribute("productCode","1632S"); itemElement.SetAttribute("description","12 Piece Drill Bit Set"); itemsElement.AppendChild(itemElement);
By now, you should see that you’ve built the following XML document:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE inventory SYSTEM "inventory.dtd"> <inventory> <date year="2002" month="6" day="22" /> <items> <item quantity="1" productCode="R-273" description="14.4 Volt Cordless Drill" /> <item quantity="1" productCode="1632S" description="12 Piece Drill Bit Set" /> </items> </inventory>
That looks fairly good, but is it valid?
You can check with the XmlValidatingReader
from
Chapter 2. Remember that one of the
XmlValidatingReader
type’s
constructors takes a Stream
. You can write the
XmlDocument
to a MemoryStream
,
flush the Stream
to ensure that all the data has
been written, set the Stream
instance’s pointer back to the beginning, and then
pass it to the XmlValidatingReader
. You can either
let the XmlSchemaException
be thrown and handle it
in a try...catch
, or register a
ValidationEventHandler
as I did in Chapter 2. In this case I’ll just let
the default InternalValidationCallback
do the
work:
Stream stream = new MemoryStream( ); XmlTextWriter textWriter = new XmlTextWriter(new StreamWriter(stream)); document.WriteTo(textWriter); textWriter.Flush( ); stream.Seek(0,SeekOrigin.Begin); XmlReader textReader = new XmlTextReader(stream); XmlReader reader = new XmlValidatingReader(textReader); try { while (reader.Read( )) { // Validation only happens when you call Read( ) } } catch (XmlSchemaException e) { Console.WriteLine(e); } finally { stream.Close( ); }
You might think that you could use a combination of
XmlNodeReader
and
XmlValidatingReader
to validate the document.
However, remember that although the
XmlValidatingReader
constructor takes an
XmlReader
, an ArgumentException
will be thrown if it’s not actually an
XmlTextReader
.
Now you can run the program and the
XmlValidatingReader
will tell you if
you’ve forgotten anything:
System.Xml.Schema.XmlSchemaException: The required attribute 'unitCost' is missing. An error occurred at (1, 140). at System.Xml.XmlValidatingReader.InternalValidationCallback(Object sender, ValidationEventArgs e) at System.Xml.Schema.Validator.SendValidationEvent(XmlSchemaException e, XmlSeverityType severity) at System.Xml.Schema.Validator.BeginChildren( ) at System.Xml.Schema.Validator.ProcessElement( ) at System.Xml.Schema.Validator.Validate( ) at System.Xml.Schema.Validator.Validate(ValidationType valType) at System.Xml.XmlValidatingReader.ReadWithCollectTextToken( ) at System.Xml.XmlValidatingReader.Read( ) at CreateInventory.Main(String[ ] args) in C:Chapter 5CreateInventory.cs:line 85
This exception indicates that an attribute required by the DTD is
missing. You can go back and add the missing
unitCost
attributes to their respective elements.
Because the DOM allows non-sequential access to the XML tree, you can
actually go back to nodes that you created early in the program and
assign the cost to each item at the end. This might be necessary in
real life if the data were coming from disparate sources—maybe
the list of items comes from a database, while the cost comes from a
flat file; you don’t want to have to scan the entire
file as each row is read from the database.
Since you still have the items
element in memory,
you can simply iterate through its child nodes, looking up the
productCode
attribute, and adding the
unitCost
attribute with the appropriate value. If
the code encounters an unknown productCode
, it
will throw an ApplicationException
:
XmlNodeList elements = itemsElement.ChildNodes; foreach (XmlElement currentElement in elements) { double cost = 0d; string productCode = currentElement.GetAttribute("productCode"); switch (productCode) { case "R-273": cost = 189.95; break; case "1632S": cost = 14.95; break; default: throw new ApplicationException("Unknown productCode: " + productCode); } currentElement.SetAttribute("unitCost",cost.ToString( )); }
There are
other ways you could navigate through the items
element’s child nodes. For example, if there were
other types of child nodes besides elements, or other elements
besides item
, you could replace the first line of
code with the following:
XmlNodeList elements = itemsElement.GetElementsByTagName("item");
Either way, you now have valid XML:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE inventory SYSTEM "inventory.dtd"> <inventory> <date year="2002" month="6" day="22" /> <items> <item quantity="15" productCode="R-273" description="14.4 Volt Cordless Drill"unitCost="189.95
" /> <item quantity="23" productCode="1632S" description="12 Piece Drill Bit Set"unitCost="14.95" />
</items> </inventory>
Finally, you should save the document to a file:
document.Save("inventory.xml");
The XmlDocument.Save(
)
method has several overloads. The one used here takes a
filename, creates all necessary FileInfo
and/or
XmlWriter
instances, and serializes the document
to the file. Other overloads take a Stream
, a
TextWriter
, or an XmlWriter
,
respectively, so you can save the document not only to a variety of
destinations, but even to alternative XML syntaxes, using, for
example, the XmlPyxWriter
I showed you in Chapter 4.
3.135.246.245