第5章 自定义视图引擎

ASP.NET 3.5 MVC框架具有良好的可扩展性,开发者可以根据自己的需要,开发自定义的视图引擎,或者使用其他的视图引擎组件。

本章实现了一个自定义的视图引擎,除需要创建自定义视图引擎类之外,还需要开发自己特定解析规则的视图类,书写具有特定含义的视图页面,最后在全局应用程序类中注册该自定义视图引擎。

本章要点:

● 正则表达式概述

● 自定义视图引擎

5.1 正则表达式概述

5.1.1 正则表达式

所谓正则表达式(Regular Expression),就是用一个“字符串”来描述一个模式,然后去验证另一个“字符串”是否符合这个模式。正则表达式可以用来验证字符串是否符合指定模式,比如验证是否是合法的邮件地址;可以用来查找字符串,从一个长的文本中查找符合指定特征的字符串,比查找固定字符串更加灵活方便;还可以用来替换指定的文本,比普通的替换功能更强大。

在实现自己的视图引擎任务中,正则表达式主要用来解析视图页面中被设置的数据源、数据对象,以及数据对象的相关属性,然后将读取的数据对象相关属性的值,在视图指定位置替换原有的属性设置,实现数据对象在视图中的呈现。

在C#中的正则表达式,位于命名空间System.Text.RegularExpressions之中,主要由8个类和一个枚举组成,它们分别是Capture类、CaptureCollection类、Group类、GroupCollection类、Match类、MatchCollection类、Regex类和RegexCompilationInfo类,枚举是RegexOptions,这些类的UML类图如图5-1所示。

active=true

图5-1 正则表达式各类的UML类图

从图中可以看出,Capture类、Group类和Match类这3个类之间的关系是逐步继承的关系,它们都有所对应的集合类CaptureCollection类、GroupCollection类和MatchCollection类。

在正则表达式应用中,通常使用Regex类的构造函数来创建字符串模式,使用Regex类的Match()方法来解析指定的字符串是否匹配该模式,可以通过按组名分组读取匹配的字符,或者通过Regex类的Replace ()方法替换匹配的字符。

5.1.2 语法规则

正则表达式拥有一套自己的语法规则,这些语法主要包括:字符匹配、重复匹配、字符定位、转义匹配和其他字符分组、字符替换和字符决策等。

对于以下定义的正则表达式:

        "^\{(? <Source>Binding—ViewData)\s+(? <Property>\w+)\}$"

上述正则表达式是一个字符分组的正则表达式,包括两个字符组,它们分别是“Source”和“Property”, “Source”组的值必须设置为“Binding”或者“ViewData”, “\s”表示这两个字符组中间可以有任意多的空白符;“\w+”表示“Property”组的值必须是英文字母,不能包括数字。

如果对于如下字符串,使用上述的正则表达式检查匹配:

        <listView source="{ViewData People}">

匹配的结果是,“Source”组的值是“ViewData”, “Property”组的值为“People”。

对于以下定义的正则表达式:

        "\{Binding\s+(? <Property>\w+)\}"

设置了匹配的字符必须包括“Binding”,然后利用字符分组“Property”组,获得属性的变量。

如果对于如下字符串,使用上述的正则表达式检查匹配:

        {Binding LastName}, {Binding FirstName}

匹配的结果是,“Property”组的值依次为“LastName”和“FirstName”。

5.2 自定义视图引擎

5.2.1 创建自己的视图引擎

要创建自己的视图引擎,可以通过继承IViewEngine接口,实现其中的3个方法,它们分别是FindPartialView()方法、FindView()方法和ReleaseView()方法;还可以通过继承抽象类VirtualPathProviderViewEngine,覆盖其中的CreateView()方法和CreatePartialView()方法。

视图引擎HoTMeaTViewEngine类的UML类图如图5-2所示。

active=true

图5-2 HoTMeaTViewEngine类的UML类图

从图中可以看出,HoTMeaTViewEngine类继承于VirtualPathProviderViewEngine抽象类,通过覆盖该抽象类中的CreateView()方法和CreatePartialView()方法,以便创建自定义的视图。

通过继承VirtualPathProviderViewEngine抽象类而创建自定义视图,可以使用抽象类中已经实现的视图寻找、视图定位等方法,简化自定义视图的开发。

