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


ASP.NET MVC以ValueProvider為核心的值提供係統: NameValueCollectionValueProvider

在進行Model綁定過程中,需要根據基於Action方法參數的綁定上下文從請求數據中提取相應的數據以提供相應的數據。具體來說,Model綁定的數據具有多個來源,可能來源於Post的表單或者JSON字符串,或者來源於當前的路由數據,也可能來源於請求地址的插敘字符串。ASP.NET MVC將這種基於不同數據來源的數據獲取/提供機製實現在一個叫做ValueProvider的組件中。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、IValueProvider與ValueProviderResult
二、NameValueCollectionValueProvider
三、兩種前綴形式
四、實例演示:返回指定前綴的Key
五、FormValueProvider與QueryStringValueProvider

一般來講,一個ValueProvider采用的數據源是一個字典類型的數據結構,我們通過它從這個字典中獲取一個Key與當前綁定上下文匹配的值。ValueProvider實現了具有如下定義的接口IValueProvider,GetValue方法根據指定的Key從數據源中獲取對應的值對象,這個Key是基於當前綁定上下文的。這個Key和存在於數據源中對應數據條目的Key可能並非完全一致,後者可能在前者基礎上添加相應的前綴,而ContainsPrefix方法用於判斷數據源字典的Key是否具有指定的前綴。

   1: public interface IValueProvider
   2: {
   3:     bool ContainsPrefix(string prefix);
   4:     ValueProviderResult GetValue(string key);
   5: }

IValueProvider的GetValue返回的是一個ValueProviderResult對象,我們可以將ValueProviderResult看成是對ValueProvider提供對象的封裝。如下麵的代碼片斷所示,ValueProviderResult具有三個隻讀屬性,其中RawValue表示原始的值對象。而AttemptedValue表示以值對象的字符串表示,該屬性主要用於顯示。

   1: [Serializable]
   2: public class ValueProviderResult
   3: {    
   4:     public ValueProviderResult(object rawValue, string attemptedValue, CultureInfo culture);    
   5:     public object ConvertTo(Type type);
   6:     public virtual object ConvertTo(Type type, CultureInfo culture);
   7:  
   8:     public string AttemptedValue { get; }
   9:     public CultureInfo Culture { get; }
  10:     public object RawValue { get; }
  11: }

ValueProviderResult提供了兩個ConvertTo方法重載以實現向指定目標類型的轉換。某些類型的格式化行為依賴於相應的語言文化(比如時間、日期和貨幣等),而這個輔助格式湖的語言文化信息通過Culture屬性表示。其中第一個ValueProviderResult方法重載通過屬性Culture表示的語言文化進行類型轉化。

前麵已經說過,Model數據源一般具有類似於字典的結構,而NameValueCollection可以表示為Key不具有唯一性的字典,將NameValueCollection對象作為數據源的ValueProvider通過具有如下定義的NameValueCollectionValueProvider類型表示。表示數據源的NameValueCollection對象在構造函數中指定,構造函數的另一個CultureInfo類型的參數表示服務於數據轉換的語言文化信息。

   1: public class NameValueCollectionValueProvider : IUnvalidatedValueProvider, IEnumerableValueProvider, IValueProvider
   2: {   
   3:     //其他成員
   4:     public NameValueCollectionValueProvider(NameValueCollection collection,  CultureInfo culture);
   5:  
   6:     public virtual bool ContainsPrefix(string prefix);
   7:     public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix);
   8:     public virtual ValueProviderResult GetValue(string key);
   9:     public virtual ValueProviderResult GetValue(string key, bool skipValidation);  
  10: }
  11:  
  12: public interface IEnumerableValueProvider : IValueProvider
  13: {
  14:     IDictionary<string, string> GetKeysFromPrefix(string prefix);
  15: }
  16:  
  17: public interface IUnvalidatedValueProvider : IValueProvider
  18: {
  19:     ValueProviderResult GetValue(string key, bool skipValidation);
  20: }

