代码改变世界

通过源代码研究ASP.NET MVC(八)

2011-01-25 20:47  Ivony...  阅读(12064)  评论(9编辑  收藏  举报

通过源代码研究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(七)

 

第八篇,上一篇发布后,有朋友说这个系列更新太慢了,不给力。有几个原因,首先是Jumony M2的计划中,并不包括MVC Model的支持,换言之对于Jumony而言,到第六篇的时候,所得到的结论已经足够了。其次是最近忙于Jumony的开发,以及Jumony入门系列的撰写。当然,ASP.NET MVC是一个非常优秀的框架,事实也证明通过源代码研究ASP.NET MVC并非天方夜谭,我越来越觉得,这个系列的文章以及这种实践,存在比Jumony项目更为重要的意义,所以,我会继续这个系列,也希望能获得大家的支持。

通过源代码来研究任何东西。用脚趾头想想都能知道是一件非常枯燥和艰难的事情,但我想这个实践却很有意义。我们常说一个好的源代码就是最好的文档,那么就让我们来真正验证一下,对于MVC如此庞大和复杂的框架,一个好的源代码是不是真的可以当作资料来学习呢?不试,又怎么知道?同时,我一直在强调程序设计语言本身对于程序的重要性,通过这个实践,也能补充不少论据。

在这个系列中,我尽可能的当作自己一无所知(事实上也基本是这样),仅仅通过源代码源代码的而不借助任何其他资料来研究。你会看到我在尽可能细致的考证每一个推论,每一个猜测,尽管结论是那么的明显。同时你也会看到我不可避免的犯错,卡壳。这也许是因为我的能力所限,当然也有可能是不是源代码本身出了一些毛病。

通过这个系列,对于如何让别人看代码的时候完整的理解自己的意图。方法名称,程序结构以及细枝末节的那些东西的重要性,通过这个不可能完成的任务旅程,是否会有更深的理解呢?

废话到此结束。

 

在上一篇中,我们进展神速的从Controller或者说ControllerActionInvoker的世界忽然穿越到了Model的世界,IModelBinder地球人都知道这将会是Model的大门。进去之前先来回顾一下。

 

故事的原点,还得回到ControllerActionInvoker的核心逻辑:

  • 查找Action(FindAction)
  • 获取参数
  • InvokeActionMethod
  • InvokeActionResult

在第六篇,我们弄明白了查找Action的逻辑。接下来经过一系列的手续后,ControllerActionInvoker调用了GetPrameterValues方法,这个方法在获取了参数描述符后,转手调用GetParameterValue(没有s)来获取值。而GetParameterValue的逻辑也已经理清,其实很简单:

  • 获取Binder
  • 构造BindingContext
  • 绑定Model

获取Binder的详细流程已经在第七篇中摸清。那么

GetParameterValue(s)的结果,显然是会被当作Action的参数被传入(注1),而这个结果则是通过符合条件的IModelBinder来获取。

注1:具体的代码分析可以参考:深入理解ASP.NET MVC(8),当ControllerActionInvoker的主线走完后,我会回过头来将过滤器的工作原理完整的分析清楚,因为事实上所有过滤器的工作原理几乎是一样的。

一般看到接口,我立即能想到两个词,抽象和契约。事实上通过前面的分析能知道,MVC的核心抽象都是通过非常简单的接口来定义的。抽象的东西一定是简单的。Controller和View的抽象就是IController和IView接口,尽管Model部分并没有所谓的IModel接口,但事实上MVC是直接将object当作了Model的抽象。

那么来看看IModelBinder接口长什么样子:

namespace System.Web.Mvc
{

  public interface IModelBinder
  {
    object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext );
  }
}

很好,这个接口就一个方法,ControllerContext我们知道其包含了控制器和HTTP上下文的信息,而ModelBindingContext则是在GetParameterValue方法中被构建。

那么,IModelBinder的职责也被明确:

通过控制器和绑定上下文获取(绑定)模型的值。

ModelBindingContext从名称来看就是绑定上下文,他存放着绑定的时候需要的信息,但这些属性通过名称很难了解其具体用途,所以还是从IModelBinder入手。先来看看IModelBinder有一些什么实现:

image

老实说ModelBinder的实现类有点少得出乎我的意料,因为ModelBinder只有两种指定方式,在参数上通过CustomModelBinderAttribute指定或者通过类型来匹配。在参数上通过Attribute指定并不是一种特别靠谱的方式,事实上在范例和实际应用中,我们都很少看到这个特性的运用,譬如说MVC的示例网站代码:

    [HttpPost]
   
public ActionResult LogOn( LogOnModel model, string
returnUrl )
    {
     
if
( ModelState.IsValid )
      {
       
//...
      }

     
// 如果我们进行到这一步时某个地方出错,则重新显示表单
      return
View( model );
    }

这里我们看不到任何的CustomModelBinderAttribute的影子,我原想,或许是LogOnModel有在类型上用CustomModelBinderAttribute注册自己的LogOnModelBinder,因为在类型上注册比在参数上注册来的靠谱。但这样来说,对于returnUrl这样的参数,MVC至少也应该内置StringBinder或是Int32Binder之类的东西,但答案是这些都没有。仔细观察IModelBidner的这几个实现类,我发现他们大部分都是应用于特定而非一般类型的。例如HttpPostedFileBaseModelBinder,很明显这是用于创建HttpPostedFileBase实例的Binder。ByteArrayModelBinder多半是应用于byte[]类型。

那么,干脆随便挑一个类型来瞄一眼吧,譬如说HttpPostedFileBaseModelBinder:

  public class HttpPostedFileBaseModelBinder : IModelBinder
  {

   
public object BindModel( ControllerContext controllerContext, ModelBindingContext
bindingContext )
    {
     
if ( controllerContext == null
)
      {
       
throw new ArgumentNullException( "controllerContext"
);
      }
     
if ( bindingContext == null
)
      {
       
throw new ArgumentNullException( "bindingContext"
);
      }

     
HttpPostedFileBase
theFile = controllerContext.HttpContext.Request.Files[bindingContext.ModelName];
     
return
ChooseFileOrNull( theFile );
    }

   
// helper that returns the original file if there was content uploaded, null if empty
    internal static HttpPostedFileBase ChooseFileOrNull( HttpPostedFileBase
rawFile )
    {
     
// case 1: there was no <input type="file" ... /> element in the post
      if ( rawFile == null
)
      {
       
return null
;
      }

     
// case 2: there was an <input type="file" ... /> element in the post, but it was left blank
      if ( rawFile.ContentLength == 0 && String
.IsNullOrEmpty( rawFile.FileName ) )
      {
       
return null
;
      }

     
// case 3: the file was posted
      return
rawFile;
    }

  }

其实现与猜测的是一样的,直接从Request中获取与ModelName相同的文件,通过前面的分析我们知道,ModelName就是参数名。最后调用的ChooseFileOrNull方法的注释非常清楚,第一种情况是为了避免我们接受的参数在页面上对应的不是一个文件上传控件,那么返回null,第二种情况是用户没有选择文件上传,同样返回null,最后一种情况则将HttpPostedFileBase对象直接返回了。

再来看看LogOnModel这个类型,则把我原本的设想全部推翻了。

  public class LogOnModel
  {
    [
Required
]
    [
DisplayName( "用户名"
)]
   
public string UserName { get; set
; }

    [
Required
]
    [
DataType( DataType
.Password )]
    [
DisplayName( "密码"
)]
   
public string Password { get; set
; }

    [
DisplayName( "记住我?"
)]
   
public bool RememberMe { get; set
; }
  }

没错,事实上LogOnModel就是一个普通的object,并没有任何特别的诸如LogOnModelBinder之类的东西。那么,这是怎么实现的?

回想Binder的获取逻辑,没错,最后一步是,如果找不到合适的Binder,就使用ModelBinders.Binders.DefaultBinder。换言之不论是我们的LogOnModel model还是我们的string returnUrl,最终都是使用DefaultBinder来获取值的。