HoTMeaTViewEngine类的实现代码,见代码清单5-1。

代码清单5-1 HoTMeaTViewEngine类的实现代码

          1: public class HoTMeaTViewEngine : VirtualPathProviderViewEngine
          2: {
          3:  public HoTMeaTViewEngine()
          4:  {
          5:    base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.html" };
          6:
          7:    base.PartialViewLocationFormats = base.ViewLocationFormats;
          8:  }
          9:
         10:  protected override IView CreateView(ControllerContext
                        controllerContext, string viewPath, string masterPath)
         11:  {
         12:    return new HoTMeaTView(viewPath, masterPath ? ? "");
         13:  }
         14:
         15:  protected override IView CreatePartialView(ControllerContext
                        controllerContext, string partialPath)
         16:  {
         17:    return new HoTMeaTView(partialPath, "");
         18:  }
         19: }

在上述代码中,第3行到第8行定义了HoTMeaTViewEngine类的构造函数,第5行设置视图的存放位置和文件格式,第7行设置用户控件的存放位置和文件格式。

第10行到第13行实现了CreateView()方法,返回的是一个自定义的视图类型HoTMeaTView;第15行到第19行实现了CreatePartialView()方法,返回的同样是一个自定义的视图类型HoTMeaTView。

5.2.2 创建自己的视图

自定义视图HoTMeaTView类,主要实现接口IView中的Render()方法,以便在浏览器中输出指定格式的内容。

HoTMeaTView类的UML类图如图5-3所示。

active=true

图5-3 HoTMeaTView类的UML类图

在HoTMeaTView类中,设置了3个基本属性,它们分别是CurrentViewContext、MasterPath和ViewPath。

在HoTMeaTView类的构造函数中,主要实现初始化两个基本属性:MasterPath和ViewPath。

为实现Render()方法,其中定义了一个嵌套类SettingsDictionary,设置了5个私有的方法,它们分别是EvaluateCode()方法、EvaluateControl()方法、GetBoundValue()方法、GetConvertedValues()方法及GetSettings()方法。

在嵌套类SettingsDictionary中,定义了两个字典类型的属性:Attributes属性和Parameters属性。在EvaluateCode()方法中,主要实现解析指定类的静态类方法,或者实例化的方法。在EvaluateControl()方法中,主要实现解析指定控件的浏览器输出内容。在GetBoundValue()方法中,主要实现解析数据源的名称和类型;在GetConvertedValues()方法中,实现类型的转换;而在GetSettings()方法中,主要解析类或者控件的属性、参数等。

HoTMeaTView类的实现代码,见代码清单5-2。

