MVC教程--Action参数绑定Model Binding详解--内置的Model绑定器
阅读原文时间:2021年04月21日阅读:1

了 解了什么是MVC的Model绑定(Model Binding)之后我们来看看MVC内置为我们提供的Model绑定器。我们可以把Model绑定按类型复杂程度和类型分为简单类型,复杂类型,数组和 集合。我之前的文章有详细介绍MVC中的Controller是怎么接收输入值的,请看:

跟蓝狐学MVC教程--ASP.NET MVC的Controller接收输入详解

当 MVC框架接收到一个请求是,它会提取相应的路由信息,其中包括用controller名,action名,以及一些自定义的变量。MVC自带的一个 Action Inovker名ControllerActionInvoker要调用一个Action方法之前会利用model binder(model绑定器)来生成调用Action所需要的参数数据或对象(简单值或对象)。model binder(model绑定器)要实现一个接口IModelBinder,如下:

  1. namespace System.Web.Mvc {

  2. public interface IModelBinder {

  3. object BindModel(ControllerContext controllerContext,

  4. ModelBindingContext bindingContext);

  5. }

  6. }

在一个MVC的程序中可以存在多个Model Binder,正如可以存在多个视图引擎一样。每一个Model Binder可以负责绑定一个或多个Model类型。当Action Invoker要调用一个Action方法是时,它会根据其参数的类型找到相应的Model Binder来绑定相应的类型。以上面的HomeController的Index为例来说明一下:

我们请求 Url:/Home/Index/123,时Action Invoker就会检查Index方法的参数,看到Index方法需要一个类型为int的参数,因此它不会找到一个负责绑定int类型的Model Binder并调用它的BindModel。这时Model Binder就负责根据请求上下文信息提供一个int类型的值给调用Index方法用。这就意味了提供参数的值是通过请求数据信息,并在适当的时候会有类 型转换,这些数据信息可能是表单数据或者是查询字符串的值(Form或Query String)。

注意:这些数据信息的来源MVC框架并没有限制只是从表单或查询字符串的值中获取。

虽 然我们能够继承接口IModelBinder自定义一个ModelBinder来绑定我们自定义的类型,但是在实践开发过程用得最多还是MVC内置的 Binder类DefaultModelBinder。当Action Invoker找不到一个自定义的Binder来绑定一个指定类型时候,就会使用这个内置的Binder类DefaultModelBinder。默认情 况这个DefaultModelBinder会在如下的集合中查找参数名相对应的数据信息。

1、Request.Form

2、RouteData.Values

3、Request.QueryString

4、Request.Files

查找的顺序是从1-4的顺序找,比如DefaultModelBinder要查找一个我们Index方法需要的参数id的值,将按以下顺序查找:

1. Request.Form["id"] 
2. RouteData.Values["id"]

3. Request.QueryString["id"]

4. Request.Files["id"]

如果在前面找到一个名为id的值,将不会继续往下找。比如在1中Request.Form["id"]找到了id的值,那么2,3,4根本不会进行查找。

2.1、Model绑定简单类型

这 里说的简单类型也就是C#内置的类型比如:int,string,DateTime,decimal等等。上面绑定就是用绑定简单类型int。MVC要绑 定这些简单类型很简单,不用你做特别的处理。MVC框架自动根据你传入的值进行类型转换,比如当Action参数为int,而你传入值能够正确转换成 int。下面我们来做一个试验,在上面的例子中我们的路由配置的id参数是可选的。下面我们在浏览器输入"/Home/Index/",也就是id不输入 值,看看会出现什么效果:

对于 “MVCModelBindingDemo.Controllers.HomeController”中方法 “System.Web.Mvc.ActionResult Index(Int32)”的不可以为 null 的类型“System.Int32”的参数“id”,参数字典包含一个 null 项。可选参数必须为引用类型、可以为 null 的类型或声明为可选参数。
参数名: parameters

可看到我们没有提供id值就会报上面错。因为Action Index要求一个类型为Int32的参数,url中没有这个参数的值,默认就是为null。要避免这样的错误我们一般有两种做法:

1、在路由配置中指定默认值,正如:

  1. public static void RegisterRoutes(RouteCollection routes)

  2. {

  3. routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  4. routes.MapRoute(

  5. name: "Default",

  6. url: "{controller}/{action}/{id}",

  7. defaults: new { controller = "Home", action = "Index", id = 999 }

  8. );

  9. }

2、在Action参数中指定默认值,如下:

  1. public class HomeController : Controller
  2. {
  3. public ActionResult Index(int id=999)
  4. {
  5. ViewBag.ID = id;
  6. return View();
  7. }
  8. }

2.2、Model绑定复杂类型

复杂类型一般是自己定义的一些类,比如我们的Action参数的类型是一个领域对象,或者是一个 数据库的Model。当Action参数类型是一个复杂的类型时就不能像简单类型那样直接字符串解析成int这样类型转 换,DefaultModelBinder会用到反射依次访问这个Model的public公有属性,并给它赋值上相应的值。为了展示这个过程我们把之前 的HomeController改成如下:

  1. public class HomeController : Controller {

  2. private Person[] personData = {

  3. new Person {PersonId = 1, FirstName = "Adam", LastName = "Freeman",

  4. Role = Role.Admin},

  5. new Person {PersonId = 2, FirstName = "Steven", LastName = "Sanderson",

  6. Role = Role.Admin},

  7. new Person {PersonId = 3, FirstName = "Jacqui", LastName = "Griffyth",

  8. Role = Role.User},

  9. new Person {PersonId = 4, FirstName = "John", LastName = "Smith",

  10. Role = Role.User},

  11. new Person {PersonId = 5, FirstName = "Anne", LastName = "Jones",

  12. Role = Role.Guest}

  13. };

  14. public ActionResult Index(int id = 1) {

  15. Person dataItem = personData.Where(p => p.PersonId == id).First();

  16. return View(dataItem);

  17. }

  18. public ActionResult CreatePerson() {

  19. return View(new Person());

  20. }

  21. [HttpPost]

  22. public ActionResult CreatePerson(Person model) {

  23. return View("Index", model);

  24. }

  25. }

第一个CreatePerson方法没有任何的参数,返回视图传了一个Person对象给视图/Views/Home/CreatePerson.cshtml。下面是CreatePerson.cshtml内容:

  1. @model MVCModelBindingDemo.Models.Person
  2. @{
  3. ViewBag.Title = "CreatePerson";
  4. }
  5. Create Person

  6. @using(Html.BeginForm()) {
  7. @Html.LabelFor(m => m.PersonId)@Html.EditorFor(m=>m.PersonId)</div>
  8. @Html.LabelFor(m => m.FirstName)@Html.EditorFor(m=>m.FirstName)</div>
  9. @Html.LabelFor(m => m.LastName)@Html.EditorFor(m=>m.LastName)</div>
  10. @Html.LabelFor(m => m.Role)@Html.EditorFor(m=>m.Role)</div>
  11. }

本视图采用了Model强类型视图,对应Model类型是MVCModelBindingDemo.Models.Person。

在/Content/Site.css尾部加入以下css样式:

  1. label { display: inline-block; width: 100px; font-weight:bold; margin: 5px;}
  2. form label { float: left;}
  3. input.text-box { float: left; margin: 5px;}
  4. button[type=submit] { margin-top: 5px; float: left; clear: left;}
  5. form div {clear: both;}

`我们访问地址:/Home/CreatePerson时将会出现的页面中输入我们想要输入的内容,如下图:

我们最后点击提交Submit按钮,在带有[HttpPost]的CreatePerson方法打一个断点看看Action的参数model对象的各字段的值:

可 以看到每一个文本框输入的值正好就到了Person对象的对应的字段中去了。当我们用Post提交表单请求的时候就会提交到带有特性HttpPost的 CreatePerson方法,内置的model binder发现这个Action的参数是一个类Person,它就会依次处理这个类的属性,并在请求的上下问中找到相应的数据并给属性赋值。比如当发现 Person的有一个字段为PersonId时,它就会在请求数据中找到一个名为PersonId的值并转换成Int之后赋值给这个字段。如果 Action参数的类的字段又是一个类,那么又会重复最外层的步骤,依次绑定它的每个字段的值。下面我们就来演示一下这种情况。

首先,我们定义一个地址类Address:

`

  1. public class Address {
  2. public string Line1 { get; set; }
  3. public string Line2 { get; set; }
  4. public string City { get; set; }
  5. public string PostalCode { get; set; }
  6. public string Country { get; set; }
  7. }

然后,我们把给Person加上一个Adress类型的属性HomeAdres:

  1. public class Person
  2. {
  3. public int PersonId { get; set; }
  4. public string FirstName { get; set; }
  5. public string LastName { get; set; }
  6. public Role Role { get; set; }
  7. public Address HomeAddress { get; set; }
  8. }

