精通ASP.NET MVC ——模型绑定
模型綁定(Model Binding)是指,用瀏覽器以Http請求方式發送的數據來創建.Net對象的過程。
準備示例項目?
新建一個空的MVC項目,名叫MvcModels,接下去會以此項目來演示各種功能。
在Models文件夾中創建一個Person.cs類文件,代碼如下圖所示:
namespace MvcModels.Models {public 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; }}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} }定義一個Home控制器,代碼如下圖所示:
public class HomeController : Controller{private Person[] personData = {new Person { PersonId = 1,FirstName = "Adam",LastName = "Freeman" },new Person { PersonId = 2,FirstName = "Jacqui",LastName = "Griffyth"},new Person { PersonId = 3,FirstName = "John",LastName = "Smith" },new Person { PersonId = 4,FirstName = "Anne",LastName = "Jones"}};// GET: Homepublic ActionResult Index(int id){Person dataItem = personData.Where(p => p.PersonId == id).First();return View(dataItem);}}新增Index控制器對應的Index.cshtml頁面,代碼如下:?
@model MvcModels.Models.Person@{ViewBag.Title = "Index";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>Person</h2> <div><label>ID:</label> @Html.DisplayFor(m => m.PersonId)</div> <div><label>First Name:</label>@Html.DisplayFor(m => m.FirstName)</div> <div><label>Last Name:</label>@Html.DisplayFor(m => m.LastName)</div> <div><label>Roles:</label>@Html.DisplayFor(m => m.Role)</div>新增_layout.cshtml布局頁面,代碼如下:?
<!DOCTYPE html><html> <head><meta name="viewport" content="width=device-width" /><title>@ViewBag.Title</title><style>label {display:inline-block; width:100px;font-weight:bold;margin:5px;}form label {float:left;}input.text-box {float:left;margin:5px}button[type=submit] {margin-top:5px;float:left;clear:left;}form div {clear:both;}</style> </head> <body><div>@RenderBody()</div> </body> </html>運行程序,并導航到/Home/Index/1,結果如下圖所示:?
? ? ? ? ? ? ? ?
默認的動作綁定器ControllerActionInvoker要依靠模型綁定器來生成調用動作所需的數據對象。模型綁定器由IModelBinder接口所定義,接口如下圖所示:
public interface IModelBinder {object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext) }在一個MVC應用程序中,可以有多個模型綁定器,而每個綁定器可以負責綁定一個或者多個模型類型。它會考察該方法所定義的參數,并查找各個參數類型所依賴的模型綁定器。
在上述示例中,動作調用器會檢查Index方法,并發現它具有一個int型的參數。于是會查找負責int值綁定的綁定器,并調用BindModel方法。
使用默認的模型綁定器
雖然程序可以定義自定義的模型綁定器,大多數程序都是依靠內建的模型綁定器DefaultModelBinder.當動作調用器找不到綁定某個類型的自定義綁定器時,這個默認的模型綁定器便是由動作調用器所使用的一個綁定器。默認情況下,這個模型綁定器會搜索四個位置:
| 源 | 描述 |
| Request.Form | 由用戶在HTML的form(表單)元素中提供的值 |
| RouteData.Values | 用應用程序路由獲得的值 |
| Request.QueryString | 包含在請求URL中的查詢字符串部分的數據 |
| Request.Files | 請求中上傳的文件 |
這些位置被依序搜索。例如,在上述簡單示例中,DefaultModelBinder會為id參數查找以下的一個值:
1、Request.Form["id"]
2、RouteData.Values["id"]
3、Request.QueryString["id"]
4、Request.Files["id"]
只要找到值,便會停止搜索。在上述例子中,搜索到第二步就停了,不會到第三步。
當處理簡單參數類型時,DefaultModelBinder會嘗試使用 System.ComponentModel.TypeDescriptor類。將已經從請求數據獲得的字符串值轉化成參數類型。如果無法轉為這個值:例如給int值傳一個“apple”,程序就會報錯:
解決這個問題有兩種辦法:
一、在動作方法參數中設置可空類型(nullable),這為綁定器提供一個退路,一個可空的int參數可以不必為數字值,這讓模型綁定器在調用動作時,這可以讓動作方法參數設置為Null。
public ActionResult Index(int? id)二、 在動作方法中運用默認值,當模型綁定器無法為id參數找到一個值時,將默認值1來代替,如下所示:
public ActionResult Index(int id = 1)?綁定復雜類型
當動作方法的參數是復合類型時,DefaultModelBinder類將用反射來獲取public屬性集。
在Home控制器中,新增如下兩個動作方法:?
public class HomeController : Controller{private Person[] personData = {new Person { PersonId = 1,FirstName = "Adam",LastName = "Freeman" },new Person { PersonId = 2,FirstName = "Jacqui",LastName = "Griffyth"},new Person { PersonId = 3,FirstName = "John",LastName = "Smith" },new Person { PersonId = 4,FirstName = "Anne",LastName = "Jones"}};// GET: Homepublic ActionResult Index(int? id = 1){Person dataItem = personData.Where(p => p.PersonId == id).First();return View(dataItem);}public ActionResult CreatePerson(){return View(new Person());}[HttpPost]public ActionResult CreatePerson(Person model){return View("Index",model);}}為沒有參數的CreatePerson控制器方法創建一個對應的視圖:CreatePerson.cshtml?,代碼如下圖所示:
@model MvcModels.Models.Person @{ViewBag.Title = "CreatePerson";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>CreatePerson</h2> @using (Html.BeginForm()) {<div>@Html.LabelFor(m => m.PersonId) @Html.EditorFor(m => m.PersonId)</div><div>@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName)</div><div>@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName)</div><div>@Html.LabelFor(m => m.Role) @Html.EditorFor(m => m.Role)</div><button type="submit">Submit</button> }運行導航到/Home/CreatePerson,結果如下圖所示:
? ? ? ? ??
點擊submit按鈕后,可以看到已經將輸入的數據? 傳到 Index 界面了:
? ? ? ? ?
?
在表單傳遞給CreatePerson方法時,形成了一種不同的模型綁定情況。默認的模型綁定器發現,動作方法需要一個Person對象,于是會依次處理每個屬性。?對于每個簡單類型的屬性,綁定器會視圖查找請求中的一個值,就如同上一個示例所做的那樣。因此,當遇到 PersonId屬性時,綁定器會查找personId的數據值,它將在請求的表單中發現這個值。
如果一個屬性需要另一個復合類型,那么,該過程會針對新類型重復執行。獲取該類型的public屬性集,而綁定器會視圖找出這些屬性的值。不同的是,這些屬性是嵌套的。例如,Person類的HomeAddress 屬性 是Address類型。
創建易于綁定的HTML
更新CreatePerson.cshtml中的代碼,以便為Address類型捕獲一些屬性:
@model MvcModels.Models.Person @{ViewBag.Title = "CreatePerson";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>CreatePerson</h2> @using (Html.BeginForm()) {<div>@Html.LabelFor(m => m.PersonId) @Html.EditorFor(m => m.PersonId)</div><div>@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName)</div><div>@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName)</div><div>@Html.LabelFor(m => m.Role) @Html.EditorFor(m => m.Role)</div><div>@Html.LabelFor(m => m.HomeAddress.City)@Html.EditorFor(m => m.HomeAddress.City)</div><div>@Html.LabelFor(m => m.HomeAddress.Country)@Html.EditorFor(m => m.HomeAddress.Country)</div><button type="submit">Submit</button> }更新Index.html中的代碼,如下圖所示:?
@model MvcModels.Models.Person@{ViewBag.Title = "Index";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>Person</h2> <div><label>ID:</label> @Html.DisplayFor(m => m.PersonId)</div> <div><label>First Name:</label>@Html.DisplayFor(m => m.FirstName)</div> <div><label>Last Name:</label>@Html.DisplayFor(m => m.LastName)</div> <div><label>Roles:</label>@Html.DisplayFor(m => m.Role)</div><div><label>City:</label>@Html.DisplayFor(m => m.HomeAddress.City)</div> <div><label>County:</label>@Html.DisplayFor(m => m.HomeAddress.Country)</div>運行導航到/Home/CreatePerson,如下圖所示:
? ? ? ? ? ?
點擊Submit按鈕后,數據傳遞成功,情況如下:
? ? ? ? ? ? ?
簡而言之,模型綁定器查找的是 HomeAddress.Country,即,模型對象的屬性名(HomeAddress)與屬性類型(Address)d的屬性名(Country)的組合。?
指定自定義前綴?
偶爾有些時候,生成的HTML與一種類型的對象有關,但是希望綁定到另一個對象。這意味著包含的前綴與模型綁定器期望的結構不對應。這個時候需要用到屬性注解了。
在Models文件夾中創建了一個新的類文件,名稱為AddressSummary.cs,如下圖所示:
public class AddressSummary{public string City { get; set; }public string Country { get; set; }}在Home控制器中增加一個動作方法,如下圖所示:?
public ActionResult DisplaySummary(AddressSummary summary){return View(summary);}修改CreatePerson.cshtml文件中表達提交的目標:
@model MvcModels.Models.Person @{ViewBag.Title = "CreatePerson";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>CreatePerson</h2> @using (Html.BeginForm("DisplaySummary","Home")) {<div>@Html.LabelFor(m => m.PersonId) @Html.EditorFor(m => m.PersonId)</div><div>@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName)</div><div>@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName)</div><div>@Html.LabelFor(m => m.Role) @Html.EditorFor(m => m.Role)</div><div>@Html.LabelFor(m => m.HomeAddress.City)@Html.EditorFor(m => m.HomeAddress.City)</div><div>@Html.LabelFor(m => m.HomeAddress.Country)@Html.EditorFor(m => m.HomeAddress.Country)</div><button type="submit">Submit</button> }導航到/Home/CreatePerson方法:?
? ? ? ? ? ?
點擊submit方法后,結果如下圖所示:?
? ? ? ? ? ??
?由于Country和City的前綴改變了,由HomeAddress變成了AddressSummary,故綁定器無法實現綁定。只需對動作方法的參數運用Bind注解屬性即可,代碼如下圖所示:
public ActionResult DisplaySummary([Bind(Prefix ="HomeAddress")]AddressSummary summary) {return View(summary); }重新運行代碼,即可看到運行成功:?
? ? ? ? ? ? ? ??
有選擇性的綁定屬性?
如果希望對某一屬性不需要模型綁定器進行綁定,可以使用如下代碼:
public ActionResult DisplaySummary([Bind(Prefix ="HomeAddress",Exclude ="Country")]AddressSummary summary) {return View(summary); }也可以設置模型綁定器只綁定某一屬性,代碼如下圖所示:
[Bind(Include ="City")]public class AddressSummary{public string City { get; set; }public string Country { get; set; }}?
綁定到數組
默認模型綁定器的一個雅致的特性是它支持動作方法參數作為數組。在Home控制器中添加一個方法,如下圖所示:
public ActionResult Names(string[] names){names = names ?? new string[0];return View(names);}創建Names方法對應的視圖Names.csthml,如下圖所示:?
@model string[] @{ViewBag.Title = "Names";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>Names</h2> @if (Model.Length == 0) {using (Html.BeginForm()) {for (int i = 0; i < 3; i++){<div><label>@(i + 1):</label>@Html.TextBox("names")</div>}<button type="submit">Submit</button>} } else {foreach (string str in Model){<p>@str</p>}@Html.ActionLink("Back", "Names") }導航到/Home/Names,如下圖所示:
? ? ? ? ? ?
點擊Submit后,如下圖所示:?
? ? ? ? ??
遞交表單時,默認的模型綁定器明白動作方法需要一個字符串數組。于是會查找與參數具有同樣名稱的數據項。在本例中,意味著會將所有input元素的內容聚集到一起用以填充數組。?
?
?
綁定到集合?
能綁定的不僅僅是數組,還可以使用.Net集合類。
修改names動作方法為強類型集合,如下圖所示:
public ActionResult Names(IList<string> names){names = names ?? new List<string>();return View(names);}并修改Names.cshtml頁面代碼,如下所示:
@model IList<string> @{ViewBag.Title = "Names";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>Names</h2> @if (Model.Count() == 0) {using (Html.BeginForm()) {for (int i = 0; i < 3; i++){<div><label>@(i + 1):</label>@Html.TextBox("names")</div>}<button type="submit">Submit</button>} } else {foreach (string str in Model){<p>@str</p>}@Html.ActionLink("Back", "Names") }運行結果如下圖所示:
? ? ? ? ? ??
點擊提交后:?
綁定到自定義模型集合?
可以將一些單個的數據屬性綁定成一個自定義類型的數組。如上述的AddressSummary模型類.
在控制器中增加一個新的動作方法,如下圖所示:
public ActionResult Address(IList<AddressSummary> addresses){addresses = addresses ?? new List<AddressSummary>();return View(addresses);}添加Address動作方法對應的頁面,代碼如下圖所示:
@using MvcModels.Models @model IList<AddressSummary> @{ViewBag.Title = "Address";Layout = "~/Views/Shared/_Layout.cshtml"; }<h2>Address</h2> @if (Model.Count() == 0) {using (Html.BeginForm()){for (int i = 0; i < 3; i++){<fieldset><legend>Address @(i + 1)</legend><div><label>City:</label>@Html.Editor("[" + i + "].City")</div><div><label>Country:</label>@Html.Editor("[" + i + "].Country")</div></fieldset>}<button type="submit">Submit</button>} } else {foreach (AddressSummary str in Model){<p> @str.City, @str.Country</p>}@Html.ActionLink("Back","Address"); }?導航到/Home/Address,并輸入內容,頁面如下圖所示:
? ? ? ? ? ? ? ?
點擊提交后,正確顯示頁面:
? ? ? ? ? ? ??
可以看到生成的HTML代碼:?
? ? ??
?
當表單被提交時,默認的模型綁定器知道它需要創建的是一個AddressSummary對象集合,并利用 name 標簽屬性中的數組索引前綴獲取對象的類型。以【0】為前綴的那些屬性表示一個AddressSummary對象,以【1】為前綴表示第二個對象。以此類推。?
手工調用模型綁定
當動作方法定義了參數時,模型綁定過程是自動執行的,但是只要你愿意,也可以直接控制這一過程。如下將演示如何將Home控制器的Address動作方法修改成手動調用綁定過程。
修改Address方法,代碼如下圖所示:
public ActionResult Address(IList<AddressSummary> addresses){addresses = addresses ?? new List<AddressSummary>();UpdateModel(addresses);return View(addresses);}UpdataModel方法以上一條語句定義的時候的模型對象為參數,并試圖用標準的綁定該過程來獲取其public屬性的值。?
當手工調用綁定時,可以將綁定過程限制到單一的數據源。默認情況下,綁定器會搜索四個地方:表單數據、路由數據、查詢字符串,以及上傳文件。一下代碼演示了如何將綁定器限制到搜索單一位置的數據——表單數據。
public ActionResult Address(IList<AddressSummary> addresses){addresses = addresses ?? new List<AddressSummary>();UpdateModel(addresses,new FormValueProvider(ControllerContext));return View(addresses);}UpdateModel方法的這一版本以IValueProvider接口的一個實現為參數,該實現也成為了綁定過程的唯一的數據源。四個默認的數據位置的每一個都由一個IValueProvider實現表示:
| 源 | IValueProvider |
| Request.Form | FormValueProvider |
| RouteData.Value | RouteDataValueProvider |
| Request.QueryString | QueryStringValueProvider |
| Request.Files | HttpFileCollectionValueProvider |
可以用另一種跟優雅的寫法來表示:
public ActionResult Address(FormCollection formData){List<AddressSummary> addresses = new List<AddressSummary>();UpdateModel(addresses,formData);return View(addresses);}?處理綁定錯誤
用戶難免會提供一些不能綁定到相應模型屬性的值,需要對這些情況拋出些異常。特別是使用UpdateModel方法時,必須做好捕捉該異常的準備,代碼如下圖所示:
public ActionResult Address(FormCollection formData){List<AddressSummary> addresses = new List<AddressSummary>();try{UpdateModel(addresses, formData);}catch (InvalidCastException ex){//給用戶提供反饋}return View(addresses);}另一個可選的辦法是,可以使用TryUpdateModel方法。如果模型綁定成功,返回ture;否則返回false;
public ActionResult Address(FormCollection formData){List<AddressSummary> addresses = new List<AddressSummary>();if (TryUpdateModel(addresses,formData)){//正常處理}else{//給用戶提供反饋}return View(addresses);}這兩種方式的唯一區別是,你是否喜歡捕捉并處理異常。?
定制模型綁定系統
還有一些不同的方式,可以對綁定系統進行定制。
通過定義一個自定義的值提供器,可以將自己的數據源添加到模型綁定過程。值提供器(Valueprovider)需要實現IValueProvider接口,如下圖所示:
public interface IValueProvider{bool ContainsPrefix(string prefix);ValueProviderResult GetValue(string key);}ContainsPrefix方法由模型綁定器調用,以確定這個值提供器是否可以解析給定前綴的數據。
GetValue方法返回給定數據鍵的值,或者在提供器無法得到合適的數據時返回null。
新建一個CountryValueProvider類,實現以上接口:
public class CountryValueProvider : IValueProvider{public bool ContainsPrefix(string prefix){return prefix.ToLower().IndexOf("country") > -1;}public ValueProviderResult GetValue(string key){if (ContainsPrefix(key)){return new ValueProviderResult("USA", "USA", CultureInfo.InvariantCulture);}else{return null;}}}該值提供器只對請求Country屬性的值進行響應,而且總是返回 USA?。對于其他請求,返回 NULL,表示無法提供數據。
返回值必須提供一個ValueProviderResult類來返回。這個類有三個構造器參數:第一個參數是與請求鍵關聯的數據項,第二個參數是作為HTML頁面一部分的該數據的安全顯示形式,第三個參數是該值相關的文化信息。這里已經指定為了InvariantCulture。
為了在應用程序中對這個值進行注冊,需要一個工廠類,以便在MVC框架需要時為這個提供器創建實例。這個工廠類必須派生于抽象類ValueProviderFactory。代碼如下圖所示:
public class CustomValueProviderFactory : System.Web.Mvc.ValueProviderFactory {public override IValueProvider GetValueProvider(ControllerContext controllerContext){return new CountryValueProvider();} }?當模型綁定器要為綁定過程獲取值時,會調用這個GetValueProvider方法。上述實現了簡單的創建并返回了CurrentTimeProvider類的一個實例,但你可以使用ControllerContext參數提供的數據,以便創建不同的值提供器,對不同種類的請求進行響應。
然后在Global.asax的Application_Start方法中注冊:
protected void Application_Start(){AreaRegistration.RegisterAllAreas();RouteConfig.RegisterRoutes(RouteTable.Routes);ValueProviderFactories.Factories.Insert(0,new CustomValueProviderFactory());}如果希望這一提供器在其他提供器不能提供數據值時作為一個備選,那么可以用Add方法把工廠追加到集合末尾:?
ValueProviderFactories.Factories.Add(new CustomValueProviderFactory());運行程序,導航到/Home/Address,如下圖所示:
? ? ? ? ? ??
點擊Submit,如下圖所示:
? ? ? ? ? ??
?創建自定義模板綁定器
通過創建一個特定類型的自定義模型綁定器,可以覆蓋默認綁定器的行為。自定義模型綁定器需要實現IModelBinder接口。創建一個AddressSummaryBinder.cs類文件,如下圖所示:
public class AddressSummaryBinder : IModelBinder{public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext){AddressSummary model = (AddressSummary)bindingContext.Model ?? new AddressSummary();model.City = GetValue(bindingContext,"City");model.Country = GetValue(bindingContext,"Country");return model;}private string GetValue(ModelBindingContext context,string name){name = (context.ModelName == "" ? "" : context.ModelName + ".") + name;ValueProviderResult result = context.ValueProvider.GetValue(name);if (result == null || result.AttemptedValue == ""){return "<Not Specified>";}else{return (string)result.AttemptedValue;}}}BindModel方法的參數是一個ControllerContext對象,可以用它來訪問當前請求的細節。另一個是ModelBindingContext對象,該對象提供了當前尋找的模型對象的細節,并能訪問MVC應用程序中其他模型綁定工具。?
| 屬性 | 描述 |
| Model | 如果手工調用了綁定,可返回傳遞給UpdataModel方法的模型對象 |
| ModelName | 返回被綁定模型的名稱 |
| ModelType | 返回被創建模型的類型 |
| ValueProvider | 返回能用于請求中獲得數據值的IValueProvider實現 |
在調用BindModel方法時,檢查已經是否設置了?ModelBindingContext 對象 的Model屬性,如果已經設置,則該模型便是將要為之生成數據值的對象,如沒有設置,則創建AddressSummary類的一個實例。通過調用GetValue方法獲取City和Country屬性的值,然后返回已經過填充的AddressSummary對象。
在GetValue方法中,通過了ModelBindingContext.ValueProvider屬性獲得的IValueProvider實現,以獲取模型對象屬性的值。
ModelName屬性能夠告訴我們,對正在尋找的屬性的名稱,是否需要追加一個前綴。當無法為一個屬性找到值,或者該屬性為空字符串時,便提供一個默認值<Not Specified>.
?
然后在Global.asax的Application_Start方法中注冊該模型綁定器:
protected void Application_Start(){AreaRegistration.RegisterAllAreas();RouteConfig.RegisterRoutes(RouteTable.Routes);ModelBinders.Binders.Add(typeof(AddressSummary), new AddressSummaryBinder());}導航到/Home/Address,并輸入內容,如下圖所示:?
? ? ? ? ? ? ? ? ?
提交后,結構如下圖所示:?
? ? ? ? ? ? ? ? ? ?
?
?用注解屬性注冊模型綁定器
可以在模型類上使用ModelBinder注解屬性進行修飾,來注冊自定義模型綁定器。不必使用Global.asax文件。如下圖所示:
[ModelBinder(typeof(AddressSummaryBinder))]public class AddressSummary{public string City { get; set; }public string Country { get; set; }}?
?
?
?
?
?
?
?
?
?
?
?
?
?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的精通ASP.NET MVC ——模型绑定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue项目积累
- 下一篇: 在windows环境下ftp服务器的文件