代码改变世界

通过源代码研究ASP.NET MVC中的Controller和View(三)

2010-11-17 02:22  Ivony...  阅读(6225)  评论(9编辑  收藏  举报

通过源代码研究ASP.NET MVC中的Controller和View(一)

通过源代码研究ASP.NET MVC中的Controller和View(二)

 

第三篇来了,上一篇我已经把VirtualPathProviderViewEngine的FindView翻了个底朝天,在研究派生类WebFormViewEngine之前,先来看看VirtualPathProviderViewEngine有没有什么遗漏:

image

默认构造函数没有什么有价值的东西。接下来是两个CreateView方法。它们由派生类实现,被FindView所调用。FileExists是判断虚拟路径文件是否存在的,然后是FindView和ReleaseView,ReleaseView的实现没有什么特别:

VirtualPathProviderViewEngine
  
    public virtual void ReleaseView( ControllerContext controllerContext, IView view )
    {
      IDisposable disposable = view as IDisposable;
      if ( disposable != null )
      {
        disposable.Dispose();
      }
    }

后缀是Formats的属性有六个,在FindView中使用了四个,另外有Partial的两个在FindPartialView中被使用,我暂时不关心PartialView。ViewLocationCache从名称看是一个缓存,缓存与主线逻辑没有关系,暂且不管,最后剩下一个VirtualPathProvider,这是VirtualPathProvider类型的。

VirtualPathProvider并不是ASP.NET MVC中的东东,它在ASP.NET 2.0就存在了,其作用是提供对虚拟文件系统的访问。如果只是需要访问IIS提供的虚拟文件系统,那么MapPath也就够了。VirtualPathProvider可以让你提供一套自定义的虚拟文件系统出来(例如存放于数据库里面的文件系统,WinFS?)。

在这里,我看到VirtualPathProvider属性仅用于FileExists方法:

image

VirtualPathProviderViewEngine
  
    protected virtual bool FileExists( ControllerContext controllerContext, string virtualPath )
    {
      return VirtualPathProvider.FileExists( virtualPath );
    }

VirtualPathProvider是一个读写属性,其默认值是HostingEnvironment.VirtualPathProvider,这也是ASP.NET默认的VirtualPathProvider

VirtualPathProviderViewEngine
  
    protected VirtualPathProvider VirtualPathProvider
    {
      get
      {
        if ( _vpp == null )
        {
          _vpp = HostingEnvironment.VirtualPathProvider;
        }
        return _vpp;
      }
      set
      {
        _vpp = value;
      }
    }

这个属性没有任何人写过,那么,应该是留给我们自己的派生类来写了。

image

在这里来总结一下VirtualPathProviderViewEngine类:它实现了FindView方法,并利用六个Formats属性和名称产生虚拟路径,交由派生类指定的VirtualPathProvider(或默认)验证文件是否存在,如果存在则调用派生类实现的CreateView方法产生视图对象返回。

谈点儿个人看法,从设计角度来说,我很难给VirtualPathProviderViewEngine列出一个清晰的职责,或者说其内聚程度并不高。六个Formats属性必须由外部或者派生类按照规定的格式指定,VirtualPathProvider也可以由派生类来修改。视图对象的产生亦由派生类来负责,同时制定了必须由虚拟路径产生视图的规则。或者说我宁愿相信这个类型完全是在WebFormViewEngine设计完后将一部分职能外提的产物。如果仔细品味整个FindView的过程,我会发现其实这个基类在大多数步骤都对外部或派生类存在依赖,而不是派生类可以对既定步骤进行干涉,这并不是一个良好的设计。

虽说是VirtualPathProviderViewEngine,但事实上对VirtualPathProvider的利用仅限于判断文件是否存在,而这个逻辑还是可以被重写的(FileExists方法是virutal的)。所以我们说VirtualPathProviderViewEngine除了缓存的实现之外,差不多唯一确定的就是如下三部曲的游戏规则,换言之如果你要从VirtualPathProviderViewEngine继承,则需要遵守这个规则:

  1. 通过name和Fotmats来产生虚拟路径
  2. 判断虚拟路径文件是否存在
  3. 通过虚拟路径产生视图对象

 

VirtualPathProviderViewEngine的研究就到此为止了,尽管铺展开来的话题还有很多,但是如果都铺展开来,那就写不完了。

那么VirtualPathProviderViewEngine是通过其六个Formats属性来确定如何产生虚拟路径的,那么这六个Formats是被谁设置的呢?

由于这六个属性都是自动属性,所以我只需查查它们的set被谁调用了,结果是这样的:

image

这样的结果非常欢喜,那来看看WebFormViewEngine的构造函数吧:

WebFormViewEngine
  
    public WebFormViewEngine()
    {
      MasterLocationFormats = new[]
      {
        "~/Views/{1}/{0}.master",
        "~/Views/Shared/{0}.master"
      };

      AreaMasterLocationFormats = new[]
      {
        "~/Areas/{2}/Views/{1}/{0}.master",
        "~/Areas/{2}/Views/Shared/{0}.master",
      };

      ViewLocationFormats = new[]
      {
        "~/Views/{1}/{0}.aspx",
        "~/Views/{1}/{0}.ascx",
        "~/Views/Shared/{0}.aspx",
        "~/Views/Shared/{0}.ascx"
      };

      AreaViewLocationFormats = new[]
      {
        "~/Areas/{2}/Views/{1}/{0}.aspx",
        "~/Areas/{2}/Views/{1}/{0}.ascx",
        "~/Areas/{2}/Views/Shared/{0}.aspx",
        "~/Areas/{2}/Views/Shared/{0}.ascx",
      };

      PartialViewLocationFormats = ViewLocationFormats;
      AreaPartialViewLocationFormats = AreaViewLocationFormats;
    }

这里没有什么特别,初始化了六个Formats属性。同时值得注意的是,由于这六个属性都是public的,事实上你可以自己去改它们来修改虚拟路径产生(查找)的逻辑。

 

那么看看WebFormViewEngine的CreateView实现:

WebFormViewEngine
  
    protected override IView CreatePartialView( ControllerContext controllerContext, string partialPath )
    {
      return new WebFormView( partialPath, null );
    }

    protected override IView CreateView( ControllerContext controllerContext, string viewPath, string masterPath )
    {
      return new WebFormView( viewPath, masterPath );
    }

没有任何特别之处,WebFormViewEngine创建的View对象是WebFormView的。

就在我正打算离开WebFormViewEngine研究WebFormView类型的时候,我发现了一个有意思的东西:

WebFormViewEngine
 
    [SuppressMessage( "Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
        Justification = "Exceptions are interpreted as indicating that the file does not exist." )]
    protected override bool FileExists( ControllerContext controllerContext, string virtualPath )
    {
      try
      {
        object viewInstance = BuildManager.CreateInstanceFromVirtualPath( virtualPath, typeof( object ) );
        return viewInstance != null;
      }
      catch ( HttpException he )
      {
        if ( he is HttpParseException )
        {
          // The build manager found something, but instantiation failed due to a runtime lookup failure
          throw;
        }

        if ( he.GetHttpCode() == (int) HttpStatusCode.NotFound )
        {
          // If BuildManager returns a 404 (Not Found) that means that a file did not exist.
          // If the view itself doesn't exist, then this method should report that rather than throw an exception.
          if ( !base.FileExists( controllerContext, virtualPath ) )
          {
            return false;
          }
        }

        // All other error codes imply other errors such as compilation or parsing errors
        throw;
      }
    }

是的,WebFormViewEngine重写了FileExists的逻辑。他调用BuiderManager.CreateInstanceFromVirtualPath方法创建了一个对象,如果没有返回空值,则认为文件存在。

下面的catch块倒是有点让人费解,首先是判断异常是不是HttpParseException,如果是就不处理继续抛出。然后判断GetHttpCode的值是不是NotFound,是则根据基类的Exists方法结果来返回。

我们来归纳一下这个逻辑:

  • 只有HttpException类型的异常会被处理
  • HttpException类型的异常中,HttpParseException类型的不会被处理
  • GetHttpCode方法返回不是NotFound的不会被处理
  • 如果GetHttpCode方法返回的是NotFound,并且文件真的不存在(base.FileExists),则返回false

事实上整个catch块可以浓缩成一个if语句:

  • 仅当产生了HttpException类型的异常,且异常类型不为HttpParseException,HttpCode是NotFound并且虚拟路径真的不存在的时候,返回false。
  • 其他任何情况,不处理异常,继续抛出。

很奇怪的逻辑,初看之下并没有什么头绪。不妨先来整理一下相关的知识点:

  • HttpParseException顾名思义就是ASP.NET分析器失败的时候产生的异常,这里做这种特别处理,可以推测在try块的操作中会用到分析器(PageParserTemplateParser等)。
  • GetHttpCode获取的是HTTP错误代码(如500服务器错误、404找不到页面),而那个NotFound就是404。

也许是写这代码的临时工怕自己也会搞不清这里在干什么,写了一些注释在上面,来看看:

第一个注释大体的意思是:“build manager找到了一些东西,但由于一个运行时查找失败导致实例化失败了”。好吧我承认我真的不知道这个注释在说什么,或者说我搞不清楚runtime lookup failure和HttpParseException有什么关系,也许是我的英文实在太烂。OK,索性放弃理解这段注释。

下面的注释也比较废:“大意是,如果BuildManager返回了一个404错误,那意味着有一个文件不存在。如果是view它自己不存在,这个方法需要报告这种情况(view本身不存在)而不是抛出一个异常。”

无法读出更多的信息了。不过在研究BuilderManager.CreateInstanceFromVirtualPath之前,我还是尝试先多猜测一些信息。比如说这个方法名告诉我们,这个方法将创建一个实例,从指定的虚拟路径。结合下面的HttpParseException,我猜测,这个方法是不是打算创建一个Page或者UserControl的实例呢(这个过程可能会引发HttpParseException)?这好象是显而易见的。再来考虑一下FileExists方法的用途,是用于验证一个虚拟路径所指向的文件是否存在的,事实上如果我来写,就不打算重写这个方法。重写这个方法来试图创建Page或UserControl的实例意义何在?难道微软的程序员真是在办公室里面空调吹多了而导致某种卵状器官疼痛?

无论怎样,来研究BuilderManager吧:

WebFormViewEngine
 
    internal IBuildManager BuildManager
    {
      get
      {
        if ( _buildManager == null )
        {
          _buildManager = new BuildManagerWrapper();
        }
        return _buildManager;
      }
      set
      {
        _buildManager = value;
      }
    }

很明显这是一个BuildManagerWrapper的实例,从Reflector的结果来看,我真的觉得研究别人的internal东东某器官有点疼:

image

这表明这个属性永远都是BuildManagerWrapper值的实例,而下面则佐证了这一点:

image

IBuildManager的源代码里面发现了一个有意思的注释:

namespace System.Web.Mvc
{
  using System.Collections;
  using System.IO;

  // REVIEW: Should we make this public?
  internal interface IBuildManager
  {
    object CreateInstanceFromVirtualPath( string virtualPath, Type requiredBaseType );
    ICollection GetReferencedAssemblies();

    // ASP.NET 4 methods
    Stream ReadCachedFile( string fileName );
    Stream CreateCachedFile( string fileName );
  }
}

REVIEW后面的英文大概是:“我们是不是应该把它弄成public的?”。

看看CreateInstanceFromVirtualPath的实现:

BuildManagerWrapper
 
    object IBuildManager.CreateInstanceFromVirtualPath( string virtualPath, Type requiredBaseType )
    {
      return BuildManager.CreateInstanceFromVirtualPath( virtualPath, requiredBaseType );
    }

好疼~~,,,难怪这个类型叫做Wrapper:

namespace System.Web.Mvc
{
  using System.Collections;
  using System.IO;
  using System.Web.Compilation;

  internal sealed class BuildManagerWrapper : IBuildManager
  {
    private static readonly Func<string, Stream> _readCachedFileDelegate =
            TypeHelpers.CreateDelegate<Func<string, Stream>>( typeof( BuildManager ), "ReadCachedFile", null /* thisParameter */);
    private static readonly Func<string, Stream> _createCachedFileDelegate =
            TypeHelpers.CreateDelegate<Func<string, Stream>>( typeof( BuildManager ), "CreateCachedFile", null /* thisParameter */);

   
    #region IBuildManager Members
    object IBuildManager.CreateInstanceFromVirtualPath( string virtualPath, Type requiredBaseType )
    {
      return BuildManager.CreateInstanceFromVirtualPath( virtualPath, requiredBaseType );
    }

    ICollection IBuildManager.GetReferencedAssemblies()
    {
      return BuildManager.GetReferencedAssemblies();
    }

    // ASP.NET 4 methods
    Stream IBuildManager.ReadCachedFile( string fileName )
    {
      return (_readCachedFileDelegate != null) ? _readCachedFileDelegate( fileName ) : null;
    }

    Stream IBuildManager.CreateCachedFile( string fileName )
    {
      return (_createCachedFileDelegate != null) ? _createCachedFileDelegate( fileName ) : null;
    }
    #endregion
  }
}

这个Wrapper没有一个方法是自己实现的,都是调用的System.Web.Compilation.BuildManager的静态方法。不过你可能发现了有两个方法的实现有点特殊,弄了委托,而且看起来很复杂。但实际上只要细细分析下,再结合那个注释就能明白。TypeHelpers.CreateDelegate显然是协助创建委托的,传入类型和方法名称。如果创建成功,就返回一个指向这个方法的委托,否则便返回null。ReadCachedFile和CreateCachedFile这两个方法应该是ASP.NET 4里面新增的。通过这样的手法,这个项目(ASP.NET MVC)如果用于非ASP.NET 4的环境(例如3.5),那么那两个委托就会创建不成功而是null,下面的IBuildManager.ReadCachedFile和IBuildManager.CreateCachedFile也就会返回null。。。。。。。

这样不需要条件编译也能适应不同的.NET版本。

 

OK,到这里研究就已经结束了,System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath我们都知道是干什么用的,创建给定虚拟路径文件的实例,例如Page、UserControl等。但,这似乎并无必要,而且创建的实例在WebFormViewEngine.FileExists中被残忍的丢弃(仅仅只是判断其是否为null)。。。。

换个思维来考虑问题,FileExists从VirtualPathProviderViewEngine设定的逻辑上来说,是应该检查文件是否存在的。但事实上,最后CreateView的时候,更关心的应该是这个虚拟路径是不是能够创建一个Page或UserControl的实例,而不是aspx或ascx文件是否存在,即使这两者根本就是等同的。

好吧,一言蔽之,VirtualPathProviderViewEngine的设计真的很有问题。按照派生类这种实现,还不如直接把CreateView改成TryCreateView再删掉FileExists方法算了。

 

继续,接下来是WebFormView,我们现在知道IView的职责是呈现HTML,也知道WebFormViewEngine所设定的虚拟路径文件都是aspx、ascx和master,同时重写了FileExists方法尝试创建虚拟路径所指向的页面、用户控件的实例,并以创建成功与否作为文件是否存在的依据,根据上面的分析,事实上这里都不是判断文件是否存在,而是确保能够CreateView也即创建WebFormView对象。

那么WebFormView里面应该是创建页面、用户控件实例,然后输出吧。看看WebFormView.Render是怎样实现的:

WebFormView
 
    public virtual void Render( ViewContext viewContext, TextWriter writer )
    {
      if ( viewContext == null )
      {
        throw new ArgumentNullException( "viewContext" );
      }

      object viewInstance = BuildManager.CreateInstanceFromVirtualPath( ViewPath, typeof( object ) );
      if ( viewInstance == null )
      {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.WebFormViewEngine_ViewCouldNotBeCreated,
                ViewPath ) );
      }

      ViewPage viewPage = viewInstance as ViewPage;
      if ( viewPage != null )
      {
        RenderViewPage( viewContext, viewPage );
        return;
      }

      ViewUserControl viewUserControl = viewInstance as ViewUserControl;
      if ( viewUserControl != null )
      {
        RenderViewUserControl( viewContext, viewUserControl );
        return;
      }

      throw new InvalidOperationException(
          String.Format(
              CultureInfo.CurrentUICulture,
              MvcResources.WebFormViewEngine_WrongViewBase,
              ViewPath ) );
    }