接下来,我们修改一个CreatePerson.cshtml视图:

  1. @model MVCModelBindingDemo.Models.Person
  2. @{
  3. ViewBag.Title = "CreatePerson";
  4. }
  5. Create Person

  6. @using (Html.BeginForm())
  7. {
  8. @Html.LabelFor(m => m.PersonId)@Html.EditorFor(m => m.PersonId)</div>
  9. @Html.LabelFor(m => m.FirstName)@Html.EditorFor(m => m.FirstName)</div>
  10. @Html.LabelFor(m => m.LastName)@Html.EditorFor(m => m.LastName)</div>
  11. @Html.LabelFor(m => m.Role)@Html.EditorFor(m => m.Role)</div>
  12. @Html.LabelFor(m => m.HomeAddress.City)
  13. @Html.EditorFor(m=> m.HomeAddress.City)
  • @Html.LabelFor(m => m.HomeAddress.Country)
  • @Html.EditorFor(m=> m.HomeAddress.Country)
  • </div>
  • </div>
  • }
  • 页面录入数据如下图:

    提交之后,打断点监控model变量如下图:

    可以看到页面录入的City和Country的值自动到了model的HomeAdress对象中去了。我们最后来看看输入页面City和County部分的html是什么样的:

    1. <div>
    2. <label for="HomeAddress_City">City</label>
    3. <input class="text-box single-line" id="HomeAddress_City" name="HomeAddress.City" type="text" value="" />
    4. </div>
    5. <div>
    6. <label for="HomeAddress_Country">Country</label>
    7. <input class="text-box single-line" id="HomeAddress_Country" name="HomeAddress.Country" type="text" value="" />
    8. </div>

    从上面看input的name属性都是"HomeAdress.“开头,也就是默认name的命名规则为”属性名.字段名“的格式。这样MVC的Model Binder才能利用反射对应到这个值是哪一个属性的的哪一个字段。

    2.3、利用Bind特性类有选择的绑定类的字段属性

    下面我们举一个实例对Bind特性的使用:

    1. [HttpPost]
    2. public ActionResult CreatePerson([Bind(Prefix = "HomeAddress", Exclude = "PersonId", Include = "FirstName,LastName")]Person model)
    3. {
    4. return View("Index", model);
    5. }

    Prefix:是表示字段的前缀,这样model的每个字段都会加”前缀.字段名“,比如字段FirstName,就会去找name为”HomeAddress.FirstName“的值。

    Excude:是表示排除某些字段不绑定。

    Inlude:是表示只绑定某些字段。

    2.4、Model绑定数组和集合

    有进时候我们的Action参数需要是一个数组或者一个List集合,这其实原理和上面的Model Binder绑定复杂类型对象也差不多。

    2.4.1、绑定数组

    下面我我们为HomeController加一个数组参数的Action:

    1. public ActionResult Names(string[] names) {
    2. names = names ?? new string[0];
    3. return View(names);
    4. }

    最后生成表单部分的Hmtl代码如下: 

    可以看到name和id全部都为names,这样最后提交的表单这三个names的input的值就是Action的参数数组names的三个元素。

    2.4.2、绑定集合

    我们为HomeController添加一个Action参数为IList类型,如下:

    1. public ActionResult CreatePersonByList(IList<Person> person)
    2. {
    3. person = person ?? new List<Person>();
    4. return View(person);
    5. }

    接下来我们为这个Action添加一个视图CreatePersonByList.cshtml: 

    1. @model IList<MVCModelBindingDemo.Models.Person>

    2. @{

    3. ViewBag.Title = "CreatePersonByList";

    4. }

    5. CreatePersonByList

    6. @if (Model.Count() == 0)

    7. {

    8. using (Html.BeginForm())

    9. {

    10. for (int i = 0; i < 3; i++)

    11. {

    12. Person @(i + 1)</legend>

    13. }

    14. }

    15. }

    16. else

    17. {

    18. foreach (var item in Model)

    19. {

    20. @item.FirstName, @item.LastName</p>

    21. }

    22. @Html.ActionLink("返回", "CreatePersonByList");

    23. }

    注意: 上面视图的第一行声明了model的类型为IList正好是Action参数的类型。

    最后表单部分的Html代码如下:

    1. Person 1
    2. Person 2
    3. Person 3

    可以从上面看到绑定Action参数为List每一个元素是通过在html中的元素的name约定规则为:"[下标].字段名",这样下标为1也就意味了List的第2个元素。