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

在开始之前,先来温习下上一篇文章中的结论(推论):

  • IView是所有HTML视图的抽象
  • ActionResult是Controller对View的控制的抽象,也是View的入口。
  • ViewResult用于指明呈现某个HTML视图(IView实例)。
  • ViewResult是IView实例的使用者,查找、呈现和释放IView实例。
  • IViewEngine是IView的管理者,被ViewResult调用,用于查找和释放IView实例。

三个类型已经可以得出初步的结论:

ViewResult

  • 创建者:Controller
  • 职责:呈现HTML视图
  • 公开接口:ExecuteResult( ControllerContext ) : void

IViewEngine

  • 创建者:全局
  • 职责:查找和管理IView(视图)对象
  • 公开接口:FindView( controllerContext : ControllerContext, viewName : string, masterName : string, useCache : bool ) : ViewEngineResult,ReleaseView( IView ) : void

IView

  • 创建者:IViewEngine
  • 职责:呈现HTML
  • 公开接口:Render( TextWriter ) : void

在呈现视图这个过程中,ViewResult是主控,IView是操作执行,IViewEngine是查找映射。如果类比到其他的架构(如ASP.NET),则可以建立这样的类比:

ViewResult 类比为 HttpApplication,是呈现视图的主控对象。

IView 类比为 IHttpHandler,是呈现视图的操作执行。

IViewEngine 类比为 IHttpHandlerFactory,负责视图的查找映射。

PS:大家可以比较一下类比类型的公开接口是何其相似。

同时我发现到,ViewResult调用IViewEngine.FindView接口参数里,除了ControllerContext,没有开放类型(非sealed类型)。这意味着这个接口很难存在“私有协议”(自创名称)这种手法。

简单的说“私有协议”这种手法就是指假设有两个类型A和B(A和B都是开放类型),A是B的某个接口的使用者,同时这个接口中存在一个开放类型C(非sealed类型)。那么我们就可以写出两个类型A'和B',同时写一个私有的(internal)类型C'。由于C'是私有的,非A'和B'便不能访问其扩展成员,此时在A'和B'之间就建立了一个“私有协议”。

显然举例说明更好,由于IView类型是一个开放类型(非sealed),我们可以建立一个IViewEngine到ViewResult之间的私有协议,如下:

  public class MyViewResult : ViewResult  {    protected override ViewEngineResult FindView( ControllerContext context )    {      var result = base.FindView( context );

      var myView = result.View as PrivateView;

      if ( myView != null )        myView.ControllerContext = context;

      return result;    }

  }

  public class MyViewEngine : IViewEngine  {

    //...

    public ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )    {      return new ViewEngineResult( new PrivateView(), this );    }

    //...  }

  internal class PrivateView : IView  {

    public ControllerContext ControllerContext    {      get;      internal set;    }

    #region IView 成员

    public void Render( ViewContext viewContext, System.IO.TextWriter writer )    {      throw new NotImplementedException();    }

    #endregion  }

注意这里的PrivateView 类型,这是一个internal的类型,只有我们的MyViewResult和MyViewEngine才能使用和发现。在使用中我们利用私有协议传输了更多的信息:ControllerContext,而这些信息对于这个程序集之外的代码是不可见的(因为携带的类型根本就不可见),这就形成了私有协议。这个例子仅为说明这种手法,并无实际意义(因为事实上IViewEngine在创建IView对象的时候就可以把ControllerContext传进去了,而无需这么复杂)。

在IViewEngine.FindView的参数里,只有ControllerContext是开放类型,私有协议只能在这之上建立。但从设计上来说,在Context上建立私有协议并不是一个好的idea。

或者说这种接口的设计透露出来设计者的思想:我不太愿意ViewResult和IViewEngine之间存在强耦合关系。

最后来看看ViewEngineResult的设计:

  public class ViewEngineResult  {

    public ViewEngineResult( IEnumerable<string> searchedLocations )    {      if ( searchedLocations == null )      {        throw new ArgumentNullException( "searchedLocations" );      }

      SearchedLocations = searchedLocations;    }

    public ViewEngineResult( IView view, IViewEngine viewEngine )    {      if ( view == null )      {        throw new ArgumentNullException( "view" );      }      if ( viewEngine == null )      {        throw new ArgumentNullException( "viewEngine" );      }

      View = view;      ViewEngine = viewEngine;    }

    public IEnumerable<string> SearchedLocations    {      get;      private set;    }

    public IView View    {      get;      private set;    }

    public IViewEngine ViewEngine    {      get;      private set;    }  }

这个类型除了属性啥都没有,难道就是传说中的贫血对象?

顺带提一句我对某些胡乱发明(至少我认为是)的诸如“贫血对象”这样的词汇相当不满。尽管这里的设计明显有一些坏味道。

OK,那么很显然的这个对象只是为了IViewEngine的FindView方法不至于多一个out IEnumerable<string> searchedLocations而已,这种设计存在的意义更多的在于改善编码体验,同时,避免在公开接口中定义out参数。

结合ViewResult.FindView和ViewEngineCollection.FindView的实现,我能够搞清楚这个类型对我们来说唯一有价值的东西就是View属性,如果这个属性为null,则表示没有找到视图。

那么对于架构设计的研究就到此为止了。接下来研究下ASP.NET MVC的默认视图模型实现。

由于IViewEngine是IView对象的管理者,所以先用Reflector查找一下IViewEngine的实现类有哪些:

OK,只有一个类型,看来问题变得很简单了。

FindView是IViewEngine的主要方法,这个方法在VirtualPathProviderViewEngine里面实现:

    public virtual ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )    {      if ( controllerContext == null )      {        throw new ArgumentNullException( "controllerContext" );      }      if ( String.IsNullOrEmpty( viewName ) )      {        throw new ArgumentException( MvcResources.Common_NullOrEmpty, "viewName" );      }

      string[] viewLocationsSearched;      string[] masterLocationsSearched;

      string controllerName = controllerContext.RouteData.GetRequiredString( "controller" );      string viewPath = GetPath( controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched );      string masterPath = GetPath( controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched );

      if ( String.IsNullOrEmpty( viewPath ) || (String.IsNullOrEmpty( masterPath ) && !String.IsNullOrEmpty( masterName )) )      {        return new ViewEngineResult( viewLocationsSearched.Union( masterLocationsSearched ) );      }

      return new ViewEngineResult( CreateView( controllerContext, viewPath, masterPath ), this );    }

