[ASP.NET MVC 小牛之路]15 - Model Binding

Model Binding(模型綁定)是 MVC 框架根據 HTTP 請求數據建立 .NET 對象的一個過程咱們以前全部示例中傳遞給 Action 方法參數的對象都是在 Model Binding 中建立的。本文將介紹 Model Binding 如何工做,及如何使用 Model Binding,最後將演示如何自定義一個 Model Binding 以知足一些高級的需求。html

本文目錄數組

理解 Model Binding

在閱讀本節以前,讀者最好對 URL 路由和 ControllerActionInvoker 有必定的瞭解,可閱讀本系列的 [ASP.NET MVC 小牛之路]07 - URL Routing 和 [ASP.NET MVC 小牛之路]10 - Controller 和 Action (2) 兩篇文章。 框架

Model Binding(模型綁定) 是 HTTP 請求和 Action 方法之間的橋樑,它根據 Action 方法中的 Model 類型建立 .NET 對象,並將 HTTP 請求數據通過轉換賦給該對象。ide

爲了理解 Model Binding 如何工做,咱們來作個簡單的Demo,像往常同樣建立一個 MVC 應用程序,添加一個 HomeController,修改其中的 Index 方法以下:工具

public ActionResult Index(int id = 0) {
    return View((object)new[] { "Apple", "Orange", "Peach" }[id > 2 ? 0 : id]);
}

添加 Index.cshtml 視圖,修改代碼以下:post

@{
    ViewBag.Title = "Index";
}

<h2>Change the last segment of the Url to request for one fruit. </h2>
<h4>You have requested for a(an): @Model</h4>

運行應用程序,定位到 /Home/Index/1,顯示以下:ui

MVC 框架通過路由系統將 Url 的最後一個片斷 /1 解析出來,將它做爲 Index action 方法的參數來響應用戶的請求。這裏的 Url 片斷值被轉換成 int 類型的參數就是一個簡單的 Model Binding 的例子,這裏的 int 類型就是「Model Binding」中的「Model」。url

Model Binding 過程是從路由引擎接收和處理請求後開始的,這個示例使用的是應用程序默認的路由實例,以下:spa

public static void RegisterRoutes(RouteCollection routes) { 
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 
    routes.MapRoute( 
        name: "Default", 
        url: "{controller}/{action}/{id}", 
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
    );
}

當咱們請求 /Home/Index/1 URL 時,路由系統便將最後一個片斷值 1 賦給了 id 變量。action invoker 經過路由信息知道當前的請求須要 Index action 方法來處理,但它調用 Index action 方法以前必須先拿到該方法參數的值。在本系列前面文章中咱們知道,Action 方法是由默認的 Action Invoker(即 ControllerActionInvoker 類) 來調用的。Action Invoker 依靠 Model Binder(模型綁定器) 來建立調用 Action 方法須要的數據對象。咱們能夠經過 Model Binder 實現的接口來了解它的功能,該接口是 IModelBinder,定義以下:3d

namespace System.Web.Mvc { 
    public interface IModelBinder { 
        object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext); 
    } 
}

在一個 MVC 中能夠有多個 Model Binder,每一個 Binder 都負責綁定一種或多或類型的 Model。當 action invoker 須要調用一個 action 方法時,它先看這個 action 方法須要的參數,而後爲每一個參數找到和參數的類型對應的 Model Binder。對於咱們這個簡單示例,Action Invoker 會先檢查 Index action 方法,發現它有一個 int 類型的參數,而後它會定位到負責給 int 類型提供值的 Binder,並調用該 Binder 的 BindModel 方法。該方法再根據 Action 方法參數名稱從路由信息中獲取 id 的值,最後把該值提供給 Action Invoker。

Model Binder 的運行機制

Model Binder(模型綁定器),顧名思義,能夠形象的理解爲將數據綁定到一個 Model 的工具。這個 Model 是 Action 方法須要用到的某個類型(既能夠是方法參數的類型也能夠是方法內部對象的類型),要綁定到它上面的值能夠來自於多種數據源。

MVC 框架內置默認的 Model Binder 是 DefaultModelBinder 類。當 Action Invoker 沒找到自定義的 Binder 時,則默認使用 DefaultModelBinder。默認狀況下,DefaultModelBinder 從以下 4 種途徑查找要綁定到 Model 上的值:

  1. Request.Form,HTML form 元素提供的值。
  2. RouteData.Values,經過應用程序路由提供的值。
  3. Request.QueryString,所請求 URL 的 query string 值。
  4. Request.Files,客戶端上傳的文件。

DefaultModelBinder 按照該順序來查找須要的值。如對於上面的例子,DefaultModelBinder 會按照以下順序爲 id 參數查找值:

  1. Request.Form["id"]
  2. RouteData.Values["id"]
  3. Request.QueryString["id"]
  4. Request.Files["id"]

一旦找到則中止查找。在咱們的例子中,走到第 2 步在路由變量中找到了 id 的值後便不會再往下查找。

若是請求 Url 的 id 片斷是一個字符串類型的值(如「abc」),DefaultModelBinder 會怎麼處理呢?

對於簡單類型,DefaultModelBinder 會經過 System.ComponentModel 命名空間下的 TypeDescriptor 類將其轉換成和參數相同的類型。若是轉換失敗,DefaultModelBinder 則不會把值綁定到參數 Model 上。有一點須要注意,對於值類型,你們應儘可能使用可空類型或可選參數的 action 方法([ASP.NET MVC 小牛之路]02 - C#知識點提要 中有介紹),不然當值類型的參數沒有綁定到值時程序會報錯。

另外,DefaultModelBinder 是根據當前區域來類型轉換的,時間類型最容易出現問題,若是日期格式不正確則會轉換失敗。.NET 中通用的時間格式是 yyyy-MM-dd,因此咱們最好確保在URL中的時間格式是通用格式(universal format)。

綁定到複合類型

所謂的複合類型是指任何不能被 TypeConverter 類轉換的類型(大多指自定義類型),不然稱爲簡單類型。對於複合類型,DefaultModelBinder 類經過反射獲取該類型的全部公開屬性,而後依次進行綁定。

舉個例子來講明。如對於下面這個Person 類:

public class Person { 
    public int PersonId { get; set; } 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public Address HomeAddress { get; set; } 
}
public class Address { 
    public string City { get; set; } 
    public string Country { get; set; } 
}

有這麼一個 action 方法:

public ActionResult CreatePerson(Person model) { 
      return View(model);  
}

默認的 model binder 發現 action 方法須要一個 Person 對象的參數,會依次處理 Person 的每一個屬性。對於每一個簡單類型的屬性,它和前面的例子同樣去請求的數據中查找須要的值。例如,對於 PersonId 屬性,對於像下面這樣提交上來的表單:

@using(Html.BeginForm()) { 
    <div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m=>m.PersonId)</div> 

Binder 將會在 Request.Form["PersonId"] 中找到它須要的值。

若是一個複合類型的屬性也是個複合類型,如 Person 類的 HomeAddress 屬性。該屬性是一個 Address 類型,它的 Country 屬性在 View 中的使用是:

@using(Html.BeginForm()) { 
    <div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m=>m.PersonId)</div> 
    <div> 
        @Html.LabelFor(m => m.HomeAddress.Country)
        @Html.EditorFor(m=> m.HomeAddress.Country)     </div>
...

@Html.EditorFor(m=> m.HomeAddress.Country) 生成的 Html 代碼是:

<input class="text-box single-line" id="HomeAddress_Country"name="HomeAddress.Country" type="text" value="" />

表單提交後,model binder 會在 Request.Form["HomeAddress.Country"] 中查找到 Person.HomeAddress 的 Country 屬性的值。當Model binder 檢查到 Person 類型參數的 HomeAddress 屬性是一個複合類型,它會重複以前的查找工做,爲 HomeAddress 的每一個屬性查找值,惟一不一樣的是,查找的時候用的名稱不同。

應用 Bind 特性

有時候咱們還會遇到這樣的狀況,某個 action 方法的參數類型是某個對象的屬性的類型,以下面這個 DisplayAddress action 方法:

public ActionResult DisplayAddress(Address address) { 
    return View(address); 
}

它的參數是 Address 類型,是 Person 對象的 HomeAddress 屬性的類型。若咱們如今的 Index.cshtml View 中的 Model 是 Person 類型,其中有以下這樣的 form 表單:

@model MvcApplication1.Models.Person 
...
@using(Html.BeginForm("DisplayAddress", "Home")) { 
    <div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m=>m.PersonId)</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> 
}

那麼咱們如何把 Person 類型的對象傳遞給 DisplayAddress(Address address) 方法呢?點提交按鈕後,Binder 能爲 Address 類型的參數綁定 Person 對象中的 HomeAddress 屬性值嗎?咱們不妨建立一個 DisplayAddress.cshtml 視圖來驗證一下:

@model MvcApplication1.Models.Address

@{
    ViewBag.Title = "Address";
}
<h2>Address Summary</h2>
<div><label>City:</label>@Html.DisplayFor(m => m.City)</div>
<div><label>Country:</label>@Html.DisplayFor(m => m.Country)</div> 

運行程序,點提交按鈕,效果以下:

 

Address 兩個屬性的值沒有顯示出來,說明 Address 類型的參數沒有綁定到值。問題在於生成 form 表單的 name 屬性有 HomeAddress 前綴(name="HomeAddress.Country"),它不是 Model Binder 在綁定 Address 這個 Mdoel 的時候要匹配的名稱。要解決這個問題能夠對 action 方法的參數類型應用 Bind 特性,它告訴 Binder 只查找特定前綴的名稱。使用以下:

public ActionResult DisplayAddress([Bind(Prefix="HomeAddress")]Address address) {
    return View(address);
}

再運行程序,點提交按鈕,效果以下:

 

這種用法雖然有點怪,可是很是有用。更有用的地方在於:DisplayAddress action 方法的參數類型 Address 不必定必須是 Person 的 HomeAddress 屬性的類型,它能夠是其餘類型,只要該類型中含有和 City

或 Country 同名的屬性就都會被綁定到。

不過,要注意的是,使用 Bind 特性指定了前綴後,須要提交的表單元素的 name 屬性必須有該前綴才能被綁定。

Bind 特性還有兩個屬性,Exclude 和 Include。它們能夠指定在 Mdoel 的屬性中,Binder 不查找或只查找某個屬性,即在查找時要麼只包含這個屬性要麼不包含這個屬性。以下面的 action 方法:

public ActionResult DisplayAddress([Bind(Prefix = "HomeAddress", Exclude = "Country")]Address address) {
    return View(address);
}

這時 Binder 在綁定時不會對 Address 這個 Model 的 Country 屬性綁定值。

上面 Bind 特性的應用只對當前 Action 有效。若是要使得 Bind 特性對 Model 的影響在整個應用程序都有效,能夠把它放在該 Model 的定義處,如:

[Bind(Include = "Country")] public class Address {
    public string City { get; set; }
    public string Country { get; set; }
}

對 Address 類應用了 [Bind(Include = "Country")] 特性之後,Binder 在給 Address 模型綁定時只會給 Country 屬性綁定值。

綁定到數組

Model Binder 把請求提交的數據綁定到數組和集合模型上有很是好的支持,下面先來演示MVC如何支持對數組模型的綁定。

先看一個帶有數組參數的 action 方法:

public class HomeController : Controller {
    public ActionResult Names(string[] names) {
        names = names ?? new string[0];
        return View(names);
    }
}

Names action方法有一個名爲 names 的數組參數,Model Binder 將查找全部名稱爲 names 的條目的值,並建立一個 Array 對象存儲它們。

接着咱們再來爲Names action建立View:Names.cshtml,View 中包含若干名稱爲 names 的表單元素:

@model string[]
@{
    ViewBag.Title = "Names";
}

<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");
}

當 View 的 Model 中沒有數據時,View 生成的表單部分的 Html 代碼以下:

<form action="/Home/Names" method="post"> 
    <div><label>1:</label><input id="names" name="names" type="text" value="" /></div> 
    <div><label>2:</label><input id="names" name="names" type="text" value="" /></div> 
    <div><label>3:</label><input id="names" name="names" type="text" value="" /></div> 
    <button type="submit">Submit</button> 
</form> 

當咱們提交表單後,Model Binder 查看 action 方法須要一個 string 類型的數組,它便從提交的數據中查找全部和參數名相同的條目的值組裝成一個數組。運行程序,能夠看到以下效果:

 

綁定到集合

簡單類型的集合(如 IList<string>)的綁定和數組是同樣的。你們能夠把上面例子的 action 方法參數類型和 View 的 Model 類型換成 IList<string> 看下效果,這裏就不演示了。咱們來看看 Model Binder 是如何支持複合類型集合的綁定的。

先建立一個帶有 IList<Address> 參數的 action 方法:

public ActionResult Address(IList<Address> addresses) {
    addresses = addresses ?? new List<Address>();
    return View(addresses);
}

