深入ASP.NET MVC之七:ActionResult的執行(View的加載和渲染)


書再接回上文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的地方去找。例如如下的文件夾結構:

image

在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);
        }

這里的Output是:

        public TextWriter Output {
            get {
                return OutputStack.Peek();
            }
        }

頁面渲染的過程包括了兩層的間接遞歸,還是比較復雜的,需要仔細體會。

至此,本系列已經分析完成了整個ASP.NET頁面的生命周期。接下來還將看幾個重要的部分,model驗證,model template,和一些重要的html helper方法,最后還有asp.net mvc的擴展性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM