iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
3

前言

MVCModel-Binding建立複雜物件(牽扯到複雜模型綁定.)

這篇會跟大家介紹MVC是如何把達成這個複雜的動作

我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.

IModelBinder(DefaultModelBinder)

DefaultModelBinder將Http請求傳來資料轉換為強型別物件,DefaultModelBinder是如何取得使用Model資料呢?

實現IValueProvider來處理。

ModelBinders

IModelBinder.BindModel方法使用兩個參數

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,但一個是使用第一點,另一個使用第四點.

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());

ModelBinderDictionary

一般參數透過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 提供參數填值

IValueProvider介面有一個重要方法GetValue會返回ValueProviderResult物件對於ValueProvider參數封裝

ValueProviderResult GetValue(string key)

ValueProvider工廠集合(ValueProviderFactories)

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

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類型集合.

 這個方法很巧妙利用一個共同參數類型簽章來達成多態轉折點

public virtual NameValueCollection Form
{
    get
    {
        //....
    }
}

public virtual NameValueCollection QueryString
{
    get
    {
        //....
    }
}

Http傳值到Server有許多方式,這裡介紹MVC利用哪個ValueProviderFormQueryString填值到物件上,很巧妙使用NameValueCollectionValueProvider建構子參數NameValueCollection決定是要使用FormQueryString填充值到參數.

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參數擁有兩個只讀屬性

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

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

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

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

小結:

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

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

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

UML_Model

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

  • BindComplexModel
  • BindSimpleModel

上一篇
[Day17] Action方法如何被執行InvokeAction(二)
下一篇
[Day19] Http參數如何綁定到Action參數上(簡單和複雜模型綁定探討)
系列文
從Asp.net框架角度進入Asp.net MVC原始碼30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言