在本系列的前面两篇文章(《简单类型+复杂类型》、《数组》)我们通过创建的实例程序模拟了ASP.NET MVC默认使用的DefaultModelBinder对简单类型、复杂类型以及数组对象的Model绑定。现在我们按照相同的方式来分析基于集合和字典类型的Model绑定是如何实现的。[源代码从这里下载][本文已经同步到《How ASP.NET MVC Works?》中]
一、集合
这里的集合指的是除数组和字典之外的所有实现IEnumerable
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: if (parameterType.IsArray)
16: {
17: return BindArrayModel(parameterType, prefix);
18: }
19: object model = CreateModel(parameterType);
20: Type enumerableType = ExtractGenericInterface(parameterType, typeof(IEnumerable<>));
21: if (null != enumerableType)
22: {
23: return BindCollectionModel(prefix, model, enumerableType);
24: }
25: foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(parameterType))
26: {
27: string key = prefix == "" ? property.Name : prefix + "." + property.Name;
28: property.SetValue(model, BindModel(property.PropertyType, key));
29: }
30: return model;
31: }
32:
33: private object BindCollectionModel(string prefix, object model, Type enumerableType)
34: {
35: List<object> list = new List<object>();
36: bool numericIndex;
37: IEnumerable<string> indexes = GetIndexes(prefix, out numericIndex);
38: Type elementType = enumerableType.GetGenericArguments()[0];
39:
40: if (!string.IsNullOrEmpty(prefix) && this.ValueProvider.ContainsPrefix(prefix))
41: {
42: IEnumerable enumerable = this.ValueProvider.GetValue(prefix).ConvertTo(enumerableType) as IEnumerable;
43: if (null != enumerable)
44: {
45: foreach (var value in enumerable)
46: {
47: list.Add(value);
48: }
49: }
50: }
51: foreach (var index in indexes)
52: {
53: string indexPrefix = prefix + "[" + index + "]";
54: if (!this.ValueProvider.ContainsPrefix(indexPrefix) && numericIndex)
55: {
56: break;
57: }
58: list.Add(BindModel(elementType, indexPrefix));
59: }
60: if (list.Count == 0)
61: {
62: return null;
63: }
64: ReplaceHelper.ReplaceCollection(elementType, model, list);
65: return model;
66: }
67:
68: private Type ExtractGenericInterface(Type queryType, Type interfaceType)
69: {
70: Funcbool> predicate = t => t.IsGenericType && (t.GetGenericTypeDefinition() == interfaceType);
71: if (!predicate(queryType))
72: {
73: return queryType.GetInterfaces().FirstOrDefault(predicate);
74: }
75: return queryType;
76: }
77: }
如上面的代码片断所示,在BindModel方法中我们通过调用ExtractGenericInterface判断目标类型是否实现了IEnumerable
1: internal static class ReplaceHelper
2: {
3: private static MethodInfo replaceCollectionMethod = typeof(ReplaceHelper).GetMethod("ReplaceCollectionImpl", BindingFlags.Static |BindingFlags.NonPublic);
4:
5: public static void ReplaceCollection(Type collectionType, object collection, object newContents)
6: {
7: replaceCollectionMethod.MakeGenericMethod(new Type[] { collectionType }).Invoke(null, new object[] { collection, newContents });
8: }
9: private static void ReplaceCollectionImpl(ICollection collection, IEnumerable newContents)
10: {
11: collection.Clear();
12: if (newContents != null)
13: {
14: foreach (object obj2 in newContents)
15: {
16: T item = (obj2 is T) ? ((T)obj2) : default(T);
17: collection.Add(item);
18: }
19: }
20: }
21: }
为了让演示针对集合类型的Model绑定,我们对实例中的HomeController作了如下的修改。Action方法的参数类型替换成IEnumerable
1: public class HomeController : Controller
2: {
3: private IValueProvider GetValueProvider()
4: {
5: NameValueCollection requestData = new NameValueCollection();
6: requestData.Add("[0].Name", "Foo");
7: requestData.Add("[0].PhoneNo", "123456789");
8: requestData.Add("[0].EmailAddress", "[email protected]");
9:
10: requestData.Add("[1].Name", "Bar");
11: requestData.Add("[1].PhoneNo", "987654321");
12: requestData.Add("[1].EmailAddress", "[email protected]");
13:
14: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
15: }
16:
17: public void Action(IEnumerablecontacts)
18: {
19: foreach (Contact contact in contacts)
20: {
21: Response.Write(string.Format("{0}: {1}
", "Name", contact.Name));
22: Response.Write(string.Format("{0}: {1}
", "Phone No.", contact.PhoneNo));
23: Response.Write(string.Format("{0}: {1}
", "Email Address",contact.EmailAddress));
24: }
25: }
26: }
该程序被执行之后,在浏览器上依然会呈现出如下所示的我们希望的数据,这充分证明了我们自定义的DefaultModelBinder具有针对集合的绑定能力。
1: Name: Foo
2: PhoneNo: 123456789
3: EmailAddress: [email protected]
4:
5: Name: Bar
6: PhoneNo: 987654321
7: EmailAddress: [email protected]
二、 字典
这里的字典指的是实现了接口IDictionary
1: [0].Key : Foo
2: [0].Value.Name : Foo
3: [0].Value.EmailAddress: [email protected]
4: [0].Value.PhoneNo : 123456789
5:
6: [1].Key : Bar
7: [1].Value.Name : Bar
8: [1].Value.EmailAddress: [email protected]
9: [1].Value.PhoneNo : 987654321
现在我们对用于模拟默认Model绑定的自定义DefaultModelBinder作最后的完善,使之支持针对字典类型的Model绑定。如下面的代码片断所示,在通过调用CreateModel创建Model对象之后,我们调用ExtractGenericInterface方法判断目标类型是否是一个字典,如果是则返回具体的字典类型,然后调用BindDictionaryModel方法实施针对字典类型的Model绑定。
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: Type dictionaryType = ExtractGenericInterface(parameterType, typeof(IDictionary<,>));
22: if (null != dictionaryType)
23: {
24: return BindDictionaryModel(prefix, model, dictionaryType);
25: }
26:
27: Type enumerableType = ExtractGenericInterface(parameterType, typeof(IEnumerable<>));
28: if (null != enumerableType)
29: {
30: return BindCollectionModel(prefix, model, enumerableType);
31: }
32: foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(parameterType))
33: {
34: string key = prefix == "" ? property.Name : prefix + "." + property.Name;
35: property.SetValue(model, BindModel(property.PropertyType, key));
36: }
37: return model;
38: }
39:
40: private object BindDictionaryModel(string prefix, object model, Type dictionaryType)
41: {
42: Listobject, object>> list = new List object, object>>();
43: bool numericIndex;
44: IEnumerable<string> indexes = GetIndexes(prefix, out numericIndex);
45: Type[] genericArguments = dictionaryType.GetGenericArguments();
46: Type keyType = genericArguments[0];
47: Type valueType = genericArguments[1];
48:
49: foreach (var index in indexes)
50: {
51: string indexPrefix = prefix + "[" + index + "]";
52: if (!this.ValueProvider.ContainsPrefix(indexPrefix) && numericIndex)
53: {
54: break;
55: }
56: string keyPrefix = indexPrefix + ".Key";
57: string valulePrefix = indexPrefix + ".Value";
58: list.Add(new KeyValuePair<object, object>(BindModel(keyType, keyPrefix), BindModel(valueType, valulePrefix)));
59: }
60: if (list.Count == 0)
61: {
62: return null;
63: }
64: ReplaceHelper.ReplaceDictionary(keyType, valueType, model, list);
65: return model;
66: }
67: }
在BindDictionaryModel方法中,我们采用与数组/集合绑定一样的方式调用GetIndexes方法得到索引列表。在对该列表进行遍历过程中,我们在索引的基础上添加“.Key”和“.Value”后缀从而得到作为字典元素(KeyValuePair
1: internal static class ReplaceHelper
2: {
3: //其他成员
4: private static MethodInfo replaceDictionaryMethod = typeof(ReplaceHelper).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static |BindingFlags.NonPublic);
5:
6: public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents)
7: {
8: replaceDictionaryMethod.MakeGenericMethod(new Type[] { keyType, valueType }).Invoke(null, new object[] { dictionary, newContents });
9: }
10:
11: private static void ReplaceDictionaryImpl(IDictionary dictionary, IEnumerable object, object>> newContents)
12: {
13: dictionary.Clear();
14: foreach (KeyValuePair<object, object> pair in newContents)
15: {
16: TKey key = (TKey)pair.Key;
17: TValue local2 = (TValue)((pair.Value is TValue) ? pair.Value : default(TValue));
18: dictionary[key] = local2;
19: }
20: }
21: }
我们照例通过我们创建的实例程序来验证自定义的DefaultModelBinder是否能够支持针对字典的Model绑定。如下面的代码片断所示,我们让HomeController的Action方法接受一个IDictionary
1: public class HomeController : Controller
2: {
3: private IValueProvider GetValueProvider()
4: {
5: NameValueCollection requestData = new NameValueCollection();
6: requestData.Add("[0].Key", "Foo");
7: requestData.Add("[0].Value.Name", "Foo");
8: requestData.Add("[0].Value.PhoneNo", "123456789");
9: requestData.Add("[0].Value.EmailAddress", "[email protected]");
10:
11: requestData.Add("[1].Key", "Bar");
12: requestData.Add("[1].Value.Name", "Bar");
13: requestData.Add("[1].Value.PhoneNo", "987654321");
14: requestData.Add("[1].Value.EmailAddress", "[email protected]");
15:
16: return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
17: }
18:
19: public void Action(IDictionary<string, Contact> contacts)
20: {
21: foreach (string key in contacts.Keys)
22: {
23: Response.Write(key + "
");
24: Contact contact = contacts[key];
25: Response.Write(string.Format(" {0}: {1}
","Name", contact.Name));
26: Response.Write(string.Format(" {0}: {1}
","PhoneNo", contact.PhoneNo));
27: Response.Write(string.Format(" {0}: {1}
", "EmailAddress", contact.EmailAddress));
28: }
29: }
30: }
程序运行之后会在浏览器中得到如下的我们期望的输出结果。(S520)
1: Foo
2: Name: Foo
3: PhoneNo: 123456789
4: EmailAddress: [email protected]
5:
6: Bar
7: Name: Bar
8: PhoneNo: 987654321
9: EmailAddress: [email protected]
通过实例模拟ASP.NET MVC的Model绑定的机制:简单类型+复杂类型
通过实例模拟ASP.NET MVC的Model绑定的机制:数组
通过实例模拟ASP.NET MVC的Model绑定的机制:集合+字典