IValueProvider
物件透過一個ValueProviderFactory
工廠來產生
Action
方法綁定Model
参数由實現IModelBinder
的介面ModelBinder(DefaultModelBinder)
物件來實現
在IModelBinder
介面中有一個重要的方法object BindModel
取得Model
參數資料.
但在Http
請求傳送參數極為複雜是如何將參數動態綁定在Action
參數上呢?
最常見的Json參數透過POST Body
傳到AP端,經由MVC BindModel
來取得參數物件資料.
如下方資料.
{
"Key":"123",
"value":"",
"Adress":["test133","e2424"]
}
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
(元數據)實際上具有一個樹形層次化的資料結構.
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
可以擁有多個地址
這裡就會呈現一對多關係如下圖
就像大樹支點和葉子,這個屬性可能是葉子也可能是別人的支點.
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
類別屬性的關係.
在上面介紹了ModelMetadata
這個類別儲存了參數的各個資訊.
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()
方法在簡單模型物件建立起到一個很大的作用
在BindModel
方法中有一個BindComplexModel
方法是針對複雜模型產生的方法.
一開始先判斷ModelBindingContext.Model
是否為Null
如果是就會建立一個物件實例返回.
會依照下面機制判斷產生物件
Array
產生一個相對應陣列集合IDictionary<,>
and ICollection<>
集合產生一個相對應陣列集合IEnumerable<>
集合產生一個相對應陣列集合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
直到找到最後的簡單模型.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
方法ModelBindingContext
找到參數使用型別並利用ValueProvider
給值,最後返回物件ModelBindingContext
建立參數利用ValueProvider
給值,往下繼續重複動作直到呼叫簡單模型綁定方法,就不會繼續往下呼叫object
方法.這裡很巧妙的利用ModelBinderDictionary
取得當前參數型別並取得相對應IModelBinder
實現物件.
石頭大準備的Asp.net MVC Debugger的專案真的好用又貼心
可惜目前只能粗淺瀏覽不能深入閱讀,等鐵人賽完一定要好好拜讀!
沒問題