模型綁定(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: Home
public 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,結果如下圖所示:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLuVzVZNTOsJGboJTWxg2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL1MTO0UjNzgDM4EzNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
預設的動作綁定器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: Home
public 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; }
}