UML软件工程组织

在.Net中扩展Xslt功能之脚本篇
作者:宋华    本文选自:赛迪网  2003年03月04日
为什么要扩展Xslt样式表功能


基于Xml的可扩展样式表语言Xsl及其转换Xslt实现了将Xml源文件从一种格式到另一种格式的转换。通过在xsl文件中定义一系列遵守规则的模板,匹配这些模板的Xml文档部分就能够被转换为可显示的Html页面文件、符合其他需要的Xml文件或除此之外其他结构的文件。当在Xslt文档中这样做时,没有明显的流程控制,也没有传统编程意义上的算法,甚至不需要任何程序代码和编程语言。绝大多数时候,这能够很好地完成任务。

任何一门语言都不可能绝对完善,而Xsl毕竟只是一门标记语言,单纯依靠它并不能完成所有任务。如同没有XPath、Xslt时,Xml充其量只能是一些标记的集合,什么也不能做一样。尤其,如果我们所需求的不仅仅是改变文档的显示方式或结构,我们还需要对文档进行更多的控制时,单纯使用Xslt的局限性就暴露出来了。比如,获取用户键入的参数以控制转换输出、连接外部数据源以及引用某种计算的结果等等,这些情况下,单纯使用Xslt这样的描述语言无法提供满意的解决方案。而我们知道,程序代码会很轻易地解决这些问题。

使用嵌入式脚本扩展Xslt样式表功能


Xml是易于扩展的,这也正是其短时间内能被广泛接受的原因之一。而Xslt完全基于xml实现,它应该也是易于扩展的。事实的确如此,W3C推荐标准的Xslt提供了针对不同处理器/解析器的专门的扩展。基本上,不同处理器/解析器可以在Xpath表达式、Xslt模板主体内容或顶级元素中使用扩展函数。而且,同任何程序语言的函数/方法一样,这些扩展函数可以用来完成所要求的子功能,它们也能够“一次定义,多次调用”。更重要地,这些嵌入脚本函数不仅能在xslt标记语言中调用,还能够运用在高级语言中。脚本语言所提供的组功能比纯Xslt所提供的功能更丰富,经常可以用来对文档内容进行更复杂的操作(例如对其应用科学计算或访问外部信息源等),这个特性大大扩展了xslt的功能。

实现时,需要在一个指定的命名空间中限定所创建的扩展函数,并且在特定的Xslt处理器中提供实现。之所以必须使用命名空间,是因为命名空间不仅可以防止不同来源的Xslt名称间的冲突。更重要的是它还将扩展予以标识,这样其他的处理器就可以忽略这个专门针对特定处理器的实现,并依靠这个命名空间在Xslt其他部分引用扩展函数。