代码清单5-2 HoTMeaTView类的实现代码

          1: public class HoTMeaTView : Iview
          2: {
          3:  static Dictionary<string, string>
                    cachedFiles = new Dictionary<string, string>();
          4:
          5:  static Regex codeParser = new Regex(@"(\<code(\s+
              (? <AttributeName>\w+)\s*=\s*""(? <AttributeValue>[^>""]*)"")*\s*/\>)
              — (\<code(\s+
              (? <AttributeName>\w+)\s*=\s*""(? <AttributeValue>[^>""]*)"")*\s*\>\s*
              (\s*\<(? <ParameterName>\w+)\>(? <ParameterValue>[\s\S]*? )\</\k
              <ParameterName>\>\s*)*\s*\</code\>)",
              RegexOptions.Compiled — RegexOptions.IgnoreCase —
              RegexOptions.IgnorePatternWhitespace);
          6:
          7:  static Regex controlsParser = new Regex(@"\<(? <Control>(listView))(\s+
            (? <AttributeName>\w+)\s*=\s*""(? <AttributeValue>[^>""]*)"")*\s*\>\s*
            (\s*\<(? <ParameterName>\w+)\>(? <ParameterValue>[\s\S]*? )\</\k
            <ParameterName>\>\s*)*\s*\</\k<Control>\>",
            RegexOptions.Compiled — RegexOptions.IgnoreCase —
            RegexOptions.IgnorePatternWhitespace);
          8:
          9:  public HoTMeaTView(string viewPath, string masterPath)
          10:  {
          11:    this.ViewPath = viewPath;
          12:    this.MasterPath = masterPath;
          13:  }
          14:
          15:  public ViewContext CurrentViewContext { get; private set; }
          16:
          17:  public string ViewPath { get; private set; }
          18:
          19:  public string MasterPath { get; private set; }
          20:
          21:  public void Render(ViewContext viewContext, TextWriter writer)
          22:  {
          23:    this.CurrentViewContext = viewContext;
          24:
          25:    if (cachedFiles.ContainsKey(this.ViewPath) == false)
          26:    {
          27:     cachedFiles.Add(this.ViewPath, File.ReadAllText(
                viewContext.HttpContext.Server.MapPath(this.ViewPath)));
          28:    }
          29:
          30:    string sourceCode = cachedFiles[this.ViewPath];
          31:
          32:    sourceCode = controlsParser.Replace(sourceCode,
                          delegate(Match currentMatch)
          {
            SettingsDicitonary settings = this.GetSettings(currentMatch);
            return this.EvaluateControl(
                      currentMatch.Groups["Control"].Value.ToLower(),
                      settings.Attributes, settings.Parameters,
                      currentMatch.Value);
              });
          33:
          34:    sourceCode = codeParser.Replace(sourceCode,
                              delegate(Match currentMatch)
              {
                SettingsDicitonary settings = this.GetSettings(currentMatch);
                return this.EvaluateCode(settings.Attributes["class"],
                        settings.Attributes["method"], settings.Parameters,
                        currentMatch.Value);
                });
          35:
          36:    writer.Write(sourceCode);
          37:  }
          38:
          39:  private string EvaluateCode(string className, string methodName,
                Dictionary<string, string> parameters, string originalSource)
          40:  {
          41:    Type targetType = Type.GetType(className, false, false);
          42:
          43:    if (targetType == null)
          44:    {
          45:     return originalSource;
          46:    }
          47:
          48:    MethodInfo targetMethod = targetType.GetMethods(BindingFlags.Public
                                        — BindingFlags.Static)
                  .Where(method => method.Name == methodName)
                  .Where(method => method.GetParameters()
                  .Count(parameter => parameters.Keys.Contains(parameter.Name))
                          == parameters.Count).FirstOrDefault();
          49:
          50:    if (targetMethod ! = null)
          51:    {
          52:     return string.Format("{0}", targetMethod.Invoke(null,
                this.GetConvertedValues(parameters, targetMethod.GetParameters())));
          53:    }
          54:
          55:    targetMethod = targetType.GetMethods(BindingFlags.Public
                                — BindingFlags.Instance)
                        .Where(method => method.Name == methodName)
                        .Where(method => method.GetParameters()
                        .Count(parameter => parameters.Keys.Contains(
                        parameter.Name)) == parameters.Count).FirstOrDefault();
          56:
          57:    if (targetMethod ! = null)
          58:    {
          59:     return string.Format("{0}", targetMethod.Invoke(Activator.
                    CreateInstance(targetType), this.GetConvertedValues(
              parameters, targetMethod.GetParameters())));
          60:    }
          61:    return originalSource;
          62:  }
          63:
          64:  private string EvaluateControl(string controlName,
          Dictionary<string, string> attributes,
          Dictionary<string, string> parameters, string originalSource)
          65:  {
          66:    switch (controlName)
          67:    {
          68:     case "listview":
          69:     {
          70:      return ListView.Render((IEnumerable)this.GetBoundValue(
               attributes["source"]), parameters["itemTemplate"]);
          71:     }
          72:    }
          73:    return originalSource;
          74:  }
          75:
          76:  private object GetBoundValue(string bindingString)
          77:  {
          78:    Binding binding = BindingHelper.Parse(bindingString);
          79:
          80:    if (binding ! = null)
          81:    {
          82:     switch (binding.Source.ToLower())
          83:     {
          84:      case "request":
          85:      {
          86:        return this.CurrentViewContext.HttpContext.
                    Request[binding.Property];
          87:      }
          88:      case "routedata":
          89:      {
          90:        return this.CurrentViewContext.RouteData.
                    Values[binding.Property];
          91:      }
          92:      case "viewdata":
          93:      {
          94:        return this.CurrentViewContext.ViewData[binding.Property];
          95:      }
          96:     }
          97:    }
          98:    return null;
          99:  }
          100:
          101:  private object[] GetConvertedValues(Dictionary<string, string>
                                    parameters, ParameterInfo[] parameterInfos)
          102:  {
          103:    object[] result = new object[parameterInfos.Length];
          104:
          105:    for (int i = 0; i < parameterInfos.Length; i++)
          106:    {
          107:     string currentValue = parameters.First(param => param.Key ==
                                            parameterInfos[i].Name).Value;
          108:
          109:     object boundValue = this.GetBoundValue(currentValue);
          110:
          111:     if (boundValue ! = null)
          112:     {
          113:      result[i] = boundValue;
          114:     }
          115:     else
          116:     {
          117:      result[i] = Convert.ChangeType(currentValue,
                                          parameterInfos[i].ParameterType);
          118:     }
          119:    }
          120:    return result;
          121:  }
          122:
          123:  private SettingsDicitonary GetSettings(Match currentMatch)
          124:  {
          125:    SettingsDicitonary result = new SettingsDicitonary();
          126:
          127:    Group attributeNames = currentMatch.Groups["AttributeName"];
          128:    Group attributeValues = currentMatch.Groups["AttributeValue"];
          129:    for (int i = 0, length = attributeNames.Captures.Count;
                        i < length; i++)
          130:    {
          131:     result.Attributes.Add(attributeNames.Captures[i].Value,
                                          attributeValues.Captures[i].Value);
          132:    }
          133:
          134:    Group parameterNames = currentMatch.Groups["ParameterName"];
          135:    Group parameterValues = currentMatch.Groups["ParameterValue"];
          136:
          137:    for (int i = 0, length = parameterNames.Captures.Count;
                        i < length; i++)
          138:    {
          139:     result.Parameters.Add(parameterNames.Captures[i].Value,
                                          parameterValues.Captures[i].Value);
          140:    }
          141:    return result;
          142:  }
          143:
          144:  class SettingsDicitonary
          145:  {
          146:    public readonly Dictionary<string, string>
                            Attributes = new Dictionary<string, string>();
          147:
          148:    public readonly Dictionary<string, string>
                            Parameters = new Dictionary<string, string>();
          149:  }
          150: }

