接著上文asp.net mvc源碼分析-ActionResult篇 FindView 我們已經建立好view了,大家還記得在BuildManagerCompiledView的Render方法中最後調用的是RenderView。可能是跟人喜好問題,還有就是我工作項目用到的多數是Razor,所以這裡就講講RazorView吧。
想讓我們可看看RazorView的建構函式有什麼特別的地方
public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator)
: base(controllerContext, viewPath, viewPageActivator) {
LayoutPath = layoutPath ?? String.Empty;
RunViewStartPages = runViewStartPages;
StartPageLookup = StartPage.GetStartPage;
ViewStartFileExtensions = viewStartFileExtensions ?? Enumerable.Empty<string>();
}
其中LayoutPath 就是我們的masterPath,RunViewStartPages =true,ViewStartFileExtensions =FileExtensions,viewPageActivator=DefaultViewPageActivator的一個執行個體,viewPageActivator的設定在父類BuildManagerCompiledView的建構函式中設定。現在讓我們看看RenderView這個方法:
protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance) { if (writer == null) { throw new ArgumentNullException("writer"); } WebViewPage webViewPage = instance as WebViewPage; if (webViewPage == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, MvcResources.CshtmlView_WrongViewBase, ViewPath)); } // 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所對應的類型執行個體轉化為WebViewPage,轉換失敗則拋出異常。WebViewPage的繼承結構如下:WebViewPage-》WebPageBase-》WebPageRenderingBase-》WebPageExecutingBase。
記下來設定webViewPage的幾個重要屬性
webViewPage.OverridenLayoutPath = LayoutPath;
webViewPage.VirtualPath = ViewPath;
webViewPage.ViewContext = viewContext;
webViewPage.ViewData = viewContext.ViewData;
然後調用webViewPage.InitHelpers()
public virtual void InitHelpers() {
Ajax = new AjaxHelper<object>(ViewContext, this);
Html = new HtmlHelper<object>(ViewContext, this);
Url = new UrlHelper(ViewContext.RequestContext);
}
設定 Ajax,Html,Url3個屬性
預設 情況下RunViewStartPages為true。
startPage = StartPageLookup(webViewPage, RazorViewEngine.ViewStartFileName, ViewStartFileExtensions);
其中 ViewStartFileName = "_ViewStart";FileExtensions = new[] { "cshtml","vbhtml",};
這裡 多說明一下StartPage直接繼承於WebPageRenderingBase,我們還是來看看它的GetStartPage是怎麼實現的吧:
public static WebPageRenderingBase GetStartPage(WebPageRenderingBase page, string fileName, IEnumerable<string> supportedExtensions) { if (page == null) { throw new ArgumentNullException("page"); } if (String.IsNullOrEmpty(fileName)) { throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "fileName"), "fileName"); } if (supportedExtensions == null) { throw new ArgumentNullException("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; }
首先 WebPageRenderingBase currentPage = page;這句就不說了; var pageDirectory = VirtualPathUtility.GetDirectory(page.VirtualPath)是返回VirtualPath所對應的目錄,舉個例子吧,VirtualPath=~/Views/Home/Index.cshtml,那麼pageDirectory=~/Views/Home/,那麼現在就應該進入while迴圈了,
var path = VirtualPathUtility.Combine(pageDirectory, fileName + "." + extension);產生新的起始頁的path,path=~/Views/Home/_ViewStart.cshtml,很顯然這個檔案不存在。內部的if語句無法執行。這個foreach是迴圈的副檔名,在實際的項目開發中可以考慮將FileExtensions = new[] { "cshtml","vbhtml",};中2個元素移除一個以提高效能。
第二次計入while時pageDirectory=~/Views/,那麼現在對應的path=~/Views/_ViewStart.cshtml我們知道這個檔案預設是存在的。
var factory = currentPage.GetObjectFactory(path);
var parentStartPage = (StartPage)factory();
parentStartPage.VirtualPath = path;
parentStartPage.ChildPage = currentPage;
currentPage = parentStartPage;
這2句也很好理解不過具體實現就很複雜了,根據當前的path建立的StartPage,並設定它的VirtualPath、ChildPage ,把它作為傳回值。
直到pageDirectory=/才推出這個while迴圈。從這裡我們知道_ViewStart可以嵌套另一個_ViewStar,有點像我們的view有自己的Layout,而Layout對應的view也有Layout層層遞迴。
最後調用 webViewPage.ExecutePageHierarchy(new WebPageContext(context: viewContext.HttpContext, page: null, model: null), writer, startPage);
這裡建立了一個WebPageContext,WebPageContext也沒什麼特別的地方。ExecutePageHierarchy的具體定義是在WebPageBase中,
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和writer,在方法結束前在彈出。預設情況下我們會建立一個WebPageContext作為page的PageContext屬性,最後調用startpage的
ExecutePageHierarchy方法。該方法的實現在StartPage中
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); } } public void RunPage() { RunPageCalled = true; //ChildPage.PageContext = PageContext; ChildPage.ExecutePageHierarchy(); } public void RunPage() { RunPageCalled = true; //ChildPage.PageContext = PageContext; ChildPage.ExecutePageHierarchy(); }
這裡的 Execute();是真正調用_ViewStart.cshtml ,在 RunPage方法中有 ChildPage.ExecutePageHierarchy(), 實際是要調用WebViewPage的ExecutePageHierarchy方法。
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;
}
}
可以見render一個page是多麼的複雜啊。具體實現我們就不關心了,我們只要知道在RenderView時是遞迴render相應的view。我們只要知道在RenderView時是遞迴render相應的view。 同時我們需要知道在一次完整的http請求過程中_ViewStart.cshtml是最先執行的,_Layout.cshtml是最後執行