上一篇《ASP.NET沒有魔法——ASP.NET MVC 模型綁定解析(上篇)》文章介紹了ASP.NET MVC模型綁定的相關組件和概念,本章將介紹Controller在執行時是如何通過這些組件完成模型綁定功能的,本章的主要內容有:
● 模型綁定過程
○ 獲取ModelBinder
○ 獲取ValueProvider
○ 創建ModelMetadata
○ 模型綁定
● DefaultModelBinder的模型綁定過程
○ 簡單模型綁定
○ 復雜模型綁定
● 小結
模型的綁定過程
之前的文章介紹過MVC中Controller的創建與執行(可參考:《ASP.NET沒有魔法——ASP.NET MVC Controller的實例化與執行》),其執行過程包含臨時數據加載、Action方法名稱獲取、Action方法執行及臨時數據保存。
而模型綁定過程實際上就包含在Action方法的執行(ActionInvoker.InvokeAction方法)中,之前的文章《ASP.NET沒有魔法——ASP.NET MVC 過濾器(Filter)》也通過從過濾器的角度介紹了Action的執行,其中一點就是當授權過濾器執行完成並通過授權后將開始執行其它過濾器與Action方法,如下圖:
而Action方法的執行核心代碼如下:
代碼中的GetParameterValues方法的作用就是根據Controller的上下文以及Action的描述信息來獲取Action參數的過程,也就是模型綁定的過程,其代碼如下:
上面代碼就是獲取Action的參數數組,然后遍歷這個數組,通過GetParameterValue方法完成單個參數的綁定工作,最后以參數名稱為Key,綁定后的值為對象存儲在字典中返回。下面是GetParameterValue方法的具體代碼:
從代碼看來整個綁定過程有如下幾個步驟:
1. 根據參數獲取ModelBinder。
2. 從Controller中獲取值提供器。
3. 創建模型綁定上下文,該上下文中包含Metadata、模型名稱、值提供器等內容。
4. 通過獲取的ModelBinder完成模型綁定。
接下來就對這些核心步驟進行分析。
獲取ModelBinder
代碼中調用GetModelBinder方法,根據參數信息獲取一個ModelBinder,下圖是實現代碼:
從代碼中可以看出,獲取ModelBinder的方式有兩種,首先從通過參數描述信息(ParameterDescriptor)來獲取通過特性標記的方式應用到該參數的ModelBinder,其核心代碼如下(ParameterDescriptor默認使用的類型是ReflectedParameterDescriptor,通過反射的方式獲取特性):
注:更多關於通過特性應用自定義ModelBinder的內容可參考:https://www.codeproject.com/articles/605595/asp-net-mvc-custom-model-binder
當參數上沒有通過特性指定ModelBinder時,將通過Binders根據參數類型來獲取ModelBinder,這里的Binders實際上就是ModelBinderDictionary,前面介紹過這個字典中以ModelBinder的類型名稱為Key保存了所有的ModelBinder,ModelBinderDictionary的GetBinder方法如下:
首先通過ModelBinderProviders查找(注:默認情況下ASP.NET MVC沒有使用ModelBinderProvider:http://www.dotnetcurry.com/aspnet-mvc/1261/custom-model-binder-aspnet-mvc),然后再通過類型名稱在字典中匹配對應的ModelBinder,最后從模型類型上找是否通過特性指定了Modelbinder。
如果以上都找不到那么使用默認的ModelBinder,ModelBinderDictionary的結構如下圖:
獲取ValueProvider
ValueProvider用於提供數據,此處直接使用Controller中的ValueProvider:
而Controller中的ValueProvider是遍歷所有ValueProviderFactory根據Controller上下文創建的一個提供器集合:
一般來說所有的提供器都會被創建,如下圖的FormValue提供器,只要Controller上下文不為空就會有:
但是Json提供器比較特殊,需要請求內容為Json格式並能夠正確反序列化才會創建,這也就解釋了文章開始時只有發送Json請求時才會有Json值提供器存在的問題,下面是Json值提供器創建的代碼,必須保證反序列化的對象不為空才會將該提供器保存到字典中:
而反序列化GetDseserializedObject方法,則是需要請求類型為Json,才會進行反序列化工作:
創建ModelMetadata
ModelMetadata是用來描述模型的元數據,在進行模型綁定時元數據被包含在模型綁定上下文(ModelBindingContext)對象中,這個上下文對象除了模型元數據外,還包含屬性過濾器以及模型名稱等信息。
而代碼顯示元數據獲取實際上是通過一個元數據提供器的組件完成的其代碼如下:
根據代碼分析,實際上使用了一個名為CachedDataAnnotationsModelMetadataProvider的元數據提供器,其獲取Metadata的代碼如下:
先獲取特性列表,然后通過該特性列表創建Metadata,獲取特性很好理解,而創建Metadata的核心代碼如下(部分),它實例化一個元數據對象后,通過在特性列表中查找特定的特性然后賦值的方式實現了元數據的創建:
注:實際上ASP.NET MVC中將元數據特性分為以下幾種,除了數據類型、必填等屬性外,還包含了很多用於展示的特性,這些展示特性將在渲染View的時候使用,更多內容也將在后續介紹:
另外ASP.NET MVC中默認使用基於緩存機制的元數據組件以提高性能,相關類型如下圖所示,本文不再詳解:
模型綁定
當完成以上步驟后,將通過最開始獲取的ModelBinder通過ControllerContext(包含請求數據)以及BindingContext(包含值提供器、模型信息等數據)完成模型綁定。之前也介紹過ModelBinder是根據參數的類型來獲取的,比如對於文件數據將會使用HttpPostedFileBaseModelBinder類型來完成,如下圖代碼就是文件模型綁定的代碼:
代碼中直接根據模型名稱(即參數名稱)從請求的文件中獲取文件數據,簡單直接。
但在模型綁定中很少有如文件這種特殊類型的綁定,更多的是一些基礎類型以及自定義類型的綁定,而微軟並沒有針對每一個基礎類型都創建了對應的綁定器,所有的一切都是通過DefaultModelBinder完成的,所以DefaultModelBinder的綁定過程相對於特定類型的模型綁定過程來說相對要復雜。
DefaultModelBinder的模型綁定過程
上面提到了模型綁定的大部分情況是使用DefaultModelBinder完成的,下面就對該綁定器如何完成模型綁定進行介紹,先上代碼:
上面代碼做了以下幾件事:
1. 確保有足夠的棧空間來執行.Net中平均大小的函數。
2. 如果模型名稱不為空、數據源中不包含以該模型名稱為前綴的值、並且參數綁定信息沒有前綴時,重新初始化綁定上下文,相比原有上下文去除FallbackToEmptyPrefix以及ModelName屬性並將該模型視為復雜模型:
注:上面這種情況可參考《ASP.NET沒有魔法——ASP.NET MVC 模型綁定》文章中的例子,該例子中存在參數名obj,但是QueryString中沒有使用obj作為前綴,action參數也沒有使用bind特性指定前綴,這樣的類型將被視為復雜類型,如下圖:
3. 獲取是否驗證標記,並根據模型名稱從ValueProvider中獲取數據,獲取的數據為ValueProviderResult類型,如果該結果存在那么進行簡單模型綁定。(注:ValueProviderResult負責簡單類型轉換,后續介紹,另外這里的驗證涉及到.Net中System.Web的基礎實現,所以不再進行深入介紹)
4. 如果模型屬於復雜模型,則進行復雜模型綁定。
默認模型綁定器中看到了簡單類型與復雜類型概念的使用,從代碼中看出模型綁定的核心內容也存在於簡單、復雜模型的綁定過程中。
簡單模型綁定
之前介紹了簡單類型,實際上就是可以從字符串直接轉換的類型,除此之外還可以通過自定義類型轉換器的方式將復雜類型轉換為簡單類型。
那么對於簡單模型綁定來說,實際上核心就是獲取到對應的值后,使用類型轉換器將獲取到的值轉換為目標類型。下面是簡單模型綁定的實現代碼:
根據代碼可以看出簡單模型綁定核心如下:
1. 如果模型類型與從值提供器中獲取的類型一致,那么不需要轉換就可以直接返回。
2. 類型轉換的核心方法是DefaultModelBinder.ConvertProviderResult,實際是獲取類型轉換器完成了轉換功能,最終轉換代碼如下(ValueProviderResult的ConvertSimpleType方法,更多細節可參考反編譯代碼):
3. 簡單模型的轉換考慮到了數組類型以及集合類型的轉換。
復雜模型綁定
一般來說基本類型和一些常用的類型均為簡單類型,而復雜模型實際上是由一系列簡單類型組成的,所以復雜模型的綁定是將其拆分成一個個簡單類型完成綁定后再組裝的過程,其主要代碼如下(由於完整代碼較多,本文只介紹關鍵點,更多信息可參考DefaultModelBinder的反編譯源碼):

1 internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 2 { 3 object model = bindingContext.Model; 4 Type modelType = bindingContext.ModelType; 5 if (model == null && modelType.IsArray) 6 { 7 Type elementType = modelType.GetElementType(); 8 Type modelType2 = typeof(List<>).MakeGenericType(new Type[] 9 { 10 elementType 11 }); 12 object collection = this.CreateModel(controllerContext, bindingContext, modelType2); 13 ModelBindingContext bindingContext2 = new ModelBindingContext 14 { 15 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, modelType2), 16 ModelName = bindingContext.ModelName, 17 ModelState = bindingContext.ModelState, 18 PropertyFilter = bindingContext.PropertyFilter, 19 ValueProvider = bindingContext.ValueProvider 20 }; 21 IList list = (IList)this.UpdateCollection(controllerContext, bindingContext2, elementType); 22 if (list == null) 23 { 24 return null; 25 } 26 Array array = Array.CreateInstance(elementType, list.Count); 27 list.CopyTo(array, 0); 28 return array; 29 } 30 else 31 { 32 if (model == null) 33 { 34 model = this.CreateModel(controllerContext, bindingContext, modelType); 35 } 36 Type type = TypeHelpers.ExtractGenericInterface(modelType, typeof(IDictionary<, >)); 37 if (type != null) 38 { 39 Type[] genericArguments = type.GetGenericArguments(); 40 Type keyType = genericArguments[0]; 41 Type valueType = genericArguments[1]; 42 ModelBindingContext modelBindingContext = new ModelBindingContext(); 43 modelBindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType); 44 modelBindingContext.ModelName = bindingContext.ModelName; 45 modelBindingContext.ModelState = bindingContext.ModelState; 46 modelBindingContext.PropertyFilter = bindingContext.PropertyFilter; 47 modelBindingContext.ValueProvider = bindingContext.ValueProvider; 48 ModelBindingContext bindingContext3 = modelBindingContext; 49 return this.UpdateDictionary(controllerContext, bindingContext3, keyType, valueType); 50 } 51 Type type2 = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>)); 52 if (type2 != null) 53 { 54 Type type3 = type2.GetGenericArguments()[0]; 55 Type type4 = typeof(ICollection<>).MakeGenericType(new Type[] 56 { 57 type3 58 }); 59 if (type4.IsInstanceOfType(model)) 60 { 61 ModelBindingContext modelBindingContext2 = new ModelBindingContext(); 62 modelBindingContext2.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType); 63 modelBindingContext2.ModelName = bindingContext.ModelName; 64 modelBindingContext2.ModelState = bindingContext.ModelState; 65 modelBindingContext2.PropertyFilter = bindingContext.PropertyFilter; 66 modelBindingContext2.ValueProvider = bindingContext.ValueProvider; 67 ModelBindingContext bindingContext4 = modelBindingContext2; 68 return this.UpdateCollection(controllerContext, bindingContext4, type3); 69 } 70 } 71 this.BindComplexElementalModel(controllerContext, bindingContext, model); 72 return model; 73 } 74 }
1. 從綁定上下文中獲取模型對象(注:因為復雜模型綁定中存在綁定方法的遞歸調用,所以可能會多次調用BindComplexModel方法,而綁定上下文的模型對象就是用於保存當前遞歸的所屬對象)。
2. 如果model不存在則創建,首次綁定復雜對象時model均為null:
3. 開始綁定該模型中的基本模型,從代碼中可以看到實際上就是對模型中的屬性進行綁定:
4. 屬性綁定是獲取模型的所有屬性,然后遍歷並完成所有屬性的綁定,屬性綁定代碼如下:
a. 獲取屬性對應類型的ModelBinder:
b. 調用ModelBinder的BindModel方法(如果該屬性為簡單模型,那么執行簡單模型綁定代碼,否則遞歸調用復雜模型綁定代碼)
c. 將獲取的結果賦值給Model對象,然后獲取屬性的驗證器並對該屬性進行驗證,錯誤信息保存到ModelState列表中:
注:ModelValidatorProvider是用來獲取模型驗證器的工具,在ASP.NET MVC中有三種內置的驗證提供器如下圖所示:
其中DataAnnotationsModelValidatorProvider用來獲取通過特性標記的模型驗證器,也就是《ASP.NET沒有魔法——ASP.NET MVC 模型驗證》文章中介紹的方法,而DataErrorInfoModelValidatorProvider是一種通過實現IDataErrorInfo接口來實現數據功能的提供器,更多信息可參考:https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/models-data/validating-with-the-idataerrorinfo-interface-cs
https://www.codeproject.com/articles/39012/form-validation-with-asp-net-mvc-using-the-idataerrorinfo-interface.aspx
最后ClientDataTypeModelValidatorProvider用於客戶端驗證。
注:從代碼可以看出ASP.NET MVC中基於特性標記的數據驗證功能僅對復雜模型進行驗證,在簡單特性上使用驗證特性是無效的。
test123長度已經超出6,但是模型狀態仍然是有效的。
小結
本文主要是基於源碼介紹了ASP.NET MVC中模型綁定的過程,簡單來說模型綁定核心過程如下:
● 根據參數類型獲取ModelBinder。
● 獲取ValueProvider,如果是Json請求那么創建Json的ValueProvider。
● 根據特性標記獲取ModelMetadata。
● 根據模型類型(簡單/復雜)進行數據綁定,簡單類型主要是字符串轉換為其它簡單類型的過程(包括數組/集合的轉換),而復雜類型是將類型本身分解為簡單類型后一一綁定並驗證的過程。
參考:
https://www.codeproject.com/articles/605595/asp-net-mvc-custom-model-binder
http://www.dotnetcurry.com/aspnet-mvc/1261/custom-model-binder-aspnet-mvc
https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/models-data/validating-with-the-idataerrorinfo-interface-cs
https://www.codeproject.com/articles/39012/form-validation-with-asp-net-mvc-using-the-idataerrorinfo-interface.aspx
http://dirk.schuermans.me/?p=749