Agenda
前言 MVC 的Model-Binding
建立複雜物件(牽扯到複雜模型綁定.)
這篇會跟大家介紹MVC 是如何把達成這個複雜的動作
我有做一個可以針對於Asp.net MVC Debugger 的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.
IModelBinder(DefaultModelBinder) DefaultModelBinder
將Http請求傳來資料轉換為強型別物件,DefaultModelBinder
是如何取得使用Model
資料呢?
實現IValueProvider
來處理。
ModelBinders IModelBinder.BindModel
方法使用兩個參數
1 public object BindModel (ControllerContext controllerContext, ModelBindingContext bindingContext )
ControllerContext
:Controller
資訊,
ModelBindingContext
:當前參數綁定資訊
BindModel
方法機於Http
請求傳送資料進行Model
綁定(對於Action
方法使用參數),其中ModelBindingContext
參數會提供綁定使用的重要物件成員.
關於ModelBindingContext
建立我們會在後續部分進行的單獨介紹.
在IModelBinder.BindModel
方法中主要透過兩個重要internal
方法.
BindComplexModel
:複雜參數綁定
BindSimpleModel
:簡單參數綁定
下圖可以表示SimpleModel
和ComplexModel
ComplexModel
一個人可擁有多個房子,所以Person
類別擁有HouseCollection
引用. 取得使用ModelBinder
機制。
取得ModelBinder
會依照下面順序
參數掛有ModelBinderAttribute
標籤並將BinderType
屬性指向一個繼承IModelBinder
型別.
參數掛有繼承CustomModelBinderAttribute
類型
透過ModelBinderProviderCollection
(預設MVC 沒有提供ModelBinderProvider
)
預設DefaultModelBinder
下面兩個使用ModelBinder
都是DefaultModelBinder
,但一個是使用第一點,另一個使用第四點.
1 2 3 public ActionResult HttpModules (Person p )public ActionResult HttpModules ([ModelBinder(typeof (DefaultModelBinder ))]Person p)
在Global.cs
可透過ModelBinders.Binders.Add
方法註冊綁定類型.
如下面程式碼.
1 ModelBinders.Binders.Add(typeof (Arg),new FooModelBinder());
ModelBinderDictionary 一般參數透過DefaultModelBinder
來幫我們完成參數綁定.
但有些特別的資料需要透過ModelBinderDictionary
取得使用ModelBinder
,例如上傳檔案,我們可以使用HttpPostedFileBase
來取得檔案資訊流.
那是因為在ModelBinderDictionary
有註冊一個HttpPostedFileBaseModelBinder
來幫我們做解析.
1 2 3 4 5 6 7 8 9 10 11 private static ModelBinderDictionary CreateDefaultBinderDictionary (){ ModelBinderDictionary binders = new ModelBinderDictionary() { { typeof (HttpPostedFileBase), new HttpPostedFileBaseModelBinder() }, { typeof (byte []), new ByteArrayModelBinder() }, { typeof (Binary), new LinqBinaryModelBinder() }, { typeof (CancellationToken), new CancellationTokenModelBinder() } }; return binders; }
IValueProvider 提供參數填值 IValueProvider
介面有一個重要方法GetValue
會返回ValueProviderResult
物件對於ValueProvider
參數封裝
1 ValueProviderResult GetValue (string key )
ValueProvider工廠集合(ValueProviderFactories) 在ControllerBase
類別中有一個屬性ValueProvider
設定參數填值動作
1 2 3 4 5 6 7 8 9 10 11 12 public IValueProvider ValueProvider{ get { if (_valueProvider == null ) { _valueProvider = ValueProviderFactories.Factories.GetValueProvider(ControllerContext); } return _valueProvider; } set { _valueProvider = value ; } }
Http 傳送參數可能又多種模式(Post Form
,Query String
,Ajax
….)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static class ValueProviderFactories { private static readonly ValueProviderFactoryCollection _factories = new ValueProviderFactoryCollection() { new ChildActionValueProviderFactory(), new FormValueProviderFactory(), new JsonValueProviderFactory(), new RouteDataValueProviderFactory(), new QueryStringValueProviderFactory(), new HttpFileCollectionValueProviderFactory(), }; public static ValueProviderFactoryCollection Factories { get { return _factories; } } }
ChildActionValueProviderFactory
:取得另一個呼叫@Html.Action
傳來Model 資料
FormValueProviderFactory
:取得HTTP POST
送來的資料
JsonValueProviderFactory
:取得JSON
資料(Content-Type = application/json
)
RouteDataValueProviderFactory
:取得從網址路徑取得到路由參數值
QueryStringValueProviderFactory
:取得從Http
請求的Query String
資料
HttpFileCollectionValueProviderFactory
:取得檔案上傳功能傳來檔案
如果此次請求匹配到多個ValueProvider
機制會怎處理?
會按照上面ProviderFactory
設定順序來排執行優先順序來填值
ValueProviderFactory MVC 利用工廠模式透過ValueProviderFactory
實現的工廠來IValueProvider
填值提供者物件.
JsonValueProviderFactory 在ValueProviderFactory
IValueProvider GetValueProvider
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 public sealed class JsonValueProviderFactory : ValueProviderFactory { private static void AddToBackingStore (EntryLimitedDictionary backingStore, string prefix, object value ) { IDictionary<string , object > d = value as IDictionary<string , object >; if (d != null ) { foreach (KeyValuePair<string , object > entry in d) { AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value); } return ; } IList l = value as IList; if (l != null ) { for (int i = 0 ; i < l.Count; i++) { AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]); } return ; } backingStore.Add(prefix, value ); } private static object GetDeserializedObject (ControllerContext controllerContext ) { if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json" , StringComparison.OrdinalIgnoreCase)) { return null ; } StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream); string bodyText = reader.ReadToEnd(); if (String.IsNullOrEmpty(bodyText)) { return null ; } JavaScriptSerializer serializer = new JavaScriptSerializer(); object jsonData = serializer.DeserializeObject(bodyText); return jsonData; } public override IValueProvider GetValueProvider (ControllerContext controllerContext ) { if (controllerContext == null ) { throw new ArgumentNullException("controllerContext" ); } object jsonData = GetDeserializedObject(controllerContext); if (jsonData == null ) { return null ; } Dictionary<string , object > backingStore = new Dictionary<string , object >(StringComparer.OrdinalIgnoreCase); EntryLimitedDictionary backingStoreWrapper = new EntryLimitedDictionary(backingStore); AddToBackingStore(backingStoreWrapper, String.Empty, jsonData); return new DictionaryValueProvider<object >(backingStore, CultureInfo.CurrentCulture); } }
取得IValueProvider 透過ValueProviderFactory
返回相對應的IValueProvider
物件.
下面介紹幾個實現ValueProvider
物件
NameValueCollectionValueProvider NameValueCollectionValueProvider
可從NameValueCollection
集合取得參數.
因為Request.Form
和Request.QueryString
都是NameValueCollection
類型集合.
這個方法很巧妙利用一個共同參數類型簽章來達成多態轉折點
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public virtual NameValueCollection Form{ get { } } public virtual NameValueCollection QueryString{ get { } }
Http 傳值到Server 有許多方式,這裡介紹MVC 利用哪個ValueProvider 將Form
跟QueryString
填值到物件上,很巧妙使用NameValueCollectionValueProvider
建構子參數NameValueCollection
決定是要使用Form
或QueryString
填充值到參數.
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 public sealed class FormValueProvider : NameValueCollectionValueProvider { public FormValueProvider (ControllerContext controllerContext ) : this (controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated )) { } internal FormValueProvider (ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues ) : base (controllerContext.HttpContext.Request.Form, unvalidatedValues.Form, CultureInfo.CurrentCulture ) { } } public sealed class QueryStringValueProvider : NameValueCollectionValueProvider { public QueryStringValueProvider (ControllerContext controllerContext ) : this (controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated )) { } internal QueryStringValueProvider (ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues ) : base (controllerContext.HttpContext.Request.QueryString, unvalidatedValues.QueryString, CultureInfo.InvariantCulture ) { } }
實現IValueProvider
物件主要會依靠GetValue
方法取得ValueProviderResult
.
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 [Serializable ] public class ValueProviderResult { private static readonly CultureInfo _staticCulture = CultureInfo.InvariantCulture; private CultureInfo _instanceCulture; protected ValueProviderResult () { } public ValueProviderResult (object rawValue, string attemptedValue, CultureInfo culture ) { RawValue = rawValue; AttemptedValue = attemptedValue; Culture = culture; } public string AttemptedValue { get ; protected set ; } public CultureInfo Culture { get { if (_instanceCulture == null ) { _instanceCulture = _staticCulture; } return _instanceCulture; } protected set { _instanceCulture = value ; } } public object RawValue { get ; protected set ; } public object ConvertTo (Type type ) { return ConvertTo(type, null ); } public virtual object ConvertTo (Type type, CultureInfo culture ) { } }
ValueProviderResult
對於ValueProvider
物件做封裝,一般存放Http
參數擁有兩個只讀屬性
RawValue
表示物件值
AttemptedValue
主要用於顯示
ValueProviderResult
提供兩個ConvertTo
重載方法實現向指定目標類型轉換。
某些類型格式化依賴於相應的語言文化(比如時間、日期和貨幣等),這個語言文化通過Culture
屬性來達成.
最終會呼叫一個UnwrapPossibleArrayType
方法來建立物件
小結: 在ControllerActionInvoker.GetParameterValue
取得參數方法,ModelBing
動作有兩個重要的屬性
IValueProvider
:提供如何填值
IModelBinder
:建立物件(綁定關聯) 預設使用DefaultModelBinder
類別.
目前分享的IValueProvider
和IModelBinder
UML類別關聯圖如下
下篇會介紹ModelBind
模型綁定重點邏輯,有分簡單參數綁定和複雜參數綁定
BindComplexModel
BindSimpleModel
__此文作者__:Daniel Shih(石頭) __此文地址__: https://isdaniel.github.io/Ithelp-day18/ __版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!