逻辑很简单,首先是得到controllerName,然后调用GetPath方法,这个方法的参数数量,呃,,,那个,,,,(private的方法也不能写成这样啊!怒!)

GetPath方法会返回一个字符串,看起来这个字符串应该是path,然后是一个if,他的逻辑是判断GetPath返回的结果是不是空。如果是空,则返回一个没有View的ViewEngineResult,否则调用CreateView方法来创建视图返回。

简单说:

  • 获取路径
  • 路径为空则返回没有视图的ViewEngineResult
  • 否则创建视图返回

那么我发现CreateView方法是一个抽象的方法,这意味着我大体上能推测出VirtualPathProviderViewEngine类型的主要职责便是将ViewResult通过FindView方法传递来的信息转换成路径信息交由派生类创建视图对象:

viewName -> viewPath

masterName -> masterPath

这就是主要工作,这个工作是由GetPath方法完成的:

    private string GetPath( ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations )    {      searchedLocations = _emptyLocations;

      if ( String.IsNullOrEmpty( name ) )      {        return String.Empty;      }

      string areaName = AreaHelpers.GetAreaName( controllerContext.RouteData );      bool usingAreas = !String.IsNullOrEmpty( areaName );      List<ViewLocation> viewLocations = GetViewLocations( locations, (usingAreas) ? areaLocations : null );

      if ( viewLocations.Count == 0 )      {        throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture,            MvcResources.Common_PropertyCannotBeNullOrEmpty, locationsPropertyName ) );      }

      bool nameRepresentsPath = IsSpecificPath( name );      string cacheKey = CreateCacheKey( cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName );

      if ( useCache )      {        return ViewLocationCache.GetViewLocation( controllerContext.HttpContext, cacheKey );      }

      return (nameRepresentsPath) ?          GetPathFromSpecificName( controllerContext, name, cacheKey, ref searchedLocations ) :          GetPathFromGeneralName( controllerContext, viewLocations, name, controllerName, areaName, cacheKey, ref searchedLocations );    }

我的神,这个方法有够复杂,我们慢慢看。

首先是入口检查和out变量初始化,尽管那个_emptyLocations明白无误的告诉了我们他是一个空的数组,我还是去瞄了一眼:

    private static readonly string[] _emptyLocations = new string[0];

然后获取了areaName,useAreas标识是不是使用了区域,然后获取了viewLocations,大体上这些变量从名称就能推测其意义。

