通過例項模擬ASP.NET MVC的Model繫結機制:簡單型別+複雜型別
總的來說,針對目標Action方法引數的Model繫結完全由元件ModelBinder來實現,在預設情況下使用的ModelBinder型別為DefaultModelBinder,接下來我們將按照逐層深入的方式介紹實現在DefaultModelBinder的預設Model繫結機制。[原始碼從這裡下載][本文已經同步到《How ASP.NET MVC Works?》中]
目錄
一、簡單型別
二、複雜型別
三、陣列
四、集合
五、字典
一、簡單型別
對於旨在繫結目標Action方法引數值的Model來說,最簡單的莫過於簡單引數型別的情況。通過《
1: public class DefaultModelBinder
2: {
3:public IValueProvider ValueProvider { get; private set; }
4: public DefaultModelBinder(IValueProvider valueProvider)
5: {
6: this.ValueProvider = valueProvider;
7: }
8:
9: public IEnumerable<object> GetParameterValues(ActionDescriptor actionDescriptor)
10: {
11: foreach (ParameterDescriptor parameterDescriptor in actionDescriptor.GetParameters())
12: {
13: string prefix = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName;
14: yield return GetParameterValue(parameterDescriptor, prefix);
15: }
16: }
17:
18: public object GetParameterValue(ParameterDescriptor parameterDescriptor, string prefix)
19: {
20: object parameterValue = BindModel(parameterDescriptor.ParameterType, prefix);
21: if (null == parameterValue && string.IsNullOrEmpty(parameterDescriptor.BindingInfo.Prefix))
22: {
23: parameterValue = BindModel( parameterDescriptor.ParameterType, "");
24: }
25: return parameterValue ?? parameterDescriptor.DefaultValue;
26: }
27:
28: public object BindModel(Type parameterType, string prefix)
29: {
30: if (!this.ValueProvider.ContainsPrefix(prefix))
31: {
32: return null;
33: }
34: return this.ValueProvider.GetValue(prefix).ConvertTo(parameterType);
35: }
36: }
方法GetParameterValues根據指定的用於描述Action方法的ActionDescriptor獲取最終執行該方法的所有引數值。在該方法中,我們通過呼叫ActionDescriptor的GetParameters方法得到用於描述其引數的所有ParameterDescriptor物件,並將每一個ParameterDescriptor作為引數呼叫GetParameterValue方法得到具體某個引數的值。GetParameterValue除了接受一個型別為ParameterDescriptor的引數外,還接受一個用於表示字首的字串引數。如果通過ParameterDescriptor的BindingInfo屬性表示的ParameterBindingInfo物件具有字首,則採用該字首;否則採用引數名稱作為字首。
對於GetParameterValue方法來說,它又通過呼叫另一個將引數型別作為引數的BindModel方法來提供具體的引數值,BindModel方法同樣接受一個表示字首的字串作為其第二個引數。GetParameterValue最初將通過ParameterDescriptor獲取到的引數值和字首作為引數呼叫BindModel方法,如果返回值為Null並且引數並沒有顯示執行字首,會傳入一個空字串作為字首再一次呼叫BindModel方法,這實際上模擬了之前提到過的去除字首的後備Model繫結機制(針對於ModelBindingContext的FallbackToEmptyPrefix屬性)。如果最終得到的物件不為Null,則將其作為引數值返回;否則返回引數的預設值。
BindModel方法的邏輯非常簡單。先將傳入的字首作為引數呼叫ValueProvider的ContainsPrefix方法判斷當前的ValueProvider保持的資料是否具有該字首。如果返回之為False,直接返回Null,否則以此字首作為Key呼叫GetValue方法得到一個ValueProviderResult呼叫,並最終呼叫ConvertTo方法轉換為引數型別並返回。
為了驗證我們自定義的DefaultModelBinder能夠真正地用於針對簡單引數型別的Model繫結沒我們將它應用到一個具體的ASP.NET MVC應用中。在通過Visual Studio的ASP.NET MVC專案模板建立的空Web應用中,我們建立瞭如下一個預設的HomeController。HomeController具有一個ModelBinder屬性,其型別正是我們自定義的DefaultModelBinder,該屬性通過方法GetValueProvider提供。
1: public class HomeController : Controller
2: {
3: public DefaultModelBinder ModelBinder { get; private set; }
4: public HomeController()
5: {
6: this.ModelBinder = new DefaultModelBinder(GetValueProvider());
7: }
8: private void InvokeAction(string actionName)
9: {
10: ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController));
11: ReflectedActionDescriptor actionDescriptor = (ReflectedActionDescriptor)controllerDescriptor
12: .FindAction(ControllerContext, actionName);
13: actionDescriptor.MethodInfo.Invoke(this,this.ModelBinder.GetParameterValues(actionDescriptor).ToArray());
14: }
15: public void Index()
16: {
17: InvokeAction("Action");
18: }
19:
20: private IValueProvider GetValueProvider()
21: {
22: NameValueCollection requestData = new NameValueCollection();
23: requestData.Add("foo", "abc");
24: requestData.Add("bar", "123");
25: requestData.Add("baz", "123.45");
26: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
27: }
28: public void Action(string foo, [Bind(Prefix="baz")]double bar)
29: {
30: Response.Write(string.Format("{0}: {1}<br/>", "foo", foo));
31: Response.Write(string.Format("{0}: {1}<br/>", "bar", bar));
32: }
33: }
InvokeAction方法用於執行指定的Action方法。在該方法中我們先根據當前Controller的型別建立一個ControllerDescriptor物件,並通過調其FindAction方法得到用於描述指定Action方法的ActionDescriptor物件。通過之前的介紹我們知道這是一個ReflectedActionDescriptor物件,所以我們將其轉化成ReflectedActionDescriptor型別得到Action方法對應的MethodInfo物件。最後呼叫DefaultModelBinder的GetParameterValues方法得到目標Action方法所有的引數,將其傳入MethodInfo的Invoke方法以反射的形式對指定的Action方法進行執行。
預設的Action方法Index中我們通過執行InvokeAction方法來執行定義在HomeController的Action方法。通過上面的程式碼片斷可以看出,該方法的兩個引數foo和bar均為簡單型別(string和double),在引數bar上還應用了BindAttribute並指定了相應的字首(“baz”)。在該Action方法中,我們將兩個引數值呈現出來。
而在用於提供ValueProvider的GetValueProvider方法返回的是一個NameValueCollectionValueProvider物件。作為資料來源的NameValueCollection物件包含三個名稱為foo、bar和baz的資料(abc、123、123.45),我們可以將它們看成是Post的標單輸入元素。
當我們執行該程式的時候會在瀏覽器中得到如下的輸出結果。我們可以看到目標Action方法的兩個引數值均通過我們自定義的DefaultModelBinder得到了有效的繫結。而實際上引數值的提供最終是通過ValueProvider實現的,它在預設的情況下會根據引數名稱進行匹配(foo引數),如果引數應用BindAttribute並顯式指定了字首,則會按照這個字首進行匹配(bar引數)。
1: foo: abc
2: bar: 123.45
二、複雜型別
對於簡單型別的引數來說,由於支援與字串型別之間的轉換,相應ValueProvider可以直接從資料來源中提取相應的資料並直接轉換成引數型別。所以針對簡單型別的Model繫結是一步到位的過程,但是針對複雜型別的Model繫結就沒有這麼簡單了。複雜物件可以表示為一個樹形層次化結構,其物件本身和屬性代表相應的節點,葉子節點代表簡單資料型別屬性。而ValueProvider採用的資料來源是一個扁平的資料結構,它通過基於屬性名稱字首的Key實現與這個物件樹中對應葉子節點的對映。
1: public class Contact
2: {
3: public string Name { get; set; }
4: public string PhoneNo { get; set; }
5: public string EmailAddress { get; set; }
6: public Address Address { get; set; }
7: }
8: public class Address
9: {
10: public string Province { get; set; }
11: public string City { get; set; }
12: public string District { get; set; }
13: public string Street { get; set; }
14: }
以上面定於得這個Contact型別為例,它具有三個簡單型別的屬性(Name、PhoneNo和EmailAddress)和複雜型別Address的屬性;而Address屬性具有四個簡單型別的屬性。一個Contact物件的資料結構可以通過如下圖所示的樹來表示,這個樹種的所有葉子節點均為簡單型別。如果我們需要通過一個ValueProvider來構建一個完整的Contact物件,它必須能夠提供所有所有葉子節點的數值,而ValueProvider通過基於屬性名稱字首的Key實現與對應的葉子節點的對映。
實際上當我們呼叫HtmlHelper<TModel>的模板方法EditorFor/EditorForModel的時候就是按照這樣的匹配方式對標單元素進行命名的。假設在將Contact作為Model型別的強型別View中,我們通過呼叫HtmlHelper<TModel>的擴充套件方法EditorFor將Model物件的所有資訊以編輯的模式呈現出來。
1: @model Contact
2: @Html.EditorFor(m => m.Name)
3: @Html.EditorFor(m => m.PhoneNo)
4: @Html.EditorFor(m => m.EmailAddress)
5: @Html.EditorFor(m => m.Address.Province)
6: @Html.EditorFor(m => m.Address.City)
7: @Html.EditorFor(m => m.Address.District)
8: @Html.EditorFor(m => m.Address.Street)
下面的程式碼片斷代表了作為Model物件的Contact在最終呈現出來的View中代表的HTML,我們可以清楚地看到這些<input>表單元素完全是根據屬性名稱和型別層次結構進行命名的。隨便提一下,對於基於提交表單的Model繫結來說,作為匹配的是表單元素的name屬性而非id屬性,所以這裡的命名指的是name屬性而非id屬性。
1: <input id="Name" name="Name" type="text" ... />
2: <input id="PhoneNo" name="PhoneNo" type="text" ... />
3: <input id="EmailAddress" name="EmailAddress" type="text" ... />
4: <input id="Address_Province" name="Address.Province" type="text" ... />
5: <input id="Address_City" name="Address.City" type="text" ... />
6: <input id="Address_District" name