在上述代码中,第5行定义了一个正则表达式codeParser,主要定义了<code>…</code>语句块的设置方式,以便解析被设置的属性名称、属性值或者属性名称、属性值,以及参数名称、参数值;第7行定义了一个正则表达式controlsParser,定义了在视图页面中使用ListView控件的设置方式,以便解析该控件的属性名称、属性值,以及参数名称、参数值。

第15行、第17行和第19行,分别设置了属性CurrentViewContext、ViewPath和MasterPath的读写器;在第9行到第13行的构造函数中,主要实现初始化两个基本属性,它们分别是MasterPath和ViewPath。

第21行到第37行,实现Render()方法,在浏览器中输出指定的内容。第25行到第29行,主要实现输出内容的缓冲;第32行解析视图页面中的用户控件,替换匹配的字符串;第34行解析视图页面中的类别设置;最后通过第36行实现解析后内容的输出。

为实现上述的相关内容的解析,在HoTMeaTView类中定义了一个嵌套类SettingsDictionary,并设置了有关的5个私有方法。

第39行到第62行,实现了EvaluateCode()方法,用于解析指定类的静态类方法(第48行到第53行),或者实例化的方法(第55行到第60行)。

第64行到第74行,实现了EvaluateControl()方法,用于解析指定ListView控件的浏览器输出内容;如果需要设置其他的用户控件,可以在此添加相关的解析代码。

第76行到第99行,实现了GetBoundValue()方法,主要用于解析数据源的名称和类型,除了可以在视图页面中设置“ViewData”外,还可以设置“Request”和“RouteData”。

第101行到第121行,实现了GetConvertedValues()方法,主要用于类型的转换。

第123行到第142行,实现了GetSettings()方法,主要用于解析类或者控件的属性、参数等。

第144行到第149行,定义了嵌套类SettingsDictionary,设置了两个字典类型的属性,它们分别是Attributes属性和Parameters属性。

5.2.3 创建其他类

