深入分析 ASP.NET Mvc 1.0 – 4. 使用ModelBinder绑定Action的参数
在前一篇文章已经讲叙Controller.Execute(…)方法的执行流程中会调用ControllerActionInvoker类的InvokeAction(ControllerContext controllerContext, string actionName)方法, 在InvokeAction(…)方法内又调用了GetParameterValues(…)方法,这个方法为Action中的每个参数赋值,追踪到GetParameterValues(…)方法内部会发现其实每个参数的值是由GetParameterValue(…)返回的,观察GetParameterValue(…)方法的源码:
protected virtual object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor) { // collect all of the necessary binding properties Type parameterType = parameterDescriptor.ParameterType; IModelBinder binder = GetModelBinder(parameterDescriptor); IDictionary<string, ValueProviderResult> valueProvider = controllerContext.Controller.ValueProvider; string parameterName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName; Predicate<string> propertyFilter = GetPropertyFilter(parameterDescriptor); // finally, call into the binder ModelBindingContext bindingContext = new ModelBindingContext() { FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified ModelName = parameterName, ModelState = controllerContext.Controller.ViewData.ModelState, ModelType = parameterType, PropertyFilter = propertyFilter, ValueProvider = valueProvider }; object result = binder.BindModel(controllerContext, bindingContext); return result; }
代码的逻辑分为:
- 获取继承了IModelBinder接口的对象
- 执行IModelBinder对象的BindModel(…)方法并返回Action的参数值
继承IModelBinder接口的对象有三个类:DefaultModelBinder, FormCollectionModelBinder和HttpPostedFileBaseModelBinder
在GetParameterValue(…)方法中可以看到由GetModelBinder(parameterDescriptor)负责返回IModelBinder对象,但他是如何确定返回哪个ModeBinder的呢?
- 当Action的参数类型为FormCollection时,GetParameterValue(…)方法返回FormCollectoinModelBinder对象
- 当Action的参数类型为HttpPostedFileBase时,GetParameterValue(…)方法返回HttpPostedFileBaseModelBinder对象
- 当Action的参数类型除了FormCollection和HttpPostedFileBase外(int, string, boolean或强类型),GetParameterValue(…)方法返回DefaultModelBinder对象,DefaultModelBinder也是最常用的ModelBinder
来深入到GetModelBinder(…)方法的内部
private IModelBinder GetModelBinder(ParameterDescriptor parameterDescriptor) { // look on the parameter itself, then look in the global table return parameterDescriptor.BindingInfo.Binder ?? Binders.GetBinder(parameterDescriptor.ParameterType); }
通常情况下IModelBinder都是由Binders.GetBinder(parameterDescriptor.ParameterType)返回, Binders属性调用ModelBinders.Binders返回ModelBinderDictionary的静态实例,在调用ModelBinders.Binders属性返回ModelBinderDictionary对象之前先初始化ModelBinderDictionary对象并将HttpPostedFileBaseModelBinder保存到ModelBinderDictionary对象中(代码如下),最后其实是调用ModelBinderDictionary的GetBinder()方法返回的IModelBinder对象。
public static class ModelBinders { private static readonly ModelBinderDictionary _binders = CreateDefaultBinderDictionary(); public static ModelBinderDictionary Binders { get { return _binders; } } private static ModelBinderDictionary CreateDefaultBinderDictionary() { // We can't add a binder to the HttpPostedFileBase type as an attribute, so we'll just // prepopulate the dictionary as a convenience to users. ModelBinderDictionary binders = new ModelBinderDictionary() { { typeof(HttpPostedFileBase), new HttpPostedFileBaseModelBinder() } }; return binders; } }
ModelBinderDictionary的GetBinder()方法
public IModelBinder GetBinder(Type modelType) { return GetBinder(modelType, true /* fallbackToDefault */); } public virtual IModelBinder GetBinder(Type modelType, bool fallbackToDefault) { if (modelType == null) { throw new ArgumentNullException("modelType"); } return GetBinder(modelType, (fallbackToDefault) ? DefaultBinder : null); } private IModelBinder GetBinder(Type modelType, IModelBinder fallbackBinder) { // Try to look up a binder for this type. We use this order of precedence: // 1. Binder registered in the global table // 2. Binder attribute defined on the type // 3. Supplied fallback binder IModelBinder binder; if (_innerDictionary.TryGetValue(modelType, out binder)) { return binder; } binder = ModelBinders.GetBinderFromAttributes(modelType, () => String.Format(CultureInfo.CurrentUICulture, MvcResources.ModelBinderDictionary_MultipleAttributes, modelType.FullName)); return binder ?? fallbackBinder; }
在签名为private IModelBinder GetBinder(Type modelType, IModelBinder fallbackBinder) 的方法中会最返回IModelBinder对象,并且可以看出如果Action的参为类型为HttpPostedFileBase时,在_innerDictionary.TryGetValue(modelType, out binder)时就可能找到这个IModelBinder并返回;如果参数的类型为FormCollection时,ModelBinders.GetBinderFromAttributes()会返回FormControllerModelBinder否则会返回fallbackBinder也主是DefaultModelBinder对象
来看一个DefaultModelBinder对象的BindModel(…)方法的代码:
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException("bindingContext"); } bool performedFallback = false; if (!String.IsNullOrEmpty(bindingContext.ModelName) && !DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, bindingContext.ModelName)) { // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back // to the empty prefix. if (bindingContext.FallbackToEmptyPrefix) { bindingContext = new ModelBindingContext() { Model = bindingContext.Model, ModelState = bindingContext.ModelState, ModelType = bindingContext.ModelType, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; performedFallback = true; } else { return null; } } // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string)) // or by seeing if a value in the request exactly matches the name of the model we're binding. // Complex type = everything else. if (!performedFallback) { ValueProviderResult vpResult; bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out vpResult); if (vpResult != null) { return BindSimpleModel(controllerContext, bindingContext, vpResult); } } if (TypeDescriptor.GetConverter(bindingContext.ModelType).CanConvertFrom(typeof(string))) { return null; } return BindComplexModel(controllerContext, bindingContext); }
if (!String.IsNullOrEmpty(bindingContext.ModelName) && !DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, bindingContext.ModelName)) 查找指定的参数名是否包含在ValueProvider中,有两种情况:
- 在ValueProvider找到指定的参数名: 一般都是简单类型(int, boolean, string)或数组(页面中相同name的元素会以数组的形式赋给指定的参数)
- 没有在ValueProvider中找到指定的参数名:通常情况下都是strongly-typed类型,因为他的参数名不能与页面中的name属性相同,否则会有异常被抛出;还有一种情况就是页面中元素的name属性与Action的参数名不一致,这时如果参数不能接收null类型就会有异常抛出 。
接着上面的if语句,如果没有找到指定的参数并且ModelBindingContext.FallbackToEmptyPrefix==true,那么就重新创建一个ModelBindingContext对象,并设置performedFallback = true。
接下来的代码分两步执行:
- BindSimpleModel(controllerContext, bindingContext, vpResult): 为简单对象返回参数值
- BindComplexModel(controllerContext, bindingContext): 返回complex对象(这里我理解为strongly-typed)的参数值
1. 首先看看BindSimpleModel(…)方法的过程:
internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); // if the value provider returns an instance of the requested data type, we can just short-circuit // the evaluation and return that instance if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) { return valueProviderResult.RawValue; } // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following if (bindingContext.ModelType != typeof(string)) { // conversion results in 3 cases, as below if (bindingContext.ModelType.IsArray) { // case 1: user asked for an array // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType); return modelArray; } Type enumerableType = ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>)); if (enumerableType != null) { // case 2: user asked for a collection rather than an array // need to call ConvertTo() on the array type, then copy the array to the collection object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType); Type elementType = enumerableType.GetGenericArguments()[0]; Type arrayType = elementType.MakeArrayType(); object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType); Type collectionType = typeof(ICollection<>).MakeGenericType(elementType); if (collectionType.IsInstanceOfType(modelCollection)) { CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray); } return modelCollection; } } // case 3: user asked for an individual element object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType); return model; }
valueProviderResult.RawValue就是简单类型的参数值,if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) 是判断RawValue是否是指定的参数类型的实例,如果是那就直接返回RawValue。 跳转到最后ConvertProviderResult(…)方法处进行分析,上面的代码都是对数组的操作,最后也还是会调用ConvertProviderResult(…)方法,来看看ConvertProviderResult(…)方法的代码:
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) { try { object convertedValue = valueProviderResult.ConvertTo(destinationType); return convertedValue; } catch (Exception ex) { modelState.AddModelError(modelStateKey, ex); return null; } }调用ValueProviderResult类(ValueProviderResult.cs)的ConvertTo方法, 后又跳转到UnwrapPossibleArrayType(…)方法public object ConvertTo(Type type) { return ConvertTo(type, null /* culture */); } public virtual object ConvertTo(Type type, CultureInfo culture) { if (type == null) { throw new ArgumentNullException("type"); } CultureInfo cultureToUse = culture ?? Culture; return UnwrapPossibleArrayType(cultureToUse, RawValue, type); } private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType) { if (value == null || destinationType.IsInstanceOfType(value)) { return value; } // array conversion results in four cases, as below Array valueAsArray = value as Array; if (destinationType.IsArray) { Type destinationElementType = destinationType.GetElementType(); if (valueAsArray != null) { // case 1: both destination + source type are arrays, so convert each element IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length); for (int i = 0; i < valueAsArray.Length; i++) { converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType); } return converted; } else { // case 2: destination type is array but source is single element, so wrap element in array + convert object element = ConvertSimpleType(culture, value, destinationElementType); IList converted = Array.CreateInstance(destinationElementType, 1); converted[0] = element; return converted; } } else if (valueAsArray != null) { // case 3: destination type is single element but source is array, so extract first element + convert if (valueAsArray.Length > 0) { value = valueAsArray.GetValue(0); return ConvertSimpleType(culture, value, destinationType); } else { // case 3(a): source is empty array, so can't perform conversion return null; } } // case 4: both destination + source type are single elements, so convert return ConvertSimpleType(culture, value, destinationType); }在UnwrapPossibleArrayType(…)方法中首先是一些对数组的判断和操作,如果参数的类型不是Array那么就运行最后一行的ConvertSimpleType(…)方法并返回参数值private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType) { if (value == null || destinationType.IsInstanceOfType(value)) { return value; } // if this is a user-input value but the user didn't type anything, return no value string valueAsString = value as string; if (valueAsString != null && valueAsString.Length == 0) { return null; } TypeConverter converter = TypeDescriptor.GetConverter(destinationType); bool canConvertFrom = converter.CanConvertFrom(value.GetType()); if (!canConvertFrom) { converter = TypeDescriptor.GetConverter(value.GetType()); } if (!(canConvertFrom || converter.CanConvertTo(destinationType))) { string message = String.Format(CultureInfo.CurrentUICulture, MvcResources.ValueProviderResult_NoConverterExists, value.GetType().FullName, destinationType.FullName); throw new InvalidOperationException(message); } try { object convertedValue = (canConvertFrom) ? converter.ConvertFrom(null /* context */, culture, value) : converter.ConvertTo(null /* context */, culture, value, destinationType); return convertedValue; } catch (Exception ex) { string message = String.Format(CultureInfo.CurrentUICulture, MvcResources.ValueProviderResult_ConversionThrew, value.GetType().FullName, destinationType.FullName); throw new InvalidOperationException(message, ex); } }TypeConverter converter = TypeDescriptor.GetConverter(destinationType)来返回一个destinationType类型TypeConverter转换器,并用convert.CanConvertFrom和CanConvertTo两个方法是否可以从value.GetType()转换为destationType类型,如果可以转换那么就将转换的返回给Action对应的参数,否则换出一个异常。
2. BindComplexModel
在BindComplexModel中只讨论strongly-typed。先来看看BindComplexModel(…)方法的代码
internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { object model = bindingContext.Model; Type modelType = bindingContext.ModelType; // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created if (model == null && modelType.IsArray) { Type elementType = modelType.GetElementType(); Type listType = typeof(List<>).MakeGenericType(elementType); object collection = CreateModel(controllerContext, bindingContext, listType); ModelBindingContext arrayBindingContext = new ModelBindingContext() { Model = collection, ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, ModelType = listType, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType); if (list == null) { return null; } Array array = Array.CreateInstance(elementType, list.Count); list.CopyTo(array, 0); return array; } if (model == null) { model = CreateModel(controllerContext,bindingContext,modelType); } // special-case IDictionary<,> and ICollection<> Type dictionaryType = ExtractGenericInterface(modelType, typeof(IDictionary<,>)); if (dictionaryType != null) { Type[] genericArguments = dictionaryType.GetGenericArguments(); Type keyType = genericArguments[0]; Type valueType = genericArguments[1]; ModelBindingContext dictionaryBindingContext = new ModelBindingContext() { Model = model, ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, ModelType = modelType, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType); return dictionary; } Type enumerableType = ExtractGenericInterface(modelType, typeof(IEnumerable<>)); if (enumerableType != null) { Type elementType = enumerableType.GetGenericArguments()[0]; Type collectionType = typeof(ICollection<>).MakeGenericType(elementType); if (collectionType.IsInstanceOfType(model)) { ModelBindingContext collectionBindingContext = new ModelBindingContext() { Model = model, ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, ModelType = modelType, PropertyFilter = bindingContext.PropertyFilter, ValueProvider = bindingContext.ValueProvider }; object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType); return collection; } } // otherwise, just update the properties on the complex type BindComplexElementalModel(controllerContext, bindingContext, model); return model; }先从ModelBindingContext中获取model和modelType,在BindComplexModel方法的中间判断model是否为null,如果不是将调用CreateModel(…)方法来创建一个strongly-typed对象的实例: if (model == null) { model = CreateModel(controllerContext,bindingContext,modelType); } 。跳转到倒数第二行的BindComplexElementalModel(…)方法处
internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) { // need to replace the property filter + model object and create an inner binding context BindAttribute bindAttr = (BindAttribute)TypeDescriptor.GetAttributes(bindingContext.ModelType)[typeof(BindAttribute)]; Predicate<string> newPropertyFilter = (bindAttr != null) ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName) : bindingContext.PropertyFilter; ModelBindingContext newBindingContext = new ModelBindingContext() { Model = model, ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, ModelType = bindingContext.ModelType, PropertyFilter = newPropertyFilter, ValueProvider = bindingContext.ValueProvider }; // validation if (OnModelUpdating(controllerContext, newBindingContext)) { BindProperties(controllerContext, newBindingContext); OnModelUpdated(controllerContext, newBindingContext); } }先获取参数前面的BindAttribute,创建一个新的ModelBindingContext对象并重新设置了PropertyFilter属性,在下面的if语句中调用了三个方法,OnModelUpdating(…)永远返回true,接着是BindProperties(…),这个方法为strongly-typed对象的每个属性赋值,来看看它的源码:
private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) { PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext); foreach (PropertyDescriptor property in properties) { BindProperty(controllerContext, bindingContext, property); } }调用GetModelProperties(…)方法返回一个PropertyDescriptorCollection 集合对象,注意,这个集合不一定是stronly-typed中的全部属性,在GetModelProperties()方法中会根据参数前面定义的BindAttribute.Incude和BindAttribute.Exclude过滤掉不被使用的属性,将那些确定使用的属性存放到PropertyDescriptorCollection 中。在foreach中调用BindProperty(…)为每个可用的属性赋值,
protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { // need to skip properties that aren't part of the request, else we might hit a StackOverflowException string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name); if (!DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, fullPropertyKey)) { return; } // call into the property's model binder IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType); object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model); ModelBindingContext innerBindingContext = new ModelBindingContext() { Model = originalPropertyValue, ModelName = fullPropertyKey, ModelState = bindingContext.ModelState, ModelType = propertyDescriptor.PropertyType, ValueProvider = bindingContext.ValueProvider }; object newPropertyValue = propertyBinder.BindModel(controllerContext, innerBindingContext); // validation if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) { SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); } }
在BindProperty(…)内部递归调用IModelBinder.GetBinder(…)方法来为属性赋值。
ModelBinder并不复杂,Action所有的参数值都是循环调用IModelBinder.GetBinder()方法获得,而complex类型递归调用IModelBinder来为complex类型的可用属性赋值。