XML 文件:使用 JScript、C# 和 Visual Basic.NET 扩展 XSLT

XSL 转换 (XSLT) 因其可使困难的事情变容易,以及使容易的事情变困难而广为人知。它可以简化用其他方式很难实现的复杂转换逻辑。但与此同时,XSLT 的函数编程模型有时会使执行小型业务逻辑变得极其困难。通常,使用传统语言(如 VBScript、JScript® 或 Microsoft® .NET 支持的任何语言)扩展 XSLT 可提供这两个方面的最佳功能(有关 XSLT 的简介,请参阅 2000 年 8 月刊中我与 Don Box 和 John Lam 合著的文章)。

例如,请考虑以下 XML 文档,其中包含一系列要执行的操作:

<equation>
<add>3</add>
<sub>1</sub>
<mul>6</mul>
<add>8</add>
<div>4</div>
</equation>

Let's say that the goal is to evaluate the list of operations, top to bottom, assuming no operator precedence and an initial value of 0. In this case, it's like evaluating the following equation:

((((3 - 1) * 6) + 8) / 4) = 5

此问题可通过多种方法来解决。根据您的背景、过去处理此问题的方法甚至通常使用的语言,您可能会发现某些解决方案非常直观,而另一些则相当晦涩。

JScript 解决方案

您可能已经在脑海里想出了如何结合使用命令性编程语言(如 Visual Basic®、C++ 或 JScript)和 DOM 来解决此问题。使用允许您声明和更新变量的命令性语言可以大大简化任务。图 1 中的 JScript 函数可提取表示 equation 元素子级的节点列表,并计算结果。

对许多人来说,此代码非常直观,这是因为 JScript 允许您声明其值可在每个循环迭代中更新的结果变量。此函数可按如下方式进行调用:

var doc = new ActiveXObject("MSXML2.DOMDocument.4.0");
doc.Load("numbers.xml");
var result = processEquation(doc.selectNodes("/equation/*"));

XSLT 解决方案

现在,假设您使用的是允许声明变量的语言,但是当您为它们赋值后,将无法修改这些值。换句话说,它们实际上不是变量,而是在运行时指定的常量。函数编程语言是一类共享此特性的编程语言。如 Scheme、ML、Haskell、XSLT 及其他几种语言就属于此类别。

因为函数语言是模块化的且十分灵活,所以它们常用于解决极其复杂的问题(AI 就是这样一个示例)。除此之外,函数语言实现可以一种目前采用的主流命令性语言无法使用的方式进行优化。但是,所有这些都是需要一定代价的,因为对于那些不习惯使用函数模型的人来说,学习过程是比较困难的。

例如,以图 2 中的 XSLT 程序为例,它使用另一种算法来提供相同的功能。processEquation 模板可产生与图 1 中所示的 JScript 函数相同的结果。此示例不同于 JScript 版本,因为它不使用 for 循环,并且在处理每个运算之后不更新变量。相反,它使用递归来处理列表,并在每次运算之后依靠调用堆栈来跟踪增量结果。

XSLT + JScript 解决方案

如果您能理解图 2 中的代码,并感觉它与图 1 中的代码同样简单或更简单,则您可能不需要阅读本专栏的其余部分。但是,如果您与大多数开发人员一样,就会认为它不是最直观的编程模型,特别是对于这种简单任务的类型。任何人都不会反对使用 XSLT 来实现复杂的 XML 转换逻辑,但是当这些转换需要某些类型的业务逻辑时,这些实现相对于其价值来说可能会过于复杂。

在这些情况下,结合使用 XSLT(用于基于树的转换逻辑)和另一种编程语言(用于某个业务逻辑)可提供这两个方面的最佳功能。XSLT 1.0 规范综合了使用非 XSLT 代码扩展 XSLT 程序的惯例,这些非 XSLT 代码可以用给定 XSLT 处理器支持的任何编程语言来编写。

图 3 中的 XSLT 程序显示如何将图 1 中的 JScript 函数添加到图 2 中的 XSLT 程序,以替换 processEquation 模板。这种方法不仅可以简化 XSLT 代码,而且在许多情况下,还能够实现其他方法无法实现的某种功能。例如,如果您需要执行 XSLT 语言本身不支持的某项数学运算,则必须使用自定义扩展(我将在后面介绍这样的示例)。如果您使用 XSLT 来执行复杂的转换,则通常会遇到需要自定义扩展的情况。

