ASP.NET MVC3开始使用Razor作为其视图引擎,取代了原来ASP.NET Web Form引擎。笔者最近研究了一下MVC3对Razor的实现,从中找到一个切入点,能够让我们自定义基于Razor语法的视图解析引擎。在项目里面可以用于诸如邮件模板定制等方面。目前,只是一个demo版本,还在进一步完善中。CodePlex : http://codeof.codeplex.com/SourceControl/list/changesets 其中的RazorEx
目前支持的功能:
1.支持Razor语法(基本的@语法)的模板文件解析
2.支持Layout / Renderbody语法
3.支持类似asp.net 动态编译机制,在程序运行期间,如果模板文件变了,无需重新编译
4.支持名字空间引用配置
5.支持复杂的程序集引用关系
在利用Razor在ASP.NET MVC中的实现,自定义视图引擎框架(1)中,介绍了如何利用微软实现的System.Web.Razor来解析基于Razor语法的模板,最后得到一个编译单元或者源码。本文介绍如何在代码中对编译单元或者源码进行动态编译,并执行。
应该没有比动态编译更灵活的了,它允许我们动态的创建程序代码并编译执行。尽管它灵活,但是实现复杂,并且效率不高,不到万不得已不要考虑。而在这个case中却不得不用这种方式,因为模板是用户创建的,我们永远不可能预知:
在.net中,System.CodeDom.Compiler.CodeDomProvider提供了将一个或多个源程序或编译单元编译成程序集的方法。在.net4.0中Microsoft.CSharp.CSharpCodeProvider继承了上面这个类,并给予了实现。有了CSharpCodeProvider,编译一个动态的程序集十分容易:
CSharpCodeProvider provider = new CSharpCodeProvider(); CompilerParameters c_options = new CompilerParameters(); c_options.IncludeDebugInformation = false; c_options.GenerateExecutable = false; c_options.GenerateInMemory = true; c_options.ReferencedAssemblies.Add( "System.dll" ); CompilerResults results = provider.CompileAssemblyFromSource(c_options, code);
上面的代码段,实例化了一个CSharpCodeProvider以及一个CompilerParameters。前者用于编译,后者用于指定编译时的一些选项,如上面的代码的设置。最后调用CompileAssemblyFromSource,传入编译选项和源代码文本即可。另外CSharpCodeProvider的CompileAssemblyFromDom重载,可以接受编译选项对象和
CodeCompileUnit对象作为参数。在上一篇中,我们知道RazorTemplateEngine.GenerateCode方法返回的刚好是包含了CodeCompileUnit的对象,所以我们将使用CompileAssemblyFromDom方法。
在返回值CompilerResults.CompiledAssembly中,我们可以访问到编译结果的Assembly对象,再结合反射即可执行编译代码。另外,在编译过程中,如果编译失败将抛出异常。
在引擎的开发过程中,除了上一篇和上述需要知道的基本内容外,分别有以下问题需要解决:
1、动态编译时需要知道引用哪些dll,否则将无法编译成功。比如在模板里面我引用了一个复杂对象,这个对象定义显然不在引擎的程序集中,可能是用户自己的程序集,或是用户程序集引用的程序集。这就带来了一个问题,在编译时我们如何知道要引用哪些程序集?我用了一个比较笨的方案:从GetCallingAssembly开始,把相互依赖的程序集遍历一遍,并且全部在编译时引用。代码中的AssemblyReferenceResolver就实现了这个功能;
2、如上面的问题,编译的时候,源码需要有正确的命名空间的引用,否则即使引用的程序集,还是不能编译成功。为此,模仿mvc的实现,设计了一个configuration,添加下面这样的配置文件即可:
<configuration> <configSections> <section name="RazorTemplateEngineImportNamespace" type="RazorTemplateEngine.ImportNamespaceResolver,RazorTemplateEngine"/> </configSections> <RazorTemplateEngineImportNamespace> <add namespace="ModelTest"/> <add namespace="System.Collections.Generic"/> </RazorTemplateEngineImportNamespace> </configuration>
3、一个模板对应一个类,也就对应一个程序集,如果反复解析模板会反复编译,这样会很大程度上影响效率。解决方案是使用缓存:将编译过的dll和模板文件存成字典,如果已经编译过了并且模板文件的最后更新时间不晚于dll的创建时间,则直接返回之前编译的程序集;否则就进行编译。代码中的DynamicAssemblyCache类就实现这个功能;
4、如何实现模板嵌套。在基类__TemplatePage中加入下面属性和方法:
private string _layout; public virtual string Layout { get { return _layout; } set { _layout = value; } } public string ChildBody { get; set; } public virtual string RenderBody() { if(ChildBody != null) return ChildBody; return string.Empty; }
这样类似Layout=”” @RenderBody的语法就可以通过编译。配合递归的Execute即可实现。
项目现已实现基本的功能,我打算过一段试用期过后Release一个版本。源码在上面的CodePlex上,代码不多,还有待重构,有兴趣的同仁可以和我讨论,希望能实现一个健壮的引擎。