閱讀966 返回首頁    go 技術社區[雲棲]


ModelBinder——ASP.NET MVC Model綁定的核心

Model的綁定體現在從當前請求提取相應的數據綁定到目標Action方法的參數。通過前麵的介紹我們知道Action方法的參數通過ParameterDescriptor來描述,ParameterDescriptor的BindingInfo屬性表示的ParameterBindingInfo對象具有一個名為ModelBinder的組件用於完成針對當前參數的Model綁定。ModelBinder可以看成是整個Model綁定係統的核心,我們先來認識這個重要的組件。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、 ModelBinder
二、CustomModelBinderAttribute與ModelBinderAttribute
三、ModelBinders
四、ModelBinderProvider

一、 ModelBinder

用於進行Model綁定的ModelBinder對象實現了接口IModelBinder。如下麵的代碼片斷所示,IModelBinder接口具有唯一的BindModel方法用於實現針對某個參數的綁定操作,該方法的返回值表示的就是最終作為參數值的對象。

   1: public interface IModelBinder
   2: {
   3:     object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
   4: }

IModelBinder的BindModel方法接受兩個參數,一個是表示當前的Controller上下文,另一個是表示針對當前Model綁定的上下文,通過類型ModelBindingContext表示。在Controller初始化的時候,Controller上下文已經被創建出來,所以我們隻要能夠針對當前的Model綁定創建相應的ModelBindingContext,我們就能使用基於某個參數的ModelBinder得到對應的參數值。關於ModelBindingContext的創建我們會在後續部分進行的單獨介紹,我們先來介紹一下ModelBinder的提供機製。

二、CustomModelBinderAttribute與ModelBinderAttribute

如果針對某個參數的ParameterDescriptor具有相應的ModelBinder,那麼它會被優先選擇用於針對該參數的Model綁定,那麼ParameterDescriptor的ModelBinder是如何來提供的呢?這是實際上設置一個具有如下定義的CustomModelBinderAttribute特性。抽象類CustomModelBinderAttribute定義了唯一的抽象方法GetBinder用於獲取相應的ModelBinder對象。

   1: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Struct 
   2:     | AttributeTargets.Class, AllowMultiple=false, Inherited=false)]
   3: public abstract class CustomModelBinderAttribute : Attribute
   4: {
   5:     public abstract IModelBinder GetBinder();
   6: }

在ASP.NET MVC應用編程接口中,CustomModelBinderAttribute具有一個具有如下定義的唯一繼承類型ModelBinderAttribute。我們可以通過應用ModelBinderAttribute特性動態地選擇用於Model綁定的ModelBinder類型。

   1: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Interface | 
   2:     AttributeTargets.Enum | AttributeTargets.Struct | AttributeTargets.Class, 
   3:     AllowMultiple=false, Inherited=false)]
   4: public sealed class ModelBinderAttribute : CustomModelBinderAttribute
   5: {   
   6:     public ModelBinderAttribute(Type binderType);
   7:     public override IModelBinder GetBinder();
   8:  
   9:     public Type BinderType { [CompilerGenerated] get;  }
  10: }

從應用在ModelBinderAttribute類型上的AttributeUsageAttribute定義可以看出該特性不僅僅可以應用在參數上,也可以應用類型(接口、枚舉、結構和類)上,這意味我們既可以將它應用在Action方法的某個參數上,也可以將它應用在某個參數的類型上。但是ParameterDescriptor隻會解析應用在參數上的特性,所以應用在參數對象類型上的ModelBinderAttribute對它是無效的。

為了演示ModelBinderAttribute特性對ParameterDescriptor的影響,我們來進行一個簡單的實例演示。在一個通過Visual Studio的ASP.NET MVC項目模板創建的空Web應用中定義了如下幾個類型,其中FooModelBinder和BarModelBinder是顯現了IModelBinder的自定義ModelBinder類型,而Foo、Bar和Baz是三個將被作為Action方法參數的數據類型,其中Bar上應用了ModelBinderAttribute特性並將ModelBinder類型設置為BarModelBinder。

   1: public class FooModelBinder : IModelBinder
   2: {
   3:     public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:     {
   5:         throw new NotImplementedException();
   6:     }
   7: }
   8: public class BarModelBinder : IModelBinder
   9: {
  10:     public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  11:     {
  12:         throw new NotImplementedException();
  13:     }
  14: }
  15:  
  16: public class Foo { }
  17: [ModelBinder(typeof(BarModelBinder))]
  18: public class Bar { }
  19: public class Baz { }

然後再創建的默認HomeController中定義如下兩個Action方法。DoSomething方法具有三個參數,類型分別是Foo、Bar和Baz,在第一個參數上應用了ModelBinderAttribute特性並將ModelBinder類型設置為FooModelBinder。

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController));
   6:         ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(ControllerContext, "DoSomething");
   7:         IModelBinder foo = actionDescriptor.GetParameters().First(p => p.ParameterName == "foo").BindingInfo.Binder;
   8:         IModelBinder bar = actionDescriptor.GetParameters().First(p => p.ParameterName == "bar").BindingInfo.Binder;
   9:         IModelBinder baz = actionDescriptor.GetParameters().First(p => p.ParameterName == "baz").BindingInfo.Binder;
  10:  
  11:         Response.Write(string.Format("foo: {0}<br/>", null == foo? "N/A": foo.GetType().Name));
  12:         Response.Write(string.Format("bar: {0}<br/>", null == bar ? "N/A" : bar.GetType().Name));
  13:         Response.Write(string.Format("baz: {0}<br/>", null == baz ? "N/A" : baz.GetType().Name));
  14:     }
  15:  
  16:     public void DoSomething([ModelBinder(typeof(FooModelBinder))]Foo foo,Bar bar, Bar baz)
  17:     {}                
  18: }

在默認的Action方法Index中,我們針對HomeController類型的ReflectedControllerDescriptor對象並獲取到用於描述Action方法DoSomething的ActionDescriptor對象。最後我們通過該ActionDescriptor對象得到用於描述其三個參數的ParameterDescriptor對象,並將其ModelBinder類西國內呈現出來。當我們運行該程序的時候,會在瀏覽器中產生如下的輸出結果,可以看出對於分別應用在參數和參數類型上的ModelBinderAttribute特性,隻有前者會對ParameterDescriptor的ModelBinder的選擇造成影響

   1: foo: FooModelBinder
   2: bar: N/A
   3: baz: N/A

 

三、ModelBinders

如果我們不曾通過ModelBinderAttribute特性為某個Action方法參數的ModelBinder類型進行顯式定製,默認采用的Model是通過靜態類型ModelBinders來提供的。如下麵的代碼片斷所示,ModelBinders具有一個靜態隻讀屬性Binders,表示當前注冊ModelBinder列表,其類型為ModelBinderDictionary

   1: public static class ModelBinders
   2: {   
   3:     public static ModelBinderDictionary Binders { get; }
   4: }
   5:  
   6: public class ModelBinderDictionary : 
   7:   IDictionary<Type, IModelBinder>, 
   8:   ICollection<KeyValuePair<Type, IModelBinder>>, 
   9:   IEnumerable<KeyValuePair<Type, IModelBinder>>, 
  10:   IEnumerable
  11: {    
  12:     //其他成員
  13:     public IModelBinder GetBinder(Type modelType);
  14:    public virtual IModelBinder GetBinder(Type modelType, bool fallbackToDefault);
  15: }

ModelBinderDictionary是一個以數據類型(Model類型)為Key,ModelBinder對象為Value的字典,即它定義了針對某種數據類型的ModelBinder。ModelBinderDictionary具有兩個GetBinder方法重載用於獲取針對某個數據類型的ModelBinder,布爾類型的參數fallbackToDefault表示在數據類型不存在的時候是否采用默認的ModelBinder,基於默認ModelBinder的後備機製會在第一個GetBinder方法重載中采用。在這裏默認ModelBinder類型為DefaultModelBinder

在為某個參數獲取相應的ModelBinder的時候,如果對應的ParameterDescriptor的ModelBinder不存在,則通過ModelBinders的靜態屬性Binders表示獲取到當前注冊的ModelBinder列表的ModelBinderDictionary對象,並將參數類型作為參數調用其GetBinder方法獲取相應ModelBinder對象。

