Xsl模板应用基础(二、程序转换)
前文简要介绍了Xsl语言与转换流程,本文将演示使用C#实现Xml处理器,再回顾一下转换流程:
从上图可知,Xml处理器其实就是一个转换方法,有输入输出,还有过程参数。下面是核心代码片段:
1 using System.Xml; 2 using System.Xml.Xsl; 3 ...... 4 5 /// <summary> 6 /// 使用xsl格式化xml,结果写入到Result参数。 7 /// </summary> 8 /// <param name="Xdom"></param> 9 /// <param name="XslFile"></param> 10 /// <param name="Result">结果写入到此流中</param> 11 public static void ConvertXsl(XDocument Xdom, string XslFile, Dictionary<string, object>Params, Stream Result) 12 { 13 XslCompiledTransform Process = GetXslProcess(XslFile); 14 XsltArgumentList Arguments = InitArgusList(Params); 15 using (StreamWriter Writer = new StreamWriter(Result, Encoding.UTF8)) 16 { 17 Process.Transform(Xdom.CreateReader(), Arguments, Writer); 18 } 19 } 20 21 /// <summary> 22 /// 初始化Xsl转换过程需要的过程参数 23 /// </summary> 24 /// <param name="Argus"></param> 25 /// <returns></returns> 26 private static XsltArgumentList InitArgusList(Dictionary<string, object> Argus) 27 { 28 if (Argus == null) return null; 29 XsltArgumentList Arguments = new XsltArgumentList(); 30 foreach (string key in Argus.Keys) 31 { 32 if (key.StartsWith("urn:")) 33 { 34 Arguments.AddExtensionObject(key, Argus[key]); 35 } 36 else 37 { 38 Arguments.AddParam(key, String.Empty, Argus[key] == null ? String.Empty : Argus[key]); 39 } 40 } 41 return Arguments; 42 } 43 44 /// <summary> 45 /// 获取指定Xsl模板编译后的转换器 46 /// </summary> 47 /// <param name="XslFile"></param> 48 /// <returns></returns> 49 private static XslCompiledTransform GetXslProcess(string XslFile) 50 { 51 string CacheKey = XslFile; 52 if (HttpRuntime.Cache[CacheKey] != null) 53 { 54 return (XslCompiledTransform)HttpRuntime.Cache[CacheKey]; 55 } 56 57 XmlReaderSettings Setting = new XmlReaderSettings(); 58 Setting.ProhibitDtd = false; 59 Setting.XmlResolver = new XmlUrlResolver(); 60 Setting.IgnoreComments = true; 61 Setting.IgnoreWhitespace = true; 62 63 XslCompiledTransform Xct = new XslCompiledTransform(); 64 XsltSettings xs = new XsltSettings(true, true); 65 XmlUrlResolver resolver = new XmlUrlResolver(); 66 resolver.Credentials = System.Net.CredentialCache.DefaultCredentials; 67 68 //最后一次访问缓存后,缓存对象继续存活的时间,超过后自动被回收。 69 TimeSpan MaxAge = TimeSpan.FromMinutes(20); 70 using (FileStream Fs = new FileStream(XslFile, FileMode.Open, FileAccess.Read)) 71 { 72 using (XmlReader Xr = XmlReader.Create(Fs, Setting)) 73 { 74 Xct.Load(Xr, xs, resolver); 75 System.Web.Caching.CacheDependency Dep = new System.Web.Caching.CacheDependency(XslFile); 76 HttpRuntime.Cache.Insert(CacheKey, Xct, Dep, System.Web.Caching.Cache.NoAbsoluteExpiration, MaxAge); 77 } 78 } 79 return Xct; 80 }
上面提供一个公共方法 ConvertXsl() 供调用,第一个传入的参数也可以改造为 XmlDocument 类型的实例,看个人喜好了。
第二个参数是XslFile文件的完整磁盘路径。该参数在私有方法GetXslProcess()中使用,内部使用HttpRuntime.Cache容器缓存Xsl编译后的实例对象,并依赖于Xsl文件的改动(一旦Xsl文件被修改,该缓存自动失效)。由于引入了缓存,使得Xsl的转换变得非常快速高效。网上有些文章批评Xsl转换低效,所举例子大都没有使用缓存机制,每次转换都完整加载一次Xsl,效率自然低下。
第三个参数是转换过程参数。为了简化调用,参数类型设为Dictionary<string, object>。转换参数有两种,一种是值类型,内部通过AddParam(key, value)方法加入,value虽然是object类型,但一般是int、string、NodeSet简单类型。另一种参数是通过AddExtensionObject(key, value)方法加入,key是Uri类型,value可以是类的实例,在Xsl文件中可以调用该实例的成员方法,这一点很酷,相当于在Xsl中调用C#的方法,几乎就可以为所欲为了(查看MSDN中的XsltArgumentList )。上面封装的方法中,对传入第二种参数有一约定,即参数名为urn:开头的参数,将被认作扩展对象加入。
下面是调用方法:
1 public class Student 2 { 3 public string Name; 4 public int Age; 5 public string Say(string Words) 6 { 7 return "我是" + this.Name + ",今年" + this.Age.ToString() + "岁了。我想说:" + Words; 8 } 9 } 10 11 public void Main () { 12 Dictionary<string, object> Params = new Dictionary<string, object>(); 13 Student s = new Student(); 14 s.Name = "摩罗叉"; 15 s.Age = 13; 16 Params.Add("urn:ExtensionFunc", s); 17 Params.Add("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); 18 XDocument Xdom = XDocument.Load(Server.MapPath("student.xml")); 19 string XslFile = Server.MapPath("student.xsl"); 20 ConvertXsl(Xdom, XslFile, Params, Response.OutputStream); 21 }
student.xml 与前文提供的一致,仅去除第二行的处理指令。student.xsl则更新如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <!DOCTYPE xsl:stylesheet [ 3 <!ENTITY nbsp " "> 4 <!ENTITY copy "©"> 5 <!ENTITY reg "®"> 6 <!ENTITY trade "™"> 7 <!ENTITY mdash "—"> 8 <!ENTITY ldquo "“"> 9 <!ENTITY rdquo "”"> 10 <!ENTITY pound "£"> 11 <!ENTITY yen "¥"> 12 <!ENTITY euro "€"> 13 ]> 14 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:student="urn:ExtensionFunc" extension-element-prefixes="student"> 15 <xsl:output method="html"/> 16 <xsl:param name="time" /> 17 <xsl:template match="/"> 18 <xsl:text disable-output-escaping='yes'><!DOCTYPE html></xsl:text> 19 <html> 20 <head> 21 <title>学生清单</title> 22 </head> 23 <body> 24 <table border="1" cellpadding="5" cellspacing="0"> 25 <tr> 26 <th>ID</th> 27 <th>姓名</th> 28 <th>年龄</th> 29 <th>性别</th> 30 </tr> 31 <xsl:for-each select="/root/student"> 32 <tr> 33 <td> 34 <xsl:value-of select="@id"/> 35 </td> 36 <td> 37 <xsl:value-of select="name"/> 38 </td> 39 <td> 40 <xsl:value-of select="age"/> 41 </td> 42 <td> 43 <xsl:choose> 44 <xsl:when test="sex='male'"> 45 <xsl:value-of select="'男'"/> 46 </xsl:when> 47 <xsl:when test="sex='female'"> 48 <xsl:value-of select="'女'"/> 49 </xsl:when> 50 <xsl:otherwise> 51 <xsl:value-of select="'未知'"/> 52 </xsl:otherwise> 53 </xsl:choose> 54 </td> 55 </tr> 56 </xsl:for-each> 57 </table> 58 现在时间:<xsl:value-of select="$time"/><br/> 59 说点什么:<xsl:value-of select="student:Say('尼玛!')"/><br/> 60 学生总数:<xsl:value-of select="student:GetTotal(/root/student)"/><br/> 61 书名:<xsl:value-of select="student:GetBooks()/root/book/@name"/><br/> 62 </body> 63 </html> 64 65 </xsl:template> 66 </xsl:stylesheet>
从C#中的Main()方法可知,转换过程,传入两个参数,第一个是Student类实例,参数名为urn:ExtensionFunc,urn:后面的字符串表示传入对象的命名空间,可以随意定义(如果有多个对象,需保证唯一),另一个参数是当前服务器时间,参数名为time。这两个参数在student.xsl文件中有相应的体现,xsl第16行<xsl:param name="time" />表示接收传入值参数,参数名为time,调用时需使用 $time 才能得到参数值。另一个参数由于是类实例,需要在第14行先声明接收的命名空间,定义前缀(简称),Xsl中声明的命名空间必须与C#中定义的命名空间一致,然后在第59行使用简称调用 student:Say('xxx'),Say方法在 Student 类中定义。这就实现了Xsl的跨界引用,但凡是跨界,都能带来无限的想象空间。
如果在Xsl中调用扩展方法,并且传入当前上下文的节点,如(假如传入的student实例实现了GetTotal方法):
1 <xsl:value-of select="student:GetTotal(/root/student)"/>
那么该方法将得到类型为 System.Xml.XPath.XPathNodeIterator 的参数:
1 public class Student 2 { 3 ...... 4 public string GetTotal(XPathNodeIterator Iterator) 5 { 6 return Iterator.Count.ToString(); 7 } 8 }
扩展方法还能返回XML节点与节点集合,供Xsl使用:
1 public XPathNavigator GetBooks() 2 { 3 XPathNavigator Nav = XDocument.Parse("<root><book name='金刚经'/></root>").CreateNavigator(); 4 Nav.MoveToRoot(); 5 return Nav; 6 }
Xsl调用GetBooks():
1 <xsl:value-of select="student:GetBooks()/root/book/@name"/>
用浏览器访问,得到最终结果:
本文使用C#实现了Xml+Xsl=>Html 的转换过程(下载例子),并且举例转换过程中传入的两种参数的应用。在实际应用过程中,由于页面的构造完全使用Xsl格式化,不再需要使用 System.Web.UI.Page 类,因此推荐在一般处理程序(HttpHandle)中执行Xsl转换,这样可以得到完整的页面HTML代码,或可再做一次服务端缓存,或者做基于HTTP协议的客户端缓存,甚至手工实现Gzip传输,自由度比普通页面程序(aspx)大一些。