MVC的Model-Binding
建立複雜物件(牽扯到複雜模型綁定.)
這篇會跟大家介紹MVC是如何把達成這個複雜的動作
我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.
DefaultModelBinder
將Http請求傳來資料轉換為強型別物件,DefaultModelBinder
是如何取得使用Model
資料呢?
實現
IValueProvider
來處理。
IModelBinder.BindModel
方法使用兩個參數
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
,但一個是使用第一點,另一個使用第四點.
public ActionResult HttpModules(Person p)
public ActionResult HttpModules([ModelBinder(typeof(DefaultModelBinder))]Person p)
在Global.cs
可透過ModelBinders.Binders.Add
方法註冊綁定類型.
如下面程式碼.
ModelBinders.Binders.Add(typeof(Arg),new FooModelBinder());
一般參數透過DefaultModelBinder
來幫我們完成參數綁定.
但有些特別的資料需要透過ModelBinderDictionary
取得使用ModelBinder
,例如上傳檔案,我們可以使用HttpPostedFileBase
來取得檔案資訊流.
那是因為在ModelBinderDictionary
有註冊一個HttpPostedFileBaseModelBinder
來幫我們做解析.
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
介面有一個重要方法GetValue
會返回ValueProviderResult
物件對於ValueProvider
參數封裝
ValueProviderResult GetValue(string key)
在ControllerBase
類別中有一個屬性ValueProvider
設定參數填值動作
public IValueProvider ValueProvider
{
get
{
if (_valueProvider == null)
{
_valueProvider = ValueProviderFactories.Factories.GetValueProvider(ControllerContext);
}
return _valueProvider;
}
set { _valueProvider = value; }
}
Http傳送參數可能又多種模式(Post Form
,Query String
,Ajax
....)
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
設定順序來排執行優先順序來填值
MVC利用工廠模式透過ValueProviderFactory
實現的工廠來IValueProvider
填值提供者物件.
在ValueProviderFactory
IValueProvider GetValueProvider
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;
}
// primitive
backingStore.Add(prefix, value);
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
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);
}
//....
}
透過ValueProviderFactory
返回相對應的IValueProvider
物件.
下面介紹幾個實現ValueProvider
物件
NameValueCollectionValueProvider
可從NameValueCollection
集合取得參數.
因為Request.Form
和Request.QueryString
都是NameValueCollection
類型集合.
這個方法很巧妙利用一個共同參數類型簽章來達成多態轉折點
public virtual NameValueCollection Form
{
get
{
//....
}
}
public virtual NameValueCollection QueryString
{
get
{
//....
}
}
Http傳值到Server有許多方式,這裡介紹MVC利用哪個ValueProvider將Form
跟QueryString
填值到物件上,很巧妙使用NameValueCollectionValueProvider
建構子參數NameValueCollection
決定是要使用Form
或QueryString
填充值到參數.
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
.
[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 /* culture */);
}
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