在实现自己的视图引擎过程中,还创建了一个用户控件ListView类及一个解析数据源绑定的BindingHelper类。

1.ListView类

ListView控件的实现代码,见代码清单5-3。

代码清单5-3 ListView控件的实现代码

          1: public static class ListView
          2: {
          3:  public static string Render(IEnumerable source, string itemTemplate)
          4:  {
          5:    StringBuilder resultBuilder = new StringBuilder();
          6:
          7:    foreach (object item in source)
          8:    {
          9:     resultBuilder.Append(BindingHelper.PerformBinding(itemTemplate,
                                                      item));
         10:    }
         11:    return resultBuilder.ToString();
         12:  }
         13: }

在上述代码中,定义了一个Render()方法,用于输出ListView控件所设置的内容,通过调用帮助类BindingHelper的静态方法PerformBinding(),实现指定数据源和数据对象内容的输出。

2.BindingHelper类

BindingHelper类主要解析绑定数据源中参数,以及被绑定数据对象的各个属性。BindingHelper类的实现代码,见代码清单5-4。

代码清单5-4 BindingHelper类的实现代码

          1: public class Binding
          2: {
          3:  public string Source { get; set; }
          4:
          5:  public string Property { get; set; }
          6: }
          7:
          8: public static class BindingHelper
          9: {
         10:  private static Regex bindingParser = new Regex(
            @"^\{(? <Source>Binding—ViewData)\s+(? <Property>\w+)\}$",
            RegexOptions.Compiled — RegexOptions.IgnoreCase —
            RegexOptions.IgnorePatternWhitespace);
         11:
         12:  private static Regex propertyBindingParser = new Regex(
            @"\{Binding\s+(? <Property>\w+)\}", RegexOptions.Compiled —
            RegexOptions.IgnoreCase — RegexOptions.IgnorePatternWhitespace);
         13:
         14:  public static Binding Parse(string bindingString)
         15:  {
         16:    Match bindingMatch = bindingParser.Match(bindingString);
         17:
         18:    if (bindingMatch.Success)
         19:    {
         20:     return new Binding
                       {
                           Source = bindingMatch.Groups["Source"].Value,
                           Property = bindingMatch.Groups["Property"].Value,
                       };
         21:    }
         22:    else
         23:    {
         24:     return null;
         25:    }
         26:  }
         27:
         28:  public static string PerformBinding(string formatString, object item)
         29:  {
         30:    Type itemType = item.GetType();
         31:
         32:    string returnString=propertyBindingParser.Replace(formatString,
                                 delegate(Match binding)
                   {
                     string property=binding.Groups["Property"].Value;
                     PropertyInfo info=itemType.GetProperty(property);
                 string myString=string.Format("{0}", info.GetValue(item, null));
                     return myString;
                 });
         33:
         34:    return returnString;
         35:  }
         36: }

在上述代码中,第10行定义了一个正则表达式bindingParser,主要定义了数据源的设置方式,以便解析数据源的名称,被绑定的数据对象;第12行定义了一个正则表达式property BindingParser,定义了数据对象中各个属性的绑定格式,以便解析被绑定数据对象的各个属性。

第16行通过正则表达式bindingParser的Match()方法,解析视图页面中的数据源配置字符串;第20行通过提取匹配字符串中的分组“Source”和“Property”,分别提取到解析后的数据源和数据对象。

第32行通过正则表达式propertyBindingParser的Peplace()方法和传入委托的方法,得到解析后的数据对象中的各个被绑定的属性。

5.2.4 配置全局应用程序类

全局应用程序类Global.asax.cs中,需要注册新的个性化视图引擎,具体实现代码见代码清单5-5。

代码清单5-5全局应用程序类的配置代码

          1: protected void Application_Start()
          2: {
          3:   ViewEngines.Engines.Clear();
          4:   ViewEngines.Engines.Add(new HoTMeaTViewEngine());
          5: }

在上述代码中,通过调用静态类ViewEngines中的Engines属性,得到视图引擎的集合类ViewEngineCollection,第3行首先清空集合类,第4行在集合类ViewEngineCollection(ViewEnginesEngines为集合类ViewEngineCollection)中添加自己创建的视图引擎HoTMea TViewEngine,实现视图引擎的注册。

