1. 程式人生 > >通過例項模擬ASP.NET MVC的Model繫結機制:簡單型別+複雜型別

通過例項模擬ASP.NET MVC的Model繫結機制:簡單型別+複雜型別

總的來說,針對目標Action方法引數的Model繫結完全由元件ModelBinder來實現,在預設情況下使用的ModelBinder型別為DefaultModelBinder,接下來我們將按照逐層深入的方式介紹實現在DefaultModelBinder的預設Model繫結機制。[原始碼從這裡下載][本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、簡單型別
二、複雜型別
三、陣列
四、集合
五、字典

一、簡單型別

對於旨在繫結目標Action方法引數值的Model來說,最簡單的莫過於簡單引數型別的情況。通過《

初識Model元資料》的介紹我們知道,複雜型別和簡單型別之間的區別僅僅在於是否支援針對字串型別的轉換。由於引數值的資料來源在請求中以字串的形式存在,對於支援字串轉換的簡單型別來說,可以直接通過型別轉換得到引數值。我們通過一個簡單的例項來模擬實現在DefaultModelBinder中針對簡單型別的Model繫結。如下所示的是我們自定義的DefaultModelBinder,其屬性ValueProvider用於從請求中提供相應的資料值,該屬性在建構函式中被初始化。

   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實現與對應的葉子節點的對映。

image

實際上當我們呼叫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