如果viewLocations没有,那么抛了个异常,这个异常大体上的意思是属性不能为空,属性名则是locationsPropertyName,这是个参数,对应到调用的地方的那个字符串:"ViewLocationFormats"

显然这个字符串和GetViewLocations方法存在某种微妙的联系,这个联系并不难找,我们发现调用的地方是这样的:

string viewPath = GetPath( controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched );
      string masterPath = GetPath( controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched );

这个字符串的值和第二个参数是一样的,第二个参数就是locations,看看locations用来干啥了?唔,,,GetViewLocations的参数,所以,,,,

好吧,这个设计有点坏味道了,,,,

继续我们的探索,回头我会帮大家把这些方法全掰碎了看清楚。

然后有一个判断,IsSpecificPath,下面的cacheKey和if ( useCache )是缓存用的,暂且不管。然后我们看到下面是return了。

先来摸清楚那个判断干啥的,nameRepresentsPath的意思应该是:“名称代表路径”,IsSpecificPath的意思是:“是特定的路径”,传入的参数是name,结合起来分析。

IsSpecificPath方法的参数应该是一个path,那么这意味着name和path在某些时候是一个东西(nameRepresentsPath)。来看看IsSpecificPath的实现:

    private static bool IsSpecificPath( string name )    {      char c = name[0];      return (c == '~' || c == '/');    }

极为简洁。在这里顺带稍微提一下,写成return name.StartsWith( "~" ) || name.StartsWith( "/" );语义岂不更为明确。
这个方法大体上来说可以描述成,判断name是不是一个绝对路径。结合参数名,我大体上可以这样猜测,如果name是以"/"或者"~"开头,则VirtualPathProviderViewEngine则会当作路径来看待,否则当作名称来看待。向上追溯,我们就能发现这个name其实就是viewName或matserName。

现在我们大体上理清了VirtualPathProviderViewEngine.GetPath的逻辑:

  1. 获取区域名(areaName)和视图位置(viewLocations)
  2. 检查名称是不是一个绝对路径
  3. 如果useCache为true,尝试从缓存中获取路径
  4. 否则,根据名称是不是一个绝对路径调用相应的方法获取路径。

修剪和归纳一下大体是这样:

  1. 获取视图位置
  2. 缓存检索
  3. 获取路径

接下来探索GetPathFromSpecificName和GetPathFromGeneralName两个方法,直觉告诉我GetPathFromSpecificName应该会比较简单:

    private string GetPathFromSpecificName( ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations )    {      string result = name;

      if ( !FileExists( controllerContext, name ) )      {        result = String.Empty;        searchedLocations = new[] { name };      }

      ViewLocationCache.InsertViewLocation( controllerContext.HttpContext, cacheKey, result );      return result;    }

的确很简单,简单的判断了一下文件是否存在(FileExists),然后就是插入缓存和返回结果了,而这个结果(result),就是name。哈,还真是nameRepresentsPath。

那么看看GetPathFromGeneralName:

    private string GetPathFromGeneralName( ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, ref string[] searchedLocations )    {      string result = String.Empty;      searchedLocations = new string[locations.Count];

      for ( int i = 0; i < locations.Count; i++ )      {        ViewLocation location = locations[i];        string virtualPath = location.Format( name, controllerName, areaName );

        if ( FileExists( controllerContext, virtualPath ) )        {          searchedLocations = _emptyLocations;          result = virtualPath;          ViewLocationCache.InsertViewLocation( controllerContext.HttpContext, cacheKey, result );          break;        }

        searchedLocations[i] = virtualPath;      }

      return result;    }
  • 循环获取locations(就是viewLocations)里面的所有项,进行了一个Format的操作(注意这里调用的是ViewLocation.Format方法)得到virtualPath(虚拟路径)。
  • 如果某个虚拟路径的文件是存在的(FileExists),则返回这个虚拟路径(同时会清空searchedLocations)。
  • 否则,会返回一个空字符串(最上面初始化的结果)。

在循环过程中,如果虚拟路径不存在,那么他会被添加到searchedLocations(查找过的位置)。唔,,,,这里又有坏味道了?

简单解释下,因为如果在循环过程中找到了任何一个正确的结果,searchedLocations就会被置为空数组,所以最终searchedLocations只可能有两种结果:空,或者所有循环过的virtualPath。

OK,现在获取路径的逻辑已经基本搞清,唯一不明白的是ViewLocation.Format方法的实现,而ViewLocation这个对象则是从这里来的:

      List<ViewLocation> viewLocations = GetViewLocations( locations, (usingAreas) ? areaLocations : null );

