通過實(shí)例模擬ASP.NET MVC的Model綁定機(jī)制:簡單類型+復(fù)雜類型
總的來說,針對目標(biāo)Action方法參數(shù)的Model綁定完全由組件ModelBinder來實(shí)現(xiàn),在默認(rèn)情況下使用的ModelBinder類型為DefaultModelBinder,接下來我們將按照逐層深入的方式介紹實(shí)現(xiàn)在DefaultModelBinder的默認(rèn)Model綁定機(jī)制。[源代碼從這里下載][本文已經(jīng)同步到《How ASP.NET MVC Works?》中]
目錄
一、簡單類型
二、復(fù)雜類型
三、數(shù)組
四、集合
五、字典
一、簡單類型
對于旨在綁定目標(biāo)Action方法參數(shù)值的Model來說,最簡單的莫過于簡單參數(shù)類型的情況。通過《初識Model元數(shù)據(jù)》的介紹我們知道,復(fù)雜類型和簡單類型之間的區(qū)別僅僅在于是否支持針對字符串類型的轉(zhuǎn)換。由于參數(shù)值的數(shù)據(jù)源在請求中以字符串的形式存在,對于支持字符串轉(zhuǎn)換的簡單類型來說,可以直接通過類型轉(zhuǎn)換得到參數(shù)值。我們通過一個簡單的實(shí)例來模擬實(shí)現(xiàn)在DefaultModelBinder中針對簡單類型的Model綁定。如下所示的是我們自定義的DefaultModelBinder,其屬性ValueProvider用于從請求中提供相應(yīng)的數(shù)據(jù)值,該屬性在構(gòu)造函數(shù)中被初始化。
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根據(jù)指定的用于描述Action方法的ActionDescriptor獲取最終執(zhí)行該方法的所有參數(shù)值。在該方法中,我們通過調(diào)用ActionDescriptor的GetParameters方法得到用于描述其參數(shù)的所有ParameterDescriptor對象,并將每一個ParameterDescriptor作為參數(shù)調(diào)用GetParameterValue方法得到具體某個參數(shù)的值。GetParameterValue除了接受一個類型為ParameterDescriptor的參數(shù)外,還接受一個用于表示前綴的字符串參數(shù)。如果通過ParameterDescriptor的BindingInfo屬性表示的ParameterBindingInfo對象具有前綴,則采用該前綴;否則采用參數(shù)名稱作為前綴。
對于GetParameterValue方法來說,它又通過調(diào)用另一個將參數(shù)類型作為參數(shù)的BindModel方法來提供具體的參數(shù)值,BindModel方法同樣接受一個表示前綴的字符串作為其第二個參數(shù)。GetParameterValue最初將通過ParameterDescriptor獲取到的參數(shù)值和前綴作為參數(shù)調(diào)用BindModel方法,如果返回值為Null并且參數(shù)并沒有顯示執(zhí)行前綴,會傳入一個空字符串作為前綴再一次調(diào)用BindModel方法,這實(shí)際上模擬了之前提到過的去除前綴的后備Model綁定機(jī)制(針對于ModelBindingContext的FallbackToEmptyPrefix屬性)。如果最終得到的對象不為Null,則將其作為參數(shù)值返回;否則返回參數(shù)的默認(rèn)值。
BindModel方法的邏輯非常簡單。先將傳入的前綴作為參數(shù)調(diào)用ValueProvider的ContainsPrefix方法判斷當(dāng)前的ValueProvider保持的數(shù)據(jù)是否具有該前綴。如果返回之為False,直接返回Null,否則以此前綴作為Key調(diào)用GetValue方法得到一個ValueProviderResult調(diào)用,并最終調(diào)用ConvertTo方法轉(zhuǎn)換為參數(shù)類型并返回。
為了驗(yàn)證我們自定義的DefaultModelBinder能夠真正地用于針對簡單參數(shù)類型的Model綁定沒我們將它應(yīng)用到一個具體的ASP.NET MVC應(yīng)用中。在通過Visual Studio的ASP.NET MVC項(xiàng)目模板創(chuàng)建的空Web應(yīng)用中,我們創(chuàng)建了如下一個默認(rèn)的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方法用于執(zhí)行指定的Action方法。在該方法中我們先根據(jù)當(dāng)前Controller的類型創(chuàng)建一個ControllerDescriptor對象,并通過調(diào)其FindAction方法得到用于描述指定Action方法的ActionDescriptor對象。通過之前的介紹我們知道這是一個ReflectedActionDescriptor對象,所以我們將其轉(zhuǎn)化成ReflectedActionDescriptor類型得到Action方法對應(yīng)的MethodInfo對象。最后調(diào)用DefaultModelBinder的GetParameterValues方法得到目標(biāo)Action方法所有的參數(shù),將其傳入MethodInfo的Invoke方法以反射的形式對指定的Action方法進(jìn)行執(zhí)行。
默認(rèn)的Action方法Index中我們通過執(zhí)行InvokeAction方法來執(zhí)行定義在HomeController的Action方法。通過上面的代碼片斷可以看出,該方法的兩個參數(shù)foo和bar均為簡單類型(string和double),在參數(shù)bar上還應(yīng)用了BindAttribute并指定了相應(yīng)的前綴(“baz”)。在該Action方法中,我們將兩個參數(shù)值呈現(xiàn)出來。
而在用于提供ValueProvider的GetValueProvider方法返回的是一個NameValueCollectionValueProvider對象。作為數(shù)據(jù)源的NameValueCollection對象包含三個名稱為foo、bar和baz的數(shù)據(jù)(abc、123、123.45),我們可以將它們看成是Post的標(biāo)單輸入元素。
當(dāng)我們運(yùn)行該程序的時候會在瀏覽器中得到如下的輸出結(jié)果。我們可以看到目標(biāo)Action方法的兩個參數(shù)值均通過我們自定義的DefaultModelBinder得到了有效的綁定。而實(shí)際上參數(shù)值的提供最終是通過ValueProvider實(shí)現(xiàn)的,它在默認(rèn)的情況下會根據(jù)參數(shù)名稱進(jìn)行匹配(foo參數(shù)),如果參數(shù)應(yīng)用BindAttribute并顯式指定了前綴,則會按照這個前綴進(jìn)行匹配(bar參數(shù))。
1: foo: abc
2: bar: 123.45
二、復(fù)雜類型
對于簡單類型的參數(shù)來說,由于支持與字符串類型之間的轉(zhuǎn)換,相應(yīng)ValueProvider可以直接從數(shù)據(jù)源中提取相應(yīng)的數(shù)據(jù)并直接轉(zhuǎn)換成參數(shù)類型。所以針對簡單類型的Model綁定是一步到位的過程,但是針對復(fù)雜類型的Model綁定就沒有這么簡單了。復(fù)雜對象可以表示為一個樹形層次化結(jié)構(gòu),其對象本身和屬性代表相應(yīng)的節(jié)點(diǎn),葉子節(jié)點(diǎn)代表簡單數(shù)據(jù)類型屬性。而ValueProvider采用的數(shù)據(jù)源是一個扁平的數(shù)據(jù)結(jié)構(gòu),它通過基于屬性名稱前綴的Key實(shí)現(xiàn)與這個對象樹中對應(yīng)葉子節(jié)點(diǎn)的映射。
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)和復(fù)雜類型Address的屬性;而Address屬性具有四個簡單類型的屬性。一個Contact對象的數(shù)據(jù)結(jié)構(gòu)可以通過如下圖所示的樹來表示,這個樹種的所有葉子節(jié)點(diǎn)均為簡單類型。如果我們需要通過一個ValueProvider來構(gòu)建一個完整的Contact對象,它必須能夠提供所有所有葉子節(jié)點(diǎn)的數(shù)值,而ValueProvider通過基于屬性名稱前綴的Key實(shí)現(xiàn)與對應(yīng)的葉子節(jié)點(diǎn)的映射。
實(shí)際上當(dāng)我們調(diào)用HtmlHelper<TModel>的模板方法EditorFor/EditorForModel的時候就是按照這樣的匹配方式對標(biāo)單元素進(jìn)行命名的。假設(shè)在將Contact作為Model類型的強(qiáng)類型View中,我們通過調(diào)用HtmlHelper<TModel>的擴(kuò)展方法EditorFor將Model對象的所有信息以編輯的模式呈現(xiàn)出來。
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在最終呈現(xiàn)出來的View中代表的HTML,我們可以清楚地看到這些<input>表單元素完全是根據(jù)屬性名稱和類型層次結(jié)構(gòu)進(jìn)行命名的。隨便提一下,對于基于提交表單的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"... />
對于用于模擬默認(rèn)Model綁定機(jī)制的自定義DefaultModelBinder來說,我們僅僅提供了針對簡單類型的綁定,現(xiàn)在我們對其進(jìn)行完善是之可以提供對復(fù)雜類型的Model綁定。如下面的代碼片斷所示,在BindModel方法中我們創(chuàng)建了一個基于參數(shù)類型的ModelMetadata對象,并根據(jù)其IsComplexType屬性判斷參數(shù)類型是否為復(fù)雜類型。
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: }
如果參數(shù)為復(fù)雜類型,則通過調(diào)用CreateModel方法以反射的方式創(chuàng)建Model對象。CreateModel方法會被用于后面我們會介紹的基于集合和字典的Model綁定,所以我們這里還針對泛型的IDictionary<,>、IEnumerable<>、ICollection<>和IList<>類型作了相應(yīng)地處理。具體來說,如果參數(shù)類型為IDictionary<,>,則創(chuàng)建一個Dictionary<,>對象,而對后三者則創(chuàng)建一個List<>對象,具體的泛型參數(shù)根據(jù)參數(shù)類型獲取。對于一般的類型,我們直接通過Activator的CreateInstance方法根據(jù)參數(shù)類型創(chuàng)建相應(yīng)的Model對象。
通過CreateModel方法創(chuàng)建的是針對參數(shù)類型的“空”對象,我們需要通過Model綁定對它的相關(guān)屬性進(jìn)行初始化。在BindModel方法中,我們遍歷參數(shù)類型的所有屬性,并在現(xiàn)有前綴的基礎(chǔ)上加上“.{屬性名稱}”(如果當(dāng)前前綴為空,則直接采用屬性名稱)作為綁定對應(yīng)屬性的前綴遞歸地調(diào)用BindModel方法得到屬性值。我們最終通過反射的方式將得到值對屬性進(jìn)行賦值。
現(xiàn)在我們采用我們完善后的DefaultModelBinder來進(jìn)行針對復(fù)雜類型的Model綁定。如下面的代碼片斷所示,我們對HomeController的Action方法進(jìn)行了相應(yīng)的修改使之具有兩個Contact類型的參數(shù)foo和bar。在Action方法中,我們將這兩個參數(shù)代表的Contact對象的相關(guān)信息呈現(xiàn)出來。
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", "工業(yè)園區(qū)");
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對象包含的信息包含在它對應(yīng)的NameValueCollection對象中。對于添加到NameValueCollection中的針對Contact對象的某個屬性的數(shù)據(jù)條目,我們按照上面介紹的匹配規(guī)則對其命名。運(yùn)行我們的實(shí)例程序,我們會在瀏覽器中得到如下所示的輸出結(jié)果,我們從中可以看到Action方法的兩個參數(shù)foo和bar通過我們自定義的DefaultModelBinder進(jìn)行了正確地綁定,并且它們具有相同的值。
1: Foo
2: Name: 張三
3: PhoneNo: 123456789
4: EmailAddress: zhangsan@gmail.com
5: Address: 江蘇 蘇州 工業(yè)園區(qū) 星湖街328號
6:
7: Bar
8: Name: 張三
9: PhoneNo: 123456789
10: EmailAddress: zhangsan@gmail.com
11: Address: 江蘇 蘇州 工業(yè)園區(qū) 星湖街328號
之所以同一個Action方法中兩個相同類型的參數(shù)會綁定相同的數(shù)據(jù),使緣于之前介紹的去除前綴的后備Model綁定機(jī)制。由于請求數(shù)據(jù)中并不包含針對某個參數(shù)的前綴,所以在針對參數(shù)名稱作為前綴的Model綁定失敗的情況下,后備Model綁定會前綴為空字符串的情況下再次進(jìn)行。
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: }
在如上所示的代碼中,我們?yōu)镹ameValueCollectionValueProvider設(shè)置了基于“foo”和“bar”的前綴的兩套數(shù)據(jù),目的在為Action方法的foo和bar參數(shù)提供不同的數(shù)據(jù)。運(yùn)行我們的程序后會在瀏覽器上得到如下所示的輸出結(jié)果,可以看出Action方法的兩個參數(shù)被綁定了不同的值。
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
通過實(shí)例模擬ASP.NET MVC的Model綁定的機(jī)制:簡單類型+復(fù)雜類型
通過實(shí)例模擬ASP.NET MVC的Model綁定的機(jī)制:數(shù)組
通過實(shí)例模擬ASP.NET MVC的Model綁定的機(jī)制:集合+字典


總的來說,針對目標(biāo)Action方法參數(shù)的Model綁定完全由組件ModelBinder來實(shí)現(xiàn),在默認(rèn)情況下使用的ModelBinder類型為DefaultModelBinder,接下來我們將按照逐層深入的方式通過一個模擬程序來介紹實(shí)現(xiàn)在DefaultModelBinder的默認(rèn)Model綁定機(jī)制。

浙公網(wǎng)安備 33010602011771號