深入ASP.NET MVC之五:Model Binding

在上文中,谈到在action方法被执行的过程中,调用了ControllerActionInvoker的GetParameterValues方法来获得action的参数,上文没有细谈,在这个方法里面,实现了ASP.NET MVC的Model Binding功能。ASP.NET的Model Binding主要有两个接口组成,分别是:

    public interface IModelBinder {
        object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
    }
    public interface IValueProvider {
        bool ContainsPrefix(string prefix);
        ValueProviderResult GetValue(string key);
    }

这两个接口都非常简单,BindModel是真正实现数据绑定的地方,ModelBindingContext有个属性是ValueProvider用来给BindModel提供数据。ASP.NET MVC的Model Binding的“骨架”其实也不复杂,比较繁琐的是这两个接口的实现,这两个接口的实现才是真正实现绑定功能的地方。ASP.NET MVC有一些默认实现,DefaultModelBinder和一系列的ValueProvider:FormValueProvider,QueryStringProvider等,IValueProvider可以将form中的表单,querystring中的数据等抽象为键值对,对于ModelBinder来说,他并不知道这些数据是通过什么地方来的。Model Binding有两方面的功能,一是将提交上去的数据绑定到action方法的参数中,另一方面是将对象的值显示到view中,本文先侧重前一个方面。先看DefaultModelBinder的功能,简单说,这个Model Binder主要是根据Action方法的参数的名字和通过http request提交上去的Key-Value pair中的key进行比对从而进行绑定。具体来说,又分为很多情况。作为例子,如下定义几个类型:

    public class Person
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public Address Add { get; set; }

        public List<Course> Courses { get; set; }
    }

    public class Address
    {
        public string City { get; set; }
        public string Street { get; set; }
    }

    public class Course
    {
        public string Name { get; set; }
        public int Id { get; set; }
    }

(1)简单类型,比如 Action(string abc),这种情况会将 key=abc的值直接赋值给abc这个参数。

(2)复杂类型,采用递归的手法进行绑定。

  1.          (2.1)如果是数组或者IEumerable<T>的,例如 Action(List<Course> courses),内部创建一个List,进行数组绑定。数组绑定的时候,对key有如下要求,
  2.                    (2.1.1) 是以数字为index的,比如 [0].Name, [0].Id, [1].Name, [1].Id。这里的数字必须是从0开始,连续。
  3.                     (2.1.2)  有时候上面这个条件比较难以满足,比如需要通过javascript动态增删表单的时候,这时候可以自定义Index。下面针对两种情况看两个例子:
  4. 新建一个View:
<form action="@Url.Action("SeqIndex")" method="post">
    <p>
        Id:
        <input type="text" name="[0].Id" />
        &nbsp;
       Name: 
        <input type="text" name="[0].Name" />
    </p>
    <p>
        Id:
        <input type="text" name="[2].Id" />
        &nbsp;
       Name: 
        <input type="text" name="[2].Name" />
    </p>
    <p>
        Id:
        <input type="text" name="[1].Id" />
        &nbsp;
       Name: 
        <input type="text" name="[1].Name" />
    </p>
    <input type="submit" value="OK" />
</form>

和一个Action方法:

        [HttpPost]
        public ActionResult SeqIndex(Course[] courses)
        {
            return Json(courses);
        }

 

image

点击Ok之后的输出:

[{"Name":null,"Id":1},{"Name":"c","Id":3},{"Name":"b","Id":2}]

注意1: course变量的中的顺序是和form中的Index一致的。

注意2:大多时候使用MVC的辅助方法,比如TextBox,Editor等生成表单的字段更好,在这里为了更清楚说明原理,采用了原始的html写法。

注意3:对每一个类型为T进行绑定的时候,T依然可能是一个复杂类型,自然需要递归的执行这个绑定流程,以下皆如此,不再重复指出。

第二个例子,

<form action="@Url.Action("SeqIndex")" method="post">
    <p><input type="hidden" name="index" value="Monday" />
         Monday Id:<input type="text" name="[Monday].Id" />
         Monday Name:<input type="text" name="[Monday].Name" />
    </p>

    <p><input type="hidden" name="index" value="Tue" />
        Tuesday Id:<input type="text" name="[Tue].Id" />
        Tuesday Name:<input type="text" name="[Tue].Name" />
    </p>
 
      <input type="submit" value="OK" />
</form>

点击Ok之后输出:

[{"Name":"a","Id":1},{"Name":"b","Id":2}]

 

注意这里的技巧是用一个hidden的input标记Monday,Tue这些是Index。下文分析源代码的时候会看到是如何实现这种绑定的。

 

        (2.2)如果参数是IDictionary的,则新建一个Dictionary对象,将Key和从valueProvider中获得的值转换成相应类型的对象之后放入Dictionary。此时的表单应该是如下样子的:

<form action="@Url.Action("Dictionary")" method="post">
     <p><input type="hidden" name="[0].Key" value="Monday" />
         Monday Id:<input type="text" name="[0].value.Id" />
         Monday Name:<input type="text" name="[0].value.Name" />
    </p>
         <p><input type="hidden" name="[1].Key" value="Tue" />
         Tuesday Id:<input type="text" name="[1].value.Id" />
         Tuesday Name:<input type="text" name="[1].value.Name" />
    </p>
    <input type="submit" value="OK" />
</form>

Action方法为:

        [HttpPost]
        public ActionResult Dictionary(Dictionary<string, Course> courses)
        {
            return Json(courses);
        }

image

结果为:

{"Monday":{"Name":"a","Id":2},"Tue":{"Name":"b","Id":1}}

        (2.3) 除此之外,绑定的参数将是“单个”的对象。这时候遍历此对象的所有公共属性,递归的进行数据绑定。也看一个例子:

<form action="@Url.Action("Complex")" method="post">
     <p>        
         Name:<input type="text" name="Name" /> 
         Age:<input type="text" name="Age" />
    </p>
    <fieldset >
        <legend>Address</legend>
        <p>City:<input type="text" name="Add.City" /> </p>
        <p>Street:<input type="text" name="Add.Street" /> </p>
    </fieldset>
    <fieldset>
        <legend>Courses</legend>
        <p>Course 1 Name:<input type="text" name="Courses[0].Name" />
           Course 1 Id:<input type="text" name="Courses[0].Id" />
        </p>
                <p>Course 2 Name:<input type="text" name="Courses[1].Name" />
           Course 2 Id:<input type="text" name="Courses[1].Id" />
        </p>
                <p>Course 3 Name:<input type="text" name="Courses[2].Name" />
           Course 3 Id:<input type="text" name="Courses[2].Id" />
        </p>
    </fieldset>
    <input type="submit" value="OK" />
</form>
        public ActionResult Complex(Person p)
        {
            return Json(p);
        }

 

image

点击Ok之后输出为:

{"Name":"Zixin Yin","Age":28,"Add":{"City":"bellevue","Street":"15058 NE 8th PL"},"Courses":[{"Name":"math","Id":1},{"Name":"physics","Id":2},{"Name":"english","Id":3}]}

上面介绍了asp.net mvc的默认的model binder支持的绑定形式,应该说是比较强大的,基本能够应付各种需求,这个过程其实很像反序列化的过程,当然它的数据源是“扁平”的,只有Key-Value对,这是由html的表单所能提交的数据决定的,因此在绑定复杂对象的时候,需要能够区分这个key对应的是哪个对象上的属性值。比如上面的例子中,name="Add.City" ,就表明,这是Person类型(因为这是Action方法中唯一的参数类型),Add属性(Address类型)的City属性(string类型)。默认的model binder是通过点号和中括号来区分的,点号分隔开的一段段称为prefix,prefix在绑定过程中起到了十分重要的作用,这就是IValueProvider接口中ContainsPrefix方法存在的意义。这个方法的含义乍看并不明确,只有详细分析了其实现之后才能比较透彻的明白。Prefix除了分隔复杂类型的属性之外,还有一个重要的作用,就是当Action有多个参数的时候,可以指定其中一个参数的前缀,从而区分开两个参数的值,例如下面的表单:

<form action="@Url.Action("Multiple")" method="post">
    <fieldset>
        <legend>Address A</legend>
        City: <input type="text" name="City" />
        Street: <input type="text" name="Street" />
    </fieldset>
    <fieldset>
        <legend>Address B</legend>
        City: <input type="text" name="B.City" />
        Street: <input type="text" name="B.Street" />
    </fieldset>
     <input type="submit" value="OK" />
</form>

 

在表单中通过前缀B来区分,对应的在action方法中:

        public string Multiple(Address addr1,[Bind(Prefix="B")]Address addr2)
        {
            return addr1.City + " " + addr1.Street + " B:" + addr2.City + addr2.Street;
        }
posted @ 2012-11-23 08:03  yinzixin  阅读(3468)  评论(4编辑  收藏  举报