通過實例模擬ASP.NET MVC的Model綁定機製:數組
[續《通過實例模擬ASP.NET MVC的Model綁定機製:簡單類型+複雜類型]》]基於數組和集合類型的Model綁定機製比較類似,對於綁定參數類型或者參數類型的某個屬性為數組或者集合,如果ValueProvider根據對應的Key能夠匹配多條數據,那麼這些數據最終將會轉換為綁定的數組/集合的元素。此外,針對數組/集合的Model綁定還支持基於索引的方式。[源代碼從這裏下載][本文已經同步到《How ASP.NET MVC Works?》中]
對於針對NameValueConllectionProvider來說,通過GetValue方法得到的ValueProviderResult的RawValue總是一個字符串數組(不論是否具有多條數據於指定的Key相匹配,如果隻有一條匹配的數據,RawValue就是一個具有一個元素的字符串數組)。當我們調用ValueProviderResult的ConvertTo方法將提供的值轉換成某種類型時,如果目標類型是數組或者集合,那麼RawValue代表的字符串數組元素將會轉換成目標對象的元素;如果目標類型不屬於集合,那麼參與數據轉換的僅僅是RawValue數組的。
如下麵的代碼片斷所示,在默認的HomeController的默認Action方法Index中,我們創建了一個NameValueCollectionValueProvider對象,作為數據源的NameValueCollection中包含了三個同名(foo)數據條目。我們調用它的GetValue方法得到一個ValueProviderResult對象,然後我們將該對象的RawValue呈現出來。最後我們調用該ValueProviderResult對象的ConvertTo對象將提供的值轉換為int[]和int,並將轉換後的值呈現出來。
1: public class HomeController : Controller
2: {
3: public void Index()
4: {
5: NameValueCollection dataSource = new NameValueCollection();
6: dataSource.Add("foo", "123");
7: dataSource.Add("foo", "456");
8: dataSource.Add("foo", "789");
9: NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(dataSource, CultureInfo.InvariantCulture);
10:
11: ValueProviderResult result = valueProvider.GetValue("foo");
12: Response.Write(string.Format("RawValue: {0}<br/>", result.RawValue));
13: Response.Write(string.Format("ConvertTo(typeof(int[])): {0}<br/>", result.ConvertTo(typeof(int[]))));
14: Response.Write(string.Format("ConvertTo(typeof(int)): {0}<br/>", result.ConvertTo(typeof(int))));
15: }
16: }
運行這個程序之後,我們會在瀏覽器中得到如下的輸出結果,上麵針對NameValueConllectionProvider的論述可以從輸出結果中得到印證。
1: RawValue: System.String[]
2: ConvertTo(typeof(int[])): System.Int32[]
3: ConvertTo(typeof(int)): 123
NameValueConllectionProvider(FormValueProvider和QueryStringValueProvider)的數據值提供機製決定了Model綁定的默認行為。。實際上HttpFileCollectionValueProvider的數據值提供機製也類似,如果綁定的目標對象類型是一個HttpPostedFileBase數組,那麼匹配的同名文件輸入元素都將作為其數據源。
1: <input name="Foo" type="text" ... />
2: <input name="Foo" type="text" ... />
3: <input name="Foo" type="text" ... />
4: <input name="Bar" type="file" ... />
5: <input name="Bar" type="file" ... />
6: <input name="Bar" type="file" ... />
假設針對具有如下定義的Action方法ActionMethod提交的標單具有如上的輸入元素,在三個文本框中輸入的字符串將綁定到foo參數,而通過三個文件輸入元素上傳得文件將會綁定給bar參數。
1: Public void ActionMethod(string[] foo, HttpPostedFileBase[] bar)
現在我們對用於模擬默認Model綁定的自定義DefaultModelBinder進行進一步完善,使之對基於名稱的數組綁定提供支持。如下麵的代碼片斷所示,我們在BindModel方法中添加了針對數組類型的Model綁定代碼,而具體的實現定義在BindArrayModel方法中。
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:
11: ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, parameterType);
12: if (!modelMetadata.IsComplexType)
13: {
14: return this.ValueProvider.GetValue(prefix).ConvertTo(parameterType);
15: }
16: if (parameterType.IsArray)
17: {
18: return BindArrayModel(parameterType, prefix);
19: }
20: object model = CreateModel(parameterType);
21:
22: foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(parameterType))
23: {
24: string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name;
25: property.SetValue(model, BindModel(property.PropertyType, key));
26: }
27: return model;
28: }
29: private object BindArrayModel(Type parameterType, string prefix)
30: {
31: IList list = new List<object>();
32: if (this.ValueProvider.ContainsPrefix(prefix))
33: {
34: IEnumerable enumerable = this.ValueProvider.GetValue(prefix).ConvertTo(parameterType) as IEnumerable;
35: if (null != enumerable)
36: {
37: foreach (var value in enumerable)
38: {
39: list.Add(value);
40: }
41: }
42: }
43: Array array = Array.CreateInstance(parameterType.GetElementType(), list.Count);
44: list.CopyTo(array,0);
45: return array;
46: }
47: }
定義在BindArrayModel方法中針對數組的Model綁定邏輯很簡單,我們直接通過ValueProvider將通過指定前綴得到的數據值轉換為IEnumerable類型,並進一步添加到一個List<object>對象中,最終我們將該List<object>對象的元素拷貝到一個創建的數組對象並將其作為Model對象返回。
為了演示針對數組的Model綁定,我們按照如下的方式修改了Action方法。該方法具有兩個參數foo和bar,前者是一個字符串數組,後者的類型Bar的Baz屬性是一個整型數組。在Action方法中,我們將foo參數和bar參數的Baz屬性代表數組元素呈現出來。
1: public class HomeController : Controller
2: {
3: //其他成員
4: private IValueProvider GetValueProvider()
5: {
6: NameValueCollection requestData = new NameValueCollection();
7:
8: requestData.Add("foo", "abc");
9: requestData.Add("foo", "xyz");
10:
11: requestData.Add("bar.baz", "123");
12: requestData.Add("bar.baz", "456");
13:
14: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
15: }
16:
17: public void Action(string[] foo, Bar bar)
18: {
19: Response.Write("foo: <br/>");
20: Array.ForEach(foo,item=> Response.Write(" "+ item + "<br/>"));
21: Response.Write("bar.Baz: <br/>");
22: Array.ForEach(bar.Baz, item => Response.Write(" " + item + "<br/>"));
23: }
24: }
25:
26: public class Bar
27: {
28: public int[] Baz { get; set; }
29: }
通過GetValueProvider方法提供的NameValueCollectionValueProvider具有針對這兩個參數的數據源,從上麵的代碼片斷所示,針對參數foo和bar的數據項具有相同的名稱(foo和bar.baz)。該程序運行之後會在瀏覽器中得到如下所示的輸出結果。
1: foo:
2: abc
3: xyz
4: bar.Baz:
5: 123
6: 456
對於存在於作為ValueProvider數據源的NameValueCollection/Dictionary<string, object>中的數據項來說,如果它們綁定的對象是一個數組,可以采用相同的名稱。這樣的Model綁定方式僅僅是用於元素為簡單類型的數組。除此之外,也可以采用格式為的基於索引的前綴來表示。
ValueProvider基於索引的匹配策略也可以通過HtmlHelper<TModel>的模板方法EditorFor來體現。如下麵的代碼片斷所示,在一個Model類型為Contact數組的強類型View中,我們調用HtmlHelper<TModel>的擴展方法EditorFor將數組的前兩個元素的相關信息以編輯模式呈現出來。
1: @model Contact[]
2: @Html.EditorFor(m => m[0].Name)
3: @Html.EditorFor(m => m[0].PhoneNo)
4: @Html.EditorFor(m => m[0].EmailAddress)
5:
6: @Html.EditorFor(m => m[1].Name)
7: @Html.EditorFor(m => m[1].PhoneNo)
8: @Html.EditorFor(m => m[1].EmailAddress)
下麵的XML片斷代表了上麵這段代碼在最終生成的HTML中對應的6個類型為“text”的<input>元素,我們可以清楚地看到它們的名稱被添加了[0]和[1]這樣的索引前綴。如果這些元素存在於一個提交的標單中,並且目標Action方法包含一個匹配的Contact數組類型的參數,Model綁定係統將最終生成兩個元素的Contact數組作為其參數值,數組中元素的順序與索引數值保持一致。
1: <input name="[0].Name" type="text" value="" .../>
2: <input name="[0].PhoneNo" type="text" value="" .../>
3: <input name="[0].EmailAddress" type="text" value="" .../>
4:
5: <input name="[1].Name" type="text" value="" .../>
6: <input name="[1].PhoneNo" type="text" value="" .../>
7: <input name="[1].EmailAddress" type="text" value="" .../>
基於數組的Model綁定采用“基零索引”,即將作為數組下邊界的索引前綴必須是“[0]”。此外,還要求索引在數值上的。舉個簡單的例子,假設提交的標單中具有如下6個類型為“hidden”的<input>元素,它們采用了基於索引的命名,並且從數字上看索引的(缺了一個[3])。
1: <input name="[0]" type="hidden" value="foo" />
2: <input name="[1]" type="hidden" value="bar" />
3: <input name="[2]" type="hidden" value="baz" />
4:
5: <input name="[4]" type="hidden" value="123" />
6: <input name="[5]" type="hidden" value="456" />
7: <input name="[6]" type="hidden" value="789" />
如果提供的標單對應如下所示的Action方法,上述的<input>元素值將會綁定到字符串數組類型的參數array上。由於索引值不具有連續性,會,也就是說綁定後的array參數值僅僅具有三個元素(“foo”、“bar”和“baz”)。
1: public ActionResult Index(string[] array);
除了采用基零整數作為數組索引之外,我們還可以采用,但是作為索引的字符串需要和數組元素值一樣存在於ValueProvider的數據源中。索引數據項名稱為“index”,並且與數組元素數據項具有相同的前綴。同樣以上麵這個參數類型為字符串數組的Action方法為例,我們可以通過提交具有如下內容的表單來調用這個Action方法並為之提供相應的參數值。
1: <input name="index" type="hidden" value="first" />
2: <input name="index" type="hidden" value="second" />
3: <input name="index" type="hidden" value="third" />
4:
5: <input name="[first]" type="text" value="foo" />
6: <input name="[second]" type="text" value="bar" />
7: <input name="[third]" type="text" value="baz" />
被提交標單中三個類型為“text”的<input>元素值將會綁定到目標Action方法的字符串參數array。它們通過基於字符串的索引進行命名,而。這些用於定義索引字符串的<input>元素一並命名為“index”。
現在我們對用於模擬默認Model綁定的自定義DefaultModelBinder進行進一步完善,使之支持基於索引的數組綁定。如下的代碼片斷所示,我們在用於進行數組綁定的BindArrayModel方法中添加了額外的代碼用於提取索引值(整型和字符串類型)列表,並且根據這行索引值生成相應的前綴和對應的Key通過ValueProvider得到針對數組元素的值。得到的值被添加到預先創建的對象列表中並最終成為作為參數值的數組對象的元素。
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:
11: ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, parameterType);
12: if (!modelMetadata.IsComplexType)
13: {
14: return this.ValueProvider.GetValue(prefix).ConvertTo(parameterType);
15: }
16: if (parameterType.IsArray)
17: {
18: return BindArrayModel(parameterType, prefix);
19: }
20: object model = CreateModel(parameterType);
21: foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(parameterType))
22: {
23: string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name;
24: property.SetValue(model, BindModel(property.PropertyType, key));
25: }
26: return model;
27: }
28:
29: private object BindArrayModel(Type parameterType, string prefix)
30: {
31: List<object> list = new List<object>();
32: if (!string.IsNullOrEmpty(prefix) && this.ValueProvider.ContainsPrefix(prefix))
33: {
34: IEnumerable enumerable = this.ValueProvider.GetValue(prefix).ConvertTo(parameterType) as IEnumerable;
35: if (null != enumerable)
36: {
37: foreach (var value in enumerable)
38: {
39: list.Add(value);
40: }
41: }
42: }
43:
44: bool numericIndex;
45: IEnumerable<string> indexes = GetIndexes(prefix, out numericIndex);
46: foreach (var index in indexes)
47: {
48: string indexPrefix = prefix + "[" + index + "]";
49: if (!this.ValueProvider.ContainsPrefix(indexPrefix) && numericIndex)
50: {
51: break;
52: }
53: list.Add(BindModel(parameterType.GetElementType(), indexPrefix));
54: }
55: object[] array = (object[])Array.CreateInstance(parameterType.GetElementType(), list.Count);
56: list.CopyTo(array);
57: return array;
58: }
59: private IEnumerable<string> GetIndexes(string prefix, out bool numericIndex)
60: {
61: string key = string.IsNullOrEmpty(prefix)?"index": prefix+"."+"index";
62: ValueProviderResult result = this.ValueProvider.GetValue(key);
63: if (null != result)
64: {
65: string[] indexes = result.ConvertTo(typeof(string[])) as string[];
66: if (null != indexes)
67: {
68: numericIndex = false;
69: return indexes;
70: }
71: }
72: numericIndex = true;
73: return GetZeroBasedIndexes();
74: }
75: private static IEnumerable<string> GetZeroBasedIndexes()
76: {
77: int iteratorVariable0 = 0;
78: while (true)
79: {
80: yield return iteratorVariable0.ToString();
81: iteratorVariable0++;
82: }
83: }
84: }
索引列表的獲取通過方法GetIndexes實現。由於作為索引值的數據項以“index”命名,所以該方法在此基礎上加上傳入的前綴作為key調用ValueProvider的GetValue方法可以直接得到針對指定前綴的所有字符串類型的索引值。而針對基零整數的索引列表則通過GetZeroBasedIndexes方法返回。
我們現在將自定義的DefaultModelBinder用於進行基於數組的Model綁定,在之前演示實例的基礎上我們對Action方法作了如下的修改,使之具有一個Contact數組類型的參數。在該Action方法中,我們將作為數組元素的Contact對象相關信息呈現出來。對於通過GetValueProvider方法提供的NameValueCollectionValueProvider來說,我們以基零整數的方式提供了兩個Contact對象的數據。
1: public class HomeController : Controller
2: {
3: //其他成員
4: private IValueProvider GetValueProvider()
5: {
6: NameValueCollection requestData = new NameValueCollection();
7:
8: requestData.Add("[0].Name", "Foo");
9: requestData.Add("[0].PhoneNo", "123456789");
10: requestData.Add("[0].EmailAddress", "Foo@gmail.com");
11:
12: requestData.Add("[1].Name", "Bar");
13: requestData.Add("[1].PhoneNo", "987654321");
14: requestData.Add("[1].EmailAddress", "Bar@gmail.com");
15:
16: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
17: }
18:
19: public void Action(Contact[] contacts)
20: {
21: foreach (Contact contact in contacts)
22: {
23: Response.Write(string.Format("{0}: {1}<br/>", "Name", contact.Name));
24: Response.Write(string.Format("{0}: {1}<br/>", "PhoneNo", contact.PhoneNo));
25: Response.Write(string.Format("{0}: {1}<br/><br/>", "EmailAddress", contact.EmailAddress));
26: }
27: }
28: }
運行我們的程序之後會在瀏覽器中得到如下所示的輸出結果,可見目標Action的數組參數通過我們自定義的DefaultModelBinder得到了正確地綁定。(S517)
1: Name: Foo
2: PhoneNo: 123456789
3: EmailAddress: Foo@gmail.com
4:
5: Name: Bar
6: PhoneNo: 987654321
7: EmailAddress: Bar@gmail.com
上麵這個例子演示了針對基零整數作為索引的數組綁定,DefaultModelBinder同樣支持針對任意字符串作為索引的數組綁定。在下麵的代碼片斷中,我們修改了GetValueProvider方法使創建的NameValueCollectionValueProvider以字符串索引的方式為Contact數組提供數據。程序運行之後,我們可以在瀏覽器中得到相同的輸出結果。
1: public class HomeController : Controller
2: {
3: //其他成員
4: private IValueProvider GetValueProvider()
5: {
6: NameValueCollection requestData = new NameValueCollection();
7: requestData.Add("index", "first");
8: requestData.Add("index", "second");
9:
10: requestData.Add("[first].Name", "Foo");
11: requestData.Add("[first].PhoneNo", "123456789");
12: requestData.Add("[first].EmailAddress", "Foo@gmail.com");
13:
14: requestData.Add("[second].Name", "Bar");
15: requestData.Add("[second].PhoneNo", "987654321");
16: requestData.Add("[second].EmailAddress", "Bar@gmail.com");
17:
18: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
19: }
20: }
通過實例模擬ASP.NET MVC的Model綁定的機製:簡單類型+複雜類型
通過實例模擬ASP.NET MVC的Model綁定的機製:數組
通過實例模擬ASP.NET MVC的Model綁定的機製:集合+字典
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 11:34:05