我們根據ModelBinder的提供機製對上麵演示的實例進行相應的修改。我們在HomeConroller中添加了一個CheckModelBinder方法,三個參數分別表示用於描述相應Action方法的ActionDescriptor對象、參數名稱和類型。在該方法中我們先獲取到用於描述製定參數的ParameterDescriptor對象,如果它具有相應的ModelBinder,則將具體的類型名稱輸出,否則輸出通過ModelBinders獲取的針對參數類型的ModelBinder類型。

   1: public class HomeController : Controller
   2: {
   3:     //其他成員
   4:     public void Index()
   5:     {
   6:         ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController));
   7:         ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(ControllerContext, "DoSomething");
   8:  
   9:         CheckModelBinder(actionDescriptor, "foo", typeof(Foo));
  10:         CheckModelBinder(actionDescriptor, "bar", typeof(Bar));
  11:         CheckModelBinder(actionDescriptor, "baz", typeof(Baz));           
  12:     }
  13:  
  14:     private void CheckModelBinder(ActionDescriptor actionDescriptor, string parameterName, Type modelType)
  15:     { 
  16:         ParameterDescriptor parameterDescriptor = actionDescriptor.GetParameters().First(p=>p.ParameterName == parameterName);
  17:         IModelBinder modelBinder = parameterDescriptor.BindingInfo.Binder ?? ModelBinders.Binders.GetBinder(modelType);
  18:         Response.Write(string.Format("{0}: {1}<br/>", parameterName, null == modelBinder ? "N/A" : modelBinder.GetType().Name));
  19:     }
  20: }

在Index方法中,我們調用CheckModelBinder方法將Action方法DoSomething的三個參數對應的ModelBinder類型呈現出來。當我們運行該程序的時候,在瀏覽器上會得到如下的輸出結果,應用在類型Bar上的BarModelBinder會用於針對參數bar的Model綁定,而參數baz則會使用默認的DefaultModelBinder

   1: foo: FooModelBinder
   2: bar: BarModelBinder
   3: baz: DefaultModelBinder

對於上麵的這個例子,由於數據類型Baz沒有關聯ModelBinder注冊到通過ModelBinders的靜態屬性Binders表示的全局ModelBinder列表中,所以才導致DoSomething的baz參數采用默認的DefaultModelBinder。如果我們實現針對數據類型Baz進行了相應的ModelBinder注冊,那麼被注冊的ModelBinder將會自動用於該類型參數的Model綁定。同樣是針對上麵演示的這個實例,我們定義了如下一個實現了IModelBinder的BazModelBinder。

   1: public class BazModelBinder : IModelBinder
   2: {
   3:     public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:     {
   5:         throw new NotImplementedException();
   6:     }
   7: }

現在我們希望使用這個BazModelBinder用於針對所有類型為Bar的參數的Model綁定,那麼我們可以通過Global.asax在應用啟動的時候進行如下的ModelBinder注冊。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     //其他成員
   4:     protected void Application_Start()
   5:     {
   6:         //其他操作
   7:         ModelBinders.Binders.Add(typeof(Baz), new BazModelBinder());
   8:     }
   9: }

再次運行我們的程序,在瀏覽器中會得到如下的輸出結果,從中可以清楚地看出我們注冊的BazModelBinder並用於baz參數的Model綁定

   1: foo: FooModelBinder
   2: bar: BarModelBinder
   3: baz: BazModelBinder

 

四、ModelBinderProvider

ASP.NET MVC的Model綁定係統還涉及到另一個重要的組件ModelBinderProvider。顧名思義,ModelBinderProvider專門用於提供相應的ModelBinder對象,它們均實現了IModelBinderProvider麵的代碼片斷所示,IModelBinderProvider接口定義了唯一的GetBinder方法用於根據數據類型獲取相應的ModelBinder對象。不過在ASP.NET MVC現有的應用編程接口中並沒有定義任何一個實現該接口的ModelBinderProvider類型

   1: public interface IModelBinderProvider
   2: {    
   3:     IModelBinder GetBinder(Type modelType);
   4: }

我們可以利用ModelBinderProviders為應用注冊一組ModelBinderProvider對象為某個數據類型提供相應的ModelBinder。如下麵的代碼片斷所示,靜態類型ModelBinderProviders具有一個靜態隻讀屬性BinderProviders,其類型ModelBinderProviderCollection實際上是一個型ModelBinderProvider的集合,該集合表示針對當前應用的ModelBinderProvider列表。

   1: public static class ModelBinderProviders
   2: {    
   3:     public static ModelBinderProviderCollection BinderProviders { get; }
   4: }
   5:  
   6: public sealed class ModelBinderProviderCollection : Collection<IModelBinderProvider>
   7: {
   8:     //省略成員
   9: }