一开始还是入口检查,然后就调用了BuilderManager.CreateInstanceFromVirtualPath方法来创建实例,如果不出意外的话,这个方法估摸着最终也是System.Web.Compilation.BuildManager的,下面尝试将实例转换为ViewPageViewUserControl类型(分别继承于PageUserControl,是MVC视图的基础页的基类),并调用相应的方法呈现。

先来核实一下这个BuilderManager:

WebFormView
 
    internal IBuildManager BuildManager
    {
      get
      {
        if ( _buildManager == null )
        {
          _buildManager = new BuildManagerWrapper();
        }
        return _buildManager;
      }
      set
      {
        _buildManager = value;
      }
    }

没有悬念。顺便值得一提的是,事实上WebFormView.BuilderManager的实现应该是返回创建自己这个实例的WebFormViewEngine的BuilderManager,这里的实现显然有问题(临时工?)。

既然没有悬念,那么继续看两个RenderXXX方法:

WebFormView
 
    private void RenderViewPage( ViewContext context, ViewPage page )
    {
      if ( !String.IsNullOrEmpty( MasterPath ) )
      {
        page.MasterLocation = MasterPath;
      }

      page.ViewData = context.ViewData;
      page.RenderView( context );
    }

    private void RenderViewUserControl( ViewContext context, ViewUserControl control )
    {
      if ( !String.IsNullOrEmpty( MasterPath ) )
      {
        throw new InvalidOperationException( MvcResources.WebFormViewEngine_UserControlCannotHaveMaster );
      }

      control.ViewData = context.ViewData;
      control.RenderView( context );
    }

没什么太多可说的,这里看到最终的Render是交由ViewPageViewUserControl来实现的。