这种方法的缺点是,扩展后的功能将只适用于支持所用语言的处理器。如果您的 XSLT 不需要处理器之间的可移植性,则不必关心这个问题。如果它们需要处理器之间的可移植性,则您必须克服困难并确定如何用一般的 XSLT 代码来实现该功能,或者,如果这不可行,则必须为需要支持的每个处理器提供其他实现方法。

.NET 和 MSXML XSLT 处理器均提供一个简单机制,用于添加以其他语言编写的代码,这可以在 XSLT 文档中直接实现,或通过链接到现有的 COM 对象或 .NET 程序集来实现。在本专栏的其余部分中,我将讨论用 Microsoft 实现来扩展 XSLT 程序的细节。

XSLT 1.0 扩展机制

XSLT 1.0 规范定义了两种类型的扩展:扩展元素和扩展函数。这两种类型的扩展均在标准语言的基础上提供了附加功能,并可以像其他任何 XSLT 1.0 元素(包括 xsl:transform、xsl:template、xsl:value-of 等)或 XPath 1.0/XSLT 1.0 函数(如 string、substring-before、sum、document 等)那样使用。

由于 XSLT 1.0 具有基于模板的模型,因此 XSLT 处理器需要用其他额外的信息来正确地区分静态内容元素和代表其他附加行为的扩展元素。例如,请考虑以下 XSLT 转换:

<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:out="http://www.w3.org/1999/xhtml"
xmlns:ext="http://example.org/extension"
>
<xsl:template match="/">
<out:html>
<ext:doSomeFunkyMagic/>
•••
</out:html>
</xsl:template>
</xsl:transform>

在此例中,处理器将与 http://www.w3.org/1999/XSL/Transform 命名空间关联的元素(transform 和 template)识别为语言特定的指令,将其他元素(html 和 doSomeFunkyMagic)识别为实例化模板时应输出的内容。

现在,假设 doSomeFunkyMagic 元素被某类处理器指定为扩展元素。为了让处理器能够找出此元素,它需要知道哪个命名空间包含正在使用的扩展元素。这可通过可在 stylesheet/transform 元素上使用的 extension-element-prefixes 属性来完成,如下所示:

<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:out="http://www.w3.org/1999/xhtml"
xmlns:ext="http://example.org/extension"
extension-element-prefixes="ext"
>
<xsl:template match="/">
<out:html>
<ext:doSomeFunkyMagic/>
</out:html>
</xsl:template>
</xsl:transform>

现在,当处理器执行转换时,它可以确定 doSomeFunkyMagic 代表扩展元素,而不仅仅是一般的输出。

这不是扩展函数的问题,因为,正如我刚才阐释的那样,它们不会与输出内容相混淆。这意味着,如果您仅使用扩展函数,则无需使用 extension-element-prefixes 元素,但仍需要用扩展命名空间来限定函数名:

<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:out="http://www.w3.org/1999/xhtml"
xmlns:ext="http://example.org/extension"
>
<xsl:template match="/">
<out:html>
<xsl:value-of select="ext:doSomeFunkyMagic()"/>
</out:html>
</xsl:template>
</xsl:transform>

所有的内置 XPath 1.0 和 XSLT 1.0 函数名均未限定,这意味着它们始终被序列化为 NCName 并假设不来自任何命名空间。因此,如果处理器遇到带前缀的函数名,会自动假设它是扩展函数。

XSLT 1.0 规范还提供一个允许转换向处理器查询扩展支持的机制。特别是,它定义了元素可用和函数可用的函数,以便在尝试使用执行处理器之前确定它是否确实支持扩展。将这些函数与 if 或 choose 元素结合使用,可以编写能利用某些扩展的 XSLT 转换,而又不会完全牺牲可移植性,如图 4 所示。

XSLT 1.0 还提供一个 fallback 元素,并将它用作提供备份功能的较明确替换方法。将后备功能放在 fallback 元素内部之后,您便可以在可能不受支持的任何扩展元素内部使用该功能。图 5 显示被移植以使用 fallback 元素的上一个示例。

尽管规范为使用处理器特定的扩展提供了支持,但并未定义实现它们的标准机制。此机制因处理器而异,但现代的大多数实现目前都提供这种支持(未来的 XSLT 规范中可能会提供一个标准方法)。让我们看一下此机制在 Microsoft MSXML 4.0 和 .NET XSLT 实现中是如何工作的。

msxsl:script

