第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所示。
图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所示。
图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所示。
图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所示。
图5-4 HoTMeaT视图引擎的项目结构
从图中可以看出,专门新建了一个目录“HoTMeaT”,用于存放自定义视图引擎HoTMeaTViewEngine类、自定义视图HoTMeaTView类、BindingHelper类及自定义控件ListView类。被指定输出内容的类TimeHelper位于项目的根目录之下,而其他的结构,与普通ASP.NET 3.5 MVC的项目结构完全一样。
HoTMeaT视图引擎的运行界面,如图5-5所示。
图5-5 HoTMeaT视图引擎的运行界面
5.3 思考与提高
本章实现了一个自定义的视图引擎,说明ASP.NET 3.5 MVC框架具有良好的可扩展性,开发者可以根据自己的特殊需求,实现自己所需要的视图引擎,当然还需要开发自己特定解析规则的视图,书写具有特定含义的视图页面。
请读者仔细分析、阅读HoTMeaT视图引擎项目中的所有代码,编写自己所需要显示的类,然后通过在视图页面中书写<code>…</code>语句块,以便输出该类指定方法中的内容,如数据的呈现等。