ASP.NET MVC3开始使用Razor作为其视图引擎,取代了原来ASP.NET Web Form引擎。笔者最近研究了一下MVC3对Razor的实现,从中找到一个切入点,能够让我们自定义基于Razor语法的视图解析引擎。在项目里面可以用于诸如邮件模板定制等方面。目前,只是一个demo版本,还在进一步完善中。CodePlex : http://codeof.codeplex.com/SourceControl/list/changesets 其中的RazorEx
先来看看效果:
假设有一个模板文件Action1.cshtml如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @{ 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#代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | 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如下:
1 2 3 4 5 6 7 8 9 10 | < 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经过解析后生成的类。其中类名,基类名,名字空间,引用的名字空间等是可以自定义的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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等,使得在之后的编译中顺利编译成功。下面是我对基类的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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的一个继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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#代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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上去,有兴趣的同仁可以联系我,毕竟一个人的力量是有限的。下篇,我将介绍如何动态编译,并把数据填入模板中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义