真实世界的 XML:使用 .NET 框架中集成的读取器和写入器很容易操作 XML 数据

 

Dino Esposito

本文假定您熟悉 XML 和 .NET 框架

下载本文的代码:Real-WorldXML.exe(120KB)

 

*

本页内容
从 MSXML 到 .NET 中的 XML从 MSXML 到 .NET 中的 XML
XML 分析模型XML 分析模型
XmlReader 类XmlReader 类
分析属性内容分析属性内容
对 XML 文本进行操作对 XML 文本进行操作
字符串和片段字符串和片段
验证读取器验证读取器
节点读取器节点读取器
XmlTextWriter 类XmlTextWriter 类
读取和写入流读取和写入流
设计 XmlReadWriter 类设计 XmlReadWriter 类
小结小结

大约三年前,我在参加一个软件会议后相信,如果不深入理解 XML,未来就不可能参加编程工作。从初期到现在,XML 的确已经走过了很长的路程,甚至涉及到公用编程框架的最深处。在本文中,我将回顾用于处理 XML 文档的 Microsoft .NET 框架 API 的角色和内部特征,然后继续讨论几个尚未解决的问题。

从 MSXML 到 .NET 中的 XML

在 .NET 框架出现之前,编写 XML 驱动的 Windows 应用程序的习惯方式是利用 MSXML 的服务 - 基于 COM 的库。MSXML 库与 .NET 框架中的类不同,MSXML 库更像是死板的 API,而不是与基本操作系统完全集成的代码。MSXML 肯定可以与应用程序的其余部分通信,但它没有真正与周围的环境集成。

可以在 Win32 甚至在用于公共语言运行库 (CLR) 的代码中导入 MSXML 库,但它保留了一个充当服务器组件的外部黑匣。另一方面,基于 .NET 框架的应用程序可以将 XML 核心类与 .NET 框架的其他命名空间直接放在一起使用,以便最终的代码良好集成在一起,并且容易阅读。

作为独立的组件,MSXML 分析器提供了高级的功能,例如异步分析。在 .NET 框架的 XML 类中,显然缺少这个功能。但是,通过将 XML 类与 .NET 框架中其他类集成在一起,可以很容易获得相同的功能,甚至获得更大的进程控制能力。

XML 函数库应当提供至少一组基本的服务,包括分析、查询和转换。在 .NET 框架中,可以找到支持 XPath 查询和 XSLT 转换的类,以及读取和写入 XML 文档的类。此外,.NET 框架合并了执行与 XML 有关的任务的类,例如,这些任务包括对象序列化(XmlSerializer 和 SoapFormatter 类)、应用程序配置(AppSettingsReader 类)和数据持久性(DataSet 类)。在本文中,我将重点介绍完成基本 I/O 操作的类。

XML 分析模型

由于 XML 是标记语言,因此需要一个能够分析和理解词汇语法的工具来有效地使用存储在文档中的信息。该工具就是 XML 分析器 £- 它是一种读取标记文本并返回特定于平台的对象的黑匣组件。

不管基本平台是什么,程序员可用的所有 XML 分析器都属于两个主要类别中的某一个:基于树的处理器,和基于事件的处理器。通常,使用这两个类别的两个最流行和最具体的实现来识别它们:Microsoft XML Document Object Model (XMLDOM) 和 Simple API for XML (SAX)。XMLDOM 分析器是一个通用的基于树的 API,它可以将 XML 文档呈现为内存中结构。SAX 分析器提供了基于事件的 API,它可以处理在 XML 数据流中的每个重要元素。通常,可以从 SAX 流中加载 Document Object Model (DOM) 实现,所以,两种类型的处理器不是相互排斥的。

从概念上讲,SAX 分析器与 XMLDOM 分析器截然相反,两个模型之间的差异确实相当大。XMLDOM 已在它的功能集中得到很完善的定义,该模型已经没有太多可以期待的合理发展余地。当然,这个大型功能集正在走下坡路 £- 为了处理大型文档,它需要大量占用内存和带宽。

SAX 分析器的工作原理是,让客户端应用程序传递活动的、特定于平台的对象实例,以便处理分析器事件。分析器控制整个过程,并将数据传递给应用程序,而该应用程序可以自由地接受或只是忽略它。这个模型非常瘦,特点是占用的内存非常有限。

