### 在.NET Framework 中轻松处理 XML 数据

本文假设你已熟悉 XML 和.NET Framework
前言 在.NET Framework 中，XmlTextReader 和 XmlTextWriter 类提供了对 xml 数据的读和写操作。在本文中，作者讲述了 XML 阅读器(Reader)的体系结构及它们怎样与 XMLDOM 和 SAX 解释器结合。作者也演示了怎么样运用阅读器分析和验证 XML 文档，怎么样创建格式良好的 XML 文档，以及怎么样用函数读/写基于 Base64 和 BinHex 编码的大型的 XML 文档。最后，作者讲了怎么样实现一个基于流的读/写分析器，它把读写器都封装在一个单独的类里。
大概三年前，我参加了一个软件研讨会，主题是“没有 XML，就没有编程的未来”。XML 确实也在一步一步的发展，它已经嵌入到. NET Framework 中了。在本文中，我将讲解. NET Framework 中用于处理 XML 文档的 API 的角色和它的内部特性，然后我将演示一些常用的功能。
从 MSXML 到.net 的 XML
在. NET Framework 出现之前，你习惯使用 MSXML 服务----一个基于 COM 的类库---写 windows 的 XML 的驱动程序。不像. NET Framework 中的类，MSXML 类库的部分代码比 API 更深，它完全的嵌在操作系统的底层。MSXML 的确能够与你的应用程序通信，但是它不能真正的与外部环境结合。
MSXML 类库能在 win32 中被导入，也能在 CLR 中运用，但它只能作为一个外部服务器组件使用。但是基于.NET Framework 的应用程序能直接的用 XML 类与.NET Framework 的其它命名空间整合使用，并且写出来的代码易于阅读。
作为一个独立的组件，MSXML 分析器提供了一些高级的特性如异步分析。这个特性在.NET Framework 中的 XML 类及.NET Framework 的其它类都没有提供，但是，NET Framework 中的 XML 类与其它的类整合可以很轻易的获得相同的功能，在这个基础上你可以增加更多的功能。
.NET Framework 中的 XML 类提供了基本的分析、查询、转换 XML 数据的功能。在.NET Framework 中，你可以找到支持 Xpath 查询和 XSLT 转换的类，及读/写 XML 文档的类。另外，.NET Framework 也包含了其它处理 XML 的类，例如对象的序列化（XmlSerializer 和 the SoapFormatter 类），应用程序配置（AppSettingsReader 类），数据存储（DataSet 类）。在本文中，我只讨论实现基本 XML I/O 操作的类。
XML 分析模式
既然 XML 是一种标记语言，就应该有一种工具按一定的语法来分析和理解存储在文档中信息。这个工具就是 XML 分析器---一个组件用于读标记文本并返回指定平台的对象。
所有的 XML 分析器，不管它属于哪个操作平台，不外乎都分以下的两类：基于树或者基于事件的处理器。这两类通常都是用 XMLDOM（the Microsoft XML Document Object Model）和 SAX(Simple API for XML)来实现。XMLDOM 分析器是一个普通的基于树的 API---它把 XML 文档当成一个内存结构树呈现。SAX 分析器是基于事件的 API----它处理每个在 XML 数据流中的元素(它把 XML 数据放进流中再进行处理)。通常，DOM 能被一个 SAX 流载入并执行，因此，这两类的处理不是相互排斥的。
总的来说，SAX 分析器与 XMLDOM 分析器正好相反，它们的分析模式存在着极大的差别。XMLDOM 被很好的定义在它的 functionalition 集合里面，你不能扩展它。当它在处理一个大型的文档时，它要占用很大内存空间来处理 functionalition 这个巨大的集合。
SAX 分析器利用客户端应用程序通过现存的指定平台的对象的实例去处理分析事件。SAX 分析器控制整个处理过程，把数据“推出”到处理程序，该处理程序依次接受或拒绝处理数据。这种模式的优点是只需很少的内存空间。
.NET Framework 完全支持 XMLDOM 模式，但它不支持 SAX 模式。为什么呢？因为.NET Framework 支持两种不同的分析模式：XMLDOM 分析器和 XML 阅读器。它显然不支持 SAX 分析器，但这并不意味它没有提供类似 SAX 分析器的功能。通过 XML 阅读器 SAX 的所有的功能都能很容易的实现及更有效的运用。不像 SAX 分析器，.NET Framework 的阅读器整个都运作在客户端应用程序下面。这样，应用程序本身就可以只把真正需要的数据“推出”，然后从 XML 数据流中跳出来。而 SAX 分析模式要处理所有的对应用程序有用和无用的信息。
阅读器是基于.NET Framework 流模式工作的，它的工作方式类似于数据库的游标。有趣的是，实现类似游标分析模式的类提供对.NET Framework 中的 XMLDOM 分析器的底层支持。XmlReader、XmlWriter 两个抽象类是所有.NET Framework 中 XML 类的基础类，包括 XMLDOM 类、ADO.NET 驱动类及配置类。所以在.NET Framework 中你有两种可选的方法去处理 XML 数据。用 XmlReader 和 XmlWriter 类直接处理 XML 数据，或者用 XMLDOM 模式处理。更多的关于在.NET Framework 中读文档的介绍可以参见 MSDN 2002 年八月刊的 Cutting Edge 栏目文章。
XmlReader 类
XML 阅读器支持一个编程接口，接口用于连接 XML 文档，“推出”你要的数据。如果你更深入去了解阅读器，你会发现阅读器工作原理类似于我们的桌面应用程序从数据库中取出数据的原理。数据库服务返回一个游标对象，它包含所有查询结果集，并返回指向目标数据集的开始地址的引用。XML 阅读器的客户端收到一个指向阅读器实例的引用。该实例提取底层的数据流并把取出的数据呈现为一棵 XML 树。阅读器类提供只读、向前的游标,你可以用阅读器类提供的方法滚动游标遍历结果集中的每一条数据。
从阅读器中看 XML 文档不是一个标签文本文件，而是一个序列化的节点集合。它是.NET Framework 中的一种特殊的游标模式;在.NET Framework 中,你找不到其它的任何一个类似的 API 函数。
阅读器和 XMLDOM 分析器有几点不同的地方。XML 阅读器是只进的，它没有父、子、祖宗、兄弟节点的概念，而且是只读的。在.NET Framework 中，读写 XML 文档是分为两种完全不同的功能，分别由 XmlReader 和 XmlWriter 类来完成。要编辑 XML 文档，你可以用 XMLDOM 分析器，或者你自己设计一个类来实现这两种功能。让我们开始分析阅读器的程序功能。
XmlReader 是一个抽象类，你可以继承并扩展它的功能。用户程序一般都基于下面的三种类：XmlTextReader、XmlValidatingReader 或者 XmlNodeReader 类。所有的这些类都有如图一的属性和图二的方法。要注意的是，某些属性的值实际上依赖于实际的某个阅读器类，不同的类与基类可能不同。因此，在图一中每个属性的说明都是以基类为准的。例如，CanResolveEntity 属性在 XmlValidatingReader 类中只返回 true；而在其它的阅读器类中它却可以设为 false。同样的，在图二中的某些方法的实际返回值对不同的类可能不同。例如，如果节点类型不是元素节点（element node）,所有包含 Atrributes 的方法的返回值类型都是 void。
XmlTextReader 类用只进，只读的方式快速访问 XML 数据流。阅读器先验证 XML 文档是否是格式良好的，如果不是则抛出一个异常。XmlTextReader 检查 DTD 的格式是否良好，但不使用 DTD 对文档进行验证。XmlTextReader 通过 XML 文档的文件名，或它的 URL，或者从文件流中载入 XML 文档，然后快速的处理 XML 文档数据。如果你需要对文档的数据进行验证，你可以用 XmlValidatingReader 类。
可以用多种方法创建 XmlTextReader 类的实例，从硬盘中加载文件，或从 URL 地址中加载，流（streams）中加载，还有就是从文本中读入 XML 文档数据：
XmlTextReader reader = new XmlTextReader(file);
注意，所有 XmlTextReader 类的公共(public)构造函数都要求你指定数据源，数据源可以是 stream、文件或者其它。XmlTextReader 默认的构造函数是受保护的（protected），所以不能直接使用。像.NET Framework 中所有的阅读器类一样(如 SqlDataReader 类)，一旦阅读器对象连接并打开，你就可以用 Read 方法去访问数据了。开始的时候只能用 Read 方法把指针移到第一个元素；然后我们可以用 Read 方法或其它方法（如 Skip, MoveToContent 和 ReadInnerXml）移动指针到下一个节点元素。要处理整个 XML 文档的内容，可以根据 Read 方法的返回值用一个循环遍历文档内容，因为 Read 方法返回一个布尔值，当读到文档的尾节点时，Read 方法返回 false，否则它返回 true。
Figure 3 Outputting an XML Document Node Layout
string GetXmlFileNodeLayout(string file)
{
// 创建一个 XmlTextReader 类使它指向目标 XML 文档
XmlTextReader reader = new XmlTextReader(file);
// 循环取出节点的文本并放入到 StringWriter 对象实例中
StringWriter writer = new StringWriter();
string tabPrefix = "";
while (reader.Read())
{
// 写开始标志,如果节点类型为元素
if (reader.NodeType == XmlNodeType.Element)
{
//根据元素所处节点的深度，加入 reader.Depth 个 tab 符,然后把元素名写入到<>中。
tabPrefix = new string('\t', reader.Depth);
writer.WriteLine("{0}<{1}>", tabPrefix, reader.Name);
}
else
{
//写结束标志,如果节点类型为元素
if (reader.NodeType == XmlNodeType.EndElement)
{
tabPrefix = new string('\t', reader.Depth);
writer.WriteLine("{0}</{1}>", tabPrefix, reader.Name);
}
}
}
// 输出到屏幕
string buf = writer.ToString();
writer.Close();
// 关闭流
reader.Close();
return buf;
}
图三演示了一个简单的用于输出一个给定的 XML 文档的节点元素的函数。该函数先打开一个 XML 文档，然后用循环处理 XML 文档中所有的内容。每次调用 Read 方法，阅读器的指针都会向下移一个节点。大部分情况下，用 Read 方法可以处理的元素节点，但有时候，当你从一个节点移动到下一个节点时，可能是在两个不同类型的节点间移动。但是 Read 方法不能在属性节点之间移动。阅读器的 MoveToContent 方法可以让指针从头部节点位置跳到第一个内容节点位置。在 ProcessingInstruction, DocumentType, Comment, Whitespace 和 SignificantWhitespace 类型节点中也可以用 Skip 方法移动指针。
每个节点的类型是 XmlNodeType 枚举值中的一种，在如图三所示的代码中，我们只用了其中的两种类型：Element 和 EndElement。输出源码重新定制了原始的文档结构，它丢弃或者说是忽略了 XML 元素的属性和节点内容，只输出了元素节点名。假设我们运用了下面的 XML 片断：
<mags>
<mag name="MSDN Magazine">
MSDN Magazine
</mag>
<mag name="MSDN Voices">
MSDN Voices
</mag>
</mags>
用上面的程序输出的结果如下:
<mags>
<mag>
</mag>
<mag>
</mag>
</mags>
子节点的缩进量是根据阅读器的深度属性（Depth 属性）设置的，Depth 属性返回一个整形的数据，它表示当前节点的嵌套层次。所有文本都放在 StringWriter 对象中（一个非常方便的基于流的封装了 StrigBuilder 类的类）。
如前所述，阅读器不会自动通过 Read 方法访问属性节点。要访问当前元素的属性节点集合，必须用一个简单的用 MoveToNextAttribute 方法的返回值控制的循环去遍历该集合。下面的代码用于访问当前节点的所有属性，并把属性的名称和它的值用逗号分开组合成一个字符串：
if (reader.HasAttributes)
while(reader.MoveToNextAttribute())
buf += reader.Name + "=\"" + reader.Value + "\",";
reader.MoveToElement();
当你完成对属性集的处理时，调用 MoveToElement 方法使指针返回到属性所属的元素节点。准确的说，MoveToElement 方法并不是真正的移动指针，因为在处理属性集时指针从来就没有从元素节点中移开。MoveToElement 方法只不过指向某个内部成员，并依次取得成员的值。例如，用 Name 属性获得某个属性的属性名，然后调用 MoveToElement 方法把指针移到其所属的元素节点处。但是当你不需要继续处理别的节点时，就不必再调用 MoveToElement 方法了。