这个viewLocations后来成为了GetPathFromGeneralName的locations参数,如果注意观察的话,事实上这个东西仅用在了GetPathFromGeneralName方法:

先来看看ViewLocation这个类型:

    private class ViewLocation    {

      protected string _virtualPathFormatString;

      public ViewLocation( string virtualPathFormatString )      {        _virtualPathFormatString = virtualPathFormatString;      }

      public virtual string Format( string viewName, string controllerName, string areaName )      {        return String.Format( CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName );      }

    }

呃,简单的说,这个类型就是对string的一个包装,提供一个特定的Format方法。。。。

不过,好像areaName参数没有被用到,,,,

还好在这个类型定义的下面我很快发现了另一个东西:

    private class AreaAwareViewLocation : ViewLocation    {

      public AreaAwareViewLocation( string virtualPathFormatString )        : base( virtualPathFormatString )      {      }

      public override string Format( string viewName, string controllerName, string areaName )      {        return String.Format( CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName );      }

    }

这一对父子都是string的一个包装。他们包装了类似于:"abc{0}shy{1}uin{2}"这样的字符串,然后提供一个Format方法映射到String.Format。

没有更多信息了。去创建这些类型的GetViewLocations方法去看看:

    private static List<ViewLocation> GetViewLocations( string[] viewLocationFormats, string[] areaViewLocationFormats )    {      List<ViewLocation> allLocations = new List<ViewLocation>();

      if ( areaViewLocationFormats != null )      {        foreach ( string areaViewLocationFormat in areaViewLocationFormats )        {          allLocations.Add( new AreaAwareViewLocation( areaViewLocationFormat ) );        }      }

      if ( viewLocationFormats != null )      {        foreach ( string viewLocationFormat in viewLocationFormats )        {          allLocations.Add( new ViewLocation( viewLocationFormat ) );        }      }

      return allLocations;    }

神,这个方法也是如此的简洁,简单说就是把两个string[]类型的参数包装成ViewLocation然后再合并输出了。

好吧,我大体上可以弄明白了,这些GetViewLocations方法和ViewLocation类型全部都是障眼法。真正的逻辑可以简单的描述成,有若干个字符串模版,然后在GetPathFromGeneralName对这些模版调用了String.Format方法来产生virtualPath。

结合之前的研究可以得到结论了:

GetPath方法干的事情:

  1. 获取视图位置(GetViewLocations)

    • 检查是否使用了区域(Area)
    • 如果使用了区域,则把areaLocations传入
    • GetViewLocations方法会将locations和areaLocations这两个字符串数组包装和合并成一个ViewLocation的集合
    • 如果集合没有东西,那么抛异常
  2. 缓存检索
  3. 获取路径
    • 如果名称像是一个绝对路径("/"或"~"开头)

      • 检查虚拟路径所指向的文件是否存在(FileExists)
      • 存在则返回名称(当作路径)。
      • 否则返回空字符串。
    • 如果名称不像是一个绝对路径
      • 遍历所有的视图位置生成虚拟路径
      • 如果虚拟路径所指向的文件存在,则返回这个虚拟路径。
      • 如果所有生成的虚拟路径所指向的文件都不存在,则返回空字符串。

缓存处理部分我并不关心,现在从外部来看GetPath方法,那么它的参数分为三大部分:

  • 缓存部分

    • controllerContext(主要利用里面的HttpContext.Cache模块)
    • cacheKeyPrefix
    • useCache
  • 位置部分:
    • locations和areaLocations,这是虚拟路径的模版,使用的值是VirtualPathProviderViewEngine的公开属性。
    • locationsPropertyName,这个用于抛异常的时候指示使用的哪个Property。
  • 名称部分:
    • name,这个参数会是viewName或者masterName
    • controllerName,这个参数标识了控制器的名称
    • areaName,没有出现在参数中,但利用controllerContext提取了出来,事实上controllerName也是从controllerContext中提取的,性质一样。

那么这里弥漫着一股很浓烈的坏味道了。

GetPath方法的参数真是一团糟。不应当传入ControllerContext而是传入areaName和Cache,locations、areaLocations和locationsPropertyName应该绑成一个对象。换言之,这样岂不更好:

    private string GetPath( Cache cache, string cacheKeyPrefix, bool useCache, LocationsInfo locations, string name, string controllerName, string areaName );

最后用一句话来总结GetPath方法干的破事儿:

利用提供的虚拟路径模版和名称产生虚拟路径,并检查虚拟路径文件是否存在(FileExists),如果存在则返回虚拟路径。