.NET 框架为 XMLDOM 分析模型提供了完整支持,但没有为 SAX 这样做。这有一个好的理由。.NET 框架支持两种不同模型的分析器:XMLDOM 分析器和 XML 读取器。虽然明显缺少对 SAX 分析器的支持,但这并不意味着您必须拒绝它们所提供的功能。通过使用 XML 读取器,很容易实现 SAX 分析器的所有功能,并且这些功能更有效。不像 SAX 分析器,.NET 框架读取器工作在客户端应用程序的总体控制下。通过这种方式,应用程序本身就可以只获取它确实需要的数据,并跳过 XML 流的其余信息。使用 SAX,分析器可以将所有可用信息传递给客户端应用程序,然后,后者必须使用或放弃该信息。

读取器基于 .NET 框架流,其工作方式与数据库游标基本相同。有趣的是,实现这个类似游标的分析模型的类,还为 XMLDOM 分析器的 .NET 框架实现提供了基础。两个抽象类(XmlReader 和 XmlWriter)正是所有 .NET 框架 XML 类(包括 XML DOM 类、ADO.NET 相关类和配置类)的基础。因此,当需要处理 XML 数据时,在 .NET 框架中有两个可能的方法。可以使用直接建立在 XmlReader 和 XmlWriter 基础上的类,也可以使用通过众所周知的 XMLDOM 对象模型来公开信息的类。对 .NET 框架中文档读取器的更全面介绍包含在我撰写的 2002 年 8 月的 Cutting Edge 专栏中。

XmlReader 类

XML 读取器提供的编程接口可以让调用方用来连接到 XML 文档,并获取它们需要的数据。如果更密切地观察读取器,您将意识到,它们在后台的工作方式就好像是从数据库获取数据的应用程序。数据库服务器返回游标对象的引用,该对象包含所有查询结果,并使这些结果可以根据需要可用。XML 读取器的客户端将接收对读取器类的实例的引用,这个类会将基本数据流抽象化,并将它呈现为 XML 树。读取器类的方法允许您在整个内容中向前翻滚,从一个节点移动到另一个节点,而不是从一个字节移动到另一个字节,或从一个记录移动到另一个记录。

从读取器的观察角度看,XML 文档不是标记文本文件,而是节点的序列化集合。这样的游标模型是特定于 .NET 框架的;您在其他地方找不到可用的类似编程 API。

在读取器和 XMLDOM 分析器之间,有几个关键差异。XML 读取器是只进的,它们没有周围节点的概念(同代、父代、祖先、子代),并且它们只能读取。在 .NET 框架中,读取和写入 XML 文档是两个完全分离的功能,它们需要不同的、无关的类:XmlReader 和 XmlWriter。要想能够编辑 XML 文档的内容,可以使用 XMLDOM 分析器(围绕 XmlDocument 类生成),也可以设计自定义的类,用这个类将像读取器和写入器这样两个明显不同的实体合并在一个公用逻辑框架下面。让我们首先分析读取器类的编程特性。

XmlReader 是一个抽象类,您可以用它来生成更具体的功能。用户应用程序通常基于这三个派生类中的某一个:XmlTextReader、XmlValidatingReader 或 XmlNodeReader。所有这些类均共享一组公共属性(参阅图 1)和方法(参阅图 2)。注意,有时,属性实际包含的值取决于您正在代码中使用的读取器类。因此,对图 1 所提供的每个属性的描述指的是它的预期目标,即使这可能没有反映该属性在派生的读取器类中的角色。例如,CanResolveEntity 只对 XmlValidatingReader 返回 True;对于其他任何类型的读取器类,它被设置为 False。同样,图 2 列出的某些方法的行为和返回值受到读取器所处理的节点类型的影响。例如,如果节点不是元素节点,则所有包含属性的方法都是 void。

XmlTextReader 类被设计用于以只进、只读方式快速访问 XML 数据流。读取器将验证所提交的 XML 是否具有规范的格式,如果不是则产生异常。读取器还会执行快速检查,以确保所引用的文档类型定义 (DTD)(如果有定义)是存在的。XmlTextReader 类不会根据架构或 DTD 对文档内容进行验证。如果要快速处理可通过文件名、URL 或打开的流来访问的、规范格式的 XML 数据,则 XmlTextReader 分析器是理想的选择。如果需要进行数据验证,则应当使用 XmlValidatingReader 类。

可以用很多方法并从各种数据源(包括磁盘文件、URL、流和文本读取器)创建 XmlTextReader 类的实例:

XmlTextReader reader = new XmlTextReader(file);

