🚫 Ad Blocker Detected

Please disable your AD blocker to continue using this site. Ads help us keep the content free! please press keyboard F5 to refresh page after disabled AD blocker

請關閉廣告攔截器以繼續使用本網站。廣告有助於我們保證內容免費。謝謝! 關閉後請按 F5 刷新頁面

0%

提供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 許可協議。轉載請註明出處!

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