在前面出现过Html.CheckBoxFox和Html.TextBoxFox等HTML helper方法,这些方法用来指定必要的HTML元素编辑数据。MVC框架还支持另一种方法实现,称为模板化视图helper(辅助)方法,在这些方法里面我们可以指定哪一个模型对象或属性被显示或编辑,并且让MVC框架自己判断应该呈现哪一种类型的HTML元素(是TextBox还是CheckBox)。这一章里面,会介绍这些方法并阐释怎样调优和完全替换model模版系统的部件
1.使用模板化的视图Helpers(Using Templated View Helpers)
模版化视图helpers的创意就是它们更加灵活。我们不用自己去指定应该用什么HTML元素来呈现一个模型的属性,MVC自己会搞定,在我们更新了视图模型时,也不用手动的更新视图。下面是一个例子:
//在Models里面添加Persons.cs using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace ModelTemplates.Models { public partial class Person { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [DataType(DataType.Date)] public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } [AdditionalMetadata("RenderList", "true")] public bool IsApproved { get; set; } [UIHint("Enum")] public Role Role { get; set; } } public class Address { public string Line1 { get; set; } public string Line2 { get; set; } public string City { get; set; } public string PostalCode { get; set; } public string Country { get; set; } } public enum Role { Admin, User, Guest } } //添加一个HomeController如下: namespace ModelTemplates.Controllers { public class HomeController : Controller { public ActionResult Index() { Person myPerson = new Person { PersonId = 1, FirstName = "Joe", LastName = "Smith", BirthDate = DateTime.Parse("1988-1-15"), HomeAddress = new Address { Line1 = "Shudu Avenue", Line2 = "28# Dacishi Road", City = "London", Country = "UK", PostalCode = "WC2R 1SS" }, IsApproved = true, Role = Role.User }; return View(myPerson); } } } //Index视图如下 @model ModelTemplates.Models.Person @{ ViewBag.Title = "Index"; } <h2> Person</h2> <div class="field"> <label> Name:</label> @Html.EditorFor(x => x.FirstName) @Html.EditorFor(x => x.LastName) </div> <div class="field"> <label> Approved:</label> @Html.EditorFor(x => x.IsApproved) </div>
上面显示的可编辑的HTML元素,将Index视图修改成只读的,如下所示:
<div class="field"> <label> Name:</label> @Html.DisplayFor(x => x.FirstName) @Html.DisplayFor(x => x.LastName) </div> <div class="field"> <label> Approved:</label> @Html.DisplayFor(x => x.IsApproved) </div>
运行程序可以看到效果。试想如果大部分MVC程序都具有很多的编辑或显示的数据的部分,那么用这种方式就非常的方便了。下面是MVC模版化HTML helper方法:
Html.Display("FirstName") Html.DisplayFor(x => x.FirstName) Html.Editor("FirstName") Html.EditorFor(x => x.FirstName) Html.Label("FirstName") Html.LabelFor(x => x.FirstName) Html.DisplayText("FirstName") Html.DisplayTextFor(x => x.FirstName)
上面的helper方法是针对单个的model属性,MVC helper方法里面还包含了针对整个model对象生成HTML的方法。这个处理过程称为scaffolding(搭建支架),这些方法如下:
Html.DisplayForModel() Html.EditorForModel() Html.LabelForModel()
修改Index视图代码如下:
@model MVCApp.Models.Person @{ ViewBag.Title = "Index"; } <h4>Person</h4> @Html.EditorForModel()
2.样式化生成的HTML(Styling Generated HTML)
当我们使用模版化的helper方法创建编辑器时,HTML元素里面的class属性的值对设置输出的样式非常有用,如@Html.EditorFor(m => m.BirthDate),生成的HTML元素如下:<input class="text-box single-line" id="BirthDate" name="BirthDate" type="text" value="1988/1/15" />
当然也可以使用@Html.EditorForModel(),利用这些class的属性值,可以非常方便设置生成的HTML的样式。这里的样式表是~/Content/Site.css
3.使用Model元数据(Using Model Metadata)
如果需要将某个属性隐藏或设置为只读,这个时候可以使用Model元数据来定制我们的需求,采取的方式是在model的属性上添加特性(attributes).
使用元素据控制可编辑和可用性(Using Metadata to Control Editing and Visibility)
在我们的Person类里面,PersonId属性是不想被用户看见或者编辑的,这时我们可以使用HiddenInput特性,如下:
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
应用这个特性以后,我们可以运行程序,查看HTML源可以看到如下:
如果我们想从生成的HTML中排除某个属性,可以使用ScaffoldColumn特性,如:
public class Person {
[ ScaffoldColumn(false)]
public int PersonId { get; set; }
...
}
当scaffolding方法遇到ScaffoldColumn特性时,会跳过该属性,也不会创建hidden input元素。ScaffoldColumn特性不会对单个的属性方法产生影响,例如:@Html.EditorFor(m=>m.PersonId)会生成一个对PersonId属性的编辑Html元素,即使它使用了ScaffoldColumn特性。
使用Label元数据(Using Metadata for Labels)
默认情况下,Label,LabelFor,LabelForModel方法使用属性名作为label元素的内容,例如:@Html.LabelFor(m=>m.BirthDate),页面展示为:
<label for="BirthDate">BirthDate</label>,当然很多时候直接展示属性名并不是我们需要的。如果我们想指定显示的内容,可以使用Display特性。如下:
[Display(Name="生日")]
public DateTime BirthDate { get; set; }
页面显示为:<label for="BirthDate">生日</label>
如果我们在BirthDate属性上只设置了Display特性值,显示在界面上的日期是包含了时间的,如果我们不想显示时间部分,可以使用Data Value元数据。如下:
[DataType(DataType.Date)]
[Display(Name="Date of Birth")]
public DateTime BirthDate { get; set; }
这时显示的就只有日期部分了。DataType包含多个枚举值:DateTime,Date,Time,Text,MultilineText,Password,Url,EmailAddress
使用元数据来选择展示模版(Using Metadata to Select a Display Template)
模版是基于正被处理的属性的类型和使用的helper方法类别。我们可以使用UIHint特性来指定对某一个属性呈现的模版。如下:
[UIHint("MultilineText")]
public string FirstName { get; set; }
这是界面展示的FirstName是一个多行文本框。内置的视图模版有很多:
Boolean,Collection,Decimal,EmailAddress,HiddenInput,Html,MultilineText,Object,Password,String,Text,Url
注意:使用UIHint特性时,如果我们选择的模版不能对属性的类型进行操作会抛异常,例如对一个string类型的属性应用了Boolean模版。里面的Object模版也是一个比较特殊的情况,它是被Scaffolding辅助方法(Html.DisplayForModel(),Html.EditorForModel(),Html.LabelForModel())用来生成针对视图模型对象的HTML。这个模版会检查一个对象的所有属性并选择一个最适合属性类型的模版。
对伙伴类使用元数据(Applying Metadata to a Buddy Class)
不会一直都是对整个model类使用元数据,这种情况对自动生成的model类很常用,例如使用ORM工具时。任何对自动生成的model类的更改在下次生成时会被覆盖了。解决这个问题的方案就是将model类创建为partial并且创建第一个partial类来应用元数据。下面将Person类修改为partial:
public partial class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
分部类必须具有相同的名字以及声明在同一个命名空间,当然要用partial关键字。除了元数据的目的之外,还有一个关键的特性是MetadataType,这个特性能够让我们通过将伙伴类的类型作为参数传递给Person,从而将伙伴类跟Person类联系起来。如:
[MetadataType(typeof(PersonMetadataSource))] public partial class Person { } class PersonMetadataSource { [HiddenInput(DisplayValue=false)] public int PersonId { get; set; } [DataType(DataType.Date)] public DateTime BirthDate { get; set; } }
伙伴类仅仅需要包含我们想应用元数据的属性,而不必复制所有的Person类的属性。上面的示例中,使用了DataType特性来确保BirthDate属性正确显示。
应用复杂类型参数(Working with Complex Type Parameters)
模版化的过程依赖Object模版,每一个属性会被检测,但只有一个模版用来呈现代表了属性及其值的HTML元素。我们也注意到了当使用Scaffolding方法(EditorForModel,DisplayForModel)并不是所有的属性都需要呈现。
实际上,HomeAddress属性被忽略了,因为Object模版仅仅对简单类型操作——也就是可以用System.ComponentModel.TypeDescriptor的静态方法GetConverter 从string类型转换的类型,包含C#基本的类型:int,bool和doubl,还包含.NET框架里面常用的类型Guid和DateTime。这种策略导致的结果就是Scaffolding不是递归的或者说是循环的,给定一个对象处理,Scaffolding模版视图helper方法仅生成简单类型的属性并忽略复杂的对象。尽管这可能不是很方便,但却是非常明智的策略。MVC框架不知道我们的模型对象是怎样创建的,并且如果Object模版是递归的,那么我们能够容易地终结了ORM的延迟加载功能,让我们读取并呈现每一个数据库的对象。
如果我们想要为一个复杂的属性呈现HTML,需要显示的指定,如下:
<div class="column">@Html.EditorForModel()</div> <div class="column"> @Html.EditorFor(m => m.HomeAddress) </div>
当我们使用EditorFor方法时,Object模版会被显示的调用,这样所有元数据的约定也得到了尊重。
定制模版化的视图Helper系统(Customizing the Templated View Helper System)
上面展示了如何使用元数据来塑造模版helper呈现数据的方式,在MVC里面提供了一些能够定制整个模版helper的高级选项。下面会进行介绍:
创建自定义的编辑模版(Creating a Custom Editor Template)
一种最简单定制模板化的helper是创建一个自定义的模版,它我们直接呈现HTML。作为例子,我们为Person类的的Role属性创建一个自定义的模版,视图代码如下:
@model MVCApp.Models.Person <p> @Html.LabelFor(m => m.Role): @Html.EditorFor(m => m.Role) </p> <p> @Html.LabelFor(m => m.Role): @Html.DisplayFor(m => m.Role) </p>
这个视图非常简单,label和display模版非常好用,但是上面呈现Role的editor方式不是我们喜欢的,因为Role的枚举值有三个,这里只是随意的呈现一个值,跟我们的预期差的太远,这里可以使用Html.DropDownListFor方法,但是仍然不够完美,因为我们每次都在需要Role Editor的地方手动复制它。这里就可以创建一个模版视图,其实本质是在实际需要的位置创建一个部分视图。首先在Shared里创建一个EditorTemplates文件夹,然后添加一个部分视图如:
@using ModelTemplates.Models @model Role <select id="Role" name="Role"> @foreach (Role value in Enum.GetValues(typeof(Role))) { <option value="@value" @(Model == value ? "selected=\"selected\"" : "")>@value</option> } </select>
这个视图创建了一个HTML select元素并且将每一个Role的枚举值填充为选择项。当我们为这个属性呈现一个Editor元素时,部分视图会被用来生成HTML。可以修改Index视图的代码为:@Html.EditorForModel(),运行程序会看到效果。这里我们可能会很奇怪,我们并没有在Index视图里面引入该部分视图,但是却能够显示Role的HTML元素。因为我们在Shared定义了一个Role.cshtml的模版,枚举名也是Role,所以这里应该是约定可以找到的。但实际上并是这样,我们可以修改为Role1.cshtml,运行程序仍然可以得到想要的结果。这里是根据模版里面的Role的类型来匹配查找的,该模版可以用于任何类型是Role的属性。下面是一个示例:
创建一个SimpleModel:
public class SimpleModel { public string Name { get; set; } public Role Status { get; set; } } //SimpleModel视图的代码如下: @model ModelTemplates.Models.SimpleModel @{ ViewBag.Title = "SimpleModel"; } <h2> SimpleModel</h2> @Html.EditorForModel()
输入/Home/SimpleModel,可以看到效果。
理解模版搜索顺序(UNDERSTANDING THE TEMPLATE SEARCH ORDER)
之所以我们自定义的Role.cshtml能够运行,是因为MVC框架在使用内置的模版之前,先针对给定的C#类型寻找自定义的模版。下面是寻找的顺序:
1.如果是Html.EditorFor(m => m.SomeProperty,"MyTemplate"),会使用MyTemplate模版。
2.任何被指定了元数据的模版,如UIHint
3.跟指定了元数据的数据类型关联的模版,如DataType特性
4.任何跟被处理的数据类型的.NET类名保持一致的模版
5.如果被处理的数据类型是简单类型,那么内置的String模版会被使用
6.任何跟数据类型的基类保持一致的模版
7.如果数据类型实现了IEnumerable,那么内置的Collection模版会被使用
8.如果上面都匹配失败,那么Object模版会被使用
这些步骤里面一些依赖内置的模版,例如上面的例子,MVC框架寻找一个名为EditorTemplates/<name>或者是DisplayTemplates/<name>。对于我们的Role模版,这里匹配了上面第四步。被找到的视图使用跟通常视图一样的搜索模式,这意味着我们可以创建一个指定控制器的自定义模版并把它放在~/Views/<controller>/EditorTemplates文件夹下,从而重写了在~/Views/Shared文件下找到的模版。
创建自定义的展示模版(Creating a Custom Display Template)
这里的过程跟上面的类似,先在Shared下创建DisplayTemplates文件夹,并添加Role.cshtml,如下:
@model Role @foreach (Role value in Enum.GetValues(typeof(Role))) { if (value == Model) { <b>@value</b> } else { @value } }
运行程序可以看到效果,这里需要注意在Shared下创建的两个文件夹名字是约定好的,不能更改成其他的。
创建通用模版(Creating a Generic Template)
我们还可以创建针对所有的枚举并使用UIHint特性指定哪个模版被选中。如果我们看下上面模版的搜索顺序,可以发现指定了UIHint特性的模版会优先于指定具体类型的。下面是一个Enum.cshtml模版,这个模版能在对待C#枚举上更加通用。如下所示:
@model Enum @Html.DropDownListFor(m => m, Enum.GetValues(Model.GetType()) .Cast<Enum>() .Select(m => { string enumVal = Enum.GetName(Model.GetType(), m); return new SelectListItem() { Selected = (Model.ToString() == enumVal), Text = enumVal, Value = enumVal }; }))
上面的模版让我们能够处理任何枚举,在上面的例子中,我们使用了强类型的DropDownListFor helper方法并使用了一些Linq的逻辑将枚举值转化为SelectListItem。下面对Role属性应用UIHint模版:
[UIHint("Enum")]
public Role Role { get; set; }
替换内置的模版(Replacing the Built-in Templates)
如果我们创建的模版跟内置的模版具有同样的名字,MVC框架会使用自定义的版本。下面展示了对Boolean模版的替换,用来呈现bool和bool?值,如下:
@model bool? @if (ViewData.ModelMetadata.IsNullableValueType && Model == null) { @:True False <b>Not Set</b> } else if (Model.Value) { @:<b>True</b> False Not Set } else { @:True <b>False</b> Not Set }
注:搜索替换内置模版的自定义模版顺序遵循标准模版的模式,我们可以将视图放在~/Views/Shared/DisplayTemplates文件夹下,这样意味着MVC框架可以在任何需要Boolean的情形使用这个模版。我们也可以使用~/Views/<controller>/DisplayTempates限定模版只用于单个的控制器。
使用ViewData.TemplateInfo属性(Using the ViewData.TemplateInfo Property)
MVC框架提供了ViewData.TemplateInfo属性使得自定义模版更加容易,这个属性返回一个TemplateInfo对象,下面列举了关于这个类的一些非常有用的成员:
FormattedModelValue:返回一个当前model的字符串,并格式化像DataType特性的元数据。
GetFullHtmlFieldId():返回一个可用于HTML Id属性的字符串
GetFullHmlFieldName():返回一个可用于HTML Name属性的字符串
HtmlFieldPrefix:返回一个字段的前缀
关于数据格式化(Respecting Data Formatting)
可能最有用的TemplateInfo属性就是FormattedModelValue,让我们不必自己去检查和处理这些特性从而遵守了对元数据的格式化。下面是一个DateTime.cshtml的自定义模版,用来生成针对DateTime对象的Editor元素。
@model DateTime @{ var ti = ViewData.TemplateInfo; <input id="@ti.GetFullHtmlFieldId(ti.HtmlFieldPrefix)" name="@ti.GetFullHtmlFieldName(ti.HtmlFieldPrefix)" type="text" value="@ti.FormattedModelValue" /> }
运行程序,可以查看下页面源代码,如下:
运用HTML前缀(Working with HTML Prefixes)
当我们呈现一个有层级的视图时,MVC框架会追踪我们呈现的属性名并通过HtmlFieldPrefix属性给我们提供一个唯一的引用指向。这在我们处理嵌套的对象时特别有用,例如这里的HomeAddress属性,例如:@Html.EditorFor(m => m.HomeAddress.PostalCode) ,这时传递给模版的HtmlFieldPrefix的值就是HomeAddress.PostalCode.在Shared/EditorTemplates下创建一个PostalCode.cshtml的模版如下:
@model string @{ var ti = ViewData.TemplateInfo; <input id="@ti.GetFullHtmlFieldId(ti.HtmlFieldPrefix)" name="@ti.GetFullHtmlFieldName(ti.HtmlFieldPrefix)" type="text" value="@ti.FormattedModelValue" /> }
然后运行程序,查看页面源码:
当然也可以在模版直接写成:@ViewData.TemplateInfo.HtmlFieldPrefix
使用这种方式能够保证我们创建的HTML元素的唯一标识——通常是Id和Name属性。HtmlFieldPrefix属性返回的值通常不能直接的作为属性使用,所以TemplateInfo对象包含了GetFullHtmlFieldId和GetFullHtmlFieldName方法来转换为可以使用的东西,关于HTML前缀的价值在下一章会非常明朗。
传递额外的元数据到模版(Passing Additional Metadata to a Template)
如果我们想给模版提供一个额外的指引,这个又不能使用内置的属性来实现。这时就可以使用AdditionalMetadata属性,如下所示:
[AdditionalMetadata("RenderList", "true")]
public bool IsApproved { get; set; }
我们对IsApproved属性使用了AdditionalMetadata,它需要一个键值对做参数。在这个例子里面我们使用一个RenderList作为键,来指定对bool类型的属性是否应该使用dropdownlist(true)或textbox(false),通过ViewData属性模版里面检测这些值,修改Boolean.cshtml如下:
@model bool? @{ bool renderList = true; if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("RenderList")) { renderList = bool.Parse(ViewData.ModelMetadata.AdditionalValues["RenderList"].ToString()); } } @if (renderList) { SelectList list = ViewData.ModelMetadata.IsNullableValueType ? new SelectList(new[] { "True", "False", "Not Set" }, Model) : new SelectList(new[] { "True", "False" }, Model); @Html.DropDownListFor(m => m, list) } else { @Html.TextBoxFor(m => m) }
理解元数据提供体系(Understanding the Metadata Provider System)
到目前为止,展示的元数据的例子都是依赖DataAnnotationsModelMetadataProvider类,这个类用来检测和处理添加到它里面的属性,以致于模版和格式化选项能够被使用。
模型元数据系统的基础是ModelMetadata类,这个类包含了许多属性(指定一个model或属性应该怎样呈现).DataAnnotationsModelMetadata处理我们为ModelMetadata对象的属性应用和设置值的特性(Attributes),然后会被传递给模版系统处理。要了解ModelMetadata类的最常用的属性,请猛击这里
创建一个自定义的model元数据提供程序(Creating a Custom Model Metadata Provider)
自己创建的提供程序必须从ModelMetadataProvider派生,如下:
namespace System.Web.Mvc { using System.Collections.Generic; public abstract class ModelMetadataProvider { public abstract IEnumerable<ModelMetadata> GetMetadataForProperties( object container, Type containerType); public abstract ModelMetadata GetMetadataForProperty( Func<object> modelAccessor, Type containerType, string propertyName); public abstract ModelMetadata GetMetadataForType( Func<object> modelAccessor, Type modelType); } }
我们可以在自定义的里面实现上面每一个方法,一种简便的方式就是从AssociatedMetadataProvider类派生一个类,这样只需要我们实现单个方法,下面展示了一个实现的Provider:
//在ModelTemplates/Infrastructure下创建 using System.Web.Mvc; namespace ModelTemplates.Infrastructure { public class CustomModelMetadataProvider : AssociatedMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { ModelMetadata metaData = new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName); if (propertyName != null && propertyName.EndsWith("Name")) { metaData.DisplayName = propertyName.Substring(0, propertyName.Length - 4); } return metaData; } } } //在Global.asax指定我们自定义的Provider protected void Application_Start() { AreaRegistration.RegisterAllAreas(); ModelMetadataProviders.Current = new CustomModelMetadataProvider(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
自定义数据标识模型元数据提供程序(Customizing Data Annotations Model Metadata Provider)
使用了自定义的model元数据提供程序后就用不了data annotations元数据了,如果要实现一个定制的策略并且想要获取data annotations带来的好处,可以让自定义的model元数据提供程序从DataAnnotationsModelMetadataProvider派生,这个类又是从AssociatedMetadataProvider,所以我们只需要重写CreateMetadata方法,如下:
namespace ModelTemplates.Infrastructure { public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { ModelMetadata metaData = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); if (propertyName != null && propertyName.EndsWith("Name")) { metaData.DisplayName = propertyName.Substring(0, propertyName.Length - 4); } return metaData; } } }
这里运行程序的效果跟自定义model元数据时效果是不一样的,这样做以后让我们之前在model里面添加的Attributes生效了。
本章的笔记到这里就结束了,大家晚安!