對於複合類型的集合參數,在 View 中表單元素的 name 屬性應該怎樣命名才能被 Model Binder 識別爲集合呢?下面爲Address action 添加一個視圖,注意看表單部分,以下:

@using MvcApplication1.Models
@model IList<Address>
@{
    ViewBag.Title = "Address";
}

<h2>Addresses</h2>
@if (Model.Count() == 0) {
    using (Html.BeginForm()) {
        for (int i = 0; i < 2; 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 (Address str in Model) {
        <p>@str.City, @str.Country</p>
    }
    @Html.ActionLink("Back", "Address");
}

若是是「編輯」狀態(即 View Model 有值的時候)還能夠這樣寫:

...
<div><label>City:</label>@Html.EditorFor(m => m[i].City)</div>
<div><label>Country:</label>@Html.EditorFor(m => m[i].Country)</div>
...

這樣寫的目的是爲了生成以下 name 屬性值: 

<fieldset> 
    <legend>Address 1</legend> 
    <div> 
        <label>City:</label> 
        <input class="text-box single-line" name="[0].City" type="text" value="" /> 
    </div> 
    <div> 
        <label>Country:</label> 
        <input class="text-box single-line" name="[0].Country" type="text" value="" /> 
    </div> 
</fieldset>
...

當 Model Binder 發現 Address action 方法須要一個 Address 集合做爲參數時,它便從提交的數據中從索引 [0] 開始查找和 Address 的屬性名稱相同的數據值,Model Binder 將建立一個 IList<Address> 集合來存儲這些值。運行程序,Url 定位到 /Home/Address,點提交按鈕後,效果以下:

 

手動調用 Model Binding

當 action 方法定義了參數時,Model Binding 的過程是自動的。咱們也能夠對Binding的過程進行手動控制,如控制 model 對象如何被實例化、從哪裏獲取數據及傳遞了錯誤的數據時如何處理。

下面修改 Address action 方法來演示瞭如何手動調用 Model Binding,以下:

public ActionResult Address() {
    IList<Address> addresses = new List<Address>();
    UpdateModel(addresses);     return View(addresses);
}

功能上和前一個示例是同樣的。這裏的  UpdateModel 方法接收一個model 對象做爲參數,默認的 Model Binder 將爲該 model 對象的全部公開屬性進行綁定處理。

在前面咱們講到 Model Binding 從 Request.Form、RouteData.Values、Request.QueryString 和 Request.Files四個地方獲取數據。當咱們手動調用 Binding 的時候,能夠指定只從某一個來源獲取數據,以下是隻從 Request.Form 中獲取數據的例子:

public ActionResult Address() {
    IList<Address> addresses = new List<Address>();
    UpdateModel(addresses, new FormValueProvider(ControllerContext));
    return View(addresses);
}

UpdateModel 方法指定了第二個參數是一個 FormValueProvider 的實例,它將使用 Model Binder 從只從 Request.Form 中查找須要的數據。FormValueProvider 類是 IValueProvider 接口的實現,是 Value Provider 中的一種,相應的,RouteData.Values、Request.QueryString 和 Request.Files 的 Value Provider 分別是 RouteDataValueProvider、QueryStringValueProvider和HttpFileCollectionValueProvider。

另外,還有一種限制 Model Binder 數來源的方法,以下所示:

public ActionResult Address(FormCollection formData) {
    IList<Address> addresses = new List<Address>();
    UpdateModel(addresses, formData);     return View(addresses);
}

它是用 Action 方法的某個集合類型的參數來指定並存儲從某一個來源獲取的數據,這個集合類型(示例的 FormCollection) 也是 IValueProvider 接口的一個實現。

有時候用戶會提交一些 和 model 對象的屬性不匹配的數據,如不合法的日期格式或給數值類型提供文本值,這時候綁定會出現錯誤,Model Binder 會用 InvalidOperationException 來表示。能夠經過 Controller.ModelState 屬性找到具體的錯誤信息,而後反饋給用戶:

public ActionResult Address(FormCollection formData) {
    IList<Address> addresses = new List<Address>();
    try {
        UpdateModel(addresses, formData);
    }
    catch (InvalidOperationException ex) {
        var allErrors = ModelState.Values.SelectMany(v => v.Errors);
        // do something with allErrors and provide feedback to user 
    }
    return View(addresses);
}

也可使用 TryUpdateModel 方法:

public ActionResult Address(FormCollection formData) {
    IList<Address> addresses = new List<Address>();
    if (TryUpdateModel(addresses, formData)) {
        // proceed as normal 
    }
    else {
        // provide feedback to user 
    }
    return View(addresses); 
}

注意,當手動調用 Model Binding 時,這種綁定錯誤不會被識別爲異常,咱們能夠用 ModelState.IsValid 屬性來檢查提交的數據是否合法。

自定義 Value Provider

經過自定義 Value Provider 咱們能夠爲 Model Binding 添加本身的數據源。前面咱們講到了四種內置 Value Provider 實現的接口是 IValueProvider,咱們能夠實現這個接口來自定義一個 Value Provider。先來看這個接口的定義:

namespace System.Web.Mvc { 
    public interface IValueProvider { 
        bool ContainsPrefix(string prefix); 
        ValueProviderResult GetValue(string key); 
    } 
}

ContainsPrefix 方法是 Model Binder 根據給定的前綴用來判斷是否要解析所給數據。GetValue 方法根據數據的key返回所須要值。下面咱們添加一個 Infrastructure 文件夾,建立一個名爲 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("China", "China", CultureInfo.InvariantCulture);
        else
            return null;
    }
}

