414
技術社區[雲棲]
通過實例模擬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實現與對應的葉子節點的映射。
實際上當我們調用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="Address.District" type="text" ... />
7: <input id="Address_Street" name="Address.Street" type="text"... />
對於用於模擬默認Model綁定機製的自定義DefaultModelBinder來說,我們僅僅提供了針對簡單類型的綁定,現在我們對其進行完善是之可以提供對複雜類型的Model綁定。如下麵的代碼片斷所示,在BindModel方法中我們創建了一個基於參數類型的ModelMetadata對象,並根據其IsComplexType屬性判斷參數類型是否為複雜類型。
1: public class DefaultModelBinder
2: {
3: //其他成員
4: public object BindModel(Type parameterType, string prefix)
5: {
6: if (!this.ValueProvider.ContainsPrefix(prefix))
7: {
8: return null;
9: }
10: ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, parameterType);
11: if (!modelMetadata.IsComplexType)
12: {
13: return this.ValueProvider.GetValue(prefix).ConvertTo(parameterType);
14: }
15: object model = CreateModel(parameterType);
16: foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(parameterType))
17: {
18: string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name;
19: property.SetValue(model, BindModel(property.PropertyType, key));
20: }
21: return model;
22: }
23: private object CreateModel(Type modelType)
24: {
25: Type type = modelType;
26: if (modelType.IsGenericType)
27: {
28: Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
29: if (genericTypeDefinition == typeof(IDictionary<,>))
30: {
31: type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
32: }
33: else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) ||(genericTypeDefinition == typeof(IList<>)))
34: {
35: type = typeof(List<>).MakeGenericType(
36: modelType.GetGenericArguments());
37: }
38: }
39: return Activator.CreateInstance(type);
40: }
41: }
如果參數為複雜類型,則通過調用CreateModel方法以反射的方式創建Model對象。CreateModel方法會被用於後麵我們會介紹的基於集合和字典的Model綁定,所以我們這裏還針對泛型的IDictionary<,>、IEnumerable<>、ICollection<>和IList<>類型作了相應地處理。具體來說,如果參數類型為IDictionary<,>,則創建一個Dictionary<,>對象,而對後三者則創建一個List<>對象,具體的泛型參數根據參數類型獲取。對於一般的類型,我們直接通過Activator的CreateInstance方法根據參數類型創建相應的Model對象。
通過CreateModel方法創建的是針對參數類型的“空”對象,我們需要通過Model綁定對它的相關屬性進行初始化。在BindModel方法中,我們遍曆參數類型的所有屬性,並在現有前綴的基礎上加上“.{屬性名稱}”(如果當前前綴為空,則直接采用屬性名稱)作為綁定對應屬性的前綴遞歸地調用BindModel方法得到屬性值。我們最終通過反射的方式將得到值對屬性進行賦值。
現在我們采用我們完善後的DefaultModelBinder來進行針對複雜類型的Model綁定。如下麵的代碼片斷所示,我們對HomeController的Action方法進行了相應的修改使之具有兩個Contact類型的參數foo和bar。在Action方法中,我們將這兩個參數代表的Contact對象的相關信息呈現出來。
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:
9: private IValueProvider GetValueProvider()
10: {
11: NameValueCollection requestData = new NameValueCollection();
12: requestData.Add("Name", "張三");
13: requestData.Add("PhoneNo", "123456789");
14: requestData.Add("EmailAddress", "zhangsan@gmail.com");
15: requestData.Add("Address.Province", "江蘇");
16: requestData.Add("Address.City", "蘇州");
17: requestData.Add("Address.District", "工業園區");
18: requestData.Add("Address.Street", "星湖街328號");
19: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
20: }
21:
22: public void Index()
23: {
24: InvokeAction("Action");
25: }
26:
27: public void Action(Contact foo, Contact bar)
28: {
29: Response.Write("Foo<br/>");
30: Response.Write(string.Format("{0}: {1}<br/>", "Name", foo.Name));
31: Response.Write(string.Format("{0}: {1}<br/>", "PhoneNo", foo.PhoneNo));
32: Response.Write(string.Format("{0}: {1}<br/>", "EmailAddress", foo.EmailAddress));
33: Response.Write(string.Format("{0}: {1} {2} {3} {4}<br/><br/>", "Address",
34: foo.Address.Province, foo.Address.City, foo.Address.District,
35: foo.Address.Street));
36:
37: Response.Write("Bar<br/>");
38: Response.Write(string.Format("{0}: {1}<br/>", "Name", bar.Name));
39: Response.Write(string.Format("{0}: {1}<br/>", "PhoneNo", bar.PhoneNo));
40: Response.Write(string.Format("{0}: {1}<br/>", "EmailAddress", bar.EmailAddress));
41: Response.Write(string.Format("{0}: {1} {2} {3} {4}<br/>", "Address",
42: bar.Address.Province, bar.Address.City, bar.Address.District,
43: bar.Address.Street));
44: }
45: }
通過GetValueProvider方法提供的依然是一個NameValueCollectionValueProvider對象,我們將一個Contact對象包含的信息包含在它對應的NameValueCollection對象中。對於添加到NameValueCollection中的針對Contact對象的某個屬性的數據條目,我們按照上麵介紹的匹配規則對其命名。運行我們的實例程序,我們會在瀏覽器中得到如下所示的輸出結果,我們從中可以看到Action方法的兩個參數foo和bar通過我們自定義的DefaultModelBinder進行了正確地綁定,並且它們具有相同的值。
1: Foo
2: Name: 張三
3: PhoneNo: 123456789
4: EmailAddress: zhangsan@gmail.com
5: Address: 江蘇 蘇州 工業園區 星湖街328號
6:
7: Bar
8: Name: 張三
9: PhoneNo: 123456789
10: EmailAddress: zhangsan@gmail.com
11: Address: 江蘇 蘇州 工業園區 星湖街328號
之所以同一個Action方法中兩個相同類型的參數會綁定相同的數據,使緣於之前介紹的去除前綴的後備Model綁定機製。由於請求數據中並不包含針對某個參數的前綴,所以在針對參數名稱作為前綴的Model綁定失敗的情況下,後備Model綁定會前綴為空字符串的情況下再次進行。
1: public class HomeController : Controller
2: {
3: //其他成員
4: private IValueProvider GetValueProvider()
5: {
6: NameValueCollection requestData = new NameValueCollection();
7: requestData.Add("foo.Name", "Foo");
8: requestData.Add("foo.PhoneNo", "123456789");
9: requestData.Add("foo.EmailAddress", "Foo@gmail.com");
10:
11: requestData.Add("bar.Name", "Bar");
12: requestData.Add("bar.PhoneNo", "987654321");
13: requestData.Add("bar.EmailAddress", "Bar@gmail.com");
14: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
15: }
16:
17: public void Action(Contact foo, Contact bar)
18: {
19: Response.Write("Foo<br/>");
20: Response.Write(string.Format("{0}: {1}<br/>", "Name", foo.Name));
21: Response.Write(string.Format("{0}: {1}<br/><br/>", "PhoneNo", foo.PhoneNo));
22: Response.Write(string.Format("{0}: {1}<br/>", "EmailAddress", foo.EmailAddress));
23:
24: Response.Write("Bar<br/>");
25: Response.Write(string.Format("{0}: {1}<br/>", "Name", bar.Name));
26: Response.Write(string.Format("{0}: {1}<br/>", "PhoneNo", bar.PhoneNo));
27: Response.Write(string.Format("{0}: {1}<br/>", "EmailAddress", bar.EmailAddress));
28: }
29: }
在如上所示的代碼中,我們為NameValueCollectionValueProvider設置了基於“foo”和“bar”的前綴的兩套數據,目的在為Action方法的foo和bar參數提供不同的數據。運行我們的程序後會在瀏覽器上得到如下所示的輸出結果,可以看出Action方法的兩個參數被綁定了不同的值。
1: Foo
2: Name: Foo
3: PhoneNo: 123456789
4: EmailAddress: Foo@gmail.com
5:
6: Bar
7: Name: Bar
8: PhoneNo: 987654321
9: EmailAddress: Bar@gmail.com
通過實例模擬ASP.NET MVC的Model綁定的機製:簡單類型+複雜類型
通過實例模擬ASP.NET MVC的Model綁定的機製:數組
通過實例模擬ASP.NET MVC的Model綁定的機製:集合+字典
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 11:34:09