深入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)复杂类型,采用递归的手法进行绑定。
- (2.1)如果是数组或者IEumerable<T>的,例如 Action(List<Course> courses),内部创建一个List,进行数组绑定。数组绑定的时候,对key有如下要求,
- (2.1.1) 是以数字为index的,比如 [0].Name, [0].Id, [1].Name, [1].Id。这里的数字必须是从0开始,连续。
- (2.1.2) 有时候上面这个条件比较难以满足,比如需要通过javascript动态增删表单的时候,这时候可以自定义Index。下面针对两种情况看两个例子:
- 新建一个View:
<form action="@Url.Action("SeqIndex")" method="post"> <p> Id: <input type="text" name="[0].Id" /> Name: <input type="text" name="[0].Name" /> </p> <p> Id: <input type="text" name="[2].Id" /> Name: <input type="text" name="[2].Name" /> </p> <p> Id: <input type="text" name="[1].Id" /> Name: <input type="text" name="[1].Name" /> </p> <input type="submit" value="OK" /> </form>
和一个Action方法:
[HttpPost] public ActionResult SeqIndex(Course[] courses) { return Json(courses); }
点击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); }
结果为:
{"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); }
点击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; }