《Pro ASP.NET MVC 3 Framework》学习笔记之三十【模型绑定】

模型绑定(Model Binding)是使用浏览器发起Http请求时的数据创建.NET对象的过程。我们每一次定义带参数的action方法时就已经依靠了模型绑定——这些参数对象是通过模型绑定创建的。这一章会介绍模型绑定的原理以及针对高级使用必要的定制模型绑定的技术。

理解模型绑定(Understanding Model Binding)

想象下我们创建了一个控制器如下:

View Code

using System;
using System.Web.Mvc;
using MvcApp.Models; 

namespace MvcApp.Controllers
{
    public class HomeController : Controller
   {
        public ViewResult Person(int id)
       {
            // 获取一条person记录
            Person myPerson = null;
           //检索数据的逻辑...
            return View(myPerson);
        }
    }
}

action方法定义在HomeController类里面,VS默认创建的路由就是调用这里的action方法。当我们请求一个如/Home/Person/23的URL,MVC框架会将请求的详细信息映射通过一种传递合适的值或对象作为参数的方式映射到action方法。action调用者负责在调用action之前获取这些值,默认的action调用者ControllerActionInvoker依赖于Model Binders,它们是通过IModelBinder接口定义的,如下:

View Code

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

在MVC程序里面可以有多个model binders,每一个binder可以绑定一个或多个model类型。当action调用者需要调用一个action方法,它会寻找定义在方法里面的参数并且找到对应负责每一个参数类型的model binder。在最开始的例子里面,action调用者会发现我们的action方法具有一个int型的参数,所以它会定位到负责绑定int值的binder并调用自己的BindModel方法,如果没有能够处理int值的binder,那么默认的model binder会被使用。

model binder是用来生成匹配action方法的参数值,这通常意味着传递一些请求元素的数据(例如form或query string值),但是MVC框架不会对如何获取这些值有任何限制。

使用默认的Model Binder(Using the Default Model Binder)

尽管一个应用程序有多个binders,大多数都是依赖于内置的binder类——DefaultModelBinder。这也是当action调用者找不到自定义的binder时使用的binder。默认情况下,这个model binder搜索了4个路径,如下所示:
Request.Form:HTML表单提供的值
RouteData.Values:使用应用程序路由获取的值
Request.QueryString:包含在URL的请求字符串里面的数据
Request.Files:作为请求部分被上传的文件

上面四个路径是按顺序搜索的,例如在上面的例子中,action方法需要一个参数id,DefaultModelBinder会检查action方法并寻找名为id的参数。它会按下面的顺序来寻找:
1.  Request.Form["id"]
2.  RouteData.Values["id"]
3.  Request.QueryString["id"]
4.  Request.Files["id"]
只要有一个值找到,搜索就会停止。

绑定简单类型(Binding to Simple Types)

当处理简单的参数类型时,DefaultModelBinder会试图使用System.ComponentModel.TypeDescriptor类将request数据(字符串型)转换为对应action方法参数的类型。如果这个值不能转换,那么DefaultModelBinder将不能够绑定到model。如果要避免这个问题,可以修改下参数,如:public ViewResult RegisterPerson(int? id) {...},这样修改以后,如果不能匹配,参数的值会为null。还可以提供一个默认值如:public ViewResult RegisterPerson(int id = 23) {...}

绑定复杂类型(Binding to Complex Types)

如果action方法参数是一个复杂类型(就是不能使用TypeConverter转换的类型),那么DefaultModelBinder会使用反射获取公共的属性并轮流绑定每一个属性。使用前面的Person.cs来举例,如下:

View Code

public 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; }
    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; }
}

默认的model binder会检查这个类的属性是否都是简单类型,如果是,binder就会在请求里面具有相同的名称的数据项。对应例子来说就是FirstName属性会引起binder寻找一个名为FirstName的数据项。如果这个类的属性(如Address)仍然是个复杂类型,那么对这个类型重复上面的处理过程。在寻找Line1属性的值时,model binder会寻找HomeAddress.Line1的值。

