在 .NET Framework 中表示 XML 的最佳做法
发布日期: 8/25/2004 | 更新日期: 8/25/2004
Dare Obasanjo
Microsoft Corporation
摘要:Dare Obasanjo 着眼于可用来表示在单个进程和 AppDomain 内的组件之间共享的、基于 XML 的数据的选项,并讨论了每种方法在设计上的利弊。
引言
在最近的一次设计审查之后,一名职位是项目经理的同事询问在 API 中公开 XML 时是否存在设计准则,因为他曾经看到过许多不同的方法,但是无法确定该选择何种方法。我告诉他,我原来相信在 MSDN 上可以找到一些准则,但是当我查阅时却只找到一段标题为 Passing XML Data Inside the CLR(英文)的 MSDN TV,其中虽然包含此类信息,但却不容易阅读。因此我萌发了这样一个念头,即为 Don Box 的 MSDN TV 片段提供一个便于打印的版本,并提供我本人在 Microsoft 处理 XML API 方面的一些经验。
准则初探
在三种主要情况下,开发人员需要考虑使用何种 API 来表示 XML。下面简要介绍一下这些情况和准则:
•
类的字段或属性中包含 XML:如果类的字段或属性是 XML 文档或片段,则该类应提供将其属性同时作为字符串和 XmlReader 进行操作的机制。
•
方法可以接受 XML 输入或返回 XML 作为输出:可以接受或返回 XML 的方法应有助于返回 XmlReader 或 XPathNavigator,除非用户希望能够编辑 XML 数据(此时应使用 XmlDocument)。
•
将对象转换成 XML:如果对象出于序列化目的要以 XML 来表示其自身,当其需要获得的 XML 序列化进程控制比 XmlSerializer 所提供的更多时,则应使用 XmlWriter。如果对象要以 XML 来表示其自身,以便能够完全以 XML 世界成员的身份参与到其中(如允许在此对象上进行 XPath 查询或 XSLT 转换),则此对象应实现 IXPathNavigable 接口。
在下面各部分中,我详细介绍了上面提到的几种情况,并解释了我是如何获得这些准则的。
常见疑点
当您决定使用方法或属性接受或返回 XML 时,您的大脑中会出现 .NET Framework 中的许多类,它们都适用于此任务。下面列出了 .NET Framework 中最适合于表示 XML 输入或输出的五个类,并且给出了其正反两方面的简要说明。
1.
System.Xml.XmlReader(英文):XmlReader 是 .NET Framework 的拉模型 XML 分析程序。在拉模型处理过程中,XML 用户通过根据需要从 XML 制造者那里请求事件来控制程序流。拉模型 XML 分析程序(如 XmlReader)以只进的流方式进行操作,同时只显示单个节点在任意给定时间的相关信息。事实上,XmlReader 不需要整个 XML 文档加载到内存并且是只读的,这使其成为以非 XML 数据源创建 XML 外观的一个不错选择。这种以非 XML 数据源创建 XML 外观的一个示例是 XmlCsvReader(英文)。有人可能将 XmlReader 的只进特性视为一种局限性,因为它使用户无法通过 XML 文档的各个部分进行多次传递。
2.
System.Xml.XPath.XPathNavigator(英文):XPathNavigator 是 XML 数据源上的只读光标。XML 光标就像是一个透镜,一次聚焦于一个 XML 节点上,但与基于拉的 API(如 XmlReader)不同,它可以在任意给定时间将光标定位在 XML 文档的任意位置。在某种程度上,拉模型 API 是光标模型的只进版本。XPathNavigator 还是以非 XML 数据实现 XML 外观的一个很好的候选项,因为它允许您以实时方式构造数据源的 XML 视图,而不必将整个数据源转换成 XML 树。使用 XPathNavigator 创建非 XML 数据的 XML 视图的一个示例是 ObjectXPathNavigator(英文)。事实上,XPathNavigator 是只读的,而且缺少一些用户友好属性(如 InnerXml(英文)和 OuterXml(英文)),这使其在需要这些功能时不如使用 XmlDocument 或 XmlNode 那样另人满意。
3.
System.Xml.XmlWriter(英文):XmlWriter 提供了将 XML 文档推入基本存储区的一般机制。该基本存储区可以是从文件(如果使用的是 XmlTextWriter)到 XmlDocument(如果使用的是 XmlNodeWriter(英文))的任何内容。使用 XmlWriter 作为返回 XML 的方法的参数为支持各种可能的返回类型(包括文件流、字符串和 XmlDocument 实例)提供了一种可靠方式。这就是为什么 XmlSerializer.Serialize() method(英文)接受 XmlWriter 作为其中一个重载上的参数。正如其名称所包含的意思,XmlWriter 只对编写 XML 有用,不能用于读取或处理 XML。
4.
System.Xml.XmlDocument/XmlNode(英文):XmlDocument 是 W3C Document Object Model (DOM)(英文)的实现。DOM 是由 XmlNode 对象的分层树所构成的 XML 文档在内存中的表示,这些对象代表 XML 文档的逻辑组件,如元素、属性和文本节点。DOM 是在 .NET Framework 中操作 XML 的最流行的 API,因为它提供了一种直接的方法来加载、处理和保存 XML 文档。DOM 的主要缺陷是其设计需要将整个 XML 文档加载到内存中。
5.
System.String(英文):XML 是一种基于文本的格式,而且比 String 类能更好地表示文本。使用字符串作为 XML 表示方法的主要优点在于字符串是最小公分母。字符串容易写入日志文件或打印到控制台,而且如果您要使用 XML API 对 XML 进行实际处理,则可以将字符串加载到 XmlDocument 或 XPathDocument 中。使用字符串作为利用 XML 的方法或属性的主要输入或输出存在几个问题。使用字符串的第一个问题类似于 DOM,它们都需要将整个 XML 文档加载到内存中。其次,以字符串表示 XML 会增加制造者生成 XML 字符串的负担,在某些情况下这可能非常麻烦。生成 XML 字符串可能非常麻烦的一个示例就是从 XmlReader 或 XPathNavigator 获得 XML 时。第三,以字符串表示 XML 可能会导致与字符编码相关的混乱,因为无论在 XML 中放入什么编码声明,.NET Framework 中的字符串始终是 UTF-16 字符编码。最后,以字符串表示 XML 使得创建 XML 处理管道较为困难,因为管道中的每一层都必须重新分析文档。
类的字段或属性中包含 XML
在某些情况下,对象的字段或属性可能是一个 XML 文档或 XML 片段。下面的示例类代表一封电子邮件,其内容是 XHTML。邮件的 XML 内容是以字符串表示的,并且由该类的 Body 属性公开:
public class Email{
private string from;
public string From{
get { return from; }
set {from = value; }
}
private string to;
public string To{
get { return to; }
set {to = value; }
}
private string subject;
public string Subject{
get { return subject; }
set {subject = value; }
}
private DateTime sent;
public DateTime Sent{
get { return sent; }
set {sent = value; }
}
private XmlDocument body = new XmlDocument();
public string Body{
get { return body.OuterXml; }
set { body.Load(new System.IO.StringReader(value));}
}
}
使用字符串作为表示 XML 文档的字段或属性的主要表示方式是最具用户友好性的表示方式,因为 System.String 类是一般开发人员最熟悉的 XML“常见疑点”。但是,这将会增加使用该类的用户的负担,这些用户现在可能必须应付两次分析 XML 文档的成本。例如,设想一种情况:在从 SqlCommand.ExecuteXmlReader() method(英文)或 XslTransform.Transform() method(英文)所得到的 XML 中设置了这样一个属性。在这种情况下,用户将必须分析文档两次,如下例所示:
Email email = new Email();
email.From = "dareo@example.com";
email.To = "michealb@example.org";
email.Subject = "Hello World";
XslTransform transform = new XslTransform();
transform.Load("format-body.xsl");
XmlDocument body = new XmlDocument();
//1. XML 由 XmlDocument.Load() 分析
body.Load(transform.Transform(new XPathDocument("body.xml"), null));
//2. 同一 XML 被 Email.Body 属性中的 XmlDocument.Load() 再次分析
email.Body = body.OuterXml;
在上例中,同一 XML 被分析了两次,因为在将该 XML 转换成字符串之前必须将其从 XmlReader 加载到 XmlDocument 中,然后使用该 XML 设置 Email 类的 Body 属性,再由该属性本身从内部将该 XML 分析为 XmlDocument。一条很有效的经验法则就是提供对 XML 属性的访问(作为 XmlReader 和字符串)。因此,还应将下列方法添加到 Email 类,以使该类的用户获得最大的灵活性:
public void SetBody(XmlReader reader){
body.Load(reader);
}
public XmlReader GetBody(){
return new XmlNodeReader(body);
}
这为 Email 类的用户提供了一种方式,使这些用户在需要时能够以有效的方式传递、设置和检索 Body 属性中的 XML 数据。
准则 如果类的字段或属性是 XML 文档或片段,则该类应提供将其属性同时作为字符串和 XmlReader 进行操作的机制。
敏锐的读者可能会注意到,如果您直接将 XmlDocument 作为属性公开,则应满足该准则,还应使该属性的用户可以精确更改 XML。
方法可以接受 XML 输入或者返回 XML 作为输出
当设计生成或使用 XML 的方法时,开发人员有责任使这样的方法在接受输入时具有灵活性。当方法接受 XML 作为输入时,您可以将这些方法分为要求在适当位置修改数据的方法,以及只需对 XML 进行只读访问的方法。唯一一个支持读写访问的“XML 常见疑点”是 XmlDocument。下面的代码示例显示了这样一个方法:
public void ApplyDiscount(XmlDocument priceList){
foreach(XmlElement price in priceList.SelectNodes("//price")){
price.InnerText = (Double.Parse(price.InnerText) * 0.85).ToString();
}
}
对于需要对 XML 进行只读访问的方法,有两个主要选项:
•
XmlReader
•
XPathNavigator
XmlReader 提供了对 XML 的只进访问,而 XPathNavigator 不但提供了对基本 XML 源的随机访问,还提供了对数据源执行 XPath 查询的能力。以下代码示例将打印下面的 XML 文档中的 artist(艺术家)和 title(标题):
<items>
<compact-disc>
<price>16.95</price>
<artist>Nelly</artist>
<title>Nellyville</title>
</compact-disc>
<compact-disc>
<price>17.55</price>
<artist>Baby D</artist>
<title>Lil Chopper Toy</title>
</compact-disc>
</items>
XmlReader:
public static void PrintArtistAndPrice(XmlReader reader){
reader.MoveToContent(); //move from root node to document element (items)
/* 保持读取,直至获得第一个 <artist> 元素 */
while(reader.Read()){
if((reader.NodeType == XmlNodeType.Element) && reader.Name.Equals("artist")){
artist = reader.ReadElementString();
title = reader.ReadElementString();
break;
}
}
Console.WriteLine("Artist={0}, Title={1}", artist, title);
}
}
XPathNavigator:
public static void PrintArtistAndPrice(XPathNavigator nav){
XPathNodeIterator iterator = nav.Select("/items/compact-disc[1]/artist
| /items/compact-disc[1]/title");
iterator.MoveNext();
Console.WriteLine("Artist={0}", iterator.Current);
iterator.MoveNext();
Console.WriteLine("Title={0}", iterator.Current);
}
通常,返回 XML 的方法都使用类似的规则。如果希望接收方编辑 XML,则应返回 XmlDocument。否则,应根据方法是否需要提供对 XML 数据的只进流式访问而返回 XmlReader 或 XPathNavigator。
准则 接受或返回 XML 的方法应有助于返回 XmlReader 或 XPathNavigator,除非用户希望能够编辑 XML 数据(此时应使用 XmlDocument)。
上述准则的意思是指返回 XML 的方法应有助于返回 XmlReader,因为它比其他任何类型都适用于更多用户的情况。此外,当方法的调用方需要更多功能时,它们可以从返回的 XmlReader 加载 XmlDocument 或 XPathDocument。
将对象转换为 XML
XML 作为信息交换的通用语言已无所不在,这使其成为要以 XML 表示自身的某些对象的显而易见的选择,这些对象或者是出于序列化目的,或者是为了获得对其他 XML 技术的访问(如使用 XPath 进行查询或使用 XSLT 进行转换)。
当出于序列化目的将对象转换成 XML 时,显然应选择使用 XML Serialization technology in the .NET Framework(英文)。但是,在某些情况下,您对生成的 XML 所需的控制可能比 XmlSerializer 所能提供的更多。在这种情况下,工具包中的 XmlWriter 便是一个很有用的类,因为它使您不再需要该类的结构与所生成的 XML 之间存在一对一映射。下例显示了通过使用 XmlWriter 序列化 Email 类(前面几部分中已经提到)而生成的 XML。
public void Save(XmlWriter writer){
writer.WriteStartDocument();
writer.WriteStartElement("email");
writer.WriteStartElement("headers");
writer.WriteStartElement("header");
writer.WriteElementString("name", "to");
writer.WriteElementString("value", this.To);
writer.WriteEndElement(); //标题
writer.WriteStartElement("header");
writer.WriteElementString("name", "from");
writer.WriteElementString("value", this.From);
writer.WriteEndElement(); //标题
writer.WriteStartElement("header");
writer.WriteElementString("name", "subject");
writer.WriteElementString("value", this.Subject);
writer.WriteEndElement(); //标题
writer.WriteStartElement("header");
writer.WriteElementString("name", "sent");
writer.WriteElementString("value", XmlConvert.ToString(this.Sent));
writer.WriteEndElement(); //标题
writer.WriteEndElement(); //标题;
writer.WriteStartElement("body");
writer.WriteRaw(this.Body);
writer.WriteEndDocument(); //关闭所有打开的标记
}
这段代码生成下面的 XML 文档
<email>
<headers>
<header>
<name>收件人</name>
<value>michealb@example.org</value>
</header>
<header>
<name>发件人</name>
<value>dareo@example.com</value>
</header>
<header>
<name>主题</name>
<value>Hello World</value>
</header>
<header>
<name>发送时间</name>
<value>2004-03-05T15:54:13.5446771-08:00</value>
</header>
</headers>
<body><p>Hello World 是我最喜欢的示例应用程序。</p></body>
</email>
只使用 XmlSerializer 的基本功能将不可能生成上面的 XML 文档。XmlWriter 的另一个优点是它可以从基本目标提取到数据要写入的目标,因此它可以是从磁盘上的文件到内存中的字符串(甚至是 XmlNodeWriter(英文)的 XmlDocument 称谓)的任何内容。
如果要求提供一种方式,使类可以更完全地参与到 XML 世界中(如与XPath 或 XSLT 等 XML 技术进行交互),则该类的最佳选择是实现 IXPathNavigable 接口,并为该类提供 XPathNavigator。这样做的一个示例是 ObjectXPathNavigator,它为那些使您可以在上述对象上执行 XPath 查询或运行 XSLT 转换的任意对象提供了 XML 视图。
准则 如果对象出于序列化目的要以 XML 来表示其自身,当其需要获得的 XML 序列化过程控制比 XmlSerializer 所提供的更多时,则应使用 XmlWriter。如果对象要以 XML 来表示其自身,以便能够完全以 XML 世界成员的身份参与到其中(如允许在此对象上进行 XPath 查询或 XSLT 转换),则此对象应实现 IXPathNavigable 接口。
结论
在将来的 .NET Framework 版本中,将会更加强调基于光标的 XML API(如由 IXPathNavigable 接口公开的 XPathNavigator)。这类光标将成为与 .NET Framework 中的 XML 进行交互的主要机制。
Dare Obasanjo 是 Microsoft WebData 小组的成员,除其他事务外,该小组还开发了 .NET Framework 的 System.Xml 和 System.Data 命名空间、Microsoft XML 核心服务 (MSXML) 以及 Microsoft 数据访问组件 (MDAC) 中的组件。
有关本文的任何问题或评论,欢迎张贴到 GotDotNet 上的 Extreme XML 留言板。