注意,所有可用的公共构造函数都需要您指明数据源:它是不是流、文件或其他。XmlTextReader 类的默认构造函数被标记为受保护的,并且(因此)是不可直接使用的。与所有 .NET 框架读取器类一样,一旦读取器对象建立并运行,则必须使用 Read 方法来访问数据。只能使用 Read 方法,将读取器从其初始状态移动到第一个元素。要从任何节点移动到下一个节点,可以继续使用 Read 以及其他很多更专用的方法(包括 Skip、MoveToContent 和 ReadInnerXml)。要处理 XML 源的全部内容,通常可以设置一个由 Read 方法的返回值控制的循环:当有数据可读则为 False,否则为 True。

图 3 展示一个简单函数,它输出给定 XML 文档的节点布局。该函数打开文档并设置一个循环来遍历所有内容。每次调用 Read 方法时,读取器的内部指针都会向前移动一个节点。尽管您很可能使用 Read 方法来处理元素节点,但请考虑在通常情况下,当您从一个节点移动到下一个时,您不必在类型相同的节点之间移动。例如,方法不会通过属性节点。读取器的 MoveToContent 方法让您跳过所有标题节点,并将指针直接放在第一个内容节点上。因此,方法会跳过类型为 ProcessingInstruction、DocumentType、Comment、Whitespace 和 SignificantWhitespace 的节点。

每个节点都被赋予一个从 NodeType 枚举中选取的类型。在图 3 所示的代码中,只有两个类型的节点是相关的:Element 和 EndElement。代码的输出会复制原始文档的结构,但会放弃属性和文本,只保留节点布局。让我们假设我使用下面的 XML 片段:

<mags> 
   <mag name="MSDN Magazine">
   MSDN Magazine
   </mag>
   <mag name="MSDN Voices">
   MSDN Voices
   </mag>
</mags>

最后的输出如下所示:

<mags> 
   <mag>
   </mag>
   <mag>
   </mag>
</mags>

子节点的缩进可以通过使用读取器的 Depth 属性来获得,该属性返回整数值以表示当前节点的嵌套级别。所有文本都累积在 StringWriter 对象中 £- 该对象是用于 StringBuilder 类的包装,它非常方便易用,而且是基于流的。

我在前面提到过,属性节点不会被使用 Read 方法向前移动的读取器自动访问。若要访问当前元素节点的属性集合,必须使用类似的循环,但这个循环要受 MoveToNextAttribute 方法控制。下面的代码将访问当前节点(用 Read 选择的节点)的所有属性,并将它们的名称和值连成以逗号分隔的字符串:

if (reader.HasAttributes)
while(reader.MoveToNextAttribute())
   buf += reader.Name + "=/"" + reader.Value + "/","; 
reader.MoveToElement();

一旦处理完节点属性,请考虑调用读取器对象的 MoveToElement 方法。MoveToElement 将把内部指针“移动”回包含属性的元素节点。准确地说,该方法没有真地“移动”指针,因为在属性的导航期间,指针从不离开元素节点。MoveToElement 方法只是刷新某些内部成员,使它们公开元素节点的值,而不是被读取的上一属性。例如,Name 属性返回在调用 MoveToElement 之前被读取的最后一个属性的名称,和后来读取的父节点的名称。一旦处理完属性,如果对节点没有进一步的操作,并且想要继续处理下一个元素,那么,您并不真的需要调用 MoveToElement。

分析属性内容

大多数情况下,属性的内容是一个简单的文本字符串。但是,这并不意味着字符串是属性值的实际类型。有时,属性值由类型更具体(例如 Date 或 Boolean)的字符串表示法组成,您可以使用静态类(XmlConvert 或 System.Convert)的方法将这种特殊的字符串转换为本机类型。这两个类执行的任务几乎完全相同,但 XmlConvert 类按照 XML 架构定义 (XSD) 数据类型规范工作,并且忽略当前区域设置。

假设您有一个像下面这样的 XML 片段:

<person birthday="2-8-2001" />

让我们再假设,按照当前区域设置,生日属性是 February 8, 2001。如果使用 System.Convert 类将字符串转换成具体的 .NET 框架类型(DateTime 类型),则所有事情将按期望进行,并且字符串将转换成期望的日期对象。相反,如果使用 XmlConvert 来转换字符串,您将得到一个分析错误,因为 XmlConvert 类没有在字符串中识别出正确的日期。原因是 XML 日期必须采用 YYYY-MM-DD 格式才能被理解。XmlConvert 类在 CLR 类型和 XSD 类型之间充当了转换器。当转换发生时,结果将与区域设置无关。

某些情况下,属性的值由纯文本以及实体组成。在所有读取器类之中,实际上只有 XmlValidatingReader 能够解析实体。XmlTextReader 类尽管无法解析实体引用,但当文本和实体嵌入在属性的值中时,这个类可以将二者分离开来。要做到这一点,必须使用 ReadAttributeValue 方法分析属性的内容,而不只是通过 Value 属性读取它。

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;
}

