在前面出現過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生效了。
本章的筆記到這里就結束了,大家晚安!