[转载]通过源代码研究ASP.NET MVC(七) – namespace Ivony – 博客园.
通过源代码研究ASP.NET MVC中的Controller和View(一)
通过源代码研究ASP.NET MVC中的Controller和View(二)
通过源代码研究ASP.NET MVC中的Controller和View(三)
通过源代码研究ASP.NET MVC中的Controller和View(四)
通过源代码研究ASP.NET MVC中的Controller和View(五)
通过源代码研究ASP.NET MVC中的Controller和View(六)
呃,细心的朋友可能已经发现了,到这里这个系列开始改名了,这是因为,从这一篇开始,我不得不来研究ASP.NET MVC中的Model模型了。因为我发现如果要Jumony视图引擎要充分利用ASP.NET MVC的功能的话,Model的支持是绕不过去的。
先来温习一下之前得到的结论,过了这么久,您忘了没?
Controller将执行Action的操作外包给了ControllerActionInvoker,其工作流程大体上是这样的:
- 查找Action(FindAction)
- 获取参数
- InvokeActionMethod
- InvokeActionResult
那么在上一篇,我们已经完整的了解了查找Action的过程,按照传统,这一篇应该对查找Action的过程进行一个总结,但现在需要打破这个传统了,因为这个总结已经有人写过了:
别人的东西可以拿来用的,一定要坚定的拿来用,因为软件领域的轮子已经堆积成山了——懒人原则
FindAction步骤找到的Action会经由授权筛选器处理,然后在真正调用Action之前,有一个关键步骤,那就是获取参数:
IDictionary<string, object> parameters = GetParameterValues( controllerContext, actionDescriptor );
protected virtual IDictionary<string, object> GetParameterValues( ControllerContext controllerContext, ActionDescriptor actionDescriptor ) { Dictionary<string, object> parametersDict = new Dictionary<string, object>( StringComparer.OrdinalIgnoreCase ); ParameterDescriptor[] parameterDescriptors = actionDescriptor.GetParameters(); foreach ( ParameterDescriptor parameterDescriptor in parameterDescriptors ) { parametersDict[parameterDescriptor.ParameterName] = GetParameterValue( controllerContext, parameterDescriptor ); } return parametersDict; }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,”Courier New”,courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }首先是创建了一个Dictionary用来承载返回结果,然后通过actionDescriptor的GetParameters方法获取所有参数的参数描述符,下面是一个枚举,对每一个参数调用GetParameterValue方法来获取具体的值,最后返回。
从前面的研究可知,这里的actionDescriptor的实例类型显然是ReflectedActionDescriptor,其GetParameters方法实现如下:
public override ParameterDescriptor[] GetParameters() { ParameterDescriptor[] parameters = LazilyFetchParametersCollection(); // need to clone array so that user modifications aren't accidentally stored return (ParameterDescriptor[]) parameters.Clone(); } private ParameterDescriptor[] LazilyFetchParametersCollection() { return DescriptorUtil.LazilyFetchOrCreateDescriptors<ParameterInfo, ParameterDescriptor>( ref _parametersCache /* cacheLocation */, MethodInfo.GetParameters /* initializer */, parameterInfo => new ReflectedParameterDescriptor( parameterInfo, this ) /* converter */); }
这里使用了不同于获取ControllerDescriptor的缓存体系。但从这个方法冗长的名字也能一目了然他是干什么的,或者说这里的代码去除缓存后大体上就等同于:
public override ParameterDescriptor[] GetParameters() { ParameterDescriptor[] parameters = Array.ConvertAll( MethodInfo.GetParameters(), parameterInfo => new ReflectedParameterDescriptor( parameterInfo, this ) ); // need to clone array so that user modifications aren't accidentally stored return (ParameterDescriptor[]) parameters.Clone(); }
注释说明了为什么要进行Clone,这样使得用户会因为意外情况而的修改缓存的母本。因为数组本质上来说不是一个安全的容器,其大小不可变,但每一 个元素都可以通过简单的赋值而修改。不过Jumony在解决这种问题时,使用的是只读的容器,而非每次访问都创建副本。MVC的各个部分显然是由不同的团 队完成的,从命名的风格和代码习惯上就能很明显的看出来。从源代码入手研究的确是一件非常困难的事情,但好处是你可以得到很多研究结论之外的收获。
最后还是来瞄一眼证实一下猜测:
internal static class DescriptorUtil { public static TDescriptor[] LazilyFetchOrCreateDescriptors<TReflection, TDescriptor>( ref TDescriptor[] cacheLocation, Func<TReflection[]> initializer, Func<TReflection, TDescriptor> converter ) { // did we already calculate this once? TDescriptor[] existingCache = Interlocked.CompareExchange( ref cacheLocation, null, null ); if ( existingCache != null ) { return existingCache; } TReflection[] memberInfos = initializer(); TDescriptor[] descriptors = memberInfos.Select( converter ).Where( descriptor => descriptor != null ).ToArray(); TDescriptor[] updatedCache = Interlocked.CompareExchange( ref cacheLocation, descriptors, null ); return updatedCache ?? descriptors; } }
又见到了这个CompareExchange,这是针对引用类型的一个重载。第一个CompareExchange的逻辑是,如果 cacheLocation是null,那么将null赋给它,但无论有没有被赋值,会返回cacheLocation在赋值前的引用,同时以上操作都是 原子操作。
其实这段代码有点莫名其妙,我猜测CompareExchange的交换赋值是个幌子,原子操作取值是真,这是为了避免在取值的时候,恰好又被赋值而造成冲突,但事实上我看不到第一个CompareExchange的必要性。
那么第一个ExchangeCompare可以简单的理解为:
TDescriptor[] existingCache = cacheLocation;
然后判断这个cacheLocation不为null,那么就直接返回。否则进行创建。
创建descriptors的过程与上面的猜测一样,只是多了一个排除空值,接下来是第二个CompareExchange,这个的目的就非常明确 和有必要了,因为在创建descriptors过程中,在其他线程可能已经创建完毕了,这时候则抛弃创建出来的descriptor的值。因为第二个 CompareExchange的逻辑是,如果cacheLocation仍然是null,那么将descriptors赋给它。但下面的 updatedCache ?? descriptors又有点无厘头,因为似乎直接return cacheLocation就可以了。
瞄一眼完毕,那么猜测又一次命中。这样,创建ParameterDescriptor的逻辑就是new ReflectedParameterDescriptor( parameterInfo, this ),其中的parameterInfo就是MethodInfo.GetParameter()的结果,也就是Action方法的每一个参数的信息。
继续回到GetParameterValues方法,在获取ParameterDescriptor后,会针对每一个ParameterDescriptor调用GetParameterValue方法来获取具体的值:
foreach ( ParameterDescriptor parameterDescriptor in parameterDescriptors ) { parametersDict[parameterDescriptor.ParameterName] = GetParameterValue( controllerContext, parameterDescriptor ); }
那么来看看GetParameterValue方法:
protected virtual object GetParameterValue( ControllerContext controllerContext, ParameterDescriptor parameterDescriptor ) { // collect all of the necessary binding properties Type parameterType = parameterDescriptor.ParameterType; IModelBinder binder = GetModelBinder( parameterDescriptor ); IValueProvider 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 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( null, parameterType ), ModelName = parameterName, ModelState = controllerContext.Controller.ViewData.ModelState, PropertyFilter = propertyFilter, ValueProvider = valueProvider }; object result = binder.BindModel( controllerContext, bindingContext ); return result ?? parameterDescriptor.DefaultValue; }
注释写的很明白,首先是搜集所有必须的绑定属性,先是parameterType,它等于参数的类型,因为ReflectionParameterDescriptor的实现是这样的:
public override Type ParameterType { get { return ParameterInfo.ParameterType; } }
然后是binder,是GetModelBinder方法的结果,我们一会儿再来看这个方法,再接下来是valueProvider,其值是 Controller的ValueProvider属性。然后是parameterName,这个值先要检查 parameterDescriptor.BindingInfo.Prefix,如果不为空,就使用ParameterName。当 然,ParameterName的实现显然是直接把参数名返回。这个BindingInfo我也回头再来看。接下来是propertyFilter,这是 一个Predicate<T>委托类型的,这个委托的定义就是接收一个T参数,返回一个布尔值。结合名称来看,这个委托应该是提供决定哪些属 性要被绑定的依据。
接下来是创建ModelBindingContext,ModelMetadata则是通过GetMetadataForType方法获得。 FallbackToEmptyPrefix则是判断Prefix属性是不是null,ModelState直接获取ViewData中的 ModelState。其他的属性则是使用上面定义的的变量值。
最后,调用IModelBinder的BindModel方法,如果返回的值是null,则使用parameterDescriptor.DefaultValue。
坦白说,对这段代码的第一印象就是乱。首先是上面定义的变量,几乎都是用于创建ModelBindingContext的,这本来没什么问题,但在 创建ModelBindingContext实例的时候,又夹杂了一些初始化代码而不是简单的直接用上面定义的变量来赋值。或者说同一个行为,使用了两种 方式,这不是一个良好的代码习惯。其次,这里的ValueProvider使用了Controller中的属性值,而ModelState更是把手伸到了 ViewData,这使得Model这里与Controller和View都建立了强耦合,这不得不说也是一个糟糕的设计。
事实上,Model部分的代码质量明显要比其他部分的代码质量要糟糕,这一点在后面的探索中,您就会看到。
将代码改成如下形式,是不是更清晰:
protected virtual object GetParameterValue( ControllerContext controllerContext, ParameterDescriptor parameterDescriptor ) { IModelBinder binder = GetModelBinder( parameterDescriptor ); ModelBindingContext bindingContext = new ModelBindingContext() { ModelName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName, ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( null, parameterDescriptor.ParameterType ), ModelState = controllerContext.Controller.ViewData.ModelState, FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified PropertyFilter = GetPropertyFilter( parameterDescriptor ), ValueProvider = controllerContext.Controller.ValueProvider }; object result = binder.BindModel( controllerContext, bindingContext ); return result ?? parameterDescriptor.DefaultValue; }
至少,它凸显了主线逻辑,即:
- 获取Binder
- 构造BindingContext
- 绑定Model
那么,我们来看看获取Binder的逻辑是怎样的:
private IModelBinder GetModelBinder( ParameterDescriptor parameterDescriptor ) { // look on the parameter itself, then look in the global table return parameterDescriptor.BindingInfo.Binder ?? Binders.GetBinder( parameterDescriptor.ParameterType ); }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,”Courier New”,courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }首先尝试从参数的BindingInfo中来获取Binder,如果没有,那么就从Binders中来获取。
BindingInfo今天已经出场N次了,这东西哪来的?它是ParameterDescriptor的一个属性,在ReflectionParameterDescriptor构造函数中创建了其实例:
public ReflectedParameterDescriptor( ParameterInfo parameterInfo, ActionDescriptor actionDescriptor ) { if ( parameterInfo == null ) { throw new ArgumentNullException( "parameterInfo" ); } if ( actionDescriptor == null ) { throw new ArgumentNullException( "actionDescriptor" ); } ParameterInfo = parameterInfo; _actionDescriptor = actionDescriptor; _bindingInfo = new ReflectedParameterBindingInfo( parameterInfo ); } public override ParameterBindingInfo BindingInfo { get { return _bindingInfo; } }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,”Courier New”,courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }
我们继续看ReflectedParameetrBindingInfo的代码,看看Prefix和Binder属性都是怎么来的:
public ReflectedParameterBindingInfo( ParameterInfo parameterInfo )
{
_parameterInfo = parameterInfo;
ReadSettingsFromBindAttribute();
}
其构造函数很简单,设置了parameterInfo后,就调用了ReadSettingsFormBindAttribute:
private void ReadSettingsFromBindAttribute() { BindAttribute attr = (BindAttribute) Attribute.GetCustomAttribute( _parameterInfo, typeof( BindAttribute ) ); if ( attr == null ) { return; } _exclude = new ReadOnlyCollection<string>( AuthorizeAttribute.SplitString( attr.Exclude ) ); _include = new ReadOnlyCollection<string>( AuthorizeAttribute.SplitString( attr.Include ) ); _prefix = attr.Prefix; }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,”Courier New”,courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }ReadSettingsFromBindAttribute方法正如其名,从附加在参数上的BindAttribute特性中获取配置值。并设置自己的属性值。可是设置的值里面,还是不包括Binder属性。那么来看看Binder属性的实现:
public override IModelBinder Binder { get { IModelBinder binder = ModelBinders.GetBinderFromAttributes( _parameterInfo, () => String.Format( CultureInfo.CurrentUICulture, MvcResources.ReflectedParameterBindingInfo_MultipleConverterAttributes, _parameterInfo.Name, _parameterInfo.Member ) ); return binder; } }
继续看GetBinderFormAttributes方法的实现,尽管从方法名已经可以知道他大概是干什么的了:
internal static IModelBinder GetBinderFromAttributes( ICustomAttributeProvider element, Func<string> errorMessageAccessor ) { CustomModelBinderAttribute[] attrs = (CustomModelBinderAttribute[]) element.GetCustomAttributes( typeof( CustomModelBinderAttribute ), true /* inherit */); return GetBinderFromAttributesImpl( attrs, errorMessageAccessor ); }
获取附加的CustomModelBinderAttribute特性然后继续调用GetBinderFromAttributesImpl方法:
private static IModelBinder GetBinderFromAttributesImpl( CustomModelBinderAttribute[] attrs, Func<string> errorMessageAccessor ) { // this method is used to get a custom binder based on the attributes of the element passed to it. // it will return null if a binder cannot be detected based on the attributes alone. if ( attrs == null ) { return null; } switch ( attrs.Length ) { case 0: return null; case 1: IModelBinder binder = attrs[0].GetBinder(); return binder; default: string errorMessage = errorMessageAccessor(); throw new InvalidOperationException( errorMessage ); } }
逻辑很简单,如果恰好只有一个特性,那么调用特性的GetBinder返回,如果没有一个,返回null,其他情况,使用errorMessageAccessor获取异常描述,抛出异常。
个人感觉这里的代码风格有着浓重的外包风格,冗长的调用链,甚少意义的参数以及明显生硬的抽出公共代码的痕迹。
简而言之,ReflectedParameterBindingInfo的Binder属性尝试利用附加在参数上的BinderAttribute特性的GetBinder方法来获取Binder。如果没有通过BinderAttribute来获取到Binder,那么:
?? Binders.GetBinder( parameterDescriptor.ParameterType );
Binders是一个ControllerActionInvoker的一个属性:
protected internal ModelBinderDictionary Binders { get { if ( _binders == null ) { _binders = ModelBinders.Binders; } return _binders; } set { _binders = value; } }
其默认值是ModelBinders.Binders。ModelBinders.Binders则是系统中所有注册的IModelBinder的集合,来看看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; }
这里首先调用的是第一个重载,也就是只有一个参数的,然后他会调用到第二个重载,fallbackToDefault参数为true,再然后,会调 用到第三个重载,因为fallbackToDefault参数是true,所以fallbackBinder参数的值会是DefaultBinder。
第三个重载才是真正干活的方法,首先尝试从_innerDictionary中获取IModelBinder实例,以modelType为键(这里 的modelType就是parameterDescriptor.ParameterType,也就是参数的类型)。如果获存在,那么将其返回,否则, 调用ModelBinders.GetBinderFromAttributes方法,这个方法刚刚才看过。其作用是从附加在modelType的 BinderAttribute特性上获取Binder的实例。如果这也没有获取到,那么就返回fallbackBinder(也就是 DefaultBinder)。
那么我们可以发现IModelBinder的选择过程和原则:
- 尝试从附加在参数的BinderAttribute特性获取
- 尝试从ModelBinders.Binders集合中按参数类型检索
- 尝试从附加在参数的类型的BinderAttribute特性获取
- 使用ModelBinders.Binders.DefaultBinder
由上自下,优先级递减,在任何一个步骤找到了合适的Binder,则立即返回。