1. 程式人生 > >了解.net mvc實現原理ActionResult/View

了解.net mvc實現原理ActionResult/View

virt execute table dev .html x文件 wpa tar rri

了解.net mvc實現原理ActionResult/View

上一篇了解了請求至Controller的Action過程,這篇繼續看源碼處理Action收到請求數據再返回ActionResult到View的過程。 本節要討論的問題
  • Action的傳參過程
  • ActionResult
  • IView / IViewEngine / ViewEngineCollection / ViewEngineResult
記得上篇反編譯源看到Filter的執行順序提到命名1,2,3的變量,在MVC3的源碼中這個微軟改掉了。
AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
一、Action的傳參過程 我們在定義Action方法時常用的傳參大致有這麽幾種,詳細看代碼註釋 技術分享圖片View Code Action傳參支持類型綁定,字典鍵,對象,集合等等。除了使用默認的綁定方式,還可以繼承IModelBinder重寫DefaultModelBinder。 自己實現一個ModelBinder的代碼 以及處理綁定的Attribute (具體看代碼註釋),可以通過這種方式解決一些復雜對象的值綁定問題。 技術分享圖片View Code 使用自定義的ModelBinder處理參數轉換對象有三種方式 1.Global全配置 技術分享圖片View Code 2.利用我們自定義的屬性標記 技術分享圖片View Code
3.Action參數標記 技術分享圖片View Code 以上就是Action參數和Request參數之間轉換過程,使用默認的DefaultModelBinder會根據參數的復雜情況采用不同的方式處理參數。 技術分享圖片View Code 而我們自定義處理參數的規則就由自己來寫了。 了解這些參數轉換問題,那這個過程是何時發生的?根據前篇文章的分析,這個過程肯定是發生在Controller 的策略ControllerActionInvoker在執行InvokeAction方法攔截Action的時候發生的,即在處理完IAuthorizationFilter過濾器之後,處理IActionFilter之前的這段代碼 IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor); 不管使用哪種ModelBinder,通過GetParameterValues將Request 請求的參數轉換對應的類型。看一下跟進的方法是如何處理的 技術分享圖片
View Code 主要業務都在第二個處理參數描述的取值的方法裏,首先取得參數的類型,然後讀取對應的ModelBinder,如果ModelBinder為空的時候則使用默認的DefaultModelBinder,接著生成ModelBindingContext上下文,然後又觸發ModelBinder的BindModel方法,並將ModelBindingContext傳遞過去, 在BindModel中默認的或者自定義實現的對象屬性賦值過程了,最後將生成的parameters對象集合再傳回至要觸發的Action方法中。 二、ActionResult ActionResult是Action的返回結果。ActionResult 有多個派生類,每個子類功能均不同,並不是所有的子類都需要返回視圖View,有些直接返回流,有些返回字符串等。我們來看一下ActionResult派生類關系圖 技術分享圖片 具體看一下每個類的功能,由於MSDN的示意圖太簡單不能完全表現所有的子類功能
類名 抽象類 父類 功能
ActionResult abstract Object 頂層父類
ContentResult 根據內容的類型和編碼,數據內容.通過Controller的Content方法返回
EmptyResult 返回空結果
FileResult abstract 寫入文件內容,具體的寫入方式在派生類中.
FileContentResult FileResult 通過 文件byte[] 寫入Response 返回客戶端,Controller的File方法
FilePathResult FileResult 通過 文件路徑 寫入Response 返回客戶端,Controller的File方法
FileStreamResult FileResult 通過 Stream 寫入Response 返回客戶端,Controller的File方法
HttpUnauthorizedResult 拋出401錯誤
JavaScriptResult 返回javascript文件
JsonResult 返回Json格式的數據
RedirectResult 使用Response.Redirect重定向頁面
RedirectToRouteResult 根據Route規則重定向頁面
ViewResultBase abstract 調用IView.Render() 返回視圖,兩個常用屬性ViewData,TempData
PartialViewResult ViewResultBase 調用父類ViewResultBase 的ExecuteResult方法.
重寫了父類的FindView方法.
尋找用戶控件.ascx文件
ViewResult ViewResultBase

調用父類ViewResultBase 的ExecuteResult方法.
重寫了父類的FindView方法.
尋找視圖頁面(aspx,cshtml或自定義視圖)

Controller的View()方法默認封裝ViewResult返回結果