指定自定义的前缀(Specifying Custom Prefixes)

当默认的model binder寻找对应的数据项时,我们可以指定一个自定义的前缀。这对于在HTML里包含了额外的model对象时非常有用。举例如下:

View Code

@using MvcApp.Models;
@model MvcApp.Models.Person 

@{
    Person myPerson = new Person() {
        FirstName = "Jane", LastName = "Doe"
    };
} 

@using (Html.BeginForm()) { 

     @Html.EditorFor(m => myPerson)
     @Html.EditorForModel() 

    <input type="submit" value="Submit" />
} 

我们使用了EditorFor helper方法来对Person对象生成HTML,lambda表达式的输入是一个model对象(用m代替),当使用这种方式以后,生成的HTML元素的属性名会有一个前缀,这个前缀来源于我们在EditorFor里面的变量名myPerson。运行以后可以看到页面源代码如下:


public ActionResult Index(Person firstPerson,Person myPerson){...},第一个参数对象使用没有前缀的数据绑定,第二个参数寻找以参数名开头的数据绑定。
如果我们不想用这种方式,可以使用Bind特性来指定,如下:
public ActionResult Register(Person firstPerson, [Bind(Prefix="myPerson")] Person secondPerson)
这样就设置了Prefix属性的值为myPerson,这意味着默认的model binder将使用myPerson作为数据项的前缀,即使这里第二个参数的名为secondPerson。

有选择的绑定属性(Selectively Binding Properties)

想象一下如果Person类的IsApproved属性是非常敏感的信息,我们能够通过模版绑定来不呈现该属性,但是一些恶意的用户可以简单的在一个URL里附加?/IsAdmin=true后来提交表单。如果这种情况发生,model binder在绑定的过程会识别并使用这个数据的值。幸运的是,我们可以使用"Bind"特性来从绑定过程包含或排除model的属性。具体的示例如下:

public ActionResult Register([Bind(Include="FirstName, LastName")] Person person) {...}//仅仅包含Person属性里面的FirstName和LastName属性
public ActionResult Register([Bind(Exclude="IsApproved, Role")] Person person) {...}//排除了IsApproved属性

上面这样使用Bind仅仅是针对单个的action方法,如果想将这种策略应用到所有控制器的所有action方法,可以在model类本身使用该特性,如下:

View Code

    [Bind(Exclude = "IsApproved")]
    public 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; }
        public bool IsApproved { get; set; }
        public Role Role { get; set; }

        public DateTime CurrentTime { get; set; }

    }

这样就会在所有的用到给model的action方法生效。

注:如果Bind特性被应用到model类并且也在action方法的参数中使用,在没有其他的应用程序特性排除它时会被包含在绑定过来里。这意味着应用到model的类的策略不能通过应用一个较小限制策略到action方法参数来重写。下面用示例说明:

首先添加一个Model Person如下:

View Code

using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ModelBinding.Models
{
    [Bind(Exclude = "IsApproved")]
    public 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; }
        public bool IsApproved { get; set; }
        public Role Role { get; set; }

        public DateTime CurrentTime { 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
    }

}

对Person类添加了Bind特性,排除了IsApproved属性,然后添加Controller如下:

View Code

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            Person myPerson = new Person
            {
                PersonId = 1,
                FirstName = "Joe",
                LastName = "Smith",
                BirthDate = DateTime.Parse("1988/12/01"),
                HomeAddress = new Address
                {
                    Line1 = "123 North Street",
                    Line2 = "West Bridge",
                    City = "London",
                    Country = "UK",
                    PostalCode = "WC2R 1SS"
                },
                IsApproved = true,
                Role = Role.User
            };
            return View("PersonEdit", myPerson);
        }

        [HttpPost]
        public ActionResult Index(Person person, Person myPerson)
        {
            return View("PersonDisplay", person);
        }
    }

最后添加两个涉及的视图PersonEdit和PersonDisplay,如下:

View Code

//PersonEdit.cshtml
@using ModelBinding.Models;
@model ModelBinding.Models.Person
<style type="text/css">
    .check-box
    {
        margin: 0.5em 0 0 0;
    }
