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上,代碼不多,還有待重構,有興趣的同仁可以和我討論,希望能實現一個健壯的引擎。