從上麵的代碼片斷我們可以看到,除了IValueProvider接口,NameValueCollectionValueProvider還實現了IEnumerableValueProvider和IUnvalidatedValueProvider兩個接口。顧名思義,IEnumerableValueProvider主要用於針對目標類型為集合的數據提供,方法GetKeysFromPrefix以一字典的形式返回具有指定前綴的Key。在默認的情況下,在進行數據提供的同時會對數據進行驗證,而IUnvalidatedValueProvider接口提供了一個額外的GetValue方法是我們可以忽略對數據的驗證。

輔助實現Model綁定的數據提供機製是以Model元數據為基礎的,通過《初識Model元數據》我們知道用於描述一個複雜數據類型的Model元數據具有一個樹型的層次化結構,而作為數據源的NameValueCollection卻是一個“扁平”的結構,兩者之前的匹配通過前綴來表示。舉個簡單的例子,假設通過NameValueCollectionValueProvider提供對象的目標類型為具有如下定義的Contact。表示聯係地址的屬性是一個複雜類型Address,所以針對Contact類型的Model元數據樹具有兩個層級。

   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: }

由於NameValueCollection中每個元數據的值都是一個字符串,所以不可能單獨表示一個複雜類型,複雜類型對象需要通過多個元素值組裝而成。如果通過NameValueCollectionValueProvider來初始化一個完整的Contact對象,表示數據源的NameValueCollection至少需要包含7個元素,分別針對Contact除Address屬性的三個屬性值和作為Address的四個屬性值,兩類元素在NameValueCollection中通過基於屬性的前綴來區分,具體的結構如下所示。

   1: Name:Foo
   2: PhoneNo:123456789
   3: EmailAddress:Foomail.com
   4: Address.Province: 江蘇
   5: Address.City: 蘇州
   6: Address.District: 工業園區
   7: Address.Street: 星湖街328號

將點號(.)作為分隔符的前綴除了表示基於屬性的層級關係之外,還可以用於數據篩選。如下麵的代碼片斷所示,我們在ContactController中定義了一個用於添加聯係人的AddContacts,它具有兩個Contact類型的參數foo和bar,表示添加的兩個不同的聯係人。

   1: public class ContactController
   2: {
   3:     public void AddContacts(Contact foo, Contact bar)
   4:     { 
   5:         //省略實現
   6:     }
   7: }

如果我們采用NameValueCollectionValueProvider來提供作為AddContacts方法參數的兩個Contact對象,保存在NameValueCollection的數據元素必須能夠與它們進行合理映射。一般情況下這可以通過針對參數名的前綴來實現,具體數據結構如下所示。

   1: foo.Name:Foo
   2: foo.PhoneNo:123456789
   3: foo.EmailAddress:Foo@gmail.com
   4: foo.Address.Province: 江蘇
   5: foo.Address.City: 蘇州
   6: foo.Address.District: 工業園區
   7: foo.Address.Street: 星湖街328號
   8:  
   9: bar.Name:Bar
  10: bar.PhoneNo:987654321
  11: bar.EmailAddress:Bar@gmail.com
  12: bar.Address.Province: 江蘇
  13: bar.Address.City: 蘇州
  14: bar.Address.District: 工業園區
  15: bar.Address.Street: 機場路328號

除了采用基於“.”的前綴之外,數組或者集合類型的數據源元素可以采用基於“索引”的前綴,這樣的前綴通過方括號“[]”表示,如下的數據結構就可以表示包含兩個元素的Contact數組或者集合。

   1: [0].Name:Foo
   2: [0].PhoneNo:123456789
   3: [0].EmailAddress:Foo@gmail.com
   4: ...
   5: [1].Name:Bar
   6: [1].PhoneNo:987654321
   7: [1].EmailAddress:Bar@gmail.com
   8: ...
   9:  

除了采用數字作為索引之前,我們還可以按照如下的方式通過文字作為索引。針對兩種不同形式的索引的Model綁定機製有所不同,我們會在後續的部分予以講述。

   1: [foo].Name:Foo
   2: [foo].PhoneNo:123456789
   3: [foo].EmailAddress:Foo@gmail.com
   4: ...
   5: [bar].Name:Bar
   6: [bar].PhoneNo:987654321
   7: [bar].EmailAddress:Bar@gmail.com
   8: ...

