原文链接
ASP.NET MVC 2 Templates, Part 1: Introduction[翻译]
ASP.NET MVC 2 Templates, Part 2: ModelMetadata[翻译]
模版解析
在讲解内置模版前,我们需要花几分钟理解模版解析的工作原理,这样你将知道怎么正确重写模版。
路径
当解析一个模版时,系统会迭代几个名字,查找一个匹配的模版.每个名字都会请求视图引擎去查找一个命名为"DisplayTemplates/TemplateName"或者 "EditorTemplates/TemplateName"的部分视图,取决于你请求的是显示还是编辑模版.
如果你使用的是WebForms视图引擎,意味着在下面的地址中搜索显示模版:
l ~/Areas/AreaName/Views/ControllerName/DisplayTemplates/TemplateName.aspx & .ascx
l ~/Areas/AreaName/Views/Shared/DisplayTemplates/TemplateName.aspx & .ascx
l ~/Views/ControllerName/DisplayTemplates/TemplateName.aspx & .ascx
l ~/Views/Shared/DisplayTemplates/TemplateName.aspx & .ascx
(显然,搜索编辑模版的话即把DisplayTemplates替换为EditorTemplates)
模版名称
模版名称按以下顺序尝试匹配:
- ModelMetadata的TemplateHint属性
- ModelMetadata的DataTypeName 属性
- 类型的名称(见下面注意)
- 如果对象不是复合对象,那么模版名称为:"String"
- 如果对象是复合对象同时是一个接口,那么模版名称为:"Object"
- 如果对象是复合对象同时不是一个接口,那么就递归类型的继承,尝试每个类型名
当搜索类型名称时,会使用没有命名空间的名称,还有,如果类型是Nullable<T>,我们会搜索T(这样无论你使用"bool"还是"Nullable<bool>",你都能搜索到Boolean模版).这就意味着如果你为值类型编写模版,你需要考虑这个值是不是可空的.你可以使用ModelMetadata 的IsNullableValueType 属性来决定决定这个值是不是可空的.下面我们将会看到内置Boolean模版的一个例子.
TemplateInfo类
在我们实现模版前最后需要讲的是TemplateInfo类. TemplateInfo类可从ViewData中获得,同时,不像model的元数据,它构建于模版内.
TemplateInfo 类最有用的属性是FormattedModelValue,这个值要么是这个正确的字符串格式化model值(基于ModelMetadata的格式化字符串),要么是model的原始值(如果没有指定格式化字符串).
这两个玩意我们也要使用(TemplateDepth属性和Visited方法),用到的话我会解析它们.
内置的显示模版
系统内置类9个显示模版,分别是:"Boolean","Decimal","EmailAddress","HiddenInput","Html","Object","String","Text", 和"Url"."Text"和"String"的实现是相同的.有的有对应的编辑模版,有的没有.ASP.NET MVC的默认模版已在代码中实现,但这里我会用.ascx文件代替它们的功能,来说明它们究竟做了什么(同时是你编写自定义模版的起点).
DisplayTemplates/String.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
代码没有什么惊喜,只是编码和显示model
DisplayTemplates/Html.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
这个更简单,因为"Html"类型告诉我们内容是HTML,所以不应该编码.但是,如果数据是来之终端用户时,标记你的数据为"Html"要非常小心,因为可能会被XSS攻击.
DisplayTemplates/EmailAddress.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <a href="mailto:<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a>
这个模版会假设你的moel是一个emial地址,同时会自动地创建一个mailto: emial的连接.注意它是怎么使用email地址的Model的,然而它用FormattedModelValue来显示,当仍然保留未编辑的emial地址时,好让你替换格式字符串用作显示.
DisplayTemplates/Url.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <a href="<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a>
与上面的EmailAddress相似,它会把你的model作为URL解析同时自动创建一个连接
DisplayTemplates/HiddenInput.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %> <% } %>
当有[HiddenInput]特性时,这个模版就是被使用,如果用户特别请求,它会生成一个显示值,通过HideSurroundingHtml属性判断.
DisplayTemplates/Decimal.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private object FormattedValue { get { if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) { return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model); } return ViewData.TemplateInfo.FormattedModelValue; } } </script> <%= Html.Encode(FormattedValue) %>
这个模版会默认显示2位精度的decimal值,因为大多数用户会使用decimal值表示货币.注意这只会在没有指定格式化字符串时才会这样做(这就是那个嵌入的if语句的目的).
DisplayTemplates/Boolean.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private bool? ModelValue { get { bool? value = null; if (ViewData.Model != null) { value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture); } return value; } } </script> <% if (ViewData.ModelMetadata.IsNullableValueType) { %> <select class="list-box tri-state" disabled="disabled"> <option value="" <%= ModelValue.HasValue ? "" : "selected='selected'" %>>Not Set</option> <option value="true" <%= ModelValue.HasValue && ModelValue.Value ? "selected='selected'" : "" %>> True</option> <option value="false" <%= ModelValue.HasValue && !ModelValue.Value ? "selected='selected'" : "" %>> False</option> </select> <% } else { %> <input class="check-box" disabled="disabled" type="checkbox" <%= ModelValue.Value ? "checked='checked'" : "" %> /> <% } %>
这个Boolean模版是比较有趣,因为可空vs不可空生成的UI是不同的.model是不是可空的取决于ModelMetadata的IsNullableValueType属性.
不可空的布尔值的显示UI是一个不可用的checkbox,是否选中取决于model的值.可空的布尔值的显示UI是一个下拉框,有三个值:"Not Set", "True", 和 "False".
DisplayTemplates/Object.ascx
在看代码前有必要解析一下,因为复合对象模版为你做了很多事情.
这个Object模版主要的职责是显示复合对象的所有属性.并且如果一个值为空的话,它还有责任去显示这个model值的NullDisplayText,同时也有责任确保只显示一层的属性(被称为对象的"shallow dive").下一篇文章,我们会谈及自定义这个模版的方法,包括演示"deep dive"怎么做.
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <% if (Model == null) { %> <%= ViewData.ModelMetadata.NullDisplayText %> <% } else if (ViewData.TemplateInfo.TemplateDepth > 1) { %> <%= ViewData.ModelMetadata.SimpleDisplayText %> <% } else { %> <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %> <% if (prop.HideSurroundingHtml) { %> <%= Html.Display(prop.PropertyName) %> <% } else { %> <% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %> <div class="display-label"> <%= prop.GetDisplayName() %></div> <% } %> <div class="display-field"> <%= Html.Display(prop.PropertyName) %></div> <% } %> <% } %> <% } %>
看这里的源码,
<% if (Model == null) { %> <%= ViewData.ModelMetadata.NullDisplayText %> <% }
它说我只会当model为空时才会显示model的NullDisplayText.
else if (ViewData.TemplateInfo.TemplateDepth > 1) { %> <%= ViewData.ModelMetadata.SimpleDisplayText %> <% }
这个限制只进入复合对象的一层("shallow dive").TemplateInfo类会自动跟踪模版的深度,最高层模版的TemplateDepth属性是1.
else { %> <% foreach (var prop in ViewData.ModelMetadata.Properties .Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %>
这是我们显示对象属性的主要循环,我们会把属性list中那些用户曾说过"我不想显示它"的属性过滤掉.另一个过滤方法是调用TemplateInfo类看看我们是否已经在前面渲染过这个对象.这个可以帮组我们做"deep dive"模版时防止无线递归--可能来自对象的循环引用(例如一个parent/child关系,两个对象都会有指向对方的指针).
<% if (prop.HideSurroundingHtml) { %> <%= Html.Display(prop.PropertyName) %> <% }
如果用户已经要求隐藏surrounding HTML,那么我们只需要显示这个属性本身.我们不需要任何"额外"的东西包围它,例如labels.
<% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %> <div class="display-label"><%= prop.GetDisplayName() %></div> <% } %> <div class="display-field"><%= Html.Display(prop.PropertyName) %></div>
如果显示名不是null或者empty,这里显示属性的显示名,用一个div标签包围.因为显示名称默认是不为空的(因为会有属性名),这意味着用户必须明确地设置为empty才能隐藏显示名(如果你是使用默认的DataAnnotations元数据提供器,那就使用[DisplayName]特性).
内置编辑模版
编辑模版比显示稍微复杂一点,因为它要包含编辑值的能力.它是在已存在的HTML Helpers基础上构建的.有7个内置的编辑模版:"Boolean","Decimal","HiddenInput","MultilineText","Object","Password", 和"String".
这里你会看到一件特别的地方,就是我们会经常向HTML Helpers传递一个空字符串的name.正常来说,这是不合法的,但是在模版这种情况里,我们需要跟踪我们在对象的名称中的位置.这在一个复合属性嵌套一个复合属性时非常有用,因为我们想通过我们的名称得出我们在对象里的层级(例如:"Contact.HomeAddress.City" vs. "Contact.WorkAddress.City").
当你向HTML Helpers传递一个一个name时,你说的事好像是这样:"给我一个textbox来编辑这个对象命名为'City'的属性".但是如果你的模版对象不是地址(复合对象),而是城市(简单的字符串)?向HTML Helper传递一个空字符串就是说"给我一个textbox来编辑我自己".
EditorTemplates/String.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" }) %>
我们又以字符串开始,因为它是最容易理解的模版.它告诉系统我们想要一个textbox(来编辑自己),同时我们希望它由格式化的model值构成,我们公司附加两个CSS类,"text-box"和"single-line".我们在这里使用很多CSS类,你会发现我们在MVC2里提供默认的样式,使得它们看起来稍微好看一些.
EditorTemplates/Password.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.Password("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line password" }) %>
Password模版与String模版相似,除了它调用的是Html.Password和它增加了passwordcss类去渲染控件.
EditorTemplates/MultilineText.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.TextArea("", ViewData.TemplateInfo.FormattedModelValue.ToString(), 0, 0, new { @class = "text-box multi-line" }) %>
Again,没什么惊喜.我们调用TextArea方法,我们传递的row和column size参数都为0(以为我们用CSS设置样式),同时使用"multi-line"CSS类代替"single-line".
EditorTemplates/HiddenInput.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private object ModelValue { get { if (Model is System.Data.Linq.Binary) { return Convert.ToBase64String(((System.Data.Linq.Binary)Model).ToArray()); } if (Model is byte[]) { return Convert.ToBase64String((byte[])Model); } return Model; } } </script> <% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %> <% } %> <%= Html.Hidden("", ModelValue) %>
这个比显示模版复杂得多,因为它要做的事情要多得多.ModelValue属性用于如果model是一个LINQ to SQL二元对象或者是一个byte数组,那么转换model值为Base64编码值,原始的model值为放置在隐藏input里.
EditorTemplates/Decimal.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private object ModelValue { get { if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) { return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model); } return ViewData.TemplateInfo.FormattedModelValue; } } </script> <%= Html.TextBox("", ModelValue, new { @class = "text-box single-line" }) %>
Decimal编辑模版与显示版本非常相似,除了最后是生成textbox值用来编辑值.
EditorTemplates/Boolean.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private List<SelectListItem> TriStateValues { get { return new List<SelectListItem> { new SelectListItem { Text = "Not Set",Value = String.Empty,Selected = !Value.HasValue }, new SelectListItem { Text = "True",Value = "true",Selected = Value.HasValue && Value.Value }, new SelectListItem { Text = "False",Value = "false",Selected = Value.HasValue && !Value.Value }, }; } } private bool? Value { get { bool? value = null; if (ViewData.Model != null) { value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture); } return value; } } </script> <% if (ViewData.ModelMetadata.IsNullableValueType) { %> <%= Html.DropDownList("", TriStateValues, new { @class = "list-box tri-state" })%> <% } else { %> <%= Html.CheckBox("", Value ?? false, new { @class = "check-box" })%> <% } %>
Boolean编辑模版也和显示版本非常相似,除了它使用内置的HTML Helpers来生成DropDownList和CheckBox.
EditorTemplates/Object.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <% if (ViewData.TemplateInfo.TemplateDepth > 1) { %> <%= ViewData.ModelMetadata.SimpleDisplayText%> <% } else { %> <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(pm))) { %> <% if (prop.HideSurroundingHtml) { %> <%= Html.Editor(prop.PropertyName) %> <% } else { %> <% if (!String.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString())) { %> <div class="editor-label"> <%= Html.Label(prop.PropertyName) %></div> <% } %> <div class="editor-field"> <%= Html.Editor(prop.PropertyName) %> <%= Html.ValidationMessage(prop.PropertyName, "*") %> </div> <% } } } %>
Object编辑模版也和显示模版相似,除了增加调用ValidationMessage,这样我们的模版复合对象编辑模版会显示错误星号.
结语
希望这本文章能帮助你理解什么是内置的模版和深入理解每一个模版.你现在应该用这些用户控件代码片段去创建适合你自定义模版.下一边文章,我会讨论几个能改变你的模版工作的方法.