MSXML XSLT处理器使用 <msxsl:script> 元素及其属性implements-prefix实现并扩展函数以提供脚本级支持。微软特别指定的命名空间允许MSXML XSLT处理器在样式表内定位脚本代码。 <msxsl:script> 元素的language属性告诉运行期脚本使用哪个解释程序,而implements-prefix属性值是扩展函数的前缀,这个前缀xslt文档的其他地方被声明。定义这些属性之后,Xslt处理器就能够使用这些信息调用解释器运行时脚本(如C#编译器)并在 <msxsl:script> 元素中实现扩展函数的代码编写。在样式表的其他地方,依靠 <xsl:value-of> 元素提供输出的模板通过seletct属性与扩展函数相关联,比如,执行并输出扩展函数的计算结果。

<msxsl:script> 元素定义如下:


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


其中:

language属性表示语言选项,它与html页面上script元素的language属性极为类似,其值提供函数/方法定义所用的脚本语言。不过,language属性不是必须的,但如果指定,则它的值必须是下列语言之一:C#、VB、JScript、JavaScript、VisualBasic或CSharp。如果未指定,则默认语言为JScript。与其他Xslt元素及属性严格区分大小写不同的是,这里的语言名称不区分大小写,因此,"JavaScript"和"javascript"是等效的。

implements-prefix属性是强制的。用于指定命名空间前缀并将其与脚本块关联。属性的值表示命名空间前缀。前缀所代表的命名空间必须在样式表中的某个位置定义。

同其他任何Xml元素标记一样, <msxsl:script> 元素中的"msxsl"表示命名空间前缀。不过,名字"msxsl"并不重要,也就是说,并不一定非是"msxsl"不可,你一样可以命名为其他的字符串,比如:


xmlns:myxsl="urn:schemas-microsoft-com:xslt" 
xmlns:myns="urn:schemas-microsoft-com:xslt"


这样,命名空间前缀变为了"myxsl"或者"myns",相应地,元素 <msxsl:script> 就将改为 <myxsl:script>或<myns:script> 。这种改变不会对结果有什么影响,如果你在你的xslt文档所有部分保持一致的话。(在本文中,所有出现 <script> 元素的地方使用了msxsl,是为了与微软默认设置保持一致)

不知你是否已经注意到,上面的语句中,不管命名空间前缀怎样改变,其值却始终是urn:schemas-microsoft-com:xslt。这是必须的。要正确扩展xslt样式表功能,就必须指定这个命名空间。命名空间urn:schemas-microsoft-com:xslt符合W3C推荐标准,并在微软MSXML3.0以上处理器中提供支持。使用它不仅能在xslt中进行嵌入脚本编程,还能对其他扩展功能提供支持。比如,在xsl中使用node-set()函数。

通常,在 <xsl:stylesheet>元素内定义<msxsl:script>元素,然后在<msxsl:script> 元素内定义扩展函数,还可以在 <msxsl:script> 中实例化COM对象,这将大大扩展纯Xslt的功能。(不过,用户的安全设置或许会阻止你的脚本实例化客户端对象)

扩展函数包含在 <msmsl:script> 元素所定义的脚本块中。样式表可包含多个脚本块,各脚本块的操作相互独立。也就是说,如果在脚本块的内部执行,则无法调用在其他脚本块中定义的函数,除非该脚本块声明为具有同一命名空间和同一脚本语言。由于每个脚本块都可以使用自己的语言,因此脚本块的分析将遵照该语言分析器的语法规则进行。使用的语法对于所用的语言而言必须是正确的。例如,如果使用的是 C# 脚本块,则在该块中使用 XML 注释节点 <!-- an XML comment --> 是错误的,应该使用C#注释符"//"或者"/*…*/"。

下面的例子建立一个附带名字空间前缀"mycompany"的脚本块,脚本中包含一个"sum"的函数,该函数接受两个double类型参数,并计算它们的和,然后转换成字符串输出。随后,xsl元素 <xsl:value-of> 的select属性调用这个函数并输出结果。


<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                xmlns:mycompany="http://mycompany.com/mynamespace"
                version="1.0">
  <msxsl:script language="c#" implements-prefix="mycompany">
    private string sum(double a,double b) {
      return (a+b).ToString();
    }
  </msxsl:script>
  <xsl:template match="/">
    <xsl:value-of select="mycompany:sum(5.0,10.0)"/>
  </xsl:template>
</xsl:stylesheet>


为演示效果,在 <xsl:value-of select="mycompany:sum(5,10)" /> 中调用扩展函数sum时,硬性编码了两个double值5.0和10.0,实际中,参数取值应该来自于xml源文档或者通过自定义参数传入。

鉴于xml元素内容的特殊要求,即它不能直接包含"、'、<、>、&等字符以及所有非显示字符,而给定语言的运算符、标识符或分隔符恰好可能包含这些字符,它们有可能被错误地解释为XML。而将所有这些字符完全替换成对应的实体(标准实体和字符实体)引用将降低xml文件的可读性。所以,在使用 msxsl:script 元素时,强烈建议无论使用何种语言,都将脚本放置在 CDATA 节内。例如,下面的 XML 显示放置代码的 CDATA 节的模板。


<msxsl:script implements-prefix='myScript' language='c#'>
    <![CDATA[
    <!--在这里放置代码-->
    ]]>
</msxsl:script>


例如,下面的示例显示如何在脚本中使用逻辑 AND 运算符。


<msxsl:script implements-prefix='myScript' language='c#'>
    public string book(string abc, string xyz)
    {  if ((abc== abc)&&(abc== xyz)) return bar+xyz;
        else return null;
    }
</msxsl:script>


由于"&"符没有转义,因此这将引发异常。将文档作为 XML 加载,并且不对 msxsl:script 元素标记之间的文本运用任何特殊处理。

.Net中使用带扩展功能的Xslt样式表


.Net中的Xslt转换对象提供了对在Xslt文档中用脚本扩展元素来嵌入脚本语言的支持。XslTransform类专门为此提供实现。它包含的方法使用 Xslt样式表转换 Xml 数据为指定的输出。其中,用于载入Xslt转换文档的XslTransform..Load()方法接受包含嵌入脚本的样式表,实际执行转换的XslTransform.Transform()方法使用这个样式表将Xml源文档转换为指定的输出。在这里,源Xsl文档是否包含嵌入脚本并不影响程序代码的编写,你不需要为此额外添加代码。也就是说,在高级语言中编写程序代码时,包含嵌入脚本的Xsl文档与不包含嵌入脚本的Xsl文档使用一样的转换语句。

XslTransform类支持使用 <msxsl:script> 元素的嵌入脚本撰写。加载样式表时,任何已定义的函数均通过包装在类定义中被编译为Microsoft中间语言(MSIL),因此不会有任何性能损失。

综合上面的知识,下面使用Visual Studio.Net SDK创建了一个控制台应用程序UseXsltScript,该程序调用包含嵌入脚本的trans.xsl转换文档,对data.xml源文档执行转换,并将结果输出到一个新的newxml.xml文件。该例中的Xslt嵌入脚本用来计算已知三角形底边及该边对应高度的情况下,计算此三角形的面积。


using System;
using System.Xml;
using System.Xml.Xsl;
using System.IO;
namespace MyCompanyUri.UseXsltScript
{
	class Xslt
	{
		private const string xmlfile = "../../data.xml";//xml源文档
		private const string xslfile = "../../trans.xsl";//xsl转换文档
		private const string newxml="../../outxml.xml";//转换后的xml文档
		public static void Main() 
		{
			XmlDocument doc=new XmlDocument();//实例化xml文档对象
			doc.Load(xmlfile);//加载xml文档
			XslTransform xslt = new XslTransform();	
//实例化XsltTransform转换类对象
			xslt.Load(xslfile);//加载xslt转换样式表
			FileStream myStream=new 
FileStream(newxml,FileMode.OpenOrCreate);//以流方式打开文件
			XmlTextWriter writer = new 
XmlTextWriter(myStream,null);//实例化xml编写器并提供控制台输出
			writer.Formatting = Formatting.Indented;//设置输出的缩进格式
			xslt.Transform(doc, null, writer);//转换xml文档并输出
			writer.Close();
		}
	}
}


xml源文件data.xml:


<?xml version="1.0" encoding="utf-8" ?>
<triangles>
	<triangle>
		<size>
			<width>2</width>
			<height>4</height>
		</size>
	</triangle>
	<triangle>
		<size>
			<width>13</width>
			<height>8</height>
		</size>
	</triangle>
</triangles>


trans.xsl文件:


<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:myscript="urn:myns">
	<msxsl:script language="C#" implements-prefix="myscript">
     <![CDATA[
     public double triangleArea(double width,double height){
       double area = width*height/2;//面积公式:(底*高)/2
       return area;//返回三角形面积
     }
      ]]>
   </msxsl:script>
	<xsl:template match="//triangles">
		<triangles>
			<xsl:for-each select="triangle/size"><!--选定size节点-->
				<triangle>
					<xsl:copy-of select="../size" 
/><!--复制size节点 -->
					<area>
						<xsl:value-of 
select="myscript:triangleArea(width,height)" /><!--调用函数-->
					</area>
				</triangle>
			</xsl:for-each>
		</triangles>
	</xsl:template>
</xsl:stylesheet>


执行这个控制台程序,在应用程序目录的相应地方将生成outxml.xml文件。它将包含给定三角形的底边、高度尺寸及对应的面积。

另外,运行这个应用程序的时候你能够明显感觉到程序产生了短暂的延迟。这是因为在装入样式表处理嵌入式脚本之前,必须先装入c#编译器并运行该嵌入式c#脚本,这将花费2秒左右的时间。

需要注意的是:Xslt嵌入脚本中扩展函数的参数及函数返回值必须是 W3C XPath 类型之一,这些类型并不与.Net所支持的类型完全一致(关于这些W3C Xpath类型和.Net类型的对应情况请参见MSDN)。如果脚本函数使用其他的类型,或者,如果函数在样式表加载到 XslTransform 对象中时不进行编译,则将引发异常。

 

 



版权所有:UML软件工程组织