书再接回上文 Filter 和 Action 的执行 ,当 Action 方法被执行,返回了一个 ActionResult 之后,紧接着就要执行 ActionResult 了,当然还有 Filter 需要执行,这些都是发生在 ControllerActionInvoker 的 InvokeActionResultWithFilters 方法之中,这里面 filter 的执行和 action 方法被执行的时候执行相应的 filter 是一样的,已在 Filter 和 Action 的执行 中分析过了,不再讨论.直接看 ActionResult 的执行:
当然这个方法没什么好看的,这是 ActionResult 的一个抽象方法.先看下 ASP.NET MVC 3 中继承自 ActionResult 的类:
protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) {
actionResult.ExecuteResult(controllerContext);
}
System.Web.Mvc.ContentResult System.Web.Mvc.EmptyResult System.Web.Mvc.FileResult System.Web.Mvc.HttpStatusCodeResult System.Web.Mvc.JavaScriptResult System.Web.Mvc.JsonResult System.Web.Mvc.RedirectResult System.Web.Mvc.RedirectToRouteResult System.Web.Mvc.ViewResultBase
其中 ViewResultBase 是最常用的,它还有两个继承者:
System.Web.Mvc.PartialViewResult System.Web.Mvc.ViewResult
本文先重点看下 ViewResult 这个最常用的 ActionResult.它的 ExecuteResult 方法如下:
首先如果没有提供 View 的名字的话就默认是 action 的名字,然后调用 FindView 去查找对应的 View:
public override void ExecuteResult(ControllerContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName)) {
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null) {
result = FindView(context);
View = result.View;
}
TextWriter writer = context.HttpContext.Response.Output;
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
View.Render(viewContext, writer);
if (result != null) {
result.ViewEngine.ReleaseView(context, View);
}
}
这个方法实际上是调用了 ViewEngineCollection 中的对象的 FindView 方法,默认情况下 ViewEngineCollection 包括了如下对象:
protected override ViewEngineResult FindView(ControllerContext context) {
ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
if (result.View != null) {
return result;
}
// we need to generate an exception containing all the locations we searched
StringBuilder locationsText = new StringBuilder();
foreach (string location in result.SearchedLocations) {
locationsText.AppendLine();
locationsText.Append(location);
}
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
MvcResources.Common_ViewNotFound, ViewName, locationsText));
}
先看下 FindView 返回的 ViewEngineResult,这个类其实很简单,只是把一些对象组合在一起,一个构造函数是:
new WebFormViewEngine(),
new RazorViewEngine(),
public ViewEngineResult(IView view, IViewEngine viewEngine)
表示用某个 ViewEngine 找到了某个 IView,另一个构造函数是:
public ViewEngineResult(IEnumerable<string> searchedLocations)
表示没有找到的情况,这个时候就需要返回找过哪些地方,这些信息最终是被用于生成一个异常信息的.ViewEngineResult 此处的设计似乎有一点别扭.接下来看 RazorViewEngine 的 FindView 方法,RazorViewEngine 是继承自 BuildManagerViewEngine 的,这个类又是继承自 VirtualPathProviderViewEngine,看下 VirtualPathProviderViewEngine 的实现(有删节):
找到 View 的过程本质上是找到 View 文件的路径,因此调用了 GetPath 方法来查找 view 的位置,看下这边的 xxxLocationFormats,这是定义在 RazorViewEngine 的构造函数中的:
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) {
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);
}
这些字符串定义了一个 Mvc 项目文件夹的布局,RazorViewEngine 将按照上面的路径依次去寻找 view 文件.看 GetPath 方法(有删节):
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
AreaPartialViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
};
MasterLocationFormats = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
};
PartialViewLocationFormats = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
};
FileExtensions = new[] {
"cshtml",
"vbhtml",
};
首先判断当前请求是否位于一个 area 中,然后获得 View 的位置:
private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) {
string areaName = AreaHelpers.GetAreaName(controllerContext.RouteData);
bool usingAreas = !String.IsNullOrEmpty(areaName);
List<ViewLocation> viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null);
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);
}
接下来是访问缓存来找物理路径,不分析其缓存的实现,看实际获取路径的方法,首先 nameRepresentsPath 这个布尔量的含义:
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;
}
其实就是看这个 location 是不是一个绝对路径.用 razor engine 的默认方式的话,这里传进来的 name 是 view name,应该永远都是 false 的.另一种情况应该是路由到一个具体的文件的时候会发生(猜测,待确认).因此,接下来会执行 GetPathFromGeneralName:
private static bool IsSpecificPath(string name) {
char c = name[0];
return (c == '~' || c == '/');
}
这个方法其实比较简单,就是依次调用刚才准备好的 ViewLocation,利用 Format 方法将路径格式转化为真正的路径,例如 ViewLocation 的 Format 方法如下:
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;
}
然后判断虚拟路径上的文件是否存在.这个工作最终是由 BuilderManager 这个类完成的.BuilderManager 是 ASP.NET 的组成部分,其具体实现就不分析了.如果文件存在则返回.
public virtual string Format(string viewName, string controllerName, string areaName) {
return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName);
}
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
这里的 CreateView 方法是 RazorViewEngine 中定义的:
至此,RazorViewEngine 的工作就完成,它找到并返回了一个 IView 对象:RazorView.
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) {
var view = new RazorView(controllerContext, viewPath,
layoutPath: masterPath, runViewStartPages: true, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator);
return view;
}
注意到在这个实现中,viewLocation 实际上包括了 area location 和 view location.也就是如果一个在 area 中 action 方法返回 view 之后,在查找 view 文件的过程中,如果在 area 对应的地方没有找到,那么它还会到普通 view 的地方去找.例如如下的文件夹结构:
在 Admin 中的 HomeController 里面直接 return View(),但是在这个 Area 的 View 里并没有 Index.cshtml,因此它最终找到的 view 是全局的 View 下面的 Index.cshtml.个人觉得这种设计有点不符合直觉,area 中的 action 就应该局限于 area 中查找 view.
接下来就会调用 Render 方法,对于 RazorView 来说,这个方法是定义在它的基类 BuildManagerCompiledView 中的:
首先获得 View 的 type,这里也是通过 BuildManger 来完成的,每个 cshtml 都会被 asp.net 编译成一个类.这些自动生成的类文件通常在 C:\Users\[User Name]\AppData\Local\Temp\Temporary ASP.NET Files 目录下面,这些文件都放在哈希过的目录之中,比较难找.根据 这篇文档 ,临时文件存放在哪里是可以通过 web.config 配置的:
public void Render(ViewContext viewContext, TextWriter writer) {
if (viewContext == null) {
throw new ArgumentNullException("viewContext");
}
object instance = null;
Type type = BuildManager.GetCompiledType(ViewPath);
if (type != null) {
instance = _viewPageActivator.Create(_controllerContext, type);
}
if (instance == null) {
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.CshtmlView_ViewCouldNotBeCreated,
ViewPath
)
);
}
RenderView(viewContext, writer, instance);
}
<compilation debug="true" targetFramework="4.5" tempDirectory="F:/Project/tempASP"/>
找到对应的 cs 文件之后,可以看到生成的类是类似:
public class _Page_Views_home_Index_cshtml : System.Web.Mvc.WebViewPage<dynamic>
这样的.如果是强类型的 View,就应该是 WebViewPage 了.找到类型后,会调用一个 activator 的 Create 方法来创建实例, 这里采用了依赖注入的手法,但是在默认情况下,也只是调用反射来创建一个实例而已,在 Mvc 框架中,这种地方已经出现多次了.创建好了 WebViewPage 之后,就调用 RenderView 方法,这个方法是在 RazorView 中实现的:
渲染 View 仍然是一个非常复杂的过程.MVC3 之中引入了 viewStart 页面的概念,这是一个在所有 view 被 render 之前都会被执行的页面,所以首先执行了一个 StartPageLookup 方法来查找 viewStart 页面.先看后两个参数,
protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance) {
// An overriden master layout might have been specified when the ViewActionResult got returned.
// We need to hold on to it so that we can set it on the inner page once it has executed.
webViewPage.OverridenLayoutPath = LayoutPath;
webViewPage.VirtualPath = ViewPath;
webViewPage.ViewContext = viewContext;
webViewPage.ViewData = viewContext.ViewData;
webViewPage.InitHelpers();
WebPageRenderingBase startPage = null;
if (RunViewStartPages) {
startPage = StartPageLookup(webViewPage, RazorViewEngine.ViewStartFileName, ViewStartFileExtensions);
}
webViewPage.ExecutePageHierarchy(new WebPageContext(context: viewContext.HttpContext, page: null, model: null), writer, startPage);
}
internal static readonly string ViewStartFileName = "_ViewStart";
在这里定义了 viewStart 页面是以_ViewStart 为文件名的文件.这个方法实际上是定义在 StartPage 类中的(有删节):
结合注释,应该可以看明白这代码的查找规则,首先从当前 View 所在的目录开始,依次往上层搜索_ViewStart.cshtml(vbhtml)的文件,如果找到了就获得其类型,并且设置上一个找到的 ViewStart 页面为其 ChildPage(最初的 ViewStart 页面的 ChildPage 就是当前 View).
public static WebPageRenderingBase GetStartPage(WebPageRenderingBase page, string fileName, IEnumerable<string> supportedExtensions) {
// Build up a list of pages to execute, such as one of the following:
// ~/somepage.cshtml
// ~/_pageStart.cshtml --> ~/somepage.cshtml
// ~/_pageStart.cshtml --> ~/sub/_pageStart.cshtml --> ~/sub/somepage.cshtml
WebPageRenderingBase currentPage = page;
var pageDirectory = VirtualPathUtility.GetDirectory(page.VirtualPath);
// Start with the requested page's directory, find the init page,
// and then traverse up the hierarchy to find init pages all the
// way up to the root of the app.
while (!String.IsNullOrEmpty(pageDirectory) && pageDirectory != "/" && Util.IsWithinAppRoot(pageDirectory)) {
// Go through the list of support extensions
foreach (var extension in supportedExtensions) {
var path = VirtualPathUtility.Combine(pageDirectory, fileName + "." + extension);
if (currentPage.FileExists(path, useCache: true)) {
var factory = currentPage.GetObjectFactory(path);
var parentStartPage = (StartPage)factory();
parentStartPage.VirtualPath = path;
parentStartPage.ChildPage = currentPage;
currentPage = parentStartPage;
break;
}
}
pageDirectory = currentPage.GetDirectory(pageDirectory);
}
// At this point 'currentPage' is the root-most StartPage (if there were
// any StartPages at all) or it is the requested page itself.
return currentPage;
}
找到了 ViewStart 之后,接下来就执行 ExecutePageHierachy 这个方法来渲染 View,这个方法里面要完成相当多的工作,主要是 ViewStart 的执行,和 Layout 的执行.这里的困难之处在于对于有 Layout 的页面来说,Layout 的内容是先输出的,然后是 RenderBody 内的内容,最后还是 Layout 的内容.如果仅仅是这样的话,只要初始化一个 TextWriter,按部就班的往里面写东西就可以了,但是实际上,Layout 并不能首先执行,而应该是 View 的代码先执行,这样的话 View 就有可能进行必要的初始化,供 Layout 使用.例如我们有如下的一个 View:
再看如下的 Layout:
@{
ViewBag.Title = "Code in View";
Layout = "_LayoutPage1.cshtml";
}
这样可以在页面显示 Code in View 字样. 但是反过来,如果试图在 View 中显示在 Layout 里面的 "Data from Layout" 则是行不通的, 什么也不会被显示.所以 RenderBody 是先于 Layout 中其他代码执行的,这种 Layout 的结构称为 Page Hierachy.在这样的代码执行顺序下,还要实现文本输出的顺序,因此 asp.net mvc 这里的实现中就使用了栈,这个栈是 OutputStack,里面压入了 TextWriter.注意到这只是一个页面的处理过程,一个页面之中还会有 Partial View 和 Action 等,这些的处理方式都是一样的,因此还需要一个栈来记录处理到了哪个(子)页面,因此还有一个栈,称之为 TemplateStack,里面压入的是 PageContext,PageContext 维护了 view 的必要信息,比如 Model 之类的,当然也包括上面提到的 OutputStack.有了上面的基本信息,下面看代码,先看入口点:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewBag.ToView = "Data from Layout";
}
<div>
Data In View: @ViewBag.Title
</div>
<div>
@RenderBody();
</div>
首先就是 pageContext 入栈:
// This method is only used by WebPageBase to allow passing in the view context and writer.
public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage) {
PushContext(pageContext, writer);
if (startPage != null) {
if (startPage != this) {
var startPageContext = Util.CreateNestedPageContext<object>(parentContext: pageContext, pageData: null, model: null, isLayoutPage: false);
startPageContext.Page = startPage;
startPage.PageContext = startPageContext;
}
startPage.ExecutePageHierarchy();
}
else {
ExecutePageHierarchy();
}
PopContext();
}
然后区分了是否有 ViewStart 文件,如果有,就执行 startPage.ExecutePageHierachy(),先看这个方法,
public void PushContext(WebPageContext pageContext, TextWriter writer) {
_currentWriter = writer;
PageContext = pageContext;
pageContext.Page = this;
InitializePage();
// Create a temporary writer
_tempWriter = new StringWriter(CultureInfo.InvariantCulture);
// Render the page into it
OutputStack.Push(_tempWriter);
SectionWritersStack.Push(new Dictionary<string, SectionWriter>(StringComparer.OrdinalIgnoreCase));
// If the body is defined in the ViewData, remove it and store it on the instance
// so that it won't affect rendering of partial pages when they call VerifyRenderedBodyOrSections
if (PageContext.BodyAction != null) {
_body = PageContext.BodyAction;
PageContext.BodyAction = null;
}
}
这个方法比较简单,而且这部分的代码注释都比较多,还是比较好理解的.第一步就是把当前的 httpcontext 压栈,然后执行_ViewStart 中的代码,所以在所有的 view 的组成部分中,_ViewStart 代码是最先执行的,然后执行 RunPage:
public override void ExecutePageHierarchy() {
// Push the current pagestart on the stack.
TemplateStack.Push(Context, this);
try {
// Execute the developer-written code of the InitPage
Execute();
// If the child page wasn't explicitly run by the developer of the InitPage, then run it now.
// The child page is either the next InitPage, or the final WebPage.
if (!RunPageCalled) {
RunPage();
}
}
finally {
TemplateStack.Pop(Context);
}
}
这就让它的 "子页面" 开始执行.如果页面没启用 ViewStart,那么在 ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBasestartPage) 中,直接就是执行的 ExecutePageHierachy 方法,下面来看这个方法:
public void RunPage() {
RunPageCalled = true;
ChildPage.ExecutePageHierarchy();
}
再看 base.ExecutePageHierachy, 这是一个定义在 WebPageBase 类中的方法(有删节):
public override void ExecutePageHierarchy() {
// Change the Writer so that things like Html.BeginForm work correctly
ViewContext.Writer = Output;
base.ExecutePageHierarchy();
// Overwrite LayoutPage so that returning a view with a custom master page works.
if (!String.IsNullOrEmpty(OverridenLayoutPath)) {
Layout = OverridenLayoutPath;
}
}
这个方法就是将 context 压栈,然后执行相应的 view 的代码,然后出栈.有了这些出入栈的操作,可以保证 View 的代码,也就是 Execute 的时候的 writer 是正确的.Execute 中的方法除去 PartialView, Action 之类的,最终调用的是 WebPageBase 中的
public override void ExecutePageHierarchy() {
// Unlike InitPages, for a WebPage there is no hierarchy - it is always
// the last file to execute in the chain. There can still be layout pages
// and partial pages, but they are never part of the hierarchy.
TemplateStack.Push(Context, this);
try {
// Execute the developer-written code of the WebPage
Execute();
}
finally {
TemplateStack.Pop(Context);
}
}
这里的 Output 是:
public override void WriteLiteral(object value) {
Output.Write(value);
}
页面渲染的过程包括了两层的间接递归,还是比较复杂的,需要仔细体会.
public TextWriter Output {
get {
return OutputStack.Peek();
}
}
至此,本系列已经分析完成了整个 ASP.NET 页面的生命周期.接下来还将看几个重要的部分,model 验证,model template,和一些重要的 html helper 方法,最后还有 asp.net mvc 的扩展性.
https://www.cnblogs.com/yinzixin/archive/2012/12/05/2799459.html
来源: http://www.bubuko.com/infodetail-2476270.html