書再接回上文Filter和Action的執行 ,當Action方法被執行,返回了一個ActionResult之后,緊接着就要執行ActionResult了,當然還有Filter需要執行,這些都是發生在ControllerActionInvoker的InvokeActionResultWithFilters方法之中,這里面filter的執行和action方法被執行的時候執行相應的filter是一樣的,已在Filter和Action的執行 中分析過了,不再討論。直接看ActionResult的執行:
protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) { actionResult.ExecuteResult(controllerContext); }
當然這個方法沒什么好看的,這是ActionResult的一個抽象方法。先看下ASP.NET MVC 3中繼承自ActionResult的類:
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方法如下:
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); } }
首先如果沒有提供View的名字的話就默認是action的名字,然后調用FindView去查找對應的View:
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)); }
這個方法實際上是調用了ViewEngineCollection中的對象的FindView方法,默認情況下ViewEngineCollection包括了如下對象:
new WebFormViewEngine(), new RazorViewEngine(),
先看下FindView返回的ViewEngineResult,這個類其實很簡單,只是把一些對象組合在一起,一個構造函數是:
public ViewEngineResult(IView view, IViewEngine viewEngine)
表示用某個ViewEngine找到了某個IView,另一個構造函數是:
public ViewEngineResult(IEnumerable<string> searchedLocations)
表示沒有找到的情況,這個時候就需要返回找過哪些地方,這些信息最終是被用於生成一個異常信息的。ViewEngineResult此處的設計似乎有一點別扭。接下來看RazorViewEngine 的FindView方法,RazorViewEngine是繼承自BuildManagerViewEngine的,這個類又是繼承自VirtualPathProviderViewEngine,看下VirtualPathProviderViewEngine的實現(有刪節):
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); }
找到View的過程本質上是找到View文件的路徑,因此調用了GetPath方法來查找view的位置,看下這邊的xxxLocationFormats,這是定義在RazorViewEngine的構造函數中的:
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", };
這些字符串定義了一個Mvc項目文件夾的布局,RazorViewEngine將按照上面的路徑依次去尋找view文件。看GetPath方法(有刪節):
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); }
首先判斷當前請求是否位於一個area中,然后獲得View的位置:
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; }
接下來是訪問緩存來找物理路徑,不分析其緩存的實現,看實際獲取路徑的方法,首先nameRepresentsPath這個布爾量的含義:
private static bool IsSpecificPath(string name) { char c = name[0]; return (c == '~' || c == '/'); }
其實就是看這個location是不是一個絕對路徑。用razor engine的默認方式的話,這里傳進來的name是view name,應該永遠都是false的。另一種情況應該是路由到一個具體的文件的時候會發生(猜測,待確認)。因此,接下來會執行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; }
這個方法其實比較簡單,就是依次調用剛才准備好的ViewLocation,利用Format方法將路徑格式轉化為真正的路徑,例如ViewLocation的Format方法如下:
public virtual string Format(string viewName, string controllerName, string areaName) { return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName); }
然后判斷虛擬路徑上的文件是否存在。這個工作最終是由BuilderManager這個類完成的。BuilderManager是ASP.NET的組成部分,其具體實現就不分析了。如果文件存在則返回。
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
這里的CreateView方法是RazorViewEngine中定義的:
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; }
至此,RazorViewEngine的工作就完成,它找到並返回了一個IView對象:RazorView。
注意到在這個實現中,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中的:
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); }
首先獲得View的type,這里也是通過BuildManger來完成的,每個cshtml都會被asp.net編譯成一個類。這些自動生成的類文件通常在 C:\Users\[User Name]\AppData\Local\Temp\Temporary ASP.NET Files 目錄下面,這些文件都放在哈希過的目錄之中,比較難找。根據這篇文檔,臨時文件存放在哪里是可以通過web.config配置的:
<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<T>了。找到類型后,會調用一個activator的Create方法來創建實例,這里采用了依賴注入的手法,但是在默認情況下,也只是調用反射來創建一個實例而已,在Mvc框架中,這種地方已經出現多次了。創建好了WebViewPage之后,就調用RenderView方法,這個方法是在RazorView中實現的:
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); }
渲染View仍然是一個非常復雜的過程。MVC3之中引入了viewStart頁面的概念,這是一個在所有view被render之前都會被執行的頁面,所以首先執行了一個StartPageLookup方法來查找viewStart頁面。先看后兩個參數,
internal static readonly string ViewStartFileName = "_ViewStart";
在這里定義了viewStart頁面是以_ViewStart為文件名的文件。這個方法實際上是定義在StartPage類中的(有刪節):
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; }
結合注釋,應該可以看明白這代碼的查找規則,首先從當前View所在的目錄開始,依次往上層搜索_ViewStart.cshtml(vbhtml)的文件,如果找到了就獲得其類型,並且設置上一個找到的ViewStart頁面為其ChildPage(最初的ViewStart頁面的ChildPage就是當前View)。
找到了ViewStart之后,接下來就執行ExecutePageHierachy這個方法來渲染View,這個方法里面要完成相當多的工作,主要是ViewStart的執行,和Layout的執行。這里的困難之處在於對於有Layout的頁面來說,Layout的內容是先輸出的,然后是RenderBody內的內容,最后還是Layout的內容。如果僅僅是這樣的話,只要初始化一個TextWriter,按部就班的往里面寫東西就可以了,但是實際上,Layout並不能首先執行,而應該是View的代碼先執行,這樣的話View就有可能進行必要的初始化,供Layout使用。例如我們有如下的一個View:
@{ ViewBag.Title = "Code in View"; Layout = "_LayoutPage1.cshtml"; }
再看如下的Layout:
@{ Layout = "~/Views/Shared/_Layout.cshtml"; ViewBag.ToView = "Data from Layout"; } <div> Data In View: @ViewBag.Title </div> <div> @RenderBody(); </div>
這樣可以在頁面顯示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。有了上面的基本信息,下面看代碼,先看入口點:
// 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(); }
首先就是pageContext入棧:
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; } }然后區分了是否有ViewStart文件,如果有,就執行startPage.ExecutePageHierachy(),先看這個方法,
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); } }
這個方法比較簡單,而且這部分的代碼注釋都比較多,還是比較好理解的。第一步就是把當前的httpcontext壓棧,然后執行_ViewStart中的代碼,所以在所有的view的組成部分中,_ViewStart代碼是最先執行的,然后執行RunPage:
public void RunPage() { RunPageCalled = true; ChildPage.ExecutePageHierarchy(); }
這就讓它的“子頁面”開始執行。如果頁面沒啟用ViewStart,那么在ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)中,直接就是執行的ExecutePageHierachy方法,下面來看這個方法:
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; } }
再看base.ExecutePageHierachy,這是一個定義在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); } }
這個方法就是將context壓棧,然后執行相應的view的代碼,然后出棧。有了這些出入棧的操作,可以保證View的代碼,也就是Execute的時候的writer是正確的。Execute中的方法除去PartialView, Action之類的,最終調用的是WebPageBase中的
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的擴展性。