[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

相关文章
相关标签/搜索