閱讀299 返回首頁    go 阿裏雲 go 技術社區[雲棲]


通過實例模擬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綁定的機製:集合+字典


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-26 11:34:03

  上一篇:go  通過實例模擬ASP.NET MVC的Model綁定機製:數組
  下一篇:go  ASP.NET MVC以ModelValidator為核心的Model驗證體係: ModelValidator