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上去,有兴趣的同仁可以联系我,毕竟一个人的力量是有限的。下篇,我将介绍如何动态编译,并把数据填入模板中。