分析属性值
大部分情况下，属性值都是一个简单的文本字符串。然而，这并不意味着实际应用中的属性值都是字符型的。有时候，属性值是由许多种类型的数据组合而成的，例如 Date 或 Boolean,这时，你就要用 XmlConvert 或 System.Convevt 类的方法把这些类型转换成原来的类型。XmlConvert 和 System.Convevt 类都能实现数据类型的转换，但是 XmlConvert 类依据 XSD 中指定的数据类型进行转换，而不管它现在是什么类型。
假设你有以下的 XML 数据片断：
<person birthday="2-8-2001" />
让我们先确认，birthdaay 属性值是 February 8, 2001,如果你用 System.Convert 类把该字符串转换成.NET Framework 中的 DateTime 类型，这样，我们就可以把它当成 date 类型使用了。相比下，如果你用 XmlConvert 类来转换字符串，你将看到一个分析错误，因为 XmlConvert 类不能正确解释这个字符串中的日期。因为在 XML 中，日期型数据的格式必须是 YYYY-MM-DD 形式的。XmlConvert 类担任 CLR 类型与 XSD 类型之间的相互转换工作。当转换工作发生时，转换结果是局部的。
在某些解决方案中，属性值是由纯文本和实体共同组成的。在所有的阅读器类中，只有 XmlValidatingReader 类能处理实体。XmlTextReader 虽然不能处理实体，但它们同时出现在属性值中的时候，它只能把文本值取出来。出现这种情况，你必须用 ReadAttributeValue 方法替代简单的读方法来分析属性值的内容。
ReadAttributeValue 方法分析属性值,然后把各个组成的要素分隔开（如把纯文本和实体分开）。你可以用 ReadAttributeValue 方法的返回值作为循环条件，遍历整个属性值中的要素。既然 XmlTextReader 类不能处理实体，那么你可以自己写一个用于处理实体的类。下面的代码片断演示了怎么调用一个自定义的处理类：
while(reader.ReadAttributeValue())
{
if (reader.NodeType == XmlNodeType.EntityReference)
// Resolve the "reader.Name" reference and add
// the result to a buffer
buf += YourResolverCode(reader.Name);
else
// Just append the value to the buffer
buf += reader.Value;
}
当属性值全部被分析后，ReadAtributeValue 方法返回 False, 从而结束循环。属性值的最终结果就是全局变量 buffer 的值了。
处理 XML 文本(Text)
当我们在处理 XML 标签文本时，如果不能正确的处理，它的错误原因能很快地确定。例如一个字符转换错误，它必然是传输了非 XML 文本到一个 XML 数据流中。不是所有在给定的平台中有效的字符都是有效的 XML 字符。只有在 XML 规范(www.w3.org/TR/2000/REC-xml-20001006.html)中规定的有效的字符才能安全的用作元素和属性名。
XmlConvert 类提供了把非 XML 标准的命名转换成标准的 XML 命名的功能。当标签名中包含有无效的 XML 字符时，EncodeName 和 DecodeName 方法能把它们调整成符合 Schema 的 XML 命名。包括 SQL Server™ 和 Microsoft Office，这些应用程序允许及支持 Unicode 文档，然而，这些文档中的字符有些也不是有效的 XML 命名。典型的情况是在你处理数据库中包含空格的列名时。虽然 SQL Server 允许长列名，但这对 XML 流来说可能就不是有效的命名。空格会被十六进制代码 Invoice_0x0020_Details 替代。下面的代码演示了怎么样在程序中获得该字符串：
XmlConvert.EncodeName("Invoice Details");
与此相反的方法是 DecodeName。该方法把 XML 文本转换成其原始的格式。要注意的是它只能转换完整的十六进制代码，只有_0x0020_才被当成一个空格，而_0x20_就不是了：
XmlConvert.DecodeName("Invoice_0x0020_Details");
在 XML 文档中的空格即重要也不重要。说它重要，是当它出现在元素的内容中或者它在注释语句中时,它能表示实际意义。例如下面的情况：
<MyNode xml:space="preserve">
<!-- any space here must be preserved -->
•••
</MyNode>
在 xml 中，空格不只是代表空格（空白），也代表回车、换行和缩进。
通过 XmlTextReader 类的 WhiteSpaceHandling 属性你可以处理空格。这个属性接受及返回一个 WhiteSpaceHandling 枚举值(该枚举类有三种可选值)。默认值是 All,它表示有意义和无意义的空格都会作为节点返回---- 分别为 SignificantWhitespace 和 Whitespace 节点。 另一个枚举值是 None,它表示对任何空格都不作为节点返回。最后，就是 Signficant 枚举值,它表示忽略没有意义的空格，而只返回节点类型为 SignficantWhitespace 的节点。注意 WhiteSpaceHandling 属性是少数阅读器属性中的一个。它能被改变在任何时候和给 Read 操作带来影响。而 Normalization 及 XmlResolver 属性是“Sensitive”的。
String 和 Fragment
程序员把在 MSXML 的程序剪切下来，会发现在 COM 和.NET Framework XML API 之间的差别很大。.NET Framework 类本身没有提供方法去分析存储在字符串中 XML 数据。不像 MSXML 分析器对象，XmlTestReader 类没有提供任何一种 LoadXML 方法从一个格式良好的字符中创建阅读器。没有提供类似 LoadXML 的方法因为你可以用特殊的 text reader---StringReader 类来获得同样的功能。
XmlTextReader 其中一个构造函数接受一个 TextReader 派生对象和一个 XML reader 作参数(该阅读器以 text reader 的内容为基础创建)。一个 text reader 类是一个流，这个流是输入的字符经优化生成的。StringReader 类继承 TextReader 类，并用一个内存中字符串作为其输入流。下面的代码片断演示了怎样初始化一个 XML reader，用一个格式良好的 XML 字符串作为其输入：
string xmlText = "...";
StringReader strReader = new StringReader(xmlText);
XmlTextReader reader = new XmlTextReader(strReader);
另外，用 StringWriter 类代替 TextWrite 类，你可以从内存字符中创建一个 XML 文档。
一个指定类型的 XML 字符串是一个 XML 片断（fragment）. XML 片断由 XML 文本构成，但没有根节点的 XML 文档不是格式良好的 XML 文档，所以不能被应用。一个 XML 片断是原始的文档的一部分，所以它可能缺少根节点。例如，下面的 XML 文本是一个有效的 XML 片断，但不是一个有效的 XML 文档，因为它没有根节点：
<firstname>Dino</firstname>
<lastname>Esposito</lastname>
.NET Framework XML API 允许程序员把 XML 片断与一个分析器内容结合使用，分析器内容由类似 encoding 字符集，DTD 文档，命名空间，语言和空格处理程序构成：
public XmlTextReader(
string xmlFragment,
XmlNodeType fragType,
XmlParserContext context
);
xmlFragment 参数包括了 XML 字符串分析。FragType 参数表示 fragment 的类型，它给出了 fragment 根节点的类型。只有 element,attibute 和 document 类型的节点才能作为 fragment 的根节点，分析器的内容才能被 XmlParserContext 类解释。
带验证的阅读器
XmlValidatingReader 类实现了 XmlReader 类，它提供了支持多种类型的 XML 验证：DTD，XML-Data Reduced(XDR)架构，以及 XSD，DTD 和 XSD 都是 W3C 官方推荐的。而 XDR 是 Microsoft 早期用于处理 XML 构架的一种格式。
你可以用 XmlVlidatingReader 类去验证 XML 文档和 XML 片断。XmlValidatingReader 类工作在 XML 阅读器上面---是一个典型的 XMLTextReader 类实例。XMLTextReade 用于读取文档的节点，但是 XmlVlidatingReader 依据需要的验证类型去验证每一个 XML 块。
XmlVlidatingReader 类只实现了非常小的 XML 阅读器必备的一个功能子集。该类总是工作在一个已存在的 XML 阅读器上面，它监视方法和属性。如果你深入该类的构造函数，你会发现它很明显的依靠一个已存在的文本阅读器。带验证的 XML 阅读器不能直接的从一个文件或一个 URL 序列化。该类的构造函数列表如下：
public XmlValidatingReader(XmlReader);
public XmlValidatingReader(Stream, XmlNodeType, XmlParserContext);
public XmlValidatingReader(string, XmlNodeType, XmlParserContext);
带验证的 XML 阅读器能分析任何的 XML 片断，XML 片断通过一个 string 或者一个 stream 提供,也可以分析任何阅读器提供的 XML 文档。
XmlVlidatingReader 类中有重大改变的方法非常少(相对其它 reader 类来说)，另外对 Read，它有 Skip 和 ReadTypedValue 方法。Skip 方法跳过当前节点所有的子节点（你不能跳过不良格式的 XML 文本，它是相当有用的算法），Skip 方法也验证被跳过的内容。ReadTypedValue 方法返回指定 XML 架构 (XSD) 类型对应的 CLR 类型。如果该方法找到了 XSD 类型对应的 CLR 类型，则返回 CLR 的类型名。如果找不到，则把该节点的值作为一个字符串值返回。
带验证的 XML 阅读器正如其名，它是一个基于节点的阅读器，它验证当前节点的结构是否符合当前的 schema。验证是增量式的；它没有方法返回表示文档是否有效的布尔值。通常你都是用 Read 方法去读输入的 XML 文档。实际上，你也可以用带验证的阅读器去读 XML 文档。在每一步中，当前被访问的节点的结构是否与指定的 schema 符合，如果不符合，抛出一个异常。图四是一个控制台应用程序，它有一个要输入文件名的命令行，最后输出验证结果。
Figure 4 Console App
using System;
using System.Xml;
using System.Xml.Schema;
class MyXmlValidApp
{
public MyXmlValidApp(String fileName)
{
try {
Validate(fileName);
}
catch (Exception e) {
Console.WriteLine("Error:\t{0}", e.Message);
Console.WriteLine("Exception raised: {0}",
e.GetType().ToString());
}
}
private void Validate(String fileName)
{
XmlTextReader xtr = new XmlTextReader(fileName);
XmlValidatingReader vreader = new XmlValidatingReader(xtr);
vreader.ValidationType = ValidationType.Auto;
vreader.ValidationEventHandler += new
ValidationEventHandler(this.ValidationEventHandle);
vreader.Read();
vreader.MoveToContent();
while (vreader.Read()) {}
xtr.Close();
vreader.Close();
}
public void ValidationEventHandle(Object sender,
ValidationEventArgs args)
{
Console.Write("Validation error: " + args.Message + "\r\n");
}
public static void Main(String[] args)
{
MyXmlValidApp o = new MyXmlValidApp(args[0]);
return;
}
}
ValidationType 属性设置验证的类型，它可以是:DTD, XSD, XDR 或者 none。如果没有指定验证的类型（用 ValidationType.Auto 选项），阅读器将自动的根据文档用最适合的验证类型。在验证过程中出现任何错误，都会触发 ValidationEventHandler 事件。如果未提供事件 ValidationEventHandler 事件处理程序，则抛出一个 XML 异常。定义 ValidationEventHandler 事件处理程序是用于捕捉任何在 XML 源文件中存在错误而引发 XML 异常的一种方法。要注意的是阅读器的原理是检查一个文档是否是格式良好的，以及检查文档是否与架构吻合。如果带验证的阅读器发现一个有严重的格式错误的 XML 文档，只会触发 XmlException 异常,它不会触发其它的事件。
验证发生在用户用 Read 方法向前移动指针时，一旦节点被分析和读取，它获得传送过来的处理验证的内部的对象。验证操作是基于节点类型及被要求的验证类型。它确认节点所有的属性和节点包含的子节点是否符合验证条件。
验证对象在内部调用两个不同风格的对象：DTD 分析器和架构生成器(schema builder)。DTD 分析器处理当前节点的内容和不符合 DTD 的子树。架构生成器根据 XDR 或者 XSD 架构对当前的节点构建一个 SOM（schema object model）。架构生成器类实际上是所有指定为 XDR 和 XSD 架构生成器的基类。为什么呢，虽然 XDR 和 XSD 架构的许多相同的方法被加工处理过，但是它们在执行时的性能没有区别。
如果节点有子节点，用另一个临时的阅读器收集子节点信息，因此节点的架构信息能被完全地验证。你可以看图五：
注意，尽管 XmlValidatingReader 类的构造函数可以接受一个 XmlReader 类作为其阅读器，但是该阅读器只能是 XmlTextReader 类的一个实例或者是它的一个派生类的实例。这意味着你不能用其它从 XmlReader 派生的类（例如一个自定义的 XML 阅读器）。在 XmlValidatingReader 类的内部，它假设阅读器是一个子 XmlTextReader 对象及把传入的阅读器显式的转换成 XmlTextReader 类。如果你用 XmlNodeReader 或者自定义的阅读器器，程序在编译时会出错，运行时抛出一个异常。
节点阅读器
XML 阅读器提供一种增量式的方法（一个一个节点的读）来处理文档的内容。到目前为止，我们假设源文件是一个基于硬盘的流或者是一个字符串流，然而，我们不能保证在实际中会提供一个源文件的 XMLDOM 对象给我们。在这种情况下，我们需要一个带有特别的读方法的特别的类。对这种情况，.NET Framework 提供了 XmlNodeReader 类。
就像 XmlTextReader 访问指定 XML 流中所有节点一样，XmlNodeReader 类访问 XMLDOM 子树的所有节点。XMLDOM 类（在.NET Framework 中的 XmlDocument 类）支持基于 Xpath 的方法，例如 SelectNodes 方法和 SelectSingleNode 方法。这些方法的作用是把匹配的节点放在内存中。如果你需要处理子树中的所有节点，节点阅读器比用增量式方法处理节点的阅读器具有更高的效率：
// xmldomNode is the XML DOM node
XmlNodeReader nodeReader = new XmlNodeReader(xmldomNode);
while (nodeReader.Read())
{
// Do something here
}
当你要在配置文件（例如 web.cofig 文件）中引用自定义的数据时，先把这些数据填充到 XMLDOM 树中，然后用 XmlNodeReader 类与 XMLDOM 类结合处理这些数据。这也是高效的。

