ASP.NET MVC3开始使用Razor作为其视图引擎,取代了原来ASP.NET Web Form引擎。笔者最近研究了一下MVC3对Razor的实现,从中找到一个切入点,能够让我们自定义基于Razor语法的视图解析引擎。在项目里面可以用于诸如邮件模板定制等方面。目前,只是一个demo版本,还在进一步完善中。CodePlex : http://codeof.codeplex.com/SourceControl/list/changesets 其中的RazorEx
先来看看效果:
假设有一个模板文件Action1.cshtml如下:
@{ string str = "Hello world!"; } <html> <head>@TemplateData["Title"]</head> <body> <h1>@str</h1> <table> @foreach (var s in TemplateData["Students"] as IEnumerable<RazorLab.Student>) { <tr><td>@s.ID</td><td>@s.Name</td></tr> } </table> </body> </html>
编写C#代码如下:
public class TestController : TemplateController { public ActionResult Action1() { TemplateData["Title"] = "Hello"; TemplateData["Students"] = new List<Student> { new Student{ID = 0 ,Name = "Parker Zhou"}, new Student{ID = 1 ,Name = "Sue Kuang"} }; return Template(@"D:\Project\C#\MyMvc\RazorLab\Template\Test\Action1.cshtml"); } }
最终得到的Html如下:
<html> <head>Hello</head> <body> <h1>Hello world!</h1> <table> <tr><td>0</td><td>Parker Zhou</td></tr> <tr><td>1</td><td>Sue Kuang</td></tr> </table> </body> </html>
我设计了一个类似MVC的模式,使用户可以通过Controller向View中传递数据,利用Razor解析模板,并填入数据。
原理其实很简单,类似ASP.NET的做法,把模板读入后解析成类,再和静态的基类一起动态编译成dll,反射其中的代码,最后输出Html。在这个过程中,反射自然不用多说,关键是如何解析和动态编译,这篇我将介绍如何利用微软的源码来完成解析。由于我自己代码还没有完善,还在单元测试阶段,所以先不发上来献丑了。
System.Web.Razor
在MVC3的源码中,在这里要关注的是System.Web.Razor这个dll。
它用C#的方式实现了Razor的解析并能生成对应的编译单元。所谓编译单元是.NET中的一个类CodeCompileUnit,这个类以CodeDom的方式保存了源码结构,可以被用于产生代码,或者动态编译。
System.Web.Razor.RazorTemplateEngine
在这个Project下最重要的类是System.Web.Razor.RazorTemplateEngine,这也是我们能够直接利用的类。其中GenerateCode方法能将读入的模板解析成编译单元,它有多个重载。下面是Action1.cshtml经过解析后生成的类。其中类名,基类名,名字空间,引用的名字空间等是可以自定义的:
namespace @__TemplatePage.Namespace { using RazorTemplateEngine; using System.Collections.Generic; public class @__TemplateInherit : @__TemplatePage { #line hidden public @__TemplateInherit() { } public override void Execute() { string str = "Hello world!"; WriteLiteral("\r\n<html>\r\n<head>"); Write(TemplateData["Title"]); WriteLiteral("</head>\r\n<body>\r\n\t<h1>"); Write(str); WriteLiteral("</h1>\r\n <table>\r\n"); foreach (var s in TemplateData["Students"] as IEnumerable<RazorLab.Student>) { WriteLiteral(" <tr><td>"); Write(s.ID); WriteLiteral("</td><td>"); Write(s.Name); WriteLiteral("</td></tr>\r\n"); } WriteLiteral(" </table>\r\n</body>\r\n</html>"); } } }
生成的C#代码实际上十分容易理解。上述C#代码可以通过CSharpCodeProvider从CodeCompileUnit得到。(顺便提一下,CSharpCodeProvider只能从CodeCompileUnit得到Code,但反过来没有实现!我查了不少资料都没有,有兴趣要结合NRefactory实现一下)可以想象,我们要做的就是实现一个它的基类@__TemplatePage ,实现其中的TemplateData,WriteLiteral,Write,Execute等,使得在之后的编译中顺利编译成功。下面是我对基类的实现:
using System; using System.Collections.Generic; using System.Text; namespace RazorTemplateEngine { /// <summary> /// This is the base class which the dynamic generated class will inherit from, /// and the TemplatePageRazorHost define the class name, see TemplatePageRazorHost.DefaultBaseClass /// for more infomation /// </summary> public class __TemplatePage { /// <summary> /// Store the parse result /// </summary> private StringBuilder resultBuilder = new StringBuilder(); /// <summary> /// Store the data passed from controller /// </summary> private Dictionary<string, object> templateData = new Dictionary<string, object>(); public StringBuilder ParseResult { get { return resultBuilder; } } public Dictionary<string, object> TemplateData { get { return templateData; } set { templateData = value; } } /// <summary> /// override by the dymanic generated class, the method name is defined in /// GeneratedClassContext.DefaultExecuteMethodName in System.Web.Razor /// </summary> public virtual void Execute() { } /// <summary> /// implement method in the dymanic generated class , the method name is defined in /// GeneratedClassContext.DefaultWriteLiteralMethodName in System.Web.Razor /// </summary> /// <param name="literal"></param> public virtual void WriteLiteral(string literal) { resultBuilder.Append(literal); } /// <summary> /// implement method in the dymanic generated class , the method name is defined in /// GeneratedClassContext.DefaultWriteMethodName in System.Web.Razor /// </summary> /// <param name="obj"></param> public virtual void Write(object obj) { resultBuilder.Append(obj.ToString()); } } }
System.Web.Razor.RazorEngineHost
对于RazorTemplateEngine,生成类名,基类名,名字空间,引用的名字空间等都是有默认值,但我们可以改变这种默认设置,通过RazorEngineHost这个类,这个类中的许多属性都是virtual的,可以通过继承的方式override,这些属性可以改变RazorTemplateEngine的行为。因此,我们要做的就是实现一个继承自RazorEngineHost的类,重写其中必要的属性,以实现上述的自定义行为。最后RazorEngineHost的PostProcessGeneratedCode方法将在RazorTemplateEngine.GenerateCode方法返回结果之后,提供一个再次修改CodeDom的机会,比如加一些额外的名字空间引用。
有了上面的理解,我们要做到其实只剩下下面的示例代码了:
实现RazorEngineHost的一个继承:
public class TestRazorEnginHost : RazorEngineHost { public TestRazorEnginHost() : base(new CSharpRazorCodeLanguage()) { } public override string DefaultBaseClass { get { return "PageBase"; } set { base.DefaultBaseClass = value; } } public override string DefaultClassName { get { return "PageInherit"; } set { base.DefaultClassName = value; } } public override void PostProcessGeneratedCode(System.CodeDom.CodeCompileUnit codeCompileUnit, System.CodeDom.CodeNamespace generatedNamespace, System.CodeDom.CodeTypeDeclaration generatedClass, System.CodeDom.CodeMemberMethod executeMethod) { base.PostProcessGeneratedCode(codeCompileUnit, generatedNamespace, generatedClass, executeMethod); generatedNamespace.Imports.Add(new CodeNamespaceImport("RazorLab")); } }
下面的测试代码用于把一个基于Razor语法的模板C:\Test.cshtml变成C#代码:
TestRazorEnginHost host = new TestRazorEnginHost(); System.Web.Razor.RazorTemplateEngine rte = new System.Web.Razor.RazorTemplateEngine(host); FileStream fs = new FileStream(@"C:\Test.cshtml",FileMode.Open); StreamReader sr = new StreamReader(fs); var codeDomWrap = rte.GenerateCode(sr); CSharpCodeProvider provider = new CSharpCodeProvider(); CodeGeneratorOptions options = new CodeGeneratorOptions(); options.BlankLinesBetweenMembers = false; options.IndentString = "\t"; StringWriter sw = new StringWriter(); string code = string.Empty; try { provider.GenerateCodeFromCompileUnit(codeDomWrap.GeneratedCode, sw, options); sw.Flush(); code = sw.GetStringBuilder().ToString(); Debug.WriteLine(code); } catch { } finally { sw.Close(); }
目前,对于模板嵌套,强类型绑定等MVC框架特有支持的功能还没有时间仔细研究。相信如果这个思路投入生产的话,这样的需求应该是会有的。过几天,我把代码放到CodePlex上去,有兴趣的同仁可以联系我,毕竟一个人的力量是有限的。下篇,我将介绍如何动态编译,并把数据填入模板中。