当前的 Microsoft XSLT 处理器可以直接在 XSLT 文档中或在带外扩展对象内部实现扩展。本专栏的简介中已经包含了第一种方法的一些示例。正如示例所阐释的那样,您可以通过 Microsoft urn:schemas-microsoft-com:xslt 命名空间的 script 元素(从现在开始,我将称之为 msxsl:script 元素),将扩展代码嵌入 XSLT 文档中。msxsl:script 元素本身就是一个扩展元素,因此您可以像上一节中讨论的那样测试它是否受到支持。

下面列出在 MSXML 4.0 和 .NET 中,您对 msxsl:script 所必须使用的语法:

<msxsl:script
language = "language-name"
implements-prefix = "prefix of user's namespace">
</msxsl:script>

language 属性指定在脚本标记中使用何种语言,而 implements-prefix 属性控制所包含函数位于哪个命名空间。您必须使用限定名在 XPath 表达式中调用这些用户定义的函数中的一个。

在 MSXML 和 .NET 实现中,可以使用的语言会有所不同。在 MSXML(从 2.0 版开始)中,您可以使用 VBScript 或 JavaScript。但是,在 .NET 中,您可以像从前那样使用 .NET 支持的任何语言,包括 C#、Visual Basic、JScript.NET 甚至 JScript。使用强类型语言(如 C# 和 Visual Basic .NET)的能力使之更加具有吸引力。

请回过去看一下图 3,该图显示了 msxsl:script 与 JScript 的结合使用。请注意,processEquation 与 urn:the-xml-files:xslt 命名空间相关联,urn:the-xml-files:xslt 命名空间绑定到 transform 元素的命名空间声明中的 usr 前缀。无论何时使用函数,其名称都必须带有前缀 usr。在那个示例中,函数是在 value-of select 表达式中被调用的,如下所示:

<xsl:value-of select="usr:processEquation(/equation/*)"/>

由于这两种 Microsoft XSLT 实现都支持 JScript,因此图 3 中所示的文档可用于 MSXML 或 .NET 版本。

使用这两种 Microsoft 实现,您可以通过在单个 XSLT 文档中使用多个 msxsl:script 元素,以不同的语言来编写扩展函数(有关示例,请参见图 6)。此操作唯一的问题是,每个 msxsl:script 元素都必须在 implements-prefix 属性中指定一个不同的命名空间。您不能在多个 msxsl:script 元素中为同一个命名空间编写扩展代码。在这两种实现中,有关 msxsl:script 的其余细节会有所不同。

在 XSLT 和 MSXML 之间映射类型

图 6 中的代码显示沿两个方向传递的信息。对象作为参数传递到函数中,并作为返回值从每个函数中返回。您必须了解 XPath 类型系统如何映射到 JScript 和 VBScript 类型系统,以便正确地设计自己的扩展函数。

图 7 中的表格描述每个 XPath 类型在用于函数参数时,在 JScript 和 VBScript 中映射的类型。前三个类型是非常直接的。XPath 中的字符串、数字和布尔值直接映射到 JScript 和 VBScriptin 中的字符串、数字和布尔值。作为一个示例,图 6 中使用的函数将 JScript 和 VBScript 函数中的字符串返回到 XSLT value-of 调用表达式。

最后两种类型将需要更多的解释。XPath 节点集会映射到实现 DOM NodeList 接口 (IXMLDOMNodeList) 的 JScript 和 VBScript 对象。IXMLDOMNodeList 在本质上只是一般 DOM 节点的集合接口,它支持基本的遍历和长度查询。

让我们看一个如何在扩展函数中使用 IXMLDOMNodeList 的示例。假设您需要在以下 XML 文档中计算两点之间的距离:

<line>
<point>
<x>10</x>
<y>10</y>
</point>
<point>
<x>20</x>
<y>20</y>
</point>
</line>

这无法通过标准的 XSLT 来实现,但是借助于扩展函数和更高级的数学库,此任务会变得易如反掌。图 8 中显示的扩展函数会得到一个代表两点集合的 IXMLDOMNodeList 对象。它可使用内置的 JScript Math 对象来计算两点之间的距离:

然后,可以按如下方式调用此扩展函数:

distance: <xsl:value-of
select="math:CalcDistance(/line/point)"/>

XSLT 结果树片段也会映射到 IXMLDOMNodeList 对象,但是它们只包含 NODE_DOCUMENT_FRAGMENT 类型的一个节点。换句话说,您必须定位到 IXMLDOMDocumentFragment 对象的子级才能执行实际的处理。图 9 中的 XSLT 阐释如何处理传递到函数的结果树片段。

