ASP.NET MVC的Model元数据与Model模板:预定义模板
通过ModelMetadata表示的Model元数据的一个主要的作用在于为定义在HtmlHelper和HtmlHelper<TModel>中的模板方法(这些模板方法包括Display/DisplayFor、Editor/EditorFor、DisplayForModel/EditForModel、Lable/LabelFor和DisplayText/DisplayTextFor等)提供用于最终生成HTML的元数据信息。在调用这些方法的时候,如果我们指定了一个具体的通过分部View定义的模板,或者对应的ModelMetadata的TemplateHint属性具有一个模板名称,会自动采用该模板来生成最终的HTML。如果没有指定模板名称,则会根据数据类型在预定义的目录下去寻找做模板的分部View。如果找不到,则会利用默认的模板进行HTML的呈现。为了让读者对模板具有一个大概的认识,我们来做一个简单的实例演示。[本文已经同步到《How ASP.NET MVC Works?》中]
目录
一、 实例演示:通过模板将布尔值显示为RadioButton
二、 预定义模板
EmailAddress
HiddenInput
Html
Text与String
Url
MultilineText
Password
Decimal
Boolean
Collection
Object
一、 实例演示:通过模板将布尔值显示为RadioButton
在默认的情况下,不论是对于编辑模式还是显示模式,一个布尔类型的属性值总是以一个CheckBox的形式呈现出来。我们创建如下一个表示员工的类型Employee,它具有一个布尔类型的属性IsPartTime表示该员工是否为兼职。
1: public class Employee
2: {
3: [DisplayName("姓名")]
4: public string Name { get; set; }
5:
6: [DisplayName("部门")]
7: public string Department { get; set; }
8:
9: [DisplayName("是否兼职")]
10: public bool IsPartTime { get; set; }
11: }
如果我们直接调用HtmlHelper<TModel>的EditorForModel方法将一个Employee对象显示在某个将Employee类型作为Model的强类型View中,下图体现了默认的呈现效果。我们可以看到表示是否为兼职的IsPartTime属性对应着一个CheckBox。
现在我们希望的是将所有布尔类型对象显示为两个RadioButton,具体的显示效果如下图所示。那么我们就可以通过创建一个Model类型为Boolean的View来创建一个模板,使之改变所有布尔类型对象的默认呈现效果。
由于我们需要改变的是布尔类型对象在编辑模式下的呈现形式,所以我们需要将作为模板的分布View定义在EditorTemplates目录下,这个目录可以存在于Views/ Shared下,也可以存在于Views/{ControllerName}下。由于ASP.NET MVC是采用数据类型作为匹配条件来寻找对应的模板的,所以我们需要将分部模板View命名为Boolean。下面的代码片断体现了这个分部试图的整个定义,我们通过调用HtmlHelper的RadioButton方法将两个布尔值(True/False)映射为对应的RadioButton,并且采用<table>来布局。
1: @model bool
2: <table>
3: <tr>
4: <td>@Html.RadioButton("",true,Model)是</td>
5: <td>@Html.RadioButton("",false,!Model)否</td>
6: </tr>
7: </table>
值得一提的是,我们没有指定RadioButton的名称,而是指定一个空字符串,Html本身会对其进行命名,而命名的依据就是本章介绍的核心:Model元数据。Employee的IspartTime属性呈现在界面上对应的HTML如下所示,我们可以看到两个类型为radio的<input>元素的name被自动赋上了对应的属性名称。美中不足的是它们具有相同的ID,如果希望让ID具有唯一性,可以对模板进行更加细致的定制。
1: <div class="editor-label"><label for="IsPartTime">是否兼职</label></div>
2: <div class="editor-field">
3: <table>
4: <tr>
5: <td><input checked="checked" id="IsPartTime" name="IsPartTime" type="radio" value="True" .../>是</td>
6: <td><input id="IsPartTime" name="IsPartTime" type="radio" value="False" />否</td>
7: </tr>
8: </table>
9: </div>
二、预定义模板
上面我们介绍如何通过View的方式创建模板进而控制某种数据类型或者某个目标元素最终在UI界面上的HTML呈现方式,实际上在ASP.NET MVC的内部还定义了一系列的预定义模板。当我们调用HtmlHelper/HtmlHelper<TModel>的模板方法对Model或者Model的某个成员进行呈现的时候,系统会根据当前的呈现模式(显示模式和编辑模式)和Model元数据获取一个具体的模板(自定义模版或者预定义模版)。由于Model具有显示和编辑两种呈现模式,所以定义在ASP.NET MVC内部的默认模版分为这两种基本的类型。接下来我们就逐个介绍这些预定义模版以及最终的HTML呈现方式。
EmailAddress
该模板专门针对用于表示Email地址的字符串类型的数据成员,它将目标元素呈现为一个href属性具有“mailto:”前缀的链接(<a></a>)。由于该模板仅仅用于Email地址的显示,所以只在显示模式下有效,或者说ASP.NET MVC仅仅定义了基于显示模式的EmailAddress模板。为了演示数据在不同模板下的呈现方式,我们定义了如下一个简单的数据类型Model,我们通过在属性Foo上应用UIHintAttribute特性将模板名称设置为“EmailAddress”。
1: public class Model
2: {
3: [UIHint("EmailAddress")]
4: public string Foo { get; set; }
5: }
然后在一个基于Model类型的强类型View中,我们通过调用HtmlHelper<TModel>的DisplayFor方法将一个具体的Model对象的Foo属性以显示模式呈现出来。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
如下的代码片断表示Model的Foo属性对应的HTML,我们可以看到它就是一个针对Email地址的连接。当我们点击该链接的时候,相应的Email编辑软件(比如Outlook)会被开启用于针对目标Email地址的邮件编辑。
1: <a href="mailto:foo@gmail.com">foo@gmail.com</a>
HiddenInput
关于默认模板HiddenInput我们不应该感到模式,前面介绍的HiddenInputAttribute特性就是将表示Model元数据的ModelMetadata对象的TemplateHint属性设置为HiddenInput。如果目标元素采用HiddenInput模板,在显示模式下内容会以文本的形式显示;编辑模式下不仅会以文本的方式显示其内容,还会生成一个对应的type属性为“hidden”的<input>元素。如果表示Model元数据的ModelMetadata对象的HideSurroundingHtml属性为True(将应用在目标元素上的特性HiddenInputAttribute的DisplayValue属性设置为False),不论是显示模式还是编辑模式下显示的文本都将消失。
同样以上面定义的数据类型Model为例,我们通过在Foo属性上应用UIHintAttribute特性将模板名称设置为“HiddenInput”。
1: public class Model
2: {
3: [UIHint("HiddenInput")]
4: public string Foo { get; set; }
5: public bool Bar { get; set; }
6: public decimal Baz { get; set; }
7: }
然后在一个基于Model类型的强类型View中分别调用HtmlHelper<TModel>的DisplayFor和EditFor方法将一个具体的Model对象的Foo属性以显示和编辑模式呈现出来。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
3: @Html.EditorFor(m=>m.Foo)
分别以两种模式呈现出来的Foo属性对应的HTML如下(包含在花括号中的GUID表示属性值)。第一行是针对显示模式的,可以看出最终呈现出来仅限于表示属性值得文本;而编辑模式对应的HTML中不仅包含属性值文本,还具有一个对应的类型为“hidden”的<input>元素。
1: {42A1E9B7-2AED-4C8E-AB55-78813FC8C233}
2: {42A1E9B7-2AED-4C8E-AB55-78813FC8C233}<input id="Foo" name="Foo" type="hidden" value="{42A1E9B7-2AED-4C8E-AB55-78813FC8C233}" />
现在我们对数据类型Model做一下简单修改,将应用在属性Foo上的UIHintAttribute特性替换成HiddenInputAttribute特性,并将其DisplayValue属性设置成False。
1: public class Model
2: {
3: [HiddenInput(DisplayValue = false)]
4: public string Foo { get; set; }
5: public bool Bar { get; set; }
6: public decimal Baz { get; set; }
7: }
由于应用在目标元素上的HiddenInputAttribute特新的DisplayValue属性会最终控制对应ModelMetadata的HideSurroundingHtml属性,而后者控制是否需要生成用于显示目标内容的HTML。所以针对针对的Model定义,最终会生成如下一段HTML。
1: <input id="Foo" name="Foo" type="hidden" value="{42A1E9B7-2AED-4C8E-AB55-78813FC8C233}" />
Html
如果目标对象的内容包含一些HTML,并需要在UI界面中原样呈现出来,我们可以采用Html模板。和EmailAddress模板一样,该模板仅限于显示模式。为了演示Html模板对目标内容的呈现方法与默认呈现方式之间的差异,我们定义了如下一个数据类型Model。该数据类型具有两个字符串类型的属性Foo和Bar,其中Foo上面应用UIHintAttribute特性将模板名称设置为“Html”。
1: public class Model
2: {
3: [UIHint("Html")]
4: public string Foo { get; set; }
5: public string Bar { get; set; }
6: }
现在我们创建一个具体的Model对象,并将Foo和Bar设置为一段表示链接的文本(<a href="www.google.com">google.com</a>),最终在一个基于Model类型的强类型View中通过调用HtmlHelper<TModel>的DisplayFor方法将这两个属性以显示模式呈现出来。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
3: @Html.DisplayFor(m => m.Bar)
从如下所示的表示Foo和Bar两属性的HTML中我们不难看出:采用Html模板的Foo属性的内容原样输出,而包含在属性Bar中的HTML都进行了相应的编码。
1: <a href="www.google.com">google.com</a> <a href="www.google.com">google.com</a>
Text与String
不论是在显示模式还是编辑模式,Text和String模板具有相同的HTML呈现方式(实际上在ASP.NET MVC内部,两种模版最终生成的HTML是通过相同的方法产生的)。对于这两种模版来说,目标内容在显示模式下直接以文本的形式输出;而在编辑模式下则对应着一个单行的文本框。
为了演示两种模版的同一性,我们对上面定义数据类型Model略作修改,在属性Foo和Bar上应用UIHintAttribute特性并将模版名称分别设置为String和Text。
1: public class Model
2: {
3: [UIHint("String")]
4: public string Foo { get; set; }
5: [UIHint("Text")]
6: public string Bar { get; set; }
7: }
然后我们创建一个具体的Model对象,并在一个基于该Model类型的强类型View中通过调用HtmlHelper<TModel>的DisplayFor和EditorFor将两个属性分别以显示和编辑模式呈现出来。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
3: @Html.DisplayFor(m => m.Bar)
4: @Html.EditorFor(m=>m.Foo)
5: @Html.EditorFor(m => m.Bar)
如下所示的代码片断体现了上述四个元素对应的HTML(“Dummy text …”是Foo和Bar的属性值),可以看到采用了Text和String模板的两个属性在显示和编辑模式下具有相同的呈现方式。编辑模式下输出的类型为“text”的<input>元素表示CSS特性类型的class属性被设置为“text-box single-line”,意味着这是一个基于单行的文本框。
1: Dummy text ...
2: Dummy text ...
3: <input class="text-box single-line" id="Foo" name="Foo" type="text" value="Dummy text ..." />
4: <input class="text-box single-line" id="Bar" name="Bar" type="text" value="Dummy text ..." />
值得一提的是,ASP.NET MVC内部采用基于类型的模板匹配机制,对于字符串类型的数据成员,如果没有显式设置采用的模板名称,默认情况下会采用String模板。
Url
与EmailAddress和Html一样,模板Url也仅限于显示模式。对于某个表示为Url的字符串,如果我们希望它最终以一个连接的方式呈现在最终生成的HTML中,我们采用该模板。如下面的代码片断所示,我们通过应用UIHintAttribute特性将模板Url应用到属性Foo中。
1: public class Model
2: {
3: [UIHint("Url")]
4: public string Foo { get; set; }
5: }
我们创建一个具体的Model对象,并将Foo属性设置为一个表示Url的字符串“http://www.asp.net”,最后通过如下的方式将该属性以显示模式呈现出来。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
如下面的代码片断所示,该属性最终呈现为一个href属性和文本内容均属性值得连接(<a></a>)。
1: <a href="http://www.asp.net">http://www.asp.net</a>
MultilineText
一般的字符串在编辑模式下会呈现为一个单行的文本框(类型为“text”的<input>元素),而MultilineText模板会将表示目标内容的字符串通过一个<textarea>元素,该模板仅限于编辑模式。如下面的的代码片断所示,我们通过在字符串类型的Foo属性上应用UIHintAttribute特性将应用的模板设置为MultilineText。
1: public class Model
2: {
3: [UIHint("MultilineText")]
4: public string Foo { get; set; }
5: }
现在我们创建一个具体的Model对象并通过如下的形式将Foo属性以编辑模式呈现在某个基于Model类型的强类型View中。
1: @model Model
2: @Html.EditorFor(m=>m.Foo)
如下所示的代码片断表示Model的Foo属性呈现在UI界面中的HTML(“dummy text …”是是Foo的属性值),我们可以看到这是一个< textarea >元素。表示CSS样式类型的class属性被设置为“text-box multi-line”,意味着它是以多行的效果呈现。
1: <textarea class="text-box multi-line" id="Foo" name="Foo">dummy text ...</textarea>
Password
对于表示密码字符串来说,在编辑模式下应该呈现为一个类型为“password”的<input>元素,使我们输入的内容以掩码的形式显示出来以保护密码的安全性。在这种情况下我们可以采用Password模板,该模板和MultilineText一样也仅限于编辑模式。如下面的代码片断所示,我们在Model的Foo属性上应用UIHintAttribute特性将模式名称设置为“Password”。
1: public class Model
2: {
3: [UIHint("Password")]
4: public string Foo { get; set; }
5: }
我们创建一个具体的Model对象,并通过如下的形式将Foo属性以编辑模式呈现在某个基于Model的强类型View中。
1: @model Model
2: @Html.EditorFor(m=>m.Foo)
该Foo属性最终会以如下的形式通过一个类型为“Password”的<input>元素呈现出来,表示CSS样式类型的class属性被设置为“text-box single-line password”,意味着呈现效果为一个单行的文本框。
1: <input class="text-box single-line password" id="Foo" name="Foo" type="password" value="" />
Decimal
如果采用Decimal模板,代表目标元素的数字不论其小数位数是多少,都会最终被格式化为两位小数。在显示模式下,被格式化的数字直接以文本的形式呈现出来;在编辑模式下则对应着一个单行的文本框架。如下面的代码片断所示,我们在数据类型Model中定义了两个对象类型属性Foo和Bar,上面应用了UIHintAttribute特性并将模板名称指定为“Decimal”。
1: public class Model
2: {
3: [UIHint("Decimal")]
4: public object Foo { get; set; }
5:
6: [UIHint("Decimal")]
7: public object Bar { get; set; }
8: }
我们创建一个具体的Model对象,将它的Foo和属性分别设置为整数123和浮点数3.1415(4位小数),最终通过如下的形式将它们以显示和编辑的模式呈现在一个基于Model类型的强类型View中。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
3: @Html.DisplayFor(m=>m.Bar)
4: @Html.EditorFor(m=>m.Foo)
5: @Html.EditorFor(m =>m.Bar)
上述四个元素在最终呈现的UI界面中对应着如下的HTML,我们可以看到最终显示的都是具有两位小数的数字。
1: 123.00
2: 3.14
3: <input class="text-box single-line" id="Foo" name="Foo" type="text" value="123.00" />
4: <input class="text-box single-line" id="Bar" name="Bar" type="text" value="3.14" />
Boolean
通过本章最开始的实例演示我们知道一个布尔类型的对象在编辑模式下会以一个类型为“checkbox”的<input>元素的形式呈现,实际上在显示模式下它依然对应着这么一个元素,只是其disabled属性会被设置为True使之处于只读状态。布尔类型的这种默认呈现方式源自“Boolean”模板默认被使用。
当布尔类的目标元素以编辑模式进行呈现的时候,除了生成一个一个类型为“checkbox”的<input>元素之外还会附加产生一个类型为“hidden”的<input>元素。如下面的代码片断所示,这个hidden元素具有与CheckBox相同的名称,但是值为False,它存在的目的在于当CheckBox没有被勾选的情况下通过对应的hidden元素向服务区提交相应的值(False),因为没有被勾选的CheckBox的值是不会包含在请求中的。
1: <input id="Foo" name="Foo" type="checkbox" value="true" />
2: <input name="Foo" type="hidden" value="false" />
Boolean和String、Decimal以及后面我们介绍的Object一样属于是基于CLR类型的模板。由于ASP.NET在内部采用基于类型的模板匹配策略,如果没有显示设置采用的模板类型,相应类型的元素会默认采用与之匹配的模板。
Collection
顾名思义,Collection模板用于集合类型的目标元素的显示与编辑。对应采用该模板的类型为集合(实现了IEnumerable接口)的目标元素,在调用HtmlHelper或者HtmlHelper<TModel>以显示或者编辑模式对其进行呈现的时候,会遍历其中的每个元素,并根据基于集合元素的Model元数据决定对其的呈现方法。同样一我们定义的数据类型Model为例,我们按照如下的方式将它的Foo属性类型改为对象数组,上面应用了UIHintAttribute特性并将模板名称 设置为“Collection”。
1: public class Model
2: {
3: [UIHint("Collection")]
4: public object[] Foo { get; set; }
5: }
然后我们按照如下的方式创建一个包含三个对象的数组,作为数据元素的三个对象类型为别是数字、字符串和布尔,然后将该数组作为Foo属性创建一个具体的Model对象。
1: object[] foo = new object[]
2: {
3: 123.00,
4: "dummy text ...",
5: true
6: };
7: Model model = new Model { Foo = foo };
在一个基于Model类型的强类型View中,我们分别调用HtmlHelper<TModel>的DisplayFor和EditorFor方法将上面创建的Model对象的Foo属性以显示和编辑模式呈现出来。
1: @model Model
2: @Html.DisplayFor(m=>m.Foo)
3: @Html.EditorFor(m=>m.Foo)
Model对象的Foo属性最终呈现出来的HTML如下所示,我们可以看到不论是显示模式还是编辑模式,基本上就是对集合元素呈现的HTML的组合而已。
1: 123dummy text ...<input checked="checked" class="check-box" disabled="disabled" type="checkbox" />
2: <input class="text-box single-line" data-val="true" data-val-number="The field Double must be a number." data-val-required="The Double field is required." id="Foo_0_" name="Foo[0]" type="text" value="123" />
3: <input class="text-box single-line" id="Foo_1_" name="Foo[1]" type="text" value="dummy text ..." />
4: <input checked="checked" class="check-box" data-val="true" data-val-required="The Boolean field is required." id="Foo_2_" name="Foo[2]" type="checkbox" value="true" />
5: <input name="Foo[2]" type="hidden" value="false" />
Object
我们说过,ASP.NET 内部采用基于类型的模板匹配策略,如果通过ModelMetadata对象表示的Model元数据不能找到一个具体的模板,最终都会落到Object模板上。Object模板对目标对象的呈现方式很简单,它通过ModelMetadata的Proeprties属性得到所有基于属性的Model元数据。针对每个表示属性Model元数据的ModelMetadata,它会根据DisplayName或者属性名称生成一个标签(实际上是一个内部文本为显示名称的<div>元素),然后根据元数据将属性值以显示或者编辑的模式呈现出来。
1: public class Address
2: {
3: [DisplayName("省")]
4: public string Province { get; set; }
5: [DisplayName("市")]
6: public string City { get; set; }
7: [DisplayName("区")]
8: public string District { get; set; }
9: [DisplayName("街道")]
10: public string Street { get; set; }
11: }
针对上面定义得这个表示地址的Address。现在我们创建一个具体的Address对象并通过如下的方式调用HtmlHelper<TModel>的DisplayForModel方法将它呈现在以此作为Model的View中。
1: @model Address
2: @Html.DisplayForModel()
从如下所示的HTML中我们可以看出作为Model的Address对象的所有属性都以显示模式呈现出来,而在前面还具有相应的标签。
1: <div class="display-label">省</div>
2: <div class="display-field">江苏省</div>
3: <div class="display-label">市</div>
4: <div class="display-field">苏州市</div>
5: <div class="display-label">区</div>
6: <div class="display-field">工业园区</div>
7: <div class="display-label">街道</div>
8: <div class="display-field">星湖街328号</div>
值得一提的是,Object模板在对属性进行遍历的过程中,不论是显示模式还是编辑模式,只会处理非复杂类型。也就是如果属性成员是一个复杂类型(不能支持从字符串类型的转换),它不会出现在最终生成的HTML中。
1: public class Contact
2: {
3: [DisplayName("姓名")]
4: public string Name { get; set; }
5: [DisplayName("电话")]
6: public string PhoneNo { get; set; }
7: [DisplayName("Email地址")]
8: public string EmailAddress { get; set; }
9: [DisplayName("联系地址")]
10: public Address Address { get; set; }
11: }
通过上面的代码片断,我们定义了一个表示联系人的数据类型Contact,它具有一个类型的Address的同名属性。现在我们创建一个具体的Contact对象,并 对包括Address 属性在内的所有属性进行初始化,然后通过如下的方式通过调用HtmlHelper<TModel>的DisplayForModel方法将它呈现在以此作为Model的View中。
1: @model Contact
2: @Html.DisplayForModel()
从如下所示的HTML可以看出,Contact的数据成员Address由于是复杂类型,其内容并不会呈现出来。
1: <div class="display-label">姓名</div>
2: <div class="display-field">张三</div>
3: <div class="display-label">电话</div>
4: <div class="display-field">1234567890</div>
5: <div class="display-label">Email地址</div>
6: <div class="display-field">zhangsan@gmail.com</div>
我们可以有两种方式解决这个问题,其实就是通过为Address类型定义各类专门的模板用于地址信息的显示和编辑,通过UIHintAttribute特性将此模板名称应用到Contact的Address属性上,然后调用DisplayFor/EditorFor将该属性呈现出来。另一种就是按照类似如如下的方式手工将负责类型属性成员呈现出来。
1: @model Contact
2: @Html.DisplayForModel()
3: <div class="display-label">@Html.DisplayName("Address")</div>
4: <div class="display-field">@(Model.Address.Province + Model.Address.City + Model.Address.District+ Model.Address.Street)</div>
ASP.NET MVC的Model元数据与Model模板:预定义模板
ASP.NET MVC的Model元数据与Model模板:模板的获取与执行策略
ASP.NET MVC的Model元数据与Model模板:将ListControl引入ASP.NET MVC