DefaultBinder是ModelBinderDictionary类型的一个属性:

    public IModelBinder DefaultBinder
    {
     
get
      {
       
if ( _defaultBinder == null
)
        {
          _defaultBinder =
new DefaultModelBinder
();
        }
       
return
_defaultBinder;
      }
     
set
      {
        _defaultBinder =
value
;
      }
    }

类似的代码已经见过太多次,这说明默认情况下,他就是DefaultModelBinder类型的一个实例。

那么来看看DefaultModelBinder的实现:

    public virtual object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext )
    {
     
if ( bindingContext == null
)
      {
       
throw new ArgumentNullException( "bindingContext"
);
      }

     
bool performedFallback = false
;

     
if ( !String
.IsNullOrEmpty( bindingContext.ModelName ) && !bindingContext.ValueProvider.ContainsPrefix( 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
()
          {
            ModelMetadata = bindingContext.ModelMetadata,
            ModelState = bindingContext.ModelState,
            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.GetValue( bindingContext.ModelName );
       
if ( vpResult != null
)
        {
         
return
BindSimpleModel( controllerContext, bindingContext, vpResult );
        }
      }
     
if
( !bindingContext.ModelMetadata.IsComplexType )
      {
       
return null
;
      }

     
return
BindComplexModel( controllerContext, bindingContext );
    }

首先是一个入口检查,接下来先设置performedFallback为false,从字面上看是已经进行了回退。其实fallback到底具体是什么工作到现在还不清楚。然后是一个判断,如果ModelName不等于空,并且ValueProvider不ContainsPrefix ModelName。

不妨来分析一下这个判断干什么用的,首要条件是ModelName不为空,也就是至少存在一个ModelName。在我们这一路走来,ModelName几乎是不可能为空的,因为他默认就是参数的名称,那我们先假定这个条件是恒真的。继续看后面,ContainsPrefix从字面上可以理解为,不包含前缀,参数是ModelName,也就是说,ValueProvider不包含这个ModelName这个前缀。

再来看下面的注释下,大意是,我们不能找到任何条目是以字首开头的,如果这是顶级元素,那么回退到空白的前缀。

然后是再一个if判断,BindingContext.FallbackToEmptyPrefix,看来这个属性是控制DefaultModelBinder是否允许回退到空白前缀的。这个属性的值是什么?翻出上一篇来看看就知道了:

        FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified

还记得上篇的话就会知道,BindingInfo.Prefix是从特性中获取的,那么这里可以简单的理解为,如果没有在参数上利用BindAttribute设置Prefix的话,那么就是允许回退到空白前缀(原来是这个意思)。如果不允许就直接绑定失败了(return null),那么如果允许回退到空白前缀,那么再创建一个ModelBindingContext,并且将performedFallback设置为true,继续下面的逻辑。这里重新创建的ModelBindingContext所有属性都是从原来的ModelBindingContext实例中获取的,创建一个新的实例其主要作用是丢弃一些属性(或者说用默认值)。那么我们来比对一下丢弃了哪些属性:

原本创建ModelBindingContext的逻辑大体上是这样的:

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

可以看出来ModelName和FallbackToEmptyPrefix被丢弃了,或者说被设置成默认值了。OK,这真是一段糟糕透顶的代码,事实上搞了这么久我们都无法猜透这里到底想干什么事情。也搞不清这两个属性是要被丢弃还是需要被设置为默认值。

来确定一下ModelName和FallbackToEmptyPrefix到底是被设置成了怎样的默认值,其实我觉得事实上根本就不会有设置默认值的代码,他们俩很可能简单的一个是null而另一个是false了,也就是字段被初始化的时候的值。无论怎样,来看看,首先是构造函数:

    public ModelBindingContext()
      :
this( null
)
    {
    }

   
// copies certain values that won't change between parent and child objects,
    // e.g. ValueProvider, ModelState
    public ModelBindingContext( ModelBindingContext
bindingContext )
    {
     
if ( bindingContext != null
)
      {
        ModelState = bindingContext.ModelState;
        ValueProvider = bindingContext.ValueProvider;
      }
    }

在构造函数里,显然没有为这两个属性设置任何默认值,那么来看看这两个属性:

    public bool FallbackToEmptyPrefix
    {
     
get
;
     
set
;
    }

果然,FallbackToEmptyPrefix默认值就是false(bool字段的默认值)。

    public string ModelName
    {
     
get
      {
       
if ( _modelName == null
)
        {
          _modelName =
String
.Empty;
        }
       
return
_modelName;
      }
     
set
      {
        _modelName =
value
;
      }
    }

ModelName的默认值则是String.Empty。

来谈谈这段坑爹的代码,第一眼看到这个方法的实现的时候就感觉非常乱。这段代码就像就是程序员为了完成某个测试而拼凑出来的一团东西。也许对于开发人员而言,这段代码是一目了然的,但对于直接看源代码的人来说,这可真是坑爹的代码。事实上,我们磕磕绊绊走了这么远,这段代码到底在干啥还是毫无头绪。

好吧我们不在这里纠结,因为无法预测IValueProvider的ContainsPrefix到底会产生什么结果,而现在贸然研究IValueProvider会把战线拉长。所以不妨将真假两种可能代入然后看看后面的逻辑会是怎样。

首先我们假定这个方法返回true,那么天下太平,第一个if里面的所有代码都不会被执行,performedFallback也会是false,然后if( !performedFallback )成立,进入内部,这时,尝试调用ValueProvider的GetValue方法,如果有结果返回,那么返回BindSimpleModel方法的结果。如果没有结果返回,那么看ModelMetadata.IsComplexType,字面意思上来看,是判断是不是复杂类型,如果不是,那么返回null(表示绑定失败)。否则返回BindComplexModel方法的结果。

这个流程走下来尽管还有很多不明白的地方,但我们总算搞清了一个事实,那就是performedFallback只是决定是否尝试绑定简单模型的(BindSimpleModel),如果进行了回退(performedFallback = true ),那么就不尝试进行简单模型的绑定(BindSimpleModel)。

再来考虑一下performedFallback取决于什么,先考虑一般情况,在一般情况下,ModelName会是参数名,所以不可能为空。其次我们也不会设置参数上的BindAttribute,所以FallbackToEmptyPrefix也会是true,因为BindingInfo.Prefix默认就是null(好吧,如果你去翻源代码,你会发现_prefix字段默认情况下根本没有被赋值。由于Model这一块的源代码质量实在太令人无语,所以这里就不在文章里深究了,否则会让读者大人头昏脑胀的)。

变换一下我们可以看到一般情况下是这样的:

      if ( true && !bindingContext.ValueProvider.ContainsPrefix( 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 ( true
)
        {
          bindingContext =
new ModelBindingContext
()
          {
            ModelMetadata = bindingContext.ModelMetadata,
            ModelState = bindingContext.ModelState,
            PropertyFilter = bindingContext.PropertyFilter,
            ValueProvider = bindingContext.ValueProvider
          };
          performedFallback =
true
;
        }
       
else
        {
         
return null
;
        }
      }

很明显,一般情况下performedFallback完全取决于ValueProvider的ContainsPrefix方法。也就是performedFallback == !bindingContext.ValueProvider.ContainsPrefix( bindingContext.ModelName ),而在接下来的SimpleModelBinder中,IValueProvider也扮演了重要角色,如果我们精简掉一些东西并将performedFallback代入,那看起来就会像是这样:

      if ( bindingContext.ValueProvider.ContainsPrefix( bindingContext.ModelName ) )
      {
       
ValueProviderResult
vpResult = bindingContext.ValueProvider.GetValue( bindingContext.ModelName );
       
if ( vpResult != null
)
        {
         
return
BindSimpleModel( controllerContext, bindingContext, vpResult );
        }
      }

这样,不难看出ValueProvider在BindSimpleModel流程中具有核心地位。那么,不碰它是不行的了,他是IValueProvider类型的,先来看看这个接口吧:

namespace System.Web.Mvc
{
 
using
System;

 
public interface IValueProvider
  {
   
bool ContainsPrefix( string
prefix );
   
ValueProviderResult GetValue( string
key );
  }
}

源代码非常给力,这东西不单单是在BindSimpleModel流程中处于核心地位,事实上估摸着这东西就干这一件事情的。