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

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

模型绑定(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)的内容,因为最近比较忙,所以随笔的时间间隔比较大了,我尽量抓紧时间写吧,:-)

相关文章
|
3月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
51 7
|
3月前
|
存储 开发框架 前端开发
ASP.NET MVC 迅速集成 SignalR
ASP.NET MVC 迅速集成 SignalR
82 0
|
4月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
60 0
|
4月前
|
开发框架 前端开发 安全
ASP.NET MVC 如何使用 Form Authentication?
ASP.NET MVC 如何使用 Form Authentication?
|
4月前
|
开发框架 .NET
Asp.Net Core 使用X.PagedList.Mvc.Core分页 & 搜索
Asp.Net Core 使用X.PagedList.Mvc.Core分页 & 搜索
151 0
|
7月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
214 0
|
6月前
|
SQL 开发框架 .NET
(20)ASP.NET Core EF创建模型(必需属性和可选属性、最大长度、并发标记、阴影属性)
(20)ASP.NET Core EF创建模型(必需属性和可选属性、最大长度、并发标记、阴影属性)
|
7月前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
87 0
|
7月前
|
开发框架 前端开发 .NET
C# .NET面试系列六:ASP.NET MVC
<h2>ASP.NET MVC #### 1. MVC 中的 TempData\ViewBag\ViewData 区别? 在ASP.NET MVC中,TempData、ViewBag 和 ViewData 都是用于在控制器和视图之间传递数据的机制,但它们有一些区别。 <b>TempData:</b> 1、生命周期 ```c# TempData 的生命周期是短暂的,数据只在当前请求和下一次请求之间有效。一旦数据被读取,它就会被标记为已读,下一次请求时就会被清除。 ``` 2、用途 ```c# 主要用于在两个动作之间传递数据,例如在一个动作中设置 TempData,然后在重定向到另
365 5
|
7月前
|
开发框架 前端开发 .NET
进入ASP .net mvc的世界
进入ASP .net mvc的世界