這就自定義好了一個 Value Provider,當須要一個 Country 的值時,它始終返回"China",其它返回 null。ValueProviderResult 類的構造器有三個參數,第一個參數是原始值對象,第二個參數是原始對象的字符串表示,最後一個是轉換這個值所關聯的 culture 信息。

爲了讓 Model Binder 調用這個 Value Provider,咱們須要建立一個能實現化它的類,這個類須要繼承  ValueProviderFactory 抽象類。以下咱們建立一個這樣的類,名爲 CustomValueProviderFactory:

public class CustomValueProviderFactory : ValueProviderFactory {
    public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
        return new CountryValueProvider();
    }
}

當 model binder 在綁定的過程當中須要獲取值時會調用這裏的 GetValueProvider 方法。這裏咱們沒有作別的處理,直接返回了一個 CountryValueProvider 實例。

最後咱們須要在 Global.asax 文件中的 Application_Start 方法中進行註冊,以下:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    ValueProviderFactories.Factories.Insert(0, new CustomValueProviderFactory());
...

經過 ValueProviderFactories.Factories 靜態集合的 Insert 方法註冊了咱們的 CustomValueProviderFactory 類。Insert 方法中的 0 參數保證 Binder 將首先使用自定義的類來提供值。若是咱們想在其餘 value provider 不能提供值的時候使用,那麼咱們可使用 Add 方法,以下:

... 
ValueProviderFactories.Factories.Add(new CustomValueProviderFactory()); 
... 

運行程序,URL 定位到 /Home/Address,看到的效果以下:

 

自定義 Model Binder

咱們也能夠爲特定的 Model 自定義 Model Binder。前面講了默認的 Model Binder 實現的接口是 IModelBinder(前文列出了它的定義),自定義的 Binder 天然也須要實現該接口。下面咱們在 Infrastructure 文件夾中添加一個實現了該接口的名爲  AddressBinder 類,代碼以下:

public class AddressBinder : IModelBinder {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        Address model = (Address)bindingContext.Model ?? new Address();
        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;
    }
}

當 MVC 框架須要一個 model 類型的實現時,則調用 BindModel 方法。它的 ControllerContext 類型參數提供請求相關的上下文信息,ModelBindingContext 類型參數提供 model 對象相關的上下文信息。ModelBindingContext 經常使用的屬性有Model、ModelName、ModelType 和 ValueProvider。這裏的 GetValue 方法用到的 context.ModelName 屬性能夠告訴咱們,若是有前綴(通常指複合類型名),則須要把它加在屬性名的前面,這樣 MVC 才能獲取到以 [0].City、[0].Country 名稱傳遞的值。

而後咱們須要在 Global.asax 的 Application_Start 方法中對自定義的 Model Binder 進行註冊,以下所示:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    //ValueProviderFactories.Factories.Insert(0, new CustomValueProviderFactory());
    ModelBinders.Binders.Add(typeof(Address), new AddressBinder());
...

咱們經過 ModelBinders.Binders.Add 方法對自定義的 Model Binder 進行註冊,參數中指定了應用該 Binder 的 Model 類型和自定義的 Binder 實例。運行程序,URL 定位到 /Home/Address,效果以下:

 

 


參考:Pro ASP.NET MVC 4 4th Edition

相關文章
相關標籤/搜索