Agenda
前言 IValueProvider
物件透過一個ValueProviderFactory
工廠來產生
Action
方法綁定Model
参数由實現IModelBinder
的介面ModelBinder(DefaultModelBinder)
物件來實現
在IModelBinder
介面中有一個重要的方法object BindModel
取得Model
參數資料.
但在Http
請求傳送參數極為複雜是如何將參數動態綁定在Action
參數上呢?
最常見的Json 參數透過POST Body
傳到AP端,經由MVC BindModel
來取得參數物件資料.
如下方資料.
1 2 3 4 5 { "Key" : "123" , "value" : "" , "Adress" : [ "test133" , "e2424" ] }
1 2 3 4 5 6 public class RootObject { public string Key { get ; set ; } public string value { get ; set ; } public List<string > Adress { get ; set ; } }
網路上有個工具可方便使用Json字串取得c#
對應Model
Json to c# model
本篇就和大家分享這個機制是如何達成的
我有做一個可以針對於Asp.net MVC Debugger 的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.
Model
參數類型可能是一個簡單字串或者是一個值類型,也可能是一個複雜類型物件。
對於一個複雜類型物件,基於類型本身和物件成員元數據通過一個ModelMetadata
類別來達成
某個成員又可能是一個複雜類型物件,通過ModelMetadata
物件表示Model
狀態,所以ModelMetadata
(元數據)實際上具有一個樹形層次化的資料結構.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class ModelMetadata { public Type ModelType { get ; } public virtual bool IsComplexType { get ; } public bool IsNullableValueType { get ; } public Type ContainerType { get ; } public object Model { get ; set ; } public string PropertyName { get ; } public virtual Dictionary<string , object > AdditionalValues { get ; } protected ModelMetadataProvider Provider { get ; set ; } public virtual IEnumerable<ModelMetadata> Properties { get { if (_properties == null ) { IEnumerable<ModelMetadata> originalProperties = Provider.GetMetadataForProperties(Model, RealModelType); _propertiesInternal = SortProperties(originalProperties.AsArray()); _properties = new ReadOnlyCollection<ModelMetadata>(_propertiesInternal); } return _properties; } } }
在ModelMetadata
類別中有幾個重要的屬性.
Provider(ModelMetadataProvider)
:存放當前物件下面一個ModelMetadataProvider
資訊,ModelMetadataProvider
主要是提供ModelMetadata
是如何被產生(一般使用CachedDataAnnotationsModelMetadataProvider
這個類別使用MemoryCache
存放資訊)
IEnumerable<ModelMetadata>
:用來表示當前物件所使用屬性資訊ModelMetadata
集合
IsComplexType
:判斷是否是複雜模型.
ContainerType
:父節點類別型態(可以看做樹狀結構,可當作存放根結點類型)
ModelType
:目前屬性或參數的類型.
Model
:綁定完使用的參數
假如這邊有兩個類別Person
,AddressInfo
且一個Person
可以擁有多個地址
這裡就會呈現一對多關係如下圖
就像大樹支點和葉子,這個屬性可能是葉子也可能是別人的支點.
1 2 3 4 5 6 7 8 9 10 public class Person { public int Age{ get ; set ; } public string Name { get ; set ; } public IEnumerable<AddressInfo> Address { get ; set ; } } public class AddressInfo { public string Name { get ; set ; } }
上面類別關係圖就是簡單表示複雜模型
通過上面的介紹我們知道表示Model
元數據ModelMetadata
具有一個樹形層次結構
在每個ModelMetadata
內部都有一個型別為IEnumerable<ModelMetadata>
的Properties
屬性來引用它的下級ModelMetadata
,這就形成了一個無限巢狀的後設資料表示結構.
此圖可以表示ModelMetadata
跟Person
類別屬性的關係.
BindSimpleModel 簡單模型綁定 在上面介紹了ModelMetadata
這個類別儲存了參數的各個資訊.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 internal object BindSimpleModel ( ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult ){ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) { return valueProviderResult.RawValue; } if (bindingContext.ModelType != typeof (string )) { if (bindingContext.ModelType.IsArray) { object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType); return modelArray; } Type enumerableType = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, typeof (IEnumerable<>)); if (enumerableType != null ) { object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType); Type elementType = enumerableType.GetGenericArguments()[0 ]; Type arrayType = elementType.MakeArrayType(); object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType); Type collectionType = typeof (ICollection<>).MakeGenericType(elementType); if (collectionType.IsInstanceOfType(modelCollection)) { CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray); } return modelCollection; } } object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType); return model; } private static object ConvertProviderResult ( ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType ){ try { object convertedValue = valueProviderResult.ConvertTo(destinationType); return convertedValue; } catch (Exception ex) { modelState.AddModelError(modelStateKey, ex); return null ; } }
透過ConvertProviderResult
來將類型轉換成簡單模型綁定使用的參數實例.
在BindSimpleModel
中依照下面幾個規則來做參數物件建立.
Array
:如果此參數是陣列,判斷此陣列型別並利用ValueProviderResult.ConvertTo()
建立陣列
IEnumerable<>
:如果此參數是IEnumerable<>
集合,判斷此IEnumerable<>
型別ValueProviderResult.ConvertTo()
建立集合
object
:不是上面的型別就直接使用ValueProviderResult.ConvertTo()
建立物件.
ConvertTo()
方法在簡單模型物件建立起到一個很大的作用
BindComplexModel 複雜模型綁定 在BindModel
方法中有一個BindComplexModel
方法是針對複雜模型產生的方法.
一開始先判斷ModelBindingContext.Model
是否為Null
如果是就會建立一個物件實例返回.
會依照下面機制判斷產生物件
判斷參數類型是否Array
產生一個相對應陣列集合
判斷參數類型是否IDictionary<,>
and ICollection<>
集合產生一個相對應陣列集合
判斷參數類型是否IEnumerable<>
集合產生一個相對應陣列集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 internal object BindComplexModel ( ControllerContext controllerContext, ModelBindingContext bindingContext ){ object model = bindingContext.Model; Type modelType = bindingContext.ModelType; if (model == null && modelType.IsArray) { Type elementType = modelType.GetElementType(); Type listType = typeof (List<>).MakeGenericType(elementType); object collection = CreateModel(controllerContext, bindingContext, listType); ModelBindingContext arrayBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType), ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType); if (list == null ) { return null ; } Array array = Array.CreateInstance(elementType, list.Count); list.CopyTo(array, 0 ); return array; } if (model == null ) { model = CreateModel(controllerContext, bindingContext, modelType); } Type dictionaryType = TypeHelpers.ExtractGenericInterface(modelType, typeof (IDictionary<,>)); if (dictionaryType != null ) { Type[] genericArguments = dictionaryType.GetGenericArguments(); Type keyType = genericArguments[0 ]; Type valueType = genericArguments[1 ]; ModelBindingContext dictionaryBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType), ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType); return dictionary; } Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof (IEnumerable<>)); if (enumerableType != null ) { Type elementType = enumerableType.GetGenericArguments()[0 ]; Type collectionType = typeof (ICollection<>).MakeGenericType(elementType); if (collectionType.IsInstanceOfType(model)) { ModelBindingContext collectionBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType), ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType); return collection; } } BindComplexElementalModel(controllerContext, bindingContext, model); return model; }
最後呼叫BindComplexElementalModel
方法將剛剛建立值(model
物件)透過ValueProvider
把參數給值.
有分簡單綁定和複雜綁定,最後都還是會呼叫使用簡單綁定來值綁定給物件.
在BindProperty
方法時填充子節點ModelMetadata
的Model
屬性.
GetPropertyValue
透過(DefaultModelBinder)
再次綁定物件動作如下
ModelMetadata
是簡單模型就會把值填充給此次ModelMetadata.Model
ModelMetadata
是複雜模型就建立一個物件後呼叫BindProperty
直到找到最後的簡單模型.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 protected virtual void BindProperty (ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor ){ IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType); object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model); ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name]; propertyMetadata.Model = originalPropertyValue; ModelBindingContext innerBindingContext = new ModelBindingContext() { ModelMetadata = propertyMetadata, ModelName = fullPropertyKey, ModelState = bindingContext.ModelState, ValueProvider = bindingContext.ValueProvider }; object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder); propertyMetadata.Model = newPropertyValue; } protected virtual object GetPropertyValue (ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder ){ object value = propertyBinder.BindModel(controllerContext, bindingContext); if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Equals(value , String.Empty)) { return null ; } return value ; }
小結: MVC ModelBinding
使用到一個設計模式(組合模式),當我發現時覺得十分興奮.
因為在現實專案中我較少看到(組合模式),發輝良好的作用而在這個案例上發揮的淋漓盡致.
組合模式關係圖如下
參考圖片連結
組合模式基本上分為兩個部分葉 (Left
)和組件 (component
)他們都依賴於一個抽象,組件實現取得動作的抽象只為了獲得下面的葉,真正有動作只會在葉有動作
組合模式
很適合用在樹狀的資料結構且需求對於葉 和組件 要做大量不一樣判斷.
在模型綁定中他依靠兩個東西完成上面說的依賴關聯
ModelBindingContext
物件
object CreateModel
方法
簡單模型綁定 vs 複雜模型綁定
簡單模型綁定:透過ModelBindingContext
找到參數使用型別並利用ValueProvider
給值,最後返回物件
複雜模型綁定:透過ModelBindingContext
建立參數利用ValueProvider
給值,往下繼續重複動作直到呼叫簡單模型綁定方法,就不會繼續往下呼叫object
方法.
這裡很巧妙的利用ModelBinderDictionary
取得當前參數型別並取得相對應IModelBinder
實現物件.
__此文作者__:Daniel Shih(石頭) __此文地址__: https://isdaniel.github.io/Ithelp-day19/ __版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!