深入分析 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;

        }

代码的逻辑分为:

  1. 获取继承了IModelBinder接口的对象
  2. 执行IModelBinder对象的BindModel(…)方法并返回Action的参数值

继承IModelBinder接口的对象有三个类:DefaultModelBinder, FormCollectionModelBinderHttpPostedFileBaseModelBinder

在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中,有两种情况:

  1. 在ValueProvider找到指定的参数名: 一般都是简单类型(int, boolean, string)或数组(页面中相同name的元素会以数组的形式赋给指定的参数)
  2. 没有在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类型的可用属性赋值。

你可能感兴趣的:(asp.net)