在上文中,談到在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; }