提供ModelBing幾個重要功臣(Model) (第18天)

Agenda

前言

MVCModel-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)
  1. ControllerContext:Controller資訊,
  2. ModelBindingContext:當前參數綁定資訊

BindModel方法機於Http請求傳送資料進行Model綁定(對於Action方法使用參數),其中ModelBindingContext參數會提供綁定使用的重要物件成員.

關於ModelBindingContext建立我們會在後續部分進行的單獨介紹.

IModelBinder.BindModel方法中主要透過兩個重要internal方法.

  • BindComplexModel:複雜參數綁定
  • BindSimpleModel:簡單參數綁定

下圖可以表示SimpleModelComplexModel

BindSimpleModel

ComplexModel一個人可擁有多個房子,所以Person類別擁有HouseCollection引用.
取得使用ModelBinder機制。

取得ModelBinder會依照下面順序

  1. 參數掛有ModelBinderAttribute標籤並將BinderType屬性指向一個繼承IModelBinder型別.
  2. 參數掛有繼承CustomModelBinderAttribute類型
  3. 透過ModelBinderProviderCollection(預設MVC沒有提供ModelBinderProvider)
  4. 預設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; }
}
}
  1. ChildActionValueProviderFactory:取得另一個呼叫`@Html.Action`傳來Model資料
  2. FormValueProviderFactory:取得HTTP POST送來的資料
  3. JsonValueProviderFactory:取得JSON資料(Content-Type = application/json)
  4. RouteDataValueProviderFactory:取得從網址路徑取得到路由參數值
  5. QueryStringValueProviderFactory:取得從Http請求的Query String資料
  6. HttpFileCollectionValueProviderFactory:取得檔案上傳功能傳來檔案

如果此次請求匹配到多個ValueProvider機制會怎處理?

會按照上面ProviderFactory設定順序來排執行優先順序來填值

ValueProviderFactory

MVC利用工廠模式透過ValueProviderFactory實現的工廠來IValueProvider填值提供者物件.

JsonValueProviderFactory

ValueProviderFactoryIValueProvider 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;
}

// 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);
}
//....
}

UML_Model

取得IValueProvider

透過ValueProviderFactory返回相對應的IValueProvider物件.

下面介紹幾個實現ValueProvider物件

NameValueCollectionValueProvider

NameValueCollectionValueProvider可從NameValueCollection集合取得參數.

因為Request.FormRequest.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利用哪個ValueProviderFormQueryString填值到物件上,很巧妙使用NameValueCollectionValueProvider建構子參數NameValueCollection決定是要使用FormQueryString填充值到參數.

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 /* culture */);
}

public virtual object ConvertTo(Type type, CultureInfo culture)
{
//....
}
}

ValueProviderResult對於ValueProvider物件做封裝,一般存放Http參數擁有兩個只讀屬性

  1. RawValue表示物件值
  2. AttemptedValue主要用於顯示

ValueProviderResult提供兩個ConvertTo重載方法實現向指定目標類型轉換。

某些類型格式化依賴於相應的語言文化(比如時間、日期和貨幣等),這個語言文化通過Culture屬性來達成.

最終會呼叫一個UnwrapPossibleArrayType方法來建立物件

小結:

ControllerActionInvoker.GetParameterValue取得參數方法,ModelBing動作有兩個重要的屬性

  • IValueProvider:提供如何填值
  • IModelBinder:建立物件(綁定關聯) 預設使用DefaultModelBinder類別.

目前分享的IValueProviderIModelBinder UML類別關聯圖如下

UML_Model

下篇會介紹ModelBind模型綁定重點邏輯,有分簡單參數綁定和複雜參數綁定

  • BindComplexModel
  • BindSimpleModel

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/ithelp-day18/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^