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# 對應ModelJson 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 許可協議。轉載請註明出處!