这个版本的扩展函数会收到一个结果树片段。结果树片段是通过 XSLT 变量或 param 元素生成的。以下 XSLT 片段阐释了如何生成结果树片段,以及如何调用此新版本的扩展函数:

•••
<xsl:variable name="points">
<point>
<x>10</x>
<y>10</y>
</point>
<point>
<x>20</x>
<y>20</y>
</point>
</xsl:variable>
<xsl:value-of select="math:CalcDistance($points)"/>
•••

列在图 7 中列出的类型是可用在函数参数中的唯一类型。由于 JScript 和 VBScript 不是强类型的,您必须明确知道在调用函数时传递的对象类型。请考虑以下扩展函数:

<xsl:value-of
select="usr:doSomething(string(./author/name),
number(./price), .)"/>

如果函数将分别得到 string、number 和 IXMLDOMNodeList 类型的参数,您必须在调用方法时明确强制它们,如下所示:

<xsl:value-of
select="usr:doSomething(string(./author/name),
number(./price), .)"/>

在本例中,Xpath 的 string 和 number 函数用于将前两个节点集明确强制为预期类型。最后一个参数不需要强制,因为节点集会自动映射到 IXMLDOMNodeList 对象。

对于 MSXML 中的函数返回值会有更多的限制。从函数返回的类型只能是字符串和数字(请参见图 10)。如果返回的是数字,它会被强制为 JScript 和 VBScript Number。如果返回的是其他基元类型,它会被强制为 JScript/VBScript String。尝试返回对象会引发异常。

例如,由于以下扩展函数返回的是数字,所以是可接受的。

<ms:script language="VBScript" implements-prefix="vb">
<![CDATA[
function AuthorsByState(state)
Set doc = CreateObject("MSXML2.DOMDocument.4.0")
doc.Load "authors.xml"
// returns the number of authors for given state
AuthorsByState = doc.selectNodes("//author").length
end function
]]>
</ms:script>

但是,以下稍作修改的版本尝试向调用方返回一个 IXMLDOMNodeList 对象,因此会引发异常:

<ms:script language="VBScript" implements-prefix="vb">
<![CDATA[
function AuthorsByState(state)
Set doc = CreateObject("MSXML2.DOMDocument.4.0")
doc.Load "authors.xml"
// returns the authors collection
FindAuthorsByState = doc.selectNodes("//author")
end function
]]>
</ms:script>

您可以在扩展函数内部实例化和调用对象,但不能将它们返回给调用方。

在 XSLT 和 .NET 之间映射类型

构建 .NET 扩展函数的机制类似我刚才讨论的 MSXML 的机制。但是,.NET 实现由于以下几个原因而比 MSXML 更高级。首先,您有更多的语言可以选择,包括强类型语言,如 C# 和 Visual Basic .NET。其次,扩展函数可以是强类型函数,这可让处理器在调用函数时代替您处理某些强制。最后,对象(string 和 number 除外)可用作返回值。