5.2.5 自定义视图引擎的运行

要运行自定义视图引擎,首先需要设置视图页面,然后添加相关指定输出内容的类。

1.视图页面的设置

视图页面也就是HTML页面,需要根据自定义视图所规定的语法规则,设置符合语法规则的数据源、数据对象及各个属性,设置<code>…</code>语句块,以便输出相关类的指定方法。

视图页面Index.html的实现代码,见代码清单5-6。

代码清单5-6视图页面Index.html的实现代码

          1: <html>
          2: <head>
          3:  <title>Singing Eels - "HoT MeaT" View Engine! </title>
          4: </head>
          5: <body>
          6: <h1>Singing Eels - "HoT MeaT" View Engine! </h1>
          7: <div class="sample">
          8: <p>The time on the server is:<span style="color: #f00; ">
          9:
         10:  <code class="Eels.TimeHelper" method="GetCurrentTime" />
         11: </span></p>
         12: </div>
         13: <div class="sample">
         14: <p>The time on the server (formatted) is:<span style="color: #f00; ">
         15:
         16:  <code class="Eels.TimeHelper" method="GetCurrentTime">
         17:    <formatString>dddd, MMMM dd, yyyy</formatString>
         18:  </code>
         19: </span></p>
         20: </div>
         21: <div class="sample">
         22: <p>Check out some data-binding in HoTMeaT:</p>
         23: <ul>
         24:  <listView source="{ViewData People}">
         25:    <itemTemplate>
         26:     <li>{Binding LastName}, {Binding FirstName}</li>
         27:    </itemTemplate>
         28:  </listView>
         29: </ul>
         30: </div>
         31: </body>
         32:</html>

在上述代码中,第10行设置了一个<code>…</code>语句块,调用的类为“Eels.TimeHelper”,调用的方法为GetCurrentTime();第16行到第18行同样也设置了一个<code>…</code>语句块,调用的类别仍然是“Eels.TimeHelper”,调用的方法为GetCurrentTime(),不过该方法中需要输入字符串参数,以便设置日期的显示格式。

第24行到第28行设置了一个自定义的listView控件,设置了数据源为“ViewData”,数据对象为“People”,而被绑定的数据属性分别为“LastName”和“FirstName”。

2.TimeHelper类

指定输出内容的类为TimeHelper类,TimeHelper类的实现代码见代码清单5-7。

代码清单5-7 TimeHelper类的实现代码

          1: public class TimeHelper
          2: {
          3:  public static DateTime GetCurrentTime()
          4:  {
          5:    return DateTime.Now;
          6:  }
          7:
          8:  public static string GetCurrentTime(string formatString)
          9:  {
         10:    return DateTime.Now.ToString(formatString);
         11:  }
         12: }

在上述代码中,定义了两个重载的方法,实现日期内容的输出。第3行到第6行的Get CurrentTime()方法输出默认格式的日期内容;第8行到第11行的GetCurrentTime()方法则输出指定格式的日期内容。

3.HoTMeaT视图引擎的运行

HoTMeaT视图引擎的项目结构,如图5-4所示。

active=true

图5-4 HoTMeaT视图引擎的项目结构

从图中可以看出,专门新建了一个目录“HoTMeaT”,用于存放自定义视图引擎HoTMeaTViewEngine类、自定义视图HoTMeaTView类、BindingHelper类及自定义控件ListView类。被指定输出内容的类TimeHelper位于项目的根目录之下,而其他的结构,与普通ASP.NET 3.5 MVC的项目结构完全一样。

HoTMeaT视图引擎的运行界面,如图5-5所示。

active=true

图5-5 HoTMeaT视图引擎的运行界面

5.3 思考与提高

本章实现了一个自定义的视图引擎,说明ASP.NET 3.5 MVC框架具有良好的可扩展性,开发者可以根据自己的特殊需求,实现自己所需要的视图引擎,当然还需要开发自己特定解析规则的视图,书写具有特定含义的视图页面。

请读者仔细分析、阅读HoTMeaT视图引擎项目中的所有代码,编写自己所需要显示的类,然后通过在视图页面中书写<code>…</code>语句块,以便输出该类指定方法中的内容,如数据的呈现等。