摘要:特邀作者
Oleg Tkachenko 向您介绍了如何通过 .NET Framework 中的
XslTransform 和 XmlTextWriter 类将 XSL
转换结果处理成多个文档。(本文还包含英文链接。请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)
下载
XML06162003_sample.exe 示例文件。
注意 此下载要求安装
Microsoft
.NET Framework 1.0。
编者按 Extreme XML
的本月连载部分由特邀专栏作家 Oleg Tkachenko
撰写。Oleg 居住在以色列的霍隆,是 Multiconn
Technologies 的软件工程师。他一直致力于 XML
的研究,最近三年特别专注于 XSL
的研究,并且是有关 Microsoft XML
产品网上社区的积极投稿人。他的联系方式为 olegt@multiconn.com。
早在 1999
年 1 月,人们就需要在一次 XSL
转换中生成多个输出文档这种功能。这种需求很自然,不仅要求能够执行一对一或多对一的转换,而且还要求能进行一对多和多对多的转换。例如,为一本书的各章生成单独的
HTML 页面或从图像目录和页面模板生成 Web
照片库。遗憾的是,在之后的 1999 年 11 月发表的 W3C
XSLT 1.0 Recommendation
没有定义对多个输出文档的支持;而是假设了单个结果文档。
W3C XSL 工作组已经提出了要求,即下一版本的
XSLT 语言将支持多个输出文档,W3C
XSLT 1.1 Working Draft(已正式结稿)也相应地引入了辅助结果文档的概念以满足此要求。W3C
XSLT 2.0 Working Draft(正在进行中)更进了一步,即取消了主要和辅助结果文档的概念,所有可能的结果文档都被赋予相同的状态。
所以尽管未来版本的 XSLT
听起来很动人,但是 XSLT 1.0 的现有用户(例如
System.Xml.Xsl.XslTransform 类或 MSXML
的用户)无法使用此功能。为了解决此功能缺陷,在与
XSLT 相关的 MSDN
新闻组中通常建议了一些替代解决方案,包括:
- 通过将转换输入分成多个区块并依次对每个区块执行转换,来对其进行预处理。
- 使用扩展功能或扩展对象创建结果树片段,并另外进行写入。
- 通过将转换结果分成区块并将每个区块作为单独的文档写入,来对其进行后处理。
在本文中,将介绍如何使用 XslTransform
类和自定义的 XmlTextWriter
类在 .NET Framework
中简便有效地实现后一个解决方案。
后处理 XSL 转换结果
通过后处理实现多个输出的想法是:引入一个附加的自定义层,在其中进行
XSLT 处理器和 XSL
转换结果使用者之间的进一步转换。多个输出逻辑即在此附加层中实现。让我们将该层命名为重定向层(Redirecting
Layer)。通过这种方法,XSLT
处理器仍然生成单个结果文档,该文档包含主结果文档
(Main result document) 和可选的辅助结果文档 (Subsidiary
result document) ,这些文档由某些重定向指令(Redirecting
instruction)
标记。重定向指令是指明辅助结果文档以及定义重定向辅助结果文档的位置和方法的元素。重定向层进一步传递主结果文档并且不更改主结果文档,但是按照重定向指令将辅助结果文档重定向到指定目的地。图
1 显示了概念处理模型。
图 1.
通过后处理的多个输出概念模型
设计过程
此时必须确定如何实现重定向指令和重定向层。
重定向指令
重定向指令可以成对实现,它标记重定向的开始和结束。尽管这种方法看起来合理,但是它有一些严重的缺点。首先,XML
处理指令不能具有属性,因此难以从处理指令嵌入以及检索结构化信息。其次,XML
处理指令不能具有子项,因此不适合于标记子树。它非常容易创建包含错误样式内容的处理指令对,甚至会忘记结束处理指令。更为糟糕的是,它仍然是样式完好的
XML
文档,因此直到最终输出重定向阶段才会认识到问题。
XML 元素更适合作为重定向指令。为了避免重头开始,我决定使用
EXSLT community initiative
开发的 exsl:document
XSLT 扩展元素作为实现中的重定向指令。exsl:document
元素必须属于 http://exslt.org/exsl/index.html
命名空间,并具有以下语法(仅限于支持的属性子集):
属性
<exsl:document
href = { uri-reference }
method = { "xml" | "text" }
encoding = { string }
standalone = { "yes" | "no" }
doctype-public = { string }
doctype-system = { string }
indent = { "yes" | "no" }
<-- Content:template -->
</exsl:document>
exsl:document 元素语义
现在要系统地说明某些令人厌烦的语义了,请继续坚持。exsl:document
元素用于创建辅助结果文档。exsl:document
元素的内容为一个模板,它可以实例化地创建一系列节点。可以创建一个根节点,使该系列节点作为其子项,包含此根节点的树表示辅助结果文档。
xsl:output
元素的属性不会影响辅助结果文档的输出。辅助结果文档的输出完全由用于创建该辅助结果文档的
exsl:document 元素的属性控制。
href
属性是 exsl:document
元素必需的唯一一个属性,它指定了新结果文档的存储位置。它必须为绝对或相对
URI,不能有片段标识符。剩余属性的语义与 xsl:output
元素的 W3C XSLT 1.0
Recommendation 中定义的语义完全相同。
注意: XSLT 1.0 元素扩展机制允许将命名空间设计为扩展命名空间。如果命名空间前缀在
xsl:stylesheet 元素的 extension-element-prefixes
属性中列出,则该命名空间中的任何元素都应视为扩展元素。但是,处理模型中的
exsl:document 元素不是真正的 XSLT
扩展元素,而是 XslTransform
类的常规结果元素,因此“exsl ”命名空间前缀不应在
extension-element-prefixes
属性中列出。列出前缀会导致异常,因为 XslTransform
类不支持自定义扩展元素。但是,最好将“exsl ”命名空间前缀放入
xsl:stylesheet 元素的 exclude-result-prefixes
属性,以免“exsl ”命名空间传播到转换结果文档中。
流量控制器:重定向层
定义了重定向指令的语法和语义后,便可以介绍重定向层设计。
.NET
Framework 类库的 System.Xml.Xsl
命名空间中的 XslTransform 类被设计为带有大量
API 的可变组件,并允许将转换结果输出到 Stream、TextWriter、XmlWriter
或 XmlReader
对象。Stream 和 TextWriter 类为通用的 XML
无关 I/O 类,因此向它们输入时,XslTransform
向下序列化结果树到字符级 XML 语法,并使用 TextWriter
方法将其写入到指定的 Stream 或 TextWriter
对象。但是对于 XmlWriter,XslTransform
不序列化结果树,但使用 XmlWriter
方法将其写入到指定的 XmlWriter
对象。对于最后一个实例 XmlReader,将创建一个新的 XmlReader
对象,并且可以使用 XmlReader
方法将转换结果异步地与此对象分开。
在 XslTransform 的各种输出中,自定义 XmlTextWriter
类(用于实现 XmlWriter)看上去是最有效和清楚的选择。这样几乎可以直接访问转换结果树,因此可以避免多余的中期序列化/分析步骤。由于这个原因,我决定将重定向层作为自定义的
XmlTextWriter 类实现,并命名为 MultiXmlTextWriter。如果利弊权衡,此决定也有一些缺点。请参阅标题为附加的字符串一节以获得说明。
综上所述,可以优化概念模型(如图 2
所示)。
图 2.
优化的多个输出概念模型
自定义 XmlTextWriter
任何使用过 SAX 的人都会发现自定义
XmlWriter 与创建 SAX
过滤器有许多相同之处。从根本上说,都有一系列方法调用以用于写出
XML,如果要过滤或修改某些调用,则必须替代相应的
WriteXXX
方法。此外,像往常一样在压栈处理模式下,如果执行特定任务所需的信息多于本地方法调用中包含的信息,则必须跟踪方法调用之间某些写入者的状态。
要在 MultiXmlTextWriter
类中实现输出重定向逻辑非常简单。在写入流中检测到
exsl:document
元素的开始标记后,其属性应被提取,并且应使用在该属性中指定的参数来创建新的
writer 对象(XmlTextWriter 或 StreamWriter,这取决于“方法”属性值),然后输出将被切换到新创建的
writer 对象,直至遇到此 exsl:document
元素的结束标记。
要实现此逻辑,需要能够:
- 检测到
exsl:document
元素的开始标记。
- 提取
exsl:document 元素属性。
- 检测到
exsl:document
元素内容的起始位置。
- 重定向输出。
- 检测到
exsl:document
元素的结束标记。
要完成这些任务,还需要一台具有四种状态的简单状态计算机:
- 中继:将输出进一步中继而不更改输出(默认状态)。
- 重定向:将输出重定向(即写入
exsl:document 元素内容)。
- WritingRedirectElementAttrs:写入
exsl:document 元素属性。
- WritingRedirectElementAttrValue:写入
exsl:document 元素属性值。
检测 exsl:document
元素的开始标记很简单。只需替代 WriteStartElement
方法以查看元素的本地名称是否为“document”以及命名空间
URI 是否为“http://exslt.org/exsl/index.html”:
public override void WriteStartElement(string prefix, string localName, string ns) {
if (ns == "http://exslt.org/common" && localName == "document") {
//Detected exsl:document element start tag
...
检测到 exsl:document
元素的开始标记后,可以等待接着要被写入的 exsl:document
元素属性,以便状态计算机切换到 WritingRedirectElementAttrs
状态。
检测 exsl:document
元素的结束标记需要点技巧,但仍然相当简单。写入
exsl:document
元素内容时,必须保持深度层;每次执行 WriteStartElement
方法调用时,深度层将递增,每次执行 WriteEndElement
方法调用时,深度层将递减,所以返回 WriteEndElement
方法中的初始深度层值表示遇到了要查找的结束标记。
提取 exsl:document
元素属性需要更多的状态调整。在 WritingRedirectElementAttrs
状态中调用 WriteStartAttribute
方法后,应该保存属性名称并将状态计算机切换到 WritingRedirectElementAttrValue
状态,然后在下一个 WriteString
方法中将获得此属性值。WriteEndAttribute
方法将按以下方式取消状态计算机的设置:
public override void WriteStartAttribute(string prefix, string localName, string ns) {
if (redirectState == RedirectState.WritingRedirectElementAttrs) {
redirectState = RedirectState.WritingRedirectElementAttrValue;
currentAttributeName = localName;
...
}
public override void WriteString(string text) {
if (redirectState == RedirectState.WritingRedirectElementAttrValue) {
switch (currentAttributeName) {
case "href":
state.Href = text;
break;
...
}
public override void WriteEndAttribute() {
if (redirectState == RedirectState.WritingRedirectElementAttrValue) {
redirectState = RedirectState.WritingRedirectElementAttrs;
...
}
然后,检测 exsl:document
元素内容的起始位置可以基于这样一种构思:即在 WritingRedirectElementAttrs
状态下,每个 WriteXXX 方法调用(而不是 WriteStartAttribute)表示属性写入结束,内容写入开始。检测到元素内容的起始位置后,状态计算机应切换到重定向状态。
为了更好地了解它的工作原理,请参见下面的图
3,该图显示了 exsl:document
元素的典型处理期间状态的顺序:
图 3. 处理 exsl:document
元素期间状态的顺序
最后,只要有状态计算机,输出重定向就很容易了。我需要替代以下基本
XmlTextWriter
方法:
在这些方法的每个方法中,必须查看当前是否处于重定向状态以及是否相应地调用了当前写入者同名方法或基类
(XmlTextWriter) 同名方法:
public override void WriteComment(string text) {
if (redirectState == RedirectState.Redirecting)
state.Writer.WriteComment(text);
else
base.WriteComment(text);
}
源代码
您可以在文章示例代码的“src”目录中找到全都加了注释的
MultiXmlTextWriter 源。它们由 MultiOutput
命名空间中的两个类组成:MultiXmlTextWriter
类本身,它用于实现上述设计;OutputState
类,它作为输出状态属性集合运行(它保持当前写入者和当前深度层,以及由
exsl:document 元素属性定义的全部辅助结果文档属性)。
另请参阅“samples\xmldoc\doc”目录中的 MultiXmlTextWriter
文档。此文档使用 MultiXmlTextWriter 本身由 C# XML
文档文件生成。请参阅下面的实际示例:由
C# 文档文件生成 MSDN 样式文档以获得详细信息。
此外,最新版本的 MultiXmlTextWriter(我认为它仍有很大的增强空间)可以从
GotDotNet 站点下载。
MultiXmlTextWriter 使用模式
现在将演示如何使用 MultiXmlTextWriter
类。MultiXmlTextWriter 类扩展了 XmlTextWriter,转而实现了
XmlWriter,因此 MultiXmlTextWriter
实例可以直接传送至重载的 XslTransform.Transform()
方法,该方法将 XmlWriter
作为对象接收并向其写入转换结果。
注意: 一般来说,MultiXmlTextWriter
类不只限于与 XslTransform
一起使用,它可以以类似的方式用于拆分任何 XML
流。
以下为在 C# 代码中的典型使用方案:
namespace MultiOutput.Test {
using System;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Xml;
using System.Text;
using System.IO;
using MultiOutput;
/// <summary>
/// <c>MultiXmlTextWriter</c> test class.
/// </summary>
/// <remarks>Usage:<br/>
/// <code>MultiOutTransfrom.exe source stylesheet result</code>
/// </remarks>
public class MultiOutTransform {
static void Main(string[] args) {
try {
XPathDocument doc = new XPathDocument(args[0]);
XslTransform xslt = new XslTransform();
xslt.Load(args[1]);
MultiXmlTextWriter multiWriter =
new MultiXmlTextWriter(args[2], Encoding.UTF8);
multiWriter.Formatting = Formatting.Indented;
xslt.Transform(doc, null, multiWriter);
} catch (Exception e) {
Console.Error.WriteLine("Transformation failed, an error has occured:");
Console.Error.WriteLine(e);
}
}
}
}
您可以将上述 MultiOutTransform
类用作非常原始的 XSLT
命令行实用程序,它仍然支持多个输出。它需要三个命令行参数,即源
XML 文件名、XSLT
样式表文件名和主结果文档文件名。以下示例使用
MultiOutTransform.exe 运行。
示例:发票处理
作为在 XSLT
中使用多个输出文档的简单示例,请设想一个发票处理应用程序,它将发票文档作为输入并为客户生成一个
HTML 确认页面,同时将 SOAP 信息发送到订单处理 Web
服务。此示例的所有文件都可以在“samples\order”目录中找到。
以下为输入发票文档示例:
invoice.xml
<invoice>
<item>Wallabee</item>
<item>Wombat</item>
<item>Wren</item>
</invoice>
以下 XSLT
样式表可以完成此项工作。它将 HTML
确认页面生成为主结果文档,并在不同的目录中生成作为附加结果文档的
SOAP 信息:
invoice-processor.xsl
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common" exclude-result-prefixes="exsl">
<xsl:template match="/">
<!-- Main result document - confirmation -->
<html>
<head>
<title>Thank you for purchasing!</title>
</head>
<body>
<h2>Thank you for purchasing at fabrikam.com!</h2>
</body>
<xsl:apply-templates mode="order"/>
</html>
</xsl:template>
<xsl:template match="invoice" mode="order">
<!-- Additional result document - SOAP message for fabrikam.com
order processing web service -->
<exsl:document href="soap/order.xml" indent="yes">
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns:Order xmlns:ns="urn:fabrikam-com:orders">
<xsl:apply-templates mode="order"/>
</ns:Order>
</soap:Body>
</soap:Envelope>
</exsl:document>
</xsl:template>
<xsl:template match="item" mode="order">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
请使用前面提到的 MultiOutTransform.exe
实用程序(它封装了标准的 XslTransform 和 MultiXmlTextWriter)运行
invoice.xml,此样式表将在“soap”目录(请注意,如果此目录不存在,将由
MultiXmlTextWriter
创建此目录)中生成两个结果文档:confirmation.html
和 order.xml:
MultiOutTransform.exe invoice.xml invoice-processor.xsl confirmation.html
主结果文档:confirmation.html
<html>
<head>
<title>Thank you for purchasing!</title>
</head>
<body>
<h2>Thank you for purchasing at fabrikam.com!</h2>
</body>
</html>
辅助结果文档:order.xml
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns:Order xmlns:ns="urn:fabrikam-com:orders">
<item>Wallabee</item>
<item>Wombat</item>
<item>Wren</item>
</ns:Order>
</soap:Body>
</soap:Envelope>
实际示例:由
C# XML 文档文件生成 MSDN 样式文档
现在将演示如何在实际应用中使用 MultiXmlTextWriter
类。C# 语言有一种非常有用的功能,即可以使用 XML
文档注释记录写入的代码。然后 C#
编译器就可以将代码中的文档注释处理为 XML
文件。我将向您演示如何使用 MultiXmlTextWriter
类由此 XML 文件生成 MSDN 样式的多页 HTML 文档。
XML 文档文件包含所有已标记用于生成以下格式文档的代码的无层次列表:
<doc>
<assembly>
<name>AssemblyName</name>
</assembly>
<members>
<member name="MemberID">
... documentation comments ...
</member>
...
</members>
</doc>
其中 AssemblyName
是程序集名称,MemberID 是由 C#
编译器生成的唯一的
ID 字符串,它用于标识成员。此 ID
字符串包含以下信息:成员类型(类、属性和方法等等)、开始于命名空间根目录的成员的完全限定名称和方法参数(如果成员为方法)。
MSDN 类库文档通常包括:
- 每个命名空间的命名空间摘要页面
- 每个类的类概述页面
- 每个类的类成员页面
- 每个类的构造函数页面
- 每个构造函数的构造函数页面
- 每个方法的方法页面
- 每个属性的属性页面
我准备生成所有这些页面,并为每个命名空间生成附加
CSS 文件、框架集索引页面、命名空间列表页面、“所有类”页面和类列表页面。此示例的所有文件都可以在“samples\xmldoc”目录中找到。
使用 XSLT 样式表 xmldoc.xsl(位于“samples\xmldoc\xslt”目录)由单个
XML
文档文件生成上述所有页面很简单。它将框架集创建为主结果文档,并生成包含样式信息的
CSS 文件(请注意,CSS 格式为文本,而不是 XML,因此应使用
exsl:document 元素的 method="text"
属性生成 CSS 输出文档)。然后,源 XML
文档树将转换为分析了所有 ID
字符串的临时分层树,随之此分层树被处理若干次以生成上面列出的所有专用页面。
以下摘录说明了构造函数页面是如何创建的:
<xsl:template match="constructor" mode="class">
<!-- The namespace path (e.g. Acme/Foo/Bar) -->
<xsl:variable name="ns-path" select="translate(@namespace,'.','/')" />
<!-- Constructor page -->
<exsl:document HREF="{$ns-path}/{@class}-ctor{position()}.asp"
indent="yes">
<!-- Page title -->
<xsl:variable name="title">
<xsl:value-of select="@class" /> Constructor (<xsl:for-each
select="params/param">
<xsl:value-of select="@name" />
<xsl:if test="position() != last()">, </xsl:if>
</xsl:for-each>)</xsl:variable>
<html>
<head>
<!-- Script to set frameset title onload -->
<script type="text/javascript">
function asd(){
parent.document.title="<xsl:value-of
select="$title" />";
}
</script>
<title>
<xsl:value-of select="$title" />
</title>
<link rel="stylesheet" type="text/css">
<!-- Relative path to the CSS file -->
<xsl:attribute name="href"><xsl:call-template
name="root-path-gen"><xsl:with-param name="path" select="$ns-path"
/></xsl:call-template>xmldoc.css</xsl:attribute>
</link>
</head>
<body topmargin="0" onload="asd()">
<!-- Page header -->
<xsl:call-template name="header-gen">
<xsl:with-param name="text" select="$title" />
</xsl:call-template>
<div id="nstext" valign="bottom">
<p>
<xsl:apply-templates select="summary" />
</p>
<!-- Constructor parameters -->
<xsl:if test="param">
<h4 class="dtH4">Parameters</h4>
<dl>
<xsl:for-each select="param">
<dt>
<i>
<xsl:value-of select="@name" />
</i>
</dt>
<dd>
<xsl:apply-templates />
</dd>
</xsl:for-each>
</dl>
</xsl:if>
<!-- Exceptions -->
<xsl:if test="exception">
<h4 class="dtH4">Exceptions</h4>
<div class="tablediv">
<table cellspacing="0" class="dtTABLE">
<tr valign="top">
<th width="50%">Exception Type</th>
<th width="50%">Condition</th>
</tr>
<xsl:for-each select="exception">
<tr valign="top">
<td width="50%">
<xsl:value-of select="@name" />
</td>
<td width="50%">
<xsl:apply-templates />
</td>
</tr>
</xsl:for-each>
</table>
</div>
</xsl:if>
<xsl:apply-templates select="remarks" />
<xsl:apply-templates select="example" />
<h4 class="dtH4">See Also</h4>
<p>
<a HREF="{@class}.asp">
<xsl:value-of select="@class" /> Class</a> |
<a HREF="{@class}-members.asp">
<xsl:value-of select="@class" /> Members</a> |
<a HREF="namespace-summary.asp">
<xsl:value-of select="@namespace" />
Namespace</a>
</p>
<xsl:call-template name="footer-gen" />
</div>
</body>
</html>
</exsl:document>
</xsl:template>
如您所见,结果可能有点太多。例如,从单个
MultiXmlTextWriter.xml 文档文件(由 C# 编译器从 MultiXmlTextWriter
源生成),xmldoc.xsl
样式表可以生成完整的 MSDN 样式文档,其中包括 52
个 HTML 页面和一个 CSS 文件。所有这些只需运行一次
XSL 转换即可完成:
MultiOutTransform.exe MultiXmlTextWriter.xml xslt\xmldoc.xsl doc\index.html
以下为结果 HTML
框架集页面,它的右侧框中加载了 MultiXmlTextWriter
构造函数页面:
图 4. 生成的 MSDN
样式文档(带有 MultiXmlTextWriter 构造函数页面)
附加的字符串
当然,演示的方法有一些缺点,我必须说明一下。对我而言,速度、低内存占用和简单性非常重要,因此我决定自定义
XmlTextWriter
类来进行重定向工作,但是这要无条件地假设 XSL
转换始终在 XML
中进行,所以实际上没有办法生成真正的 HTML(不是
XHTML)结果文档。更具体地说,主结果文档始终是
XML,但辅助结果文档可以作为 XML
或文本写入,这取决于相应的 exsl:document
元素的方法属性值。
此外,忽略了
xsl:output
元素。它只影响主结果文档的输出,正如前面所说,xsl:output
元素不影响辅助结果文档的输出。它完全由 exsl:document
元素控制。但是,使用从 XmlTextWriter 类继承的 MultiXmlTextWriter
属性,特别是编码和缩进,您可以对主结果文档的输出进行某些控制。
对 XmlWriter 执行 XSL
转换时,将始终忽略禁用输出转义功能。
上述限制不是十分严格的,不过即使它们不能满足某些人的要求,以另一种方式实现重定向层可以很容易地对它们进行自定义。
同时,演示的方法还有其他一些优点。首先,它与
XSLT 1.1 和 XSLT 2.0 工作草稿以及其他 XSLT
处理器(通过扩展元素支持多个输出)共享多个输出语义。这有效地说明了基于这种实现的解决方案可以轻松地传送到其他
XSLT 处理器,并且与将来的 XSLT 2.0
模型兼容。此外,即使现在,这种解决方案也可以通过使用由
EXSLT 初始定义的 exsl:document
元素受益,即与所有 XSLT 处理器兼容,支持
exsl:document 扩展元素。
感谢
首先,感谢 Dare Obasanjo
促成并审阅了本文,尤其是生成 MSDN
样式文档的重大构想。发票处理示例受到 Kurt
Cagle 的 Web 日志的启发。
感谢 Michael Kay
对我的小型历史调查的帮助,以及他在 XSLT
技术开发方面的不间断的工作。还要感谢 Jeni Tennison、Uche
Ogbuji 以及其他 EXSLT 计划支持人员对 XSLT
语言的标准化和文档扩展所作的努力。
|