通過ModelBinderProviders的靜態屬性BinderProviders表示的ModelBinderProvider列表最終被ModelBinderDictionary使用。如下麵的代碼片斷所示,ModelBinderDictionary除了具有一個表示基於數據類型的ModelBinder字典(_innerDictionary字段)和一個默認ModelBinder(_defaultBinder)之外,還具有一個ModelBinderProvider列表(_modelBinderProviders字段)。

   1: public class ModelBinderDictionary
   2: {
   3:     //其他成員
   4:     private IModelBinder _defaultBinder;
   5:     private readonly Dictionary<Type, IModelBinder> _innerDictionary;
   6:     private ModelBinderProviderCollection _modelBinderProviders;   
   7: }

當ModelBinderDictionary被創建的時候,通過ModelBinderProviders的靜態屬性BinderProviders表示的ModelBinderProvider列表會用於初始化_modelBinderProviders字段。圍繞著ModelBinder的Model綁定係統中的核心組件之間的關係基本上可以通過下圖所示的UML來表示。

image

當我們調用GetBinder或者指定數據類型對應的ModelBinder時,_innerDictionary字段表示的ModelBinder字典會被優先選擇。如果數據類型在該字典中找不到,則選擇使用通過_modelBinderProviders字段表示的ModelBinderProvider列表進行ModelBinder的提供。隻有在兩種ModelBinder提供方式均失敗的情況下才會選擇通過_innerDictionary字段表示的默認ModelBinder。也就是說,如果我們想為某個數據類型定製某種類型的ModelBinder,按照選擇優先級具有如下幾種方式供我們選擇:

  • 將ModelBinderAttribute應用在Action方法的相應參數上並指定相應的ModelBinder類型,或者在參數上應用一個自定義的CustomModelBinderAttribute特性。
  • 通過ModelBinders的靜態屬性Binders實現針對基於某種數據類型的ModelBinder注冊。
  • 自定義ModelBinderProvider實現基於某個數據類型的ModelBinder提供機製,並通過注冊當通過ModelBinderProviders的靜態屬性BinderProviders表示的ModelBinderProvider列表中。
  • 將ModelBinderAttribute應用在數據類型上並製定相應的ModelBinder類型,或者在數據類型上應用一個自定義的CustomModelBinderAttribute特性。

前麵三種方式的ModelBinder提供機製我們已經通過實例演示過了,現在我們來演示基於自定義ModelBinderProvider的ModelBinder提供機製。在前麵的例子中我們為Foo、Bar和Baz這三種數據類型創建了相應的ModelBinder(FooModelBinder、BarModelBinder和BazModelBinder),現在我們創建如下一個自定義的ModelBinderProvider將兩種(數據類型和ModelBinder對象)進行關聯。

   1: public class MyModelBinderProvider : IModelBinderProvider
   2: {
   3:     public IModelBinder GetBinder(Type modelType)
   4:     {
   5:         if (modelType == typeof(Foo))
   6:         {
   7:             return new FooModelBinder();
   8:         }
   9:         if (modelType == typeof(Bar))
  10:         {
  11:             return new BazModelBinder();
  12:         }
  13:         if (modelType == typeof(Baz))
  14:         {
  15:             return new BazModelBinder();
  16:         }
  17:         return null;
  18:     }
  19: }

現在我們需要通過利用Global.asax通過如下的方式在應用啟動時將一個我們自定義的MyModelBinderProvider注冊到通過ModelBinderProviders的靜態屬性BinderProviders表示的ModelBinderProvider列表中。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     //其他成員
   4:     protected void Application_Start()
   5:     {
   6:         //其他操作
   7:        ModelBinderProviders.BinderProviders.Add(new MyModelBinderProvider());
   8:     }
   9: }

由於MyModelBinderProvider實現了針對Foo、Bar和Baz三種數據類型的ModelBinder的提供,所以我們可以將應用在Action方法參數和數據類型上的ModelBinderAttribute特性刪除。再次運行我們的程序,會在瀏覽器中得到如下的輸出結果,從中可以看到DoSomething方法的三個參數此時采用了我們期望的ModelBinder類型。

   1: foo: FooModelBinder
   2: bar: BarModelBinder
   3: baz: BazModelBinder

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

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

  上一篇:go  ASP.NET MVC以ValueProvider為核心的值提供係統: ValueProviderFactory
  下一篇:go  通過實例模擬ASP.NET MVC的Model綁定機製:簡單類型+複雜類型