混合内容属性的最后值由在全局缓冲区中累积文本来确定。当属性值已完全分析时,ReadAttributeValue 方法将返回 False。

对 XML 文本进行操作

处理 XML 标记文本时,如果不正确处理,某些功能可以快速成为问题。一个这样的障碍点是字符转换,有时执行该操作是必要的,以便在 XML 数据流中传输非 XML 文本。并非在给定平台上可以找到的所有字符都必须是有效的 XML 字符。只有 XML 规范 (www.w 3.org/TR/2000/RECxml 20001006.html) 中的字符才能安全用作元素和属性的名称。

XmlConvert 类为在服务器之间通过 XML 以隧道方式传输非 XML 名称提供了关键的功能。当名称包含无效 XML 字符时,方法 EncodeName 和 DecodeName 可以调整它们,使其符合 XML 名称架构。几个应用程序(包括 SQL Server?和 Microsoft Office)允许和支持在它们的文档中使用 Unicode 字符。但是,某些这样的字符不能充当有效的 XML 名称。说明 XmlConvert 重要性的典型环境出现在当您操作包含空格的数据库列名称时。尽管 SQL Server 允许使用像“Invoice Details”这样的列名称,但对 XML 流来说这不是有效名称。空格必须替换为它的十六进制编码,结果是“Invoice_0x0020_Details”。十六进制序列的唯一有效的格式是 _0xHHHH_,其中 HHHH 代表一个四位数的十六进制值。相似但不同的格式会保持不变,虽然可以将它们看作在逻辑上是等价的。下面演示如何通过编程获得该字符串:

XmlConvert.EncodeName("Invoice Details");

相反的操作由 DecodeName 完成。该方法通过对任何转义序列执行逆转义操作,将 XML 名称转换回它的原始格式。注意,只检测完整的转义格式。例如,只有 _0x0020_ 将呈现为空格,而 _0x 20_ 则不会被处理:

XmlConvert.DecodeName("Invoice_0x0020_Details");

在 XML 中,XML 文档的正文内的空白可以是重要的,也可以是不重要的。如果空白出现在元素节点的文本中,或出现在空白声明的作用范围内,则说该空白是重要的:

<MyNode xml:space="preserve">
<!-- any space here must be preserved -->
???
</MyNode>

在 XML 中,术语空白 (whitespace) 不仅包括空格 (ASCII 0x20),而且包括回车符 (ASCII 0x0D),换行符 (ASCII 0x0A) 和制表符 (ASCII 0x09)。

XmlTextReader 类通过 WhiteSpaceHandling 属性让您控制如何处理空白。该属性接受并返回从 WhiteSpaceHandling 枚举取得的值,该枚举列出了三个可行的选项。默认选项是 All,这意味着重要和不重要的空格都会作为不同的节点返回 £- 分别是 SignificantWhitespace 和 Whitespace。选项 None 表示任何空白都不会作为节点返回。最后,选项“Significant”表示放弃所有不重要的空白,并且只返回类型为 SignificantWhitespace 的节点。注意,WhiteSpaceHandling 属性是很少几个可以随时更改的读取器属性中的一个,更改将在下一次读取操作时生效。其他“敏感的”属性是 Normalization 和 XmlResolver。

字符串和片段

仔细研究了 MSXML 的程序员肯定注意到 COM 库和 .NET 框架 XML API 之间的关键差异。.NET 框架类不提供分析存储在字符串中的 XML 数据的本机方式。不像 MSXML 分析器对象,XmlTextReader 类不提供任何种类的 loadXML 方法来从格式规范的字符串生成读取器。缺少 loadXML 风格的方法存在争议。该方法是不需要的,因为使用特殊的文本读取器(StringReader 类),可以获得相同的功能。

一个 XmlTextReader 构造函数接受 TextReader 派生的对象,并用文本读取器的内容实例化 XML 读取器。文本读取器类是一个已经为字符输入进行过优化的流。StringReader 类继承自 TextReader,并使用内存中字符串作为输入流。下面的代码片段说明如何使用格式规范的 XML 字符串初始化 XML 读取器:

string xmlText = "...";
StringReader strReader = new StringReader(xmlText);
XmlTextReader reader = new XmlTextReader(strReader);

同样,通过使用 StringWriter 类代替 TextWriter 类,可以用内存字符串创建 XML 文档。