那么,默认虚拟路径模版到底有哪些呢?这些东西是在派生类(WebFormViewEngine)中定义的。

转载于:https://www.cnblogs.com/Ivony/archive/2010/11/15/aspnet-mvc-by-source-2.html

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

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

    通过源代码研究ASP.NET MVC中的Controller和View(一) 通过源代码研究ASP.NET MVC中的Controller和View(二) 第三篇来了,上一篇我已经把VirtualPa ...

  2. [MVC]Asp.net MVC中的Session

    [MVC]Asp.net MVC中的Session 2008年12月9日 分类: ASP.NET, ASP.NET MVC, C#, 开发笔记 最近使用ASP.NET MVC 中的Controller ...

  3. 如何在 ASP.NET MVC 中集成 AngularJS

    介绍 当涉及到计算机软件的开发时,我想运用所有的最新技术.例如,前端使用最新的 JavaScript 技术,服务器端使用最新的基于 REST 的 Web API 服务.另外,还有最新的数据库技术.最新 ...

  4. ASP.NET MVC中controller和view相互传值的方式

    ASP.NET MVC中Controller向view传值的方式: ViewBag.ViewData.TempData 单个值的传递 Json 匿名类型 ExpandoObject Cookie Vi ...

  5. Asp.Net MVC中身份认证和授权

    MVC自带的ActionFilter 在Asp.Net WebForm的中要做到身份认证微软为我们提供了三种方式,其中最常用的就是我们的Form认证,需要配置相应的信息.例如下面的配置信息: < ...

  6. 在Asp.Net MVC中实现RequiredIf标签对Model中的属性进行验证

    在Asp.Net MVC中可以用继承ValidationAttribute的方式,自定制实现RequiredIf标签对Model中的属性进行验证 具体场景为:某一属性是否允许为null的验证,要根据另 ...

  7. ASP.NET MVC中你必须知道的13个扩展点

         ScottGu在其最新的博文中推荐了Simone Chiaretta的文章13 ASP.NET MVC extensibility points you have to know,该文章为我 ...

  8. Asp.net mvc中的Ajax处理

    在Asp.net MVC中的使用Ajax, 可以使用通用的Jquery提供的ajax方法,也可以使用MVC中的AjaxHelper. 这篇文章不对具体如何使用做详细说明,只对于在使用Ajax中的一些需 ...

  9. 在 ASP.NET MVC 中使用 Chart 控件

    在 .NET 3.5 的时候,微软就提供了一个 Chart 控件,网络上有大量的关于在 VS2008 中使用这个控件的文章,在 VS2010 中,这个控件已经被集成到 ASP.NET 4.0 中,可以 ...

最新文章

  1. AutoCAD2012打开后一闪的解决方法
  2. python入门指南pdf-跟老齐学Python+从入门到精通 PDF 下载
  3. 自备干货!如何有效的做竞品迭代分析
  4. JBoss Portal CAS 的配置
  5. js对当前时间的相关操作
  6. HTML饼状图中心添加文字,echarts饼状图环形中间动态文字
  7. 伪随机数生成器——random模块的用法
  8. 小程序css之字体镂空
  9. linux远程备份视频教程,linux实现自动远程备份(scp+ssh)
  10. 基于java Swing 框架使用socket技术开发的即时通讯系统【源码+数据库】
  11. ENVI中的3种图像分类方法
  12. 特征码的使用办法_如何查询使用车架号查询车辆是否是事故
  13. Charset API
  14. Uncaught SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse (<anonymous>)
  15. 5.3 千亿访问量下的开放平台技术揭秘
  16. Android kotlin上传头像实现
  17. Big Brother监控安装
  18. 方便实用的--股票涨跌停价计算器
  19. APP 数据指标体系分析
  20. 用 matplotlib 自定义添加 “哆啦A梦”背景图

热门文章

  1. 2022华为首个「天才少年」,是从绩点1.8逆袭的复旦博士
  2. 深度学习基础之 Dropout
  3. 2019年9月全国程序员工资统计,看看你拖后腿了没?
  4. 看到这些代码,我自叹不如!!!
  5. Git单人本地仓库操作
  6. MySQL WindowsCMD常用命令!
  7. 2006年上半年 网络工程师 上下午试卷【附带答案】
  8. MATLAB_no.1:入门作业_histeq():_imhist()_(男孩的三个图,以及文字描述)
  9. Python+OpenCV实现自动扫雷,挑战扫雷世界记录!
  10. 使用关键点进行小目标检测