图 11 描述了 XPath 和 .NET 类型系统之间的映射。此映射应用于两个方向:函数参数和返回值。前三个类型映射到 .NET System.String、System.Boolean 和 System.Double 类型,而节点集和结果树片段分别映射到 XPathNavigator 和 XPathNodeIterator(有关这些类的详细信息,请参阅 2001 年 9 月的 XML Files 专栏。Int16、UInt16、Int32、UInt32、Int64、UInt64、Single 和 Decimal 类型会自动被强制为 Double,从而映射到 W3C XPath number 类型。如果在函数参数或返回值中使用任何其他类型,会引发异常。这些类型映射也可应用于 XSLT 全局参数。

请考虑以下版本的 CalcDistance 扩展函数,该函数是在 C# 中实现的:

<ms:script language="C#" implements-prefix="math">
<![CDATA[
double CalcDistance(XPathNodeIterator points) {
points.MoveNext();
double xdelta =
(double)points.Current.Evaluate("x - following::x");
double ydelta =
(double)points.Current.Evaluate("y - following::y");
return Math.Sqrt(Math.Pow(xdelta, 2) +
Math.Pow(ydelta, 2));
}
]]>
</ms:script>

请注意,此函数可接受 XPathNodeIterator 参数并返回一个双精度值。此函数可以像在 JScript 中实现的上一个函数那样进行调用:

<xsl:value-of select="math:CalcDistance(/line/point)"/>

此示例还利用了 System.Math 类。但是,请注意,代码中未包含任何使用语句的 C#。.NET 实现会使图 12 中列出的命名空间在 msxsl:script 扩展中自动可用。目前,您只能使用 msxsl:script 扩展中的命名空间类型。如果您需要使用其他类型,则必须依赖 XSLT 扩展对象(我将在以后小节中讨论它们)。

可重用性

在 XSLT 文档中直接嵌入扩展代码很方便,但是如果设计不当,会限制可重用性。要重用 msxsl:script 中定义的自定义函数,一种方法就是通过 XSLT 的 include 元素。您可以在 XSLT 文档中创建只包含扩展函数的全局库。例如,图 13 中的 XSLT 文档只包含一个 msxsl:script 元素,而该元素本身包含了几个函数。

假设此文件的名称为 global-extensions.xsl,下例显示如何将此文件包括在另一个 XSLT 文档中:

<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:include href="global-extensions.xsl"/>
•••
</xsl:transform>

这很像使用 ASP include 元素来重用嵌入到 ASP 页面中的脚本。尽管此方法可行,但实现可重用性的首选方法是 XSLT 扩展对象。

XSLT 扩展对象

MSXML 和 .NET 均支持在编译的组件中实现 XSLT 扩展函数。使用 MSXML 可以调用编译的 COM 组件,而使用 .NET 可以调用编译的程序集。具体细节视您希望使用的实现方式而异。

在 MSXML 实现中,COM 扩展对象支持设置和查询属性以及调用函数。与 msxsl:script 扩展一样,函数和属性中使用的类型必须遵循前面在图 7图 10 中描述的映射规则。

要在 XSLT 文档中使用扩展对象,您需要在执行转换之前将它添加到处理器的上下文中。您可以在 MSXML 中通过 IXSLProcessor 接口公开的 addObject 方法来实现此目的。其语法如下:

objXSLProcessor.addObject(obj, namespaceURI);

addObject 将得到一个与对象关联的命名空间标识符。这等同于使用 msxsl:script 元素的 implements-prefix 属性。将对象添加到处理器的上下文并与所提供的命名空间关联之后,即可以从 XPath 表达式调用它(如图 13 中所示)。

在 XSLT 中,您应通过在属性名称前面加上后跟括号的 get- 或 put- 来访问属性。例如:

get-age(), put-age(33))

让我们来看一个定义和使用扩展对象的完整示例。让我们假设以下 Visual Basic 类定义:

' Class: Person
Public name As String
Public age As Double
Public Function SayHello(ByVal other as String) As String
SayHello = "Hello " & other & ", I'm " & name
End Function

要将一个 Person 对象用作 XSLT 扩展,您必须在执行转换之前将它添加到处理器的上下文中。图 14 阐释了如何使用 MSXML 4.0 和 Visual Basic 来执行此操作。由于在调用 Transform 之前,已经将 p 引用的对象添加到处理器的上下文中,因此只要 Person 的属性名称和函数名称由 urn:person 命名空间限定,XSLT 文档就可以访问 Person 的属性和函数,如下所示:

<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:obj="urn:person"
>
<xsl:output method="text"/>
<xsl:template match="/">
name: <xsl:value-of select="obj:get-name()"/>
age:  <xsl:value-of select="obj:get-age()"/>
greeting: <xsl:value-of select="obj:SayHello('Michi')"/>
</xsl:template>
</xsl:transform>

.NET 实现中的模型几乎相同,除了现在需要调用 .NET 对象之外。主要区别在于如何将对象与处理器的上下文相关联。您可以通过 XsltArgumentList 类来完成,如下所示:

XslTransform xslt = new XslTransform();
xslt.Load(stylesheet);
XPathDocument doc = new XPathDocument(filename);
XsltArgumentList xslArg = new XsltArgumentList();
//Add a Person object
Person obj = new Person();
obj.name = "Michi";
obj.age = 6;
xslArg.AddExtensionObject("urn:person", obj);
xslt.Transform(doc, xslArg, Console.Out);

这个用于调用该 .NET 扩展对象的 XSLT 文档看上去与上一个 MSXML 示例中的文档相同。

小结

XSLT 是功能强大的函数编程语言,它可以使困难的任务变容易,也会使容易的任务变困难。在某些情况下,结合使用 XSLT 的函数编程模型和以命令性语言(如 JScript、C# 或 Visual Basic .NET)编写的部分自定义代码,可以简化整个 XSLT 文档。Microsoft 在 MSXML 和 .NET 中的 XSLT 实现均为创建这种类型的混合 XSLT 应用程序提供了优异的支持。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值