XML 片段是特殊类型的 XML 字符串。片段是由没有应用正确格式的 XML 文档的根级别规则的 XML 文本组成的。结果,XML 片段与普通文档不同,因为它可以缺少唯一的根节点。例如,下面的 XML 字符串是有效的 XML 片段,但不是有效的 XML 文档,因为 XML 文档必须有一个根节点:

<firstname>Dino</firstname>
<lastname>Esposito</lastname>

.NET 框架 XML API 允许程序员将 XML 片段与由诸如编码字符集、DTD 文档、命名空间、语言和空白处理等信息构成的分析器上下文关联起来:

public XmlTextReader(
   string xmlFragment, 
   XmlNodeType fragType,
   XmlParserContext context
);

xmlFragment 参数包含要分析的 XML 字符串。fragType 参数代表片段的类型,并由片段的根节点类型给出。只允许元素、属性和文档节点作为片段的根,并且分析器上下文由 XmlParserContext 类来表达。

验证读取器

XmlValidatingReader 类是 XmlReader 类的实现,它为几种类型的 XML 验证提供支持:DTD、XML 数据简化 (XDR) 架构和 XSD。DTD 和 XSD 是 W3C 发布的正式建议,而 XDR 是 XML 架构的早期工作草案的 Microsoft 实现。

可以使用 XmlValidatingReader 类来验证整个 XML 文档以及 XML 片段。XmlValidatingReader 类工作在 XML 读取器的顶部 £- 通常是 XmlTextReader 类的实例。文本读取器用来遍历文档节点,而验证读取器应当按照所请求的验证类型对每一片 XML 进行验证。

XmlValidatingReader 类只实现了 XML 读取器必须提供的功能中非常小的一个子集。该类总是工作在现有 XML 读取器的顶部,并反映很多方法和属性。如果查看类的构造函数,就会发现,验证读取器对现有文本读取器的依赖性非常明显。无法从文件或 URL 直接初始化 XML 验证读取器。可用构造函数的清单由下面的重载组成:

public XmlValidatingReader(XmlReader);
public XmlValidatingReader(Stream, XmlNodeType, XmlParserContext);
public XmlValidatingReader(string, XmlNodeType, XmlParserContext);

验证读取器可以分析可通过字符串或打开的流以及为其提供读取器的任何 XML 文档访问的任何 XML 片段。

类为其提供有意义实现的方法的列表非常短。除了 Read,还有 Skip 和 ReadTypedValue 方法。在基本读取器中,Skip 方法将跳过当前活动节点的子代。(需要注意的是,无法跳过格式错误的 XML 文本。)Skip 方法还会验证被跳过的内容。ReadTypedValue 方法将基本节点的值作为 CLR 类型返回。如果方法可以使用 CLR 类型映射 XSD 类型,它将同样返回它。如果直接映射是不可能的,则节点值将作为字符串返回。

验证读取器只是它的名称所指的东西 £- 根据当前架构来验证当前节点结构的、基于节点的读取器。验证过程增量发生;它没有可返回布尔值以表示给定文档是否有效的方法。同样使用 Read 方法在输入文档中移动。实际上,使用验证读取器的方式与使用任何其他基于 XML 的 .NET 框架读取器是一样的。在每一步,都会根据指定的架构对当前所访问的节点的结构进行验证,如果发现错误则产生异常。图 4 包含的控制台应用程序在命令行取得文件名称,然后输出验证过程的结果。

ValidationType 属性用于设置需要的验证类型 £- DTD、XSD、XDR 或无。如果没有指定验证类型(通过使用 ValidationType.Auto 选项),则读取器自动应用它认为最适合于该文档的验证。调用方应用程序将通过 ValidationEventHandler 事件得到任何错误通知。如果没有指定自定义事件处理程序,则对应用程序产生 XML 异常。定义 ValidationEventHandler 方法是捕获由源文档中的不一致所导致的任何 XML 异常的一个途径。注意,读取器检查文档格式是否规范的机制与检查架构遵守情况的机制是明显不同的。如果验证读取器碰巧遇到格式错误的 XML 文档,则不会触发事件,但会产生 XmlException 异常。

验证发生在用户使用 Read 方法向前移动指针时。一旦节点已被分析和读取,它就会被传递给内部的验证器对象,进行进一步处理。验证器基于节点类型和已请求的验证类型执行操作。它确保节点拥有所有属性以及期望它包含的子代。