簡單的列幾種寫法,都是Controller已經封裝好的 技術分享圖片View Code 當然你可以自己實現每種的類型返回,而不是通過Controller的方法返回。這個環節最重要的問題,當Action返回ActionResult後,這個ActionResult是如何工作的?ActionResult只有一個抽象方法 ExecuteResult ,當ActionResult實例被返回後,Controller執行器ControllerActionInvoker的InvokeAction方法在處理完IActionFilter之後調用了這段代碼InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result); 看後兩個參數,一個是IResultFilter過濾器,一個是Action返回的Result。該方法對返回ActionResult進行前置攔截後,接著調用ActionResult的ExecuteResult方法去處對應的響應業務(返回視圖,或字符串,文件流等),最後又對ActionResult後置攔截了一次。調用棧比較深 技術分享圖片View Code 註釋的地方註意看一下,InvokeActionResult 是調用ActionResult.ExecuteResult的方法,被做為委托放到IResultFilter前後攔截法中間執行。Controller的執行器ControllerActionInvoker 這幾個環節的調度者。再看一下ActionResult.ExecuteResult方法的業務,挑選個有代表性的子類實現的業務貼上來, FileResult基類 的ExecuteResult,但又調用了WriteFile方法,這個方法又下放到子類中實現了 技術分享圖片View Code 我們挑選FileStreamResult子類的WriteFile方法看看 技術分享圖片View Code 整個過程看下來FileStreamResult的ExecuteResult 將文件流寫入HttpResponse中返回到客戶端,而並不是返回視圖。再看一下ViewResult的ExecuteResult的業務,這個業務是在父類的中實現的 ViewResultBase的ExecuteResult業務,ViewResultBase還有兩個重要的性ViewData,TempData是在Acion返回的時候封裝好的。 技術分享圖片View Code 先根據上下文中的路由+Action名找到對應的IView,然後調用IView的Render會出視圖寫入context.HttpContext.Response.Output返回到客戶端。 三、IView / IViewEngine / ViewEngineCollection / ViewEngineResult
  • IView 作用:展示View對象, 將頁面讀成流通過Writer寫入Response中返回客戶端,主要方法Render用來繪制DOM對象到流中。
  • IViewEngine 作用:查找View對象(視圖頁面,例如aspx頁面),但是返回結果是ViewEngineResult ,View被保存其中。主要方法FindView,FindPartialView。
  • ViewEngineCollection 作用:視圖引擎集合
  • ViewEngineResult 作用:是IViewEngine查找View的結果

實現過程:當ViewResult執行ExecuteResult時會遍歷ViewEngineCollection中所有IViewEngine 引擎,並調用每個IViewEngine 引擎的FindView,如果找到具體的View頁面,則返回ViewEngineResult,ViewEngineResult包含了相關的IView信息,最後再調用IView的Render輸出視圖。

ViewEngineCollection 是視圖引擎集合(當IView要Render視圖的時候,要從視圖引擎集合中選擇一個視圖引擎來實現),ViewEngineCollection是ViewResult的策略屬性。可以通過屬性註入的方式更換成其它視圖引擎包括自定義的視圖引擎。MVC3中ViewResult的屬性ViewEngineCollection被默認註入了ViewEngines.Engines。 技術分享圖片View Code 再看一下ViewEngines.Engines類默認兩個成員,一個處理aspx的WebFormViewEngine引擎,一個是MVC3新加入的RazorViewEngine引擎。 技術分享圖片View Code 我們看一WebFormViewEngine類 技術分享圖片View Code 裏面已經指定了要查找View頁的路徑,主要是針對模板master,aspx,ascx等頁面相對路徑。再看他的爺爺類的FindView方法 技術分享圖片View Code 根據ControolerName+ActionName替換掉路徑中{0}去查找對應的物理文件。第一次查找到了會保存到緩存中,第二次進來的時候直接從緩存中查找。 從Request->Controller->Action->ActionResult->View->Response過程到此就介紹完了。 突然想到一個問題,這個位置微軟都使用了緩存,為什麽每次DefaultControllerFactory都要直接反射Controller呢,而不一次反射就緩存下來提高效率,反而是讓開發人員利用IOC容器去解決這個問題。 帶這個疑問去看了MVC3的DefaultControllerFactory,這個問題已經解決了。DefaultControllerFactory新增了一個DependencyResolver來處理獲取Controller的工作。但這個功能還是沒有IOC容器的功能強大,因為IOC解決了構造註入的問題,而DependencyResolver則沒有實現這個功能。.net mvc 已經整合了微軟企業庫很多內容,如果再把IOC功能直接整合進來,似乎企業庫有點尷尬了。好了,這節就到這裏了。相信你對.net MVC實現原理應該有了一定的了解。 https://www.cnblogs.com/yinzixin/archive/2012/12/05/2799459.html

書再接回上文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, WebPageRenderingBasestartPage)中,直接就是執行的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);
        }

這裏的Output是:

        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

了解.net mvc實現原理ActionResult/View