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上去,有興趣的同仁可以聯絡我,畢竟一個人的力量是有限的。下篇,我將介紹如何動態編譯,並把資料填入模板中。