XmlTextWriter 类
用在本节中的方法创建 XML 文档显然并不困难。多年以来，开发者都是通过在缓存在连接一些字符串，连接好以后再把缓存中字符串输出到文件的方式来创建 XML 文档。但是以这种方式创建 XML 文档的方法只有在你保证字符串中不存在任何细小的错误的时候才有效。.NET Framework 通过用 XMLwriter 提供了更好的创建 XML 文档的方法。
XML Writer 类以只前（forward-only）的方式输出 XML 数据到流或者文件中。更重要的是，XML Writer 在设计时就保证所有的 XML 数据都符合 W3C XML 1.0 推荐规范，你甚至不用担心忘记写闭标签，因为 XML Writer 会帮你写。XmlWriter 是所有 XML writer 的抽象基类。.NET Framework 只提供唯一的一个 writer 类----XmlTextWriter 类。
我们先来看看 XML writers 和旧的 writers 的不同点，下面的代码保存了一个 string 型的数组：
StringBuilder sb = new StringBuilder("");
sb.Append("<array>");
foreach(string s in theArray) {
sb.Append("<element value=\"");
sb.Append(s);
sb.Append("\"/>");
}
sb.Append("</array>");
代码通过循环取出数据中的元素，写好标签文本并把它们累加到一个 string 中。代码保证输出的内容是格式良好的并且注意了新行的缩进，及支持命名空间。当创建的文档结构比较简单时，这种方法可能不会有错误。然而，当你要支持处理指令，命名空间，缩进，格式化以及实体的时候，代码的数量就成指数级增长，出错的可能性也随之增长。
XML writer 写方法功能对应每个可能的 XML 节点类型，它使创建 xml 文档的过程更符合逻辑、更少的信赖于繁琐的标记语言。图六演示了怎么样用 XmlTextWriter 类的方法来连接一个 string 数据。代码很简洁，用 XML writer 的代码更容易读、结构更好。
Figure 6 Serializing a String Array
void CreateXmlFileUsingWriters(String[] theArray, string filename)
{
// Open the XML writer (用默认的字符集)
XmlTextWriter xmlw = new XmlTextWriter(filename, null);
xmlw.Formatting = Formatting.Indented;
xmlw.WriteStartDocument();
xmlw.WriteStartElement("array");
foreach(string s in theArray)
{
xmlw.WriteStartElement("element");
xmlw.WriteAttributeString("value", s);
xmlw.WriteEndElement();
}
xmlw.WriteEndDocument();
// Close the writer
xmlw.Close();
}
然而 XML writer 并不是魔术师----它不能修复输入的错误。XML writer 不会检查元素名和属性名是否有效，也不保证被用的任何的 Unicode 字符集适合当前架构的编码集。如上所述，为了避免输出错误，必须要杜绝非 XML 字符。但是 writer 没有提供这种方法。
另外，当创建一个属性节点时，Writer 不会检验属性节点的名称是否与已存在的元素节点的名称相同。最后，XmlWriter 类不是一个带验证的 Writer 类，也不保证输出是否符合 schema 或者 DTD。在.NET Framework 中带验证的 writer 类目前来说还没有提供。但是在我写的《Applied XML Programming for Microsoft .NET (Microsoft Press®, 2002)》书中，我自己写了一个带验证的 Writer 组件。你可以到下面的网址去下载源码：http://www.microsoft.com/MSPress/books/6235.asp.
图七列出了 XML writer 的一些状态值（state）。这些值都源于 WriteState 枚举类。当你创建一个 Writer，它的初始状态为 Start，表示你将要配置该对象，实际上 writer 没有开始。下一个状态是 Prolog，该状态是当你调用 WriteStartDocument 方法开始工作的时候设置的。然后，状态的转换就取决于你的写的文档及文档的内容了。Prolog 状态一直保留到当你增加一个非元素节点时，例如注释元素，处理指令及文档类型。当第一个节点也就是根节点写完后，状态就变为 Element。当你调用 WriterStartAtribute 方法时状态转换为 Attribute，而不是当你调用 WriteAtributeString 方法写属性时转换为该状态。如果那样的话，状态应该是 Element。当你写一个闭标签(>)时，状态会转换成 Content。当你写完文档后，调用 WriteEndDocument 方法，状态就会返回为 Start,直到你开始写另一个文档或者把 Writer 关掉。
Figure 7 States for XML Writer
State
Description
Attribute
The writer enters this state when an attribute is being written
Closed
The Close method has been called and the writer is no longer available for writing operations
Content
The writer enters this state when the content of a node is being written
Element
The writer enters this state when an element start tag is being written
Prolog
The writer is writing the prolog of a well-formed XML 1.0 document
Start
The writer is in an initial state, awaiting for a write call to be issued
Writer 把输出文本存在内部的一个缓冲区内。一般情况下，缓冲区会被刷新或者被清除，当 Writer 被关闭前 XML 文本应该要写出。在任何时你都可以通过调用 Flush 方法清空缓冲区，把当前的内容写到流中（通过 BaseStream 属性暴露流），然后释放部分占用的内存，Writer 仍保持为打开状态（open state），可以继续操作。注意，虽然写了部分的文档内容，但是在 Writer 没有关闭前其它的程序是不能处理该文档的。
可以用两种方法来写属性节点。第一种方法是用 WriteStartAtribute 方法去创建一个新的属性节点，更新 Writer 的状态。接着用 WriteString 方法设置属性值。写完后，用 WriteEndElement 方法结束该节点。另外，你也可以用 WriteAttributeString 方法去创建新的属性节点，当 writerr 的状态为 Element 时，WriterAttributeString 开始工作，它单独创建一个属性。同样的，WriteStartElement 方法写节点的开始标签（<），然后你可以随意的设置节点的属性和文本内容。元素节点的闭标签都带”/ >”。如果想写闭标签可以用 WriteFullEndElement 方法来写。
应该避免传送给写方法的文本中包含敏感的标记字符，例如小于号（<）。用 WriteRaw 方法写入流的字符串不会被解析，我们可以用它来对 xml 文档写入特殊的字符串。下面的两行代码，第一行输出的是”<”，第二行输出”<”:
writer.WriteString("<");
writer.WriteRaw("<");
读写流
有趣的是，reader(阅读器)和 writer 类提供了基于 Base64 和 BinHex 编码的读写数据流的方法。WriteBase64 和 WriteBinHex 方法的功能与其它的写方法的功能存在着细微的差别。它们都是基于流的，这两个方法的功能像一个 byte 数组而不是一个 string。下面的代码首先把一个 string 转换成一个 byte 数组，然后把它们写成一个 Base64 编码流。Encoding 类的 GetBytes 静态方法完成转换的任务：
writer.WriteBase64(
Encoding.Unicode.GetBytes(buf),
0, buf.Length*2);
图八中代码演示了把一个 string 数据转换为 Base64 编码的 XML 流。图九是输出的结果。
Figure 8 Persisting a String Array as Base64
using System;
using System.Text;
using System.IO;
using System.Xml;
class MyBase64Array
{
public static void Main(String[] args)
{
string outputFileName = "test64.xml";
if (args.Length > 0)
outputFileName = args[0]; // file name
// 把数组转换成 XML
String[] theArray = {"Rome", "New York", "Sydney", "Stockholm",
"Paris"};
CreateOutput(theArray, outputFileName);
return;
}
private static void CreateOutput(string[] theArray, string filename)
{
// 打开 XML writer
XmlTextWriter xmlw = new XmlTextWriter(filename, null);
//使子元素根据 Indentation 和 IndentChar 设置缩进。此选项只对元素内容进行缩进
xmlw.Formatting = Formatting.Indented;
//书写版本为“1.0”的 XML 声明
xmlw.WriteStartDocument();
//写出包含指定文本的注释 <!--...-->。
xmlw.WriteComment("Array to Base64 XML");
//开始写出 array 节点
xmlw.WriteStartElement("array");
//写出具有指定的前缀、本地名称、命名空间 URI 和值的属性
xmlw.WriteAttributeString("xmlns", "x", null, "dinoe:msdn-mag");
// 循环的写入 array 的子节点
foreach(string s in theArray)
{
//写出指定的开始标记并将其与给定的命名空间和前缀关联起来
xmlw.WriteStartElement("x", "element", null);
//把 S 转换成 byte[]数组, 并把 byte[]数组编码为 Base64 并写出结果文本，要写入的字节数为 s 总长度的 2 倍,一个 string 占的字节数是 2 字节。
xmlw.WriteBase64(Encoding.Unicode.GetBytes(s), 0, s.Length*2);
//关闭子节点
xmlw.WriteEndElement();
}
//关闭根节点，只有两级
xmlw.WriteEndDocument();
// 关闭 writer
xmlw.Close();
// 读出写入的内容
XmlTextReader reader = new XmlTextReader(filename);
while(reader.Read())
{
//获取节点名为 element 的节点
if (reader.LocalName == "element")
{
byte[] bytes = new byte[1000];
int n = reader.ReadBase64(bytes, 0, 1000);
string buf = Encoding.Unicode.GetString(bytes);
Console.WriteLine(buf.Substring(0,n));
}
}
reader.Close();
}
}
Figure 9 String Array in Internet Explorer
Reader 类有专门的解释 Base64 和 BinHex 编码流的方法。下面的代码片断演示了怎么样用 XmlTextReader 类的 ReadBase64 方法解析用 Base64 和 BinHex 编码集创建的文档。
XmlTextReader reader = new XmlTextReader(filename);
while(reader.Read()) {
if (reader.LocalName == "element") {
byte[] bytes = new byte[1000];
int n = reader.ReadBase64(bytes, 0, 1000);
string buf = Encoding.Unicode.GetString(bytes);
Console.WriteLine(buf.Substring(0,n));
}
}
reader.Close();
从 byte 型转换成 string 型是通过 Encoding 类的 GetString 方法实现的。尽管我只介绍了基于 Base64 编码集的代码，但是可以简单的用 BinHex 替换方法名就可以实现读基于 BinHex 编码的节点内容（用 ReadBinHex 方法）。这个技巧也可以用于读任何用 byte 数据形式表示的二进制数据，尤其是 image 类型的数据。
如前面所说，XML reader 和 Writer 是各自独立工作的：reader 只读，writer 只写。假设你的应用程序要管理冗长的 XML 文档，且该文档有不确定的数据。Reader 提供了一个很好的方法去读该文档的内容。另一方面，Writer 是一个非常有用的用于创建 XML 文档片断工具，但是如果你想要它即能读，又能写，那么你就要用 XMLDOM 了。如果实际的 XML 文档非常庞大，又会出现了一个问题，什么问题呢？是不是把这个 XML 文档全部加载到内存中，然后进行读和写呢？让我们先看一下怎么样建立一个混合的流分析器用于分析大型的 XMLDOM。
像一般的只读操作一样，用普通的 XML reader 去顺序的访问节点。不同的是，在读的同时你可以用 XML writer 改变属性值以及节点的内容。你用 reader 去读源文件中的每个节点，后台的 writer 创建该节点的一个拷贝。在这个拷贝中，你可以增加一些新的节点，忽略或者编辑其它的一些节点，还可以编辑属性的值。当你完成修改后，你就用新的文档替换旧的文档。
一个简单有效的办法是从只读流中拷贝节点对象到 write 流中，这种方法可以用 XmlTextWriter 类中的两个方法：WriteAttributes 方法和 WriteNode 方法。 WriteAttributes 方法读取当前 reader 中选中的节点的所有有效的属性，然后把属性当作一个单独的 string 拷贝到当前的输出流中。同样的，WriteNode 方法用类似的方法处理除属性节点外的其它类型的节点。图十所示的代码片断演示了怎么用上述的两个方法创建一个源 XML 文档的拷贝,有选择的修改某些节点。XML 树从树根开始被访问，但只输出了除属性节点类型以外的其它类型的节点。你可以把 Reader 和 Writer 整合在一个新的类中，设计一个新的接口，使它能读写流及访问属性和节点。
Figure 10 Using the WriteNode Method
XmlTextReader reader = new XmlTextReader(inputFile);
XmlTextWriter writer = new XmlTextWriter(outputFile);
// 配置 reader 和 writer
writer.Formatting = Formatting.Indented;
reader.MoveToContent();
// Write 根节点
writer.WriteStartElement(reader.LocalName);
// Read and output every other node
int i=0;
while(reader.Read())
{
if (i % 2)
writer.WriteNode(reader, false);
i++;
}
// Close the root
writer.WriteEndElement();
// Close reader and writer
writer.Close();
reader.Close();
我的 XmlTextReadWriter 类并没有从 XmlReader 或者 XmlWriter 类中继承。取而代之的是另外两个类，一个是基于只读流(stream)的操作类,另一个是基于只写流的操作类。XmlTextReadWriter 类的方法用 Reader 对象读数据，写入到 Writer 对象。为了适应不同的需求，内部的 Reader 和 Writer 对象分别通过只读的 Reader 和 Writer 属性公开。图十一列出了该类的一些方法：
Figure 11 XmlTextReadWriter Class Methods
Method
Description
AddAttributeChange
Caches all the information needed to perform a change on a node attribute. All the changes cached through this method are processed during a successive call to WriteAttributes.
Read
Simple wrapper around the internal reader's Read method.
WriteAttributes
Specialized version of the writer's WriteAttributes method, writes out all the attributes for the given node, taking into account all the changes cached through the AddAttributeChange method.
WriteEndDocument
Terminates the current document in the writer and closes both the reader and the writer.
WriteStartDocument
Prepares the internal writer to output the document and add a default comment text and the standard XML prolog.
这个新类有一个 Read 方法，它是对 Reader 的 read 方法的一个简单的封装。另外，它提供了 WriterStartDocument 和 WriteEndDocument 方法。它们分别初始化/释放(finalize)了内部 Reader 和 writer 对象，还处理所有 I/O 操作。在循环读节点的同时，我们就可以直接的修改节点。出于性能的原因，要修改属性必须先用 AddAttributeChange 方法声明。对一个节点的属性所作的所有修改都会存放在一个临时的表中，最后，通过调用 WriteAttribute 方法提交修改，清除临时表。
图十二所示的代码演示了客户端用 XmlTextReadWriter 类在读操作的同时修改属性值的优势。在本期的 msdn 中提供了 XmlTextReadWriter 类的 C#和 VB 源代码下载（见本文开头提供的链接）。
Figure 12 Changing Attribute Values
private void ApplyChanges(string nodeName, string attribName,
string oldVal, string newVal)
{
XmlTextReadWriter rw = new XmlTextReadWriter(InputFileName.Text,
OutputFileName.Text);
rw.WriteStartDocument(true, CommentText.Text);
// 手工修改根节点
rw.Writer.WriteStartElement(rw.Reader.LocalName);
// 开始修改属性
// (可以修改更多节点的属性)
rw.AddAttributeChange(nodeName, attribName, oldVal, newVal);
// 循环处理文档
while(rw.Read())
{
switch(rw.NodeType)
{
case XmlNodeType.Element:
rw.Writer.WriteStartElement(rw.Reader.LocalName);
if (nodeName == rw.Reader.LocalName)
// 修改属性
rw.WriteAttributes(nodeName);
else
// deep copy
rw.Writer.WriteAttributes(rw.Reader, false);
if (rw.Reader.IsEmptyElement)
rw.Writer.WriteEndElement();
break;
}
}
// Close the root tag
rw.Writer.WriteEndElement();
// Close the document and any internal resources
rw.WriteEndDocument();
}
XmlTextReadWriter 类不仅可以读 XML 文档，也可以写 XML 文档。你可以它来读 XML 文档的内容，如果需要，你还可以用它来做一些基本的更新操作。基本的更新操作在这里是指修改某个已存在的属性的值或者某个节点的内容，又或者是增加一个新的属性或节点。对于更复杂的操作，最好还是用 XMLDOM 分析器。
总结
Reader 和 Writer 是.NET Framework 中处理 XML 数据的根本。它们提供了对所有 XML 数据访问功能的原始的 API。Reader 像一个新的分析器类，它即有 XMLDOM 的强大，又有 SAX 的快速简单。Writer 为简单的创建 XML 文档而设计。虽然 Reader 和 Writer 都是.NET Framework 中的一小块，但是它们是相互独立的 API。在本文中，我们只讨论了怎么样用 Reader 和 Writer 完成一些主要的工作, 介绍了验证分析器的原理机制，并把 Reader 和 writer 整合在一个单独的类中。上述所有的这些类都是轻量级的，类似于游标式的 XMLDOM 分析器