验证器对象内部调用两种风格的对象:DTD 分析器和架构生成器。DTD 分析器根据 DTD 处理当前节点及其子树的内容。架构生成器基于 XDR 或 XSD 架构源代码为当前节点建立一个架构对象模型 (SOM)。架构生成器类实际上是更特殊的 XDR 和 XSD 架构生成器的基类。但重要的是,处理 XDR 和 XSD 架构的方式基本相同,并且在性能上没有差异。

如果节点有子代,则用另一个临时读取器来收集有关节点子代的信息,以便可以完整地调查节点的架构信息,如图5 所示。


5 对子节点使用临时读取器

注意,尽管一个 XmlValidatingReader 构造函数的签名通常引用 XmlReader 类作为基本读取器,但读取器只能是 XmlTextReader 类或从它派生的类的实例。这意味着,您无法使用碰巧继承自 XmlReader(例如,自定义的 XML 读取器)的任何类。在内部,XmlValidatingReader 类假定基本读取器是 XmlTextReader 对象,并且明确地将输入读取器转换为 XmlTextReader。如果使用 XmlNodeReader 或自定义读取器类,则编译时不会遇到任何错误,但将在运行时产生异常。

节点读取器

XML 读取器类可以按增量方式、逐节点地处理文档内容。迄今为止,我假设源文档是基于磁盘的流或字符串。但是,也可以使用 XMLDOM 对象作为源文档。但在这种情况下,需要具有特殊 Read 方法的特殊类。为此,.NET 框架提供了 XmlNodeReader 类。

正如 XmlTextReader 访问所指定的 XML 流的所有节点一样,XmlNodeReader 类访问构成 XMLDOM 子树的所有节点。XML DOM 类(在 .NET 框架中是 XmlDocument)提供了基于 XPath 的方法,例如 SelectNodes 和 SelectSingleNode。这些方法的实际结果是将匹配的节点集加载到内存中。如果需要处理子树中的所有节点,则节点读取器更有效率,因为它只是以类似游标的增量方式逐个处理节点:

// xmldomNode is the XML DOM node
XmlNodeReader nodeReader = new XmlNodeReader(xmldomNode);
while (nodeReader.Read()) {
    // Do something here
}

在处理填满从配置文件(例如,web.config)摘取的自定义数据的 XMLDOM 树时,将 XmlNodeReader 类与 XML DOM 类一起使用也是有好处的。

XmlTextWriter 类

以编程方式创建 XML 文档再也不是特别困难的事情。这些年来,开发人员实现该操作的方式是:将几个字符串在缓冲区中连在一起,在完成后,再将缓冲区内容全部写到文件中。但是,只有当您能够保证细微的错误永远不会进入代码流时,以这种方式创建 XML 文档才是有效的。.NET 框架通过使用 XML 写入器,为创建 XML 文档提供了更有效率和更精彩的手段。

XML 写入器类用于将 XML 数据以只进方式输出到流或文件中。更重要的是,由设计保证 XML 写入器产生的所有 XML 数据都遵守 W3C XML 1.0 和命名空间建议。您甚至不必关心单独的尖括号,也不必担心您让其处于打开状态的最后一个元素节点。XmlWriter 是所有 XML 写入器的基本抽象类。.NET 框架只提供了一个实际的写入器类 £- XmlTextWriter 类。

要查看 XML 写入器和旧风格写入器之间的差异,请考虑下面的代码保留一个字符串数组:

StringBuilder sb = new StringBuilder("");
sb.Append("<array>");
foreach(string s in theArray) {
   sb.Append("<element value=/"");
   sb.Append(s);
   sb.Append("/"/>");
}
sb.Append("</array>");

代码循环遍历数组元素,准备标记文本,并将它累积在字符串缓冲区中。代码负责确保输出的格式是规范的,并且处理缩进、换行和命名空间支持。只要待创建的文档是简单的并具有某种结构,这种方式就不会出现明显的错误。但是,如果必须支持处理指令、命名空间、缩进、格式化和实体,则所需代码的复杂性就会呈指数级增长,造成错误和程序问题的可能性也会以同样方式增长。

XML 写入器具有为每种可能的 XML 节点类型提供写入方法的特点,并使创建 XML 输出更符合逻辑和更少依赖于标记语言的细节。图 6 说明如何使用 XmlTextWriter 类的服务序列化一个字符串数组。尽管同等紧凑,但该代码使 XML 写入器的使用变得清晰得多且更结构化。

遗憾的是,XML 写入器没有魔力 £- 它无法修复输入错误。XML 写入器不会检查在元素和属性名中是否有无效字符,并且不保证所使用的任何 Unicode 字符将适合于当前编码架构。前面已经提到,要避免不正确的输出,非 XML 字符在作为 XML 往返转换时必须被正确转义。而写入器不提供该服务。

此外,创建属性节点时,写入器不会验证元素是否已存在同名属性。最后,XmlWriter 类不是验证写入器,它不会根据任何架构或 DTD 保证输出是有效的。.NET 框架目前还不提供验证写入器类。但是,我的书 Applied XML Programming for Microsoft .NET (Microsoft Press,2002) 中附带的代码包含了我编写的实现。可以从 http://www.microsoft.com/MSPress/books/6235.asp 下载代码。

图 7 总结了 XML 写入器的实际可行状态。所有值均来自 WriteState 枚举类型。创建写入器时,它的状态被设置为 Start,这表示您还在配置对象,实际写入阶段尚未开始。下一个状态是 Prolog,一旦您调用方法 WriteStartDocument 开始工作之后不久就会进入该状态。之后,状态转换取决于您正在写入的文档的类型及其内容。只要您添加非元素节点(比如注释、处理指令和文档类型),状态就会保持为 Prolog。写入第一个元素节点(文档根节点)时,状态就会更改为 Element。调用 WriteStartAttribute 方法时,状态切换为 Attribute,但在使用更直接的 WriteAttributeString 方法写入属性时则不切换。在这种情况下,状态继续设置为 Element。写入关闭标记将使状态转为 Content。完成后,您调用 WriteEndDocument,状态将返回到 Start,直到开始另一个文档或关闭写入器为止。

写入器将输出文本存储在内部缓冲区中。通常情况下,缓冲区会全部腾空,XML 文本只在写入器关闭时写入。通过调用 Flush 方法,可以在任何时候腾空缓冲区,并将当前内容写入基本流(通过 BaseStream 属性公开)。某些工作内存被释放,写入器保持打开状态,并且操作可以继续。但请注意,其他过程无法访问部分写入的存储文件,直到写入器关闭。

属性节点可以用两种方式写入。第一个方式涉及使用 WriteStartAttribute 方法来创建新的属性节点,并相应更新状态。然后用 WriteString 方法设置属性的值。WriteEndElement 将关闭节点的写入阶段。另一种方式是使用 WriteAttributeString 方法,该方法在状态为 Element 时工作,并以一步操作创建属性。同样,WriteStartElement 将写入节点的打开标记,然后您可以随意设置节点的属性和文本。通常,元素节点的关闭标记采用紧凑形式“/>”。如果想要完整地关闭标记,请使用 WriteFullEndElement 方法。

如果传递给写入方法的文本包含敏感标记字符(例如,小于符号“<”),则该文本全部会被自动转义。但 WriteRaw 方法让您有机会将未分析的数据输入流中。如果看一看这两行代码,就会看到,第一行代码输出 <,而第二行输出未分析的 < 字符:

writer.WriteString("<");
writer.WriteRaw("<");

读取和写入流

有趣的是,读取器和写入器类提供了读取和写入数据流的方法(即使数据流被编码为 Base64 或 BinHex)。方法 WriteBase64 和 WriteBinHex 的特点是有一个与其他写入方法略微不同的签名。因为它们是基于流的,所以方法起一个字节数组(而不是字符串)的作用。下面的代码首先将字符串转换成字节数组,然后将它作为 Base64 编码的流写入。Encoding 类的静态 GetBytes 方法执行转换:

writer.WriteBase64(
   Encoding.Unicode.GetBytes(buf), 
   0, buf.Length*2);

图 8 中的代码将一个字符串数组保留为 Base64 编码的 XML 流,而图 9 演示了当它出现在 Microsoft Internet Explorer 时的最终结果。


9 Internet Explorer 中的字符串数组

Reader 类具有相应的方法来对 Base64 和 BinHex 流进行解码。下面的代码片段说明如何使用 XmlTextReader 类的 ReadBase64 方法对先前创建的文件进行解码:

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();

字节到字符串的转换是由 Encoding 类的 GetString 方法执行的。尽管我提供了 Base64 编码架构的代码,但如果要使用 BinHex 只需替换方法名称。该技术可成功用于能够用字节数组表达的任何种类的二进制数据,尤其是图像。

设计 XmlReadWriter 类

前面已经提到,XML 读取器和写入器在相互隔离的情况下工作:读取器只读取,而写入器只写入。假设应用程序管理着冗长的、包含不稳定数据的 XML 文档。读取器就是读取该内容的好方式。另一方面,写入器是从头创建文档的非常有用的工具。但如果想要同时读取并写入文档,则必须求助于成熟的 XMLDOM。如果文档特别巨大,就会有问题。例如,可以用什么办法读取和写入 XML 文档,而不用将其完全加载到内存中?让我们考察一下如何建立一种混合类型的流式分析器,并让它充当轻量级 XMLDOM 分析器。

至于只读操作,可以使用标准的 XML 读取器按顺序访问节点。差异是,在读取时您将有机会通过在读取器的顶部使用 XML 写入器,来更改属性值和节点内容。可以使用读取器来读取源文档中的每个节点,并使用基本写入器来创建它的隐藏副本。在副本中,可以添加某些新节点,忽略或编辑某些其他节点,并编辑属性值。完成后,只需用新文档替换旧文档。

将若干批的节点从只读流(成批)复制到写入流的一个有效方式是使用 XmlTextWriter 类的两个方法:WriteAttributes 和 WriteNode。WriteAttributes 方法可以读取在读取器中当前选中的节点上可用的所有属性。下一步,该方法将把属性作为单个字符串复制到当前的输出流。同样,WriteNode 方法对任何其他类型的节点(除了属性节点)执行相同的操作。图 10 显示的一段代码使用这些方法来创建原始 XML 文件的副本,它经过修改以跳过某些节点。以通常的 node-first 方式访问 XML 树,但只写出所有其他节点。可以将读取器和写入器合并在一个新类中,并生成全新的编程接口,以便允许容易地对属性或节点进行读取/写入流访问。

我的 XmlTextReadWriter 类没有继承自 XmlReader 或 XmlWriter,而是协调这两个类的正在运行的实例的活动 £- 一个实例操作只读流,另一个处理只写流。XmlTextReadWriter 类的方法从读取器读取数据,并写入到写入器,并在这两个过程中间应用任何所请求的更改。内部读取器和写入器分别通过称为 Reader 和 Writer 的只读属性公开。图 11 列出了新类的方法。

这个类有 Read 方法,该方法是读取器的 Read 方法的简单包装。此外,它还有一对 WriteStartDocument 和 WriteEndDocument 方法,这两个方法可以初始化和终结内部读取器和写入器,并执行所有必需的 I/O 操作。涉及节点的更改由客户端在读取循环期间直接执行。出于性能原因,涉及编辑属性的更改必须首先使用 AddAttributeChange 方法进行注册。对节点属性的所有更改都被临时存储在内部表中,并在调用 WriteAttributes 时被全部写出。

图 12 中的代码说明了一个客户端如何在读取的同时利用 XmlTextReadWriter 类来更改属性值。本月的代码下载(链接位于本文顶部)提供了 XmlTextReadWriter 类的全部源代码(C# 和 Visual Basic .NET 版本)。

XmlTextReadWriter 类更像是读取器而不是写入器。原因是,使用它可以读取 XML 文档的内容,但如果需要,还可以执行某些基本的更新。按照我的理解,基本更新是更改一个或多个现有属性的值或节点的内容,或添加新的属性或节点。对于更复杂的操作,则没有可取代 XMLDOM 分析器的其他途径。

小结

读取器和写入器是 .NET 框架中 XML 数据支持的基础。它们代表了所有 XML 数据访问功能的原始 API。读取器充当了新式和创新类型的分析器,它介于真正功能强大的 XMLDOM 和由 SAX 所提供的快速和简单方式二者之间。写入器是与读取器相对应的领域,组成它的各种工具被设计用于简化 XML 文档的创建。尽管读取器和写入器通常作为 .NET 框架的一部分进行引用,但它们实际上是完全独立的 API。本文讨论了如何使用读取器和写入器完成关键任务,并介绍了验证分析器的体系结构。将读取器和写入器统一在单独的一个包含所有所需功能的类中仍然是可能的,这将产生一个轻量级、类似游标的 XMLDOM 模型。

有关相关文章,请参阅:

XML in .NET:.NET Framework XML Classes and C# Offer Simple, Scalable Data Manipulation

Implementing XmlReader Classes for Non-XML Data Structures and Formats

Applied XML Programming for Microsoft .NET,作者:Dino Esposito(Microsoft Press 2002 年出版)

有关背景信息,请参阅:

XML Data:Overview of XML

Dino Esposito 是一位定居于意大利罗马的培训讲师和顾问。他是 Building Web Solutions with ASP.NETADO.NET 的作者,这两本书都由 Microsoft Press 出版。本文改编了在他所著的 Applied XML Programming for Microsoft .NET(Microsoft Press 2002 年出版)一书中详述的某些概念。如果希望与 Dino 联系,可发送电子邮件至 dinoe@wintellect.com。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值