</style>
@{
    Person myPerson = new Person()
    {
        FirstName = "xuefei",
        LastName = "zhang"
    };
}
@using (Html.BeginForm())
{
    @Html.EditorFor(m => myPerson)
    @Html.EditorForModel()
    <input type="submit" value="Submit" />
}

//PersonDisplay.cshtml
@model ModelBinding.Models.Person
<div class="column">
    @Html.DisplayForModel()
</div>
<div class="column">
    @Html.DisplayFor(m => m.HomeAddress)
</div>

运行程序如下:

另外,我们在URL里面添加?IsApproved=true试试看有什么效果:

接着继续测试,刚才不是有说到关于策略重写的问题吗,这里我们对【HttpPost】的Index action的参数添加一个Bing特性如下:

[HttpPost]
public ActionResult Index([Bind(Include = "IsApproved")]Person person, Person myPerson)
{
    return View("PersonDisplay", person);
}

理论上这里的是没有办法对Person上应用的策略进行重写的,有图为证:

绑定到数组和集合(Binding to Arrays and Collections)

处理具有相通名字的多条数据项是默认的model binder的一个非常优雅的功能,示例说明如下:
创建两个视图Movies和MoviesDisplay,如下:

View Code

@*Movies*@
@{
    ViewBag.Title = "Movies";
}
输入三部你最喜爱的影片名:
@using (Html.BeginForm())
{
    @Html.TextBox("movies")
    @Html.TextBox("movies")
    @Html.TextBox("movies")
    <input type="submit" />
}

@*MoviesDisplay*@
@model List<string>
@{
    ViewBag.Title = "MoviesDisplay";
}
你最喜爱的电影:
@foreach (string movie in Model)
{
    <p>@movie</p>
}

添加对应的action,如下:

View Code

public ViewResult Movies()
{
     return View();
}

[HttpPost]
public ViewResult Movies(List<string> movies)
{
     return View("MoviesDisplay", movies);
}

model binder会寻找用户提交的所有值并把它们通过List<string>集合传递到Movies action方法,binder是足够的聪明的识别不同的参数类型,例如我们可以将List<string>改成IList<string>或是string[]。

绑定到自定义类型的集合(Binding to Collections of Custom Types)

上面的多个值的绑定技巧非常好用,但如果我们想应用到自定义的类型,就必须用一种合适的格式来生成HTML。添加MPerson视图和MPersonDisplay视图如下:

View Code

@*MPerson.cshtml*@
@model List<ModelBinding.Models.Person>
@{
    ViewBag.Title = "MPerson";
}
@using (Html.BeginForm())
{
    <h4>
        First Person</h4>
    <input type="hidden" name="[0].key" value="firstPerson" />
    @:First Name:@Html.TextBox("[0].value.FirstName")
    @:Last Name:@Html.TextBox("[0].value.LastName")

    <h4>
        Second Person</h4>
    <input type="hidden" name="[1].key" value="secondPerson" />
    @:First Name:@Html.TextBox("[1].value.FirstName")
    @:Last Name:@Html.TextBox("[1].value.LastName")

    <input type="submit" />
}

@*MPersonDisplay*@
@using ModelBinding.Models;
@model IDictionary<string, ModelBinding.Models.Person>
@foreach (string key in Model.Keys)
{
    @Html.DisplayFor(m => m[key]);
}

添加Controller,如下:

View Code

        public ViewResult MPerson()
        {
            List<Person> people = new List<Person> {
            new Person{FirstName="xuefei",LastName="zhang"},
            new Person{FirstName="si",LastName="Li"}
            };
            return View(people);
        }

        [HttpPost]
        public ViewResult MPerson(IDictionary<string, Person> people)
        {
            return View("MPersonDisplay", people);
        }

运行程序可以看到效果,要绑定这些数据,我们仅仅定义了一个action并接收一个视图model类型的集合参数,如:

[HttpPost]
public ViewResult Register(List<Person> people) {...}
因为我们绑定到一个集合,默认的model binder会搜索用一个索引做前缀的Person类的属性。当然,我们不必使用模版化的helper方法来生成HTML,可以显示地在视图里面做,如下:

View Code

<h4>First Person</h4>
First Name: @Html.TextBox("[0].FirstName")
Last Name: @Html.TextBox("[0].LastName") 

<h4>Second Person</h4>
First Name: @Html.TextBox("[1].FirstName")
Last Name: @Html.TextBox("[1].LastName") 

只要我们保证了索引值被恰当的创建,model binder会找到并绑定所有定义的数据元素。

使用非线性的索引绑定到集合(Binding to Collections with Nonsequential Indices)

除了上面使用数字序列的索引值外,还可以使用字符串来作为键值,这在当我们想要使用js在客户端动态的添加或移除控件时非常有用,而且不用去维护索引的顺序。采用这种方式需要定义一个hidden input元素name为指定key的index。如下:

<h4>First Person</h4>
<input type="hidden" name="index" value="firstPerson"/>
First Name: @Html.TextBox("[firstPerson].FirstName")
Last Name: @Html.TextBox("[firstPerson].LastName") 

<h4>Second Person</h4>
<input type="hidden" name="index" value="secondPerson"/>
First Name: @Html.TextBox("[secondPerson].FirstName")
Last Name: @Html.TextBox("[secondPerson].LastName") 

我们用input元素的前缀来匹配index隐藏域的值,model binder会检测到index并使用它在绑定过程中关联数据的值。

绑定到一个Dictionary(Binding to a Dictionary)

默认的model binder是能够绑定到一个Dictionary的,但是只有当我们遵循一个非常具体的命名序列时才行。如下:

<h4>First Person</h4>
<input type="hidden" name="[0].key" value="firstPerson"/>
First Name: @Html.TextBox("[0].value.FirstName")
Last Name: @Html.TextBox("[0].value.LastName") 

<h4>Second Person</h4>
<input type="hidden" name="[1].key" value="secondPerson"/>
First Name: @Html.TextBox("[1].value.FirstName")
Last Name: @Html.TextBox("[1].value.LastName") 

此时可以使用如下的action来获取值
[HttpPost]
public ViewResult Register(IDictionary<string, Person> people) {...}

手动调用模型绑定(Manually Invoking Model Binding)

模型绑定的过程是在一个action方法定义了参数时自动执行的,但是可以直接控制这个过程。这给了我们对于model对象如何实例化,数据的值从哪里获取,以及数据强制转换错误如何处理等更多明确的控制权。示例如下:

View Code

//Controller里添加action
[HttpPost]
public ActionResult RegisterMember() { 

    Person myPerson = new Person();
    UpdateModel(myPerson);
    return View(myPerson);
} 

//添加Register视图
@using ModelBinding.Models;
@model ModelBinding.Models.Person
<style type="text/css">
    .check-box
    {
        margin: 0.5em 0 0 0;
    }
</style>
@using (Html.BeginForm("RegisterMember", "Home"))
{
    @Html.EditorForModel()
    <input type="submit" value="Submit" />
}

UpdateModel方法获取一个model对象作为参数并试图使用标准绑定过程获取model对象里面公共属性的值。手动调用model绑定的其中一个原因是为了支持DI。例如,如果我们使用了一个应用程序范围的依赖解析器,那么我们能够添加DI到这里的Person对象的创建,如下:

View Code

[HttpPost]
public ActionResult RegisterMember() { 

    Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
    UpdateModel(myPerson);
    return View(myPerson);
} 

正如我们阐释的,这不是在绑定过程引入DI的唯一方式,后面还会介绍其他的方式。

将绑定限制到指定的数据源(Restricting Binding to a Specific Data Source)

当我们手动的调用绑定时,可以限制绑定到指定的数据源。默认情况下,bingder会寻找四个地方:表单数据,路由数据,querystring,以及上传的文体。下面例子说明如何限制绑定到单个数据源——表单数据。修改action方法如下:

[HttpPost]
public ActionResult RegisterMember()
{
    //Person myPerson = new Person();
    //UpdateModel(myPerson);
    Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
    UpdateModel(myPerson, new FormValueProvider(ControllerContext));
    return View(myPerson);
}

这里的UpdateModel是重载的版本接收一个IValueProvider接口实现作为参数,从而指定了绑定过程的数据源。每一个默认的数据源都对应了一个对该接口的实现,如下:
1.Request.Form——>FormValueProvider
2.RouteData.Values——>RouteDataValueProvider
3.Request.QueryString——>QueryStringValueProvider
4.Request.Files——>HttpFileCollectionValueProvider

最常用的现在数据源的方式就是只在寻找Form里面的值,有一个非常灵巧的绑定技巧,以至于我们不用创建一个FormValueProvider的实例,如下:

[HttpPost]
public ActionResult RegisterMember(FormCollection formData)
{
     Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
     UpdateModel(myPerson, formData);
     return View(myPerson);
}

FormCollection类实现了IValueProvider接口,并且如果我们定义的action方法接收一个该类型的参数,model binder会提供一个可以直接传递给UpdateModel方法的对象。

处理绑定错误(Dealing with Binding Errors)

用户难免会提交一些不能绑定到相应的model属性的值,如未验证的日期或文本当成数值。下一章会介绍相关的绑定验证的内容,这里在使用UpdateModel方法时,我们必须准备捕获处理相关的异常,并使用ModelState向用户提示错误的信息,如下:

View Code

        [HttpPost]
        public ActionResult RegisterMember(FormCollection formData)
        {
            Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
            try
            {
                UpdateModel(myPerson, formData);
            }
            catch (InvalidOperationException ex)
            {
                //这里根据ModelState提供UI反馈
                throw ex;
            }
            return View("PersonDisplay", myPerson);
        }

除了try...catch之外,还可以使用TryUpdateModel()方法,它的返回值是bool值,如下:

View Code

        [HttpPost]
        public ActionResult RegisterMember(FormCollection formData)
        {
            Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
            //try
            //{
            //    UpdateModel(myPerson, formData);
            //}
            //catch (InvalidOperationException ex)
            //{
            //    //这里根据ModelState提供UI反馈
            //    throw ex;
            //}
            if (TryUpdateModel(myPerson, formData))
            {
                //...
            }
            else
            {
                //这里根据ModelState提供UI反馈
            }
            return View("PersonDisplay", myPerson);
        }

使用模型绑定接收文件上传(Using Model Binding to Receive File Uploads)

为了接收上传的文件,需要定义一个action方法并接收一个HttpPostedFileBase类型的参数。然后,model binder将会使用跟上传的文件一致的数据填充这个参数。如下:

这里的关键是要设定enctype属性的值为"multipart/form-data".如果不这样做,浏览器只会发送文件名而不是文件本身(这是浏览器的运行原理决定的).

自定义模型绑定系统(Customizing the Model Binding System)

前面介绍都是默认的模型绑定系统,我们同样可以定制自己的模型绑定系统,下面会展示一些例子:

创建一个自定义的Value Provider

通过定义一个value provider,我们可以在模型绑定过程添加自己的数据源。value providers实现IValueProvider接口,如下:

View Code

using System.Web.Mvc;
using System.Globalization;

namespace ModelBinding.Infrastructure
{
    public class CurrentTimeValueProvider : IValueProvider
    {

        public bool ContainsPrefix(string prefix)
        {
            return string.Compare("CurrentTime", prefix, true) == 0;
        }

        public ValueProviderResult GetValue(string key)
        {
            return ContainsPrefix(key) ?
                new ValueProviderResult(DateTime.Now, null, CultureInfo.InvariantCulture)
                : null;
        }
    }

    public class CurrentTimeValueProviderFactory : ValueProviderFactory
    {

        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
            return new CurrentTimeValueProvider();
        }
    }
}

我们只响应针对CurrentTime的请求,并当接收到这样的请求时,返回DateTime.Now属性的值,对其他的请求,返回null,表示不能提供数据。我们必须将数据作为ValueProviderResult类型返回。为了注册自定义的Value Provider,我们需要创建一个用来产生Provider实例的工厂,这个类从ValueProviderFactory派生,如下:

View Code

protected void Application_Start()
{
     AreaRegistration.RegisterAllAreas();

     ValueProviderFactories.Factories.Add(0, new CurrentTimeValueProviderFactory());
     RegisterGlobalFilters(GlobalFilters.Filters);
     RegisterRoutes(RouteTable.Routes);
}

通过向ValueProviderFactories.Factories集合里面添加一个实例来注册我们自己的工厂,model binder 会按顺序寻找value provider,如果想让我们的value provider优先,可以插入序号0,就像上面的代码中写的。如果想放在最后可以直接这样添加:ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory()); 可以测下我们自己的Value Provider,添加一个Action方法如下:

public ActionResult Clock(DateTime currentTime)
{
      return Content("The time is " + currentTime.ToLongTimeString());
}

 

创建一个依赖感知的Model Binder(Creating a Dependency-Aware Model Binder)

前面有介绍过使用手动模型绑定引入依赖注入到绑定过程,但是还有一种更加优雅的方式,就是通过从DefaultModelBinder派生来创建一个DI敏感的binder并且重写CreateModel方法,如下所示:

View Code

using System.Web.Mvc;

namespace ModelBinding.Infrastructure
{
    public class DIModelBinder : DefaultModelBinder
    {
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            return DependencyResolver.Current.GetService(modelType) ?? base.CreateModel(controllerContext, bindingContext, modelType);
        }
    }
}

接着需要注册该binder,如下:

View Code

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas(); 

    ModelBinders.Binders.DefaultBinder = new DIModelBinder(); 

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
} 

 

创建一个自定义的Model Binder

我们能够通过创建一个针对具体类型的自定义model binder来重写默认的binder行为,如下:

View Code

using System.Web.Mvc;
using ModelBinding.Models;

namespace ModelBinding.Infrastructure
{
    public class PersonModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            //判断,如果存在一个model则更新,否则创建
            Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));

            //检查下这个Value Provider是否具有必须的前缀
            bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
            string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";

            //填充model对象的字段
            model.PersonId = int.Parse(GetValue(bindingContext, searchPrefix, "PersonId"));
            model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName");
            model.LastName = GetValue(bindingContext, searchPrefix, "LastName");
            model.BirthDate = DateTime.Parse(GetValue(bindingContext, searchPrefix, "BirthDate"));
            model.IsApproved = GetCheckedValue(bindingContext, searchPrefix, "IsApproved");
            model.Role = (Role)Enum.Parse(typeof(Role), GetValue(bindingContext, searchPrefix, "Role"));
            return model;
        }

        private string GetValue(ModelBindingContext context, string prefix, string key)
        {
            ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
            return vpr == null ? null : vpr.AttemptedValue;
        }

        private bool GetCheckedValue(ModelBindingContext context, string prefix, string key)
        {
            bool result = false;
            ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
            if (vpr != null)
            {
                result = (bool)vpr.ConvertTo(typeof(bool));
            }
            return result;
        }
    }
}

下面我一步步来解析这段代码,首先我们获取将要绑定的model对象如下:
Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));

当model binding过程被手动调用时,我们传递一个model对象到UpdateModel方法;该对象通过BindingContext类的Model属性是可用的,一个好的model binder会检查一个model 对象是否是可用的并且只有当它是可以的时候才会被用于绑定过程,否则我们就需要负责创建一个model对象,并使用应用程序范围级别的依赖解析器(第10章有介绍)

接着看我们是否需要使用一个前缀请求来自value provider的数据:
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";

BindingContext.ModelName属性返回绑定的model的名称,如果我们在视图里呈现这个model对象,生成的HTML不会有前缀,但是ModelName都要返回Action方法的参数名,所以我们检查value provider的值前缀是否存在。我通过BindingContext.ValueProvider属性访问value providers,这给了我们一个统一的方式来访问所有可用的value providers,并且请求按顺序传递给它们。如果value data里面存在前缀则使用。

接着我们使用value providers获取Person对象的属性值,如下:
model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName");

我们定义了一个GetValue的方法从统一的value provider获取ValueProviderResult对象并且通过AttemptedValue属性提取一个字符串值。
在前面有提到过当呈现一个CheckBox时,HTML helper方法创建一个hidden input元素来保证我们能够获取一个没有选中的值,这会稍微对Model绑定有一些影响,因为value provider将会把两个值作为字符串数组提供给我们。

为了解决这个问题,我们使用ValueProviderResult.ConvertTo方法来协调并给出正确的值:
result = (bool)vpr.ConvertTo(typeof(bool));
接着注册model binder: ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());

创建Model Binder提供程序(Creating Model Binder Providers)

一种注册自定义的model binders替代的方式就是通过实现IModelBinderProvider接口来创建一个model binder provider,如下:

View Code

using System.Web.Mvc;
using ModelBinding.Models;

namespace ModelBinding.Infrastructure
{
    public class CustomModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(Type modelType)
        {
            return modelType == typeof(Person) ? new PersonModelBinder() : null;
        }
    }
}

这种方式更加灵活,特别是在我们有多个自定义的binders或多个providers维护时。接着注册刚创建的provider:
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());

使用ModelBinder属性(Using the ModelBinder Attribute)

还有最后一种注册自定义model binder的方式就是使用ModelBinder特性到model类,如下:

View Code

[ModelBinder(typeof(PersonModelBinder))]
public 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; }
    public bool IsApproved { get; set; }
    public Role Role { get; set; }
} 

ModelBinder特性具有的单个参数让我们指定绑定对象的类型,在这个三种方式中,我们倾向于实现IModelBinderProvider接口来处理负责的需求,当然这三种方式最终实现的效果都一样,所以选择哪一个都可以。

好了,今天的笔记就到这里,下一次是关于模型验证(Model Validation)的内容,因为最近比较忙,所以随笔的时间间隔比较大了,我尽量抓紧时间写吧,:-)

时间: 2024-10-31 14:36:40

《Pro ASP.NET MVC 3 Framework》学习笔记之三十【模型绑定】的相关文章

ASP.NET MVC 3 Framework学习笔记之Model Templates

.使用模板化的视图Helpers(Using Templated View Helpers) 模版化视图helpers的创意就是它们更加灵活.我们不用自己去指定应该用什么HTML元素来呈现一个模型的属性,MVC自己会搞定,在我们更新了视图模型时,也不用手动的更新视图.下面是一个例子:  代码如下 复制代码 //在Models里面添加Persons.cs using System; using System.Collections.Generic; using System.Linq; using

《Pro ASP.NET MVC 3 Framework》学习笔记目录

<Pro ASP.NET MVC 3 Framework>简介: 作者: Adam Freeman 和 Steven Sanderson 出版社: Apress; New 平装: 820页 语种: 英语 ISBN: 1430234040 声明:笔记里面按我自己的理解翻译了大部分内容,写这个笔记的目的:为了方便自己查阅,也为园友提供学习的方便. 我无意侵犯作者的任何权利,仅仅为了自己学习.也希望路过的朋友不要用于任何商业目的. 第一部分 ASP.NET MVC3介绍   <Pro ASP.

《Pro ASP.NET MVC 3 Framework》学习笔记之一【MVC的历程,优点,HelloWorld】

序论:asp.net mvc出现已经有两三年的时间了(2009开始1.0版本),但是这么方面的中文学习资料仍然非常少,特别是asp.net mvc3,几乎就没有中文的学习书籍.在英文的书籍中有两本是非常经典的mvc3教程:<Professional ASP.NET MVC 3>--作者:Jon Galloway , Phil Haack, Brad Wilson , K. Scott Allen和<Pro ASP.NET MVC 3 Framework>--作者:Steven Sa

ASP.NET MVC Web API 学习笔记----HttpClient简介

  1. HttpClient简单介绍  依稀还记得那个时候用WebClient,HttpWebRequest来发送一个请求,现在ASP.NET MVC4中自带了一个类HttpClient,用于接收HttpResponseMessage和发送HttpRequestMesssage. 问题在于既然WebClient,HttpWebRequest可以完成相应的功能,为什么还要使用HttpClient类,.NET Framework中既然提出了这样一个类肯定是有其特别之处的,这里罗列几个不同之处: (

ASP.NET MVC Web API 学习笔记---联系人增删改查

本章节简单介绍一下使用ASP.NET MVC Web API 做增删改查.目前很多Http服务还是通过REST或者类似RESP的模型来进行数据操作的.下面我们通过创建一个简单的Web API来管理联系人          说明:为了方便数据不使用真正的数据库,而是通过内存数据模拟    1.       Web API中包含的方法 Action HTTP method Relative URI GetAllContact GET /api/contact GetContact GET /api/

《Pro ASP.NET MVC 3 Framework》学习笔记之九【Ninject的使用-下】

接着上次的Ninject的笔记,如果你是初次路过,可以先看看我前面的笔记. 一,创建依赖链(Chains of Dependency) 当我们向Ninject请求创建一个类型时,Ninject会去检查该类型和其他类型之间的耦合关系.如果有额外的依赖,Ninject也会解析它们并创建我们需要的所有类的实例.为了进一步说明,我们创建一个新的接口和一个实现该接口的类.请注意我们的例子是跟前面的笔记衔接的,所以如果你打算跟着一起操作的话,最好能够去看看前面的笔记. 创建一个IDiscountHelper

《Pro ASP.NET MVC 3 Framework》学习笔记之四【领域模型介绍】

主题:应用领域驱动开发(Applying Domain-Driven Development) Domain Model是MVC程序的"心脏",其他的一切,包括Controllers和Views仅仅是用来跟Domain Model交互的一种方式,ASP.NET MVC并没有限制使用在Domain Model上面的技术,我们可以自由的选择跟.net framework交互的技术,并且这样的选择是非常多的.不仅如此,ASP.NET MVC为我们提供了基础的架构和约定来帮助Domain Mo

《Pro ASP.NET MVC 3 Framework》学习笔记之五【依赖注入及ninject工具使用】

一,创建松耦合的组件 1."分解关注点"是MVC模式里面一个非常重要的特性.我们想要在应用程序里面创建的组件尽可能的独立,这样我们就能管理比较少的依赖关系.理想情况下,每个组件都是孤立的,不知道其他组件的存在,处理应用程序的其他领域仅仅通过抽象接口,这就是所谓的松耦合,它让我们的应用程序更加容易测试和修改.通过一个简单的例子可以帮助我们理解,假如我们想写一个发邮件的组件,暂且就把这个组件命名为MyEmailSender,接着我们实现一个接口,这个接口定义了所有需要发送邮件的功能,也暂且

《Pro ASP.NET MVC 3 Framework》学习笔记之十八【URL和Routing】

整个项目范围的依赖注入(Project-Wide Dependency Injection) 在书接下来的章节里面,我们会看到MVC框架提供的很多不同的方式来让我们扩展和自定义对请求的处理,每一种方式都会用一个实现的接口或一个派生的基类来定义. 在第一部分的SportsStore项目实例里面已经有过引入.我们从DefaultControllerFactory类派生了一个NinjectControllerFactory类,以至于我们能够创建Controller,并使用Ninject来管理DI(依赖

《Pro ASP.NET MVC 3 Framework》学习笔记之三十五 【部署】

准备要部署的应用程序 在正式进入部署MVC程序到IIS之前,会介绍一些关于应用程序迁移到生产环境之前探测错误以及一旦进入生产环境最大化性能的技术.同时也会展示关于流线型部署过程的有用的功能.   检测视图错误 Razor视图会在服务器需要的时候编译而不是在VS里面生成项目时编译,正常情况下,探测视图编译错误的方式是系统的访问每一个action,从而让每一个view都能够呈现.这显然是非常乏味而且不会一直成功的技术,特别是在基于不同的model状态呈现不同的view的时候.我们可以启用一个特别的项