• 通过实例模拟ASP.NET MVC的Model绑定的机制:集合+字典


    在本系列的前面两篇文章(《简单类型+复杂类型》、《数组》)我们通过创建的实例程序模拟了ASP.NET MVC默认使用的DefaultModelBinder对简单类型、复杂类型以及数组对象的Model绑定。现在我们按照相同的方式来分析基于集合和字典类型的Model绑定是如何实现的。[源代码从这里下载][本文已经同步到《How ASP.NET MVC Works?》中]

    一、集合

    这里的集合指的是除数组和字典之外的所有实现IEnumerable<T>接口的类型。和基于数组的Model绑定类似,ValueProvider可以将多个同名的数据项作为集合的元素,基于索引(基零整数和字符串)的数据项命名方式同样适用。我们对自定义的DefaultModelBinder作了如下的完善使之支持集合类型的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:         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:         Func<Type, bool> predicate = t => t.IsGenericType && (t.GetGenericTypeDefinition() == interfaceType);
      71:         if (!predicate(queryType))
      72:         {
      73:             return queryType.GetInterfaces().FirstOrDefault<Type>(predicate);
      74:         }
      75:         return queryType;
      76:     }
      77: }

    如上面的代码片断所示,在BindModel方法中我们通过调用ExtractGenericInterface判断目标类型是否实现了IEnumerable<T>接口,如果实现了该接口则提取泛型元素类型。针对集合的Model绑定实现在方法BindCollectionModel中,我们按照数组绑定的方式得的针对目标集合对象的所有元素对象,并将其添加到一个List<object>对象中,然后调用ReplaceHelper 的静态方法ReplaceCollection将该列表中的元素拷贝到预先创建的Model对象中。定义在ReplaceHelper的静态方法ReplaceCollection定义如下:

       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<T>(ICollection<T> 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<Contact>,该集合中的每个Contact的信息在该方法中被呈现出来。通过GetValueProvider提供的NameValueCollectionValueProvider采用基零整数索引的方式定义数据项。

       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", "Foo@gmail.com");
       9:  
      10:         requestData.Add("[1].Name", "Bar");
      11:         requestData.Add("[1].PhoneNo", "987654321");
      12:         requestData.Add("[1].EmailAddress", "Bar@gmail.com");
      13:  
      14:         return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
      15:     }
      16:          
      17:     public void Action(IEnumerable<Contact> contacts)
      18:     {
      19:         foreach (Contact contact in contacts)
      20:         {
      21:             Response.Write(string.Format("{0}: {1}<br/>", "Name", contact.Name));
      22:             Response.Write(string.Format("{0}: {1}<br/>", "Phone No.", contact.PhoneNo));
      23:             Response.Write(string.Format("{0}: {1}<br/><br/>", "Email Address",contact.EmailAddress));
      24:         }
      25:     }
      26: }

    该程序被执行之后,在浏览器上依然会呈现出如下所示的我们希望的数据,这充分证明了我们自定义的DefaultModelBinder具有针对集合的绑定能力。

       1: Name: Foo
       2: PhoneNo: 123456789
       3: EmailAddress: Foo@gmail.com
       4:  
       5: Name: Bar
       6: PhoneNo: 987654321
       7: EmailAddress: Bar@gmail.com

    二、 字典

    这里的字典指的是实现了接口IDictionary<TKey,TValue>的类型。在Model绑定过程中基于字典类型的数据映射很好理解,首先,字典是一个KeyValuePair<TKey,TValue>对象的集合,所以在字典元素这一级可以采用基于索引的匹配机制;其次,KeyValuePair<TKey,TValue>是一个复杂类型,可以按照属性名称(Key和Value)进行匹配。比如说作为某个ValueProvider数据源的NameValueCollection具有如下的结构,它可以映射为一个IDictionary<string, Contact>对象(Contact对象作为Value,其Name属性作为Key)。

       1: [0].Key               : Foo
       2: [0].Value.Name        : Foo
       3: [0].Value.EmailAddress: Foo@gmail.com
       4: [0].Value.PhoneNo     : 123456789
       5:  
       6: [1].Key               : Bar
       7: [1].Value.Name        : Bar
       8: [1].Value.EmailAddress: Bar@gmail.com
       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:         List<KeyValuePair<object, object>> list = new List<KeyValuePair<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<TKey, TValue>)Key和Value对象的前缀,并将该前缀作为参数递归地调用BindModel方法得到具体作为Key和Value的对象。在得到字典元素Key和Value之后,我们创建一个KeyValuePair<object, object>对象并添加预先创建的列表中。最后我们调用ReplaceHelper的静态方法ReplaceDictionary将该列表拷贝到作为Model的字典对象中,ReplaceHelper的静态方法ReplaceDictionary定义如下。

       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<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<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<string, Contact>类型的参数,并在该方法中将作为Key的字符串和作为Value的Contact的相关信息呈现出来。在GetValueProvider方法中提供的NameValueCollectionValueProvider按照相应的映射规则对绑定到字典对象的数据项。

       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", "Foo@gmail.com");
      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", "Bar@gmail.com");
      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 + "<br/>");
      24:             Contact contact = contacts[key];
      25:             Response.Write(string.Format("&nbsp;&nbsp;&nbsp;&nbsp;{0}: {1}<br/>","Name", contact.Name));
      26:             Response.Write(string.Format("&nbsp;&nbsp;&nbsp;&nbsp;{0}: {1}<br/>","PhoneNo", contact.PhoneNo));
      27:             Response.Write(string.Format("&nbsp;&nbsp;&nbsp;&nbsp;{0}: {1}<br/><br/>", "EmailAddress", contact.EmailAddress));
      28:         }
      29:     }
      30: }

    程序运行之后会在浏览器中得到如下的我们期望的输出结果。(S520)

       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绑定的机制:集合+字典

    作者:Artech
    出处:http://artech.cnblogs.com/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    如何在 Knative 中部署 WebSocket 和 gRPC 服务?
    全球首个开放应用模型 OAM 开源 | 云原生生态周报 Vol. 23
    从零开始入门 K8s | Kubernetes 网络概念及策略控制
    重磅发布 | 全球首个云原生应用标准定义与架构模型 OAM 正式开源
    成都,我们来啦 | Dubbo 社区开发者日
    一文读懂分布式架构知识体系(内含超全核心知识大图)
    阿里巴巴开源 Dragonwell JDK 最新版本 8.1.1-GA 发布
    可能是国内第一篇全面解读 Java 现状及趋势的文章
    从零开始入门 K8s | 可观测性:监控与日志
    阿里巴巴的云原生与开发者
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2527859.html
Copyright © 2020-2023  润新知