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。

UEW]S]EKMI)SSCFDVHD@V8B

它用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#代码可以通过CSharpCodeProviderCodeCompileUnit得到。(顺便提一下,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的类,重写其中必要的属性,以实现上述的自定义行为。最后RazorEngineHostPostProcessGeneratedCode方法将在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上去,有兴趣的同仁可以联系我,毕竟一个人的力量是有限的。下篇,我将介绍如何动态编译,并把数据填入模板中。