如果數據源元素針對不同的目標集合對象,同樣需要采用相應的前綴予以區分,相麵的數據結構可以看成是針對兩個Contact列表(first和second)的數據源。

   1: first[0].Name:Zhao
   2: first[0].PhoneNo:12
   3: first[0].EmailAddress:zhao @gmail.com
   4: ...
   5: first[1].Name:Qian
   6: first[1].PhoneNo:34
   7: first[1].EmailAddress:qian@gmail.com
   8: ...
   9:  
  10: second[0].Name:Sun
  11: second[0].PhoneNo:56
  12: second[0].EmailAddress:sun@gmail.com
  13: ...
  14: second[1].Name:Li
  15: second[1].PhoneNo:78
  16: second[1].EmailAddress:li@gmail.com

在了解兩種不同類型的前綴之後,我們來關注一下NameValueCollectionValueProvider實現的GetKeysFromPrefix方法。從該方法的定義可以看出它返回的是一個IDictionary<string, string>對象,但是這個對象具有怎樣的數據呢?我們為此來進行一個實例演示。在通過Visual Studio的ASP.NET MVC項目模板創建的空Web應用中,我們定義了如下一個默認的HomeController。在Action方法Index中我們創建了一個NameValueCollection對象,並針對它創建一個NameValueCollectionValueProvider.

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         NameValueCollection datasource = new NameValueCollection();
   6:         datasource.Add("foo.Name", "Foo");
   7:         datasource.Add("foo.PhoneNo", "123456789");
   8:         datasource.Add("foo.EmailAddress", "Foo@gmail.com");
   9:         datasource.Add("foo.Address.Province", "江蘇");
  10:         datasource.Add("foo.Address.City", "蘇州");
  11:         datasource.Add("foo.Address.District", "工業園區");
  12:         datasource.Add("foo.Address.Street", "星湖街328號");
  13:         NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(datasource, CultureInfo.InvariantCulture);
  14:  
  15:         var keyDictionary = valueProvider.GetKeysFromPrefix("foo");
  16:         Response.Write("foo<br/>");
  17:         foreach (var item in keyDictionary)
  18:         {
  19:             Response.Write(string.Format("{0}: {1}<br/>", item.Key, item.Value));
  20:         }
  21:  
  22:         keyDictionary = valueProvider.GetKeysFromPrefix("foo.Address");
  23:         Response.Write("<br/>foo.Address<br/>");
  24:         foreach (var item in keyDictionary)
  25:         {
  26:             Response.Write(string.Format("{0}: {1}<br/>", item.Key, item.Value));
  27:         }
  28:     }
  29: }

通過上麵的代碼片斷可以看出,作為NameValueCollectionValueProvider的數據元素是按照Contact類型的屬性定義來添加的。我們分別將“foo”和“foo.Address”作為前綴返回以此作為前綴的Key。運行該程序後會在瀏覽器上得到如下的輸出結果。我們可以看到對於針對指定前綴返回的字典對象,其Key和Value的不同之處在於前者沒有包含指定的前綴而後者包含。此外,字典對象包含的元素全部處於同一級別,將“foo”指定為前綴時返回的元素針對於Contact的四個屬性。雖然NameValueCollection中並不包含一個名為“foo.Address”的元素,但是依然會將其單獨作為以“foo”為前綴的Key。

   1: foo
   2: Name            : foo.Name
   3: PhoneNo         : foo.PhoneNo
   4: EmailAddress    : foo.EmailAddress
   5: Address         : foo.Address
   6:  
   7: foo.Address
   8: Province        : foo.Address.Province
   9: City            : foo.Address.City
  10: District        : foo.Address.District
  11: Street          : foo.Address.Street

接下來我們采用相應的方式來演示基於索引的前綴,為此我們將HomeController的Index反方法進行了如下的改寫。作為數據源的NameValueCollection對象針對一個包含兩個元素的Contact集合,前綴“first”可以作為集合對象的名稱。

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         NameValueCollection datasource = new NameValueCollection();
   6:         datasource.Add("first[0].Name", "Foo");
   7:         datasource.Add("first[0].PhoneNo", "123456789");
   8:         datasource.Add("first[0].EmailAddress", "Foo@gmail.com");
   9:  
  10:         datasource.Add("first[1].Name", "Bar");
  11:         datasource.Add("first[1].PhoneNo", "987654321");
  12:         datasource.Add("first[1].EmailAddress", "Bar@gmail.com");
  13:         NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(datasource, CultureInfo.InvariantCulture);
  14:  
  15:         var keyDictionary = valueProvider.GetKeysFromPrefix("first");
  16:         Response.Write("first<br/>");
  17:         foreach (var item in keyDictionary)
  18:         {
  19:             Response.Write(string.Format("{0}: {1}<br/>", item.Key, item.Value));
  20:         }
  21:  
  22:         keyDictionary = valueProvider.GetKeysFromPrefix("first[0]");
  23:         Response.Write("<br/>first[0]<br/>");
  24:         foreach (var item in keyDictionary)
  25:         {
  26:             Response.Write(string.Format("{0}: {1}<br/>", item.Key, item.Value));
  27:         }
  28:  
  29:         keyDictionary = valueProvider.GetKeysFromPrefix("first[1]");
  30:         Response.Write("<br/>first[1]<br/>");
  31:         foreach (var item in keyDictionary)
  32:         {
  33:             Response.Write(string.Format("{0}: {1}<br/>", item.Key, item.Value));
  34:         }
  35:     }
  36: }

我們分別針對三個前綴“first”、“first[0]”和“first[1]”獲取相應字典對象並將其Key和Value呈現出來。該程序執行之後會在瀏覽器中產生如下的輸出,如果我們將“[”和“]”視為和”.”一樣的分割符,GetKeysFromPrefix針對索引作為前綴的規則與基於“.”前綴的規則沒有本質的區別。

   1: first
   2: 0: first[0]
   3: 1: first[1]
   4:  
   5: first[0]
   6: Name        : first[0].Name
   7: PhoneNo     : first[0].PhoneNo
   8: EmailAddress: first[0].EmailAddress
   9:  
  10: first[1]
  11: Name        : first[1].Name
  12: PhoneNo     : first[1].PhoneNo
  13: EmailAddress: first[1].EmailAddress

在ASP.NET MVC 應用編程接口中,NameValueCollectionValueProvider具有兩個繼承者,即FormValueProviderQueryStringValueProvider。對於FormValueProvider來說,最終作為數據源的NameValueCollection對象通過請求表單創建,Name和Value分別來源於表單元素的名稱和值,它的定義基本上可以通過如下的代碼表示(實際定義有所差異)。

   1: public sealed class FormValueProvider : NameValueCollectionValueProvider
   2: {
   3:     public FormValueProvider(ControllerContext controllerContext)
   4:         : base(controllerContext.RequestContext.HttpContext.Request.Form, CultureInfo.CurrentCulture)
   5:     { }
   6: }

對於QueryStringValueProvider來說,無須多說,其作為數據源的NameValueCollection對象愛那個自然來源於請求的查詢字符串,其定義基本上可以通過如下的代碼表示(實際定義有所差異)。

   1: public sealed class QueryStringValueProvider: NameValueCollectionValueProvider
   2: {
   3:     public NameValueCollection(ControllerContext controllerContext)
   4:         : base(controllerContext.RequestContext.HttpContext.Request.QueryString, CultureInfo.CurrentCulture)
   5:     { }
   6: }

 

ASP.NET MVC以ValueProvider為核心的值提供係統: NameValueCollectionValueProvider
ASP.NET MVC以ValueProvider為核心的值提供係統: DictionaryValueProvider
ASP.NET MVC以ValueProvider為核心的值提供係統: ValueProviderFactory


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

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

  上一篇:go  ASP.NET MVC如何實現自定義驗證(服務端驗證+客戶端驗證)
  下一篇:go  ASP.NET MVC以ValueProvider為核心的值提供係統: DictionaryValueProvider