閱讀550 返回首頁    go windows


ASP.NET MVC的View是如何被呈現出來的?[設計篇]

在前麵的四篇文章中,我們介紹了各種ActionResult以及相關的請求響應機製,但是與“View的呈現”相關的ActionResult是ViewResult。通過ViewResult的執行實現的對View的呈現比上麵我們介紹的各種ActionResult要複雜得多,ASP.NET MVC內部設計了一個擴展的View引擎實現了最終的View呈現工作。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、View引擎中的View
二、ViewEngine
三、ViewResult的執行

ASP.NET MVC為我們提供了兩種View引擎,它們針對不同的動態View設計方式。一種是傳統的Web Form引擎,由於該引擎下View的設計與我們定義.aspx頁麵一致,又稱為ASPX引擎。另外一種則是本書默認采用同時也是推薦使用的Razor引擎。在兩種View引擎的工作機製之前,有一個必須要知道的問題:View如何表示?提到View,很多ASP.NET MVC的開發人員可能首先想到的就是定義UI界麵的.aspx文件(Web Form引擎)或者.cshtml/.vbhtml文件(Razor引擎)。其實對於View引擎來說,View是一個實現了IView接口類型的對象。如下麵的代碼片斷所示,IView的定義非常簡單,僅僅具有唯一的Render方法根據指定的View上下文和TextWriter對象實現對View的呈現。

   1: public interface IView
   2: {    
   3:     void Render(ViewContext viewContext, TextWriter writer);
   4: }
   5:  
   6: public class ViewContext : ControllerContext
   7: {
   8:     //其他成員
   9:     public virtual bool ClientValidationEnabled { get; set; }
  10:     public virtual bool UnobtrusiveJavaScriptEnabled { get; set; }
  11:  
  12:     public virtual TempDataDictionary TempData { get; set; }    
  13:     [Dynamic]
  14:     public object                     ViewBag { [return: Dynamic] get; }
  15:     public virtual ViewDataDictionary ViewData { get; set; }
  16:     public virtual IView              View { get; set; }
  17:     public virtual TextWriter         Writer { get; set; }
  18: }
  19:  
  20: public abstract class HttpResponseBase
  21: {
  22:     //其他成員
  23:     public virtual TextWriter Output { get; set; }
  24: }

IView用於呈現View的Render方法具有兩個參數,一個是表示View上下文的ViewContext對象。通過上麵的代碼片斷可以看出ViewContext是ControllerContext的子類,用於表示狀態數據的ViewData、ViewBag和TempData對應著ControllerBase的同名屬性。也就是說當執行從Controller的某個Action方法返回的ViewResult的時候,通過創建的ViewContext保持的狀態數據直接來源於Controller對象。

ViewContext具有兩個布爾類型屬性ClientValidationEnabled和UnobtrusiveJavaScriptEnabled表示是否支持客戶端驗證和Unobtrusive JavaScript。默認的情況下著兩個屬性通過同名的AppSettings配置項進行設置。如果應用不具有對應的配置,兩個屬性默認值為False。

   1: <configuration>
   2:   <appSettings>
   3:     <add key="ClientValidationEnabled" value="true"/>
   4:     <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
   5:   </appSettings>
   6: </configuration>

配置的範圍是針對整個Web應用而言的,這個全局屬性還可以通過HtmlHelper的同名靜態屬性進行設置。值得一提的是,ASP.NET MVC 允許我們針對某個View開啟或者關閉對客戶端驗證和UnobtrusiveJavaScriptEnabled的支持,而這可以通過當前View的HtmlHelper的實例方法EnableClientValidation/EnableUnobtrusiveJavaScript來實現。

   1: public class HtmlHelper
   2: {
   3:     //其他成員    
   4:     public void EnableClientValidation();
   5:     public void EnableClientValidation(bool enabled);
   6:     public void EnableUnobtrusiveJavaScript();
   7:     public void EnableUnobtrusiveJavaScript(bool enabled);
   8:    
   9:     public static bool ClientValidationEnabled { get; set; }    
  10:     public static bool UnobtrusiveJavaScriptEnabled { get; set; }    
  11: }

接口IView的Render方法的第二個參數是一個TextWriter對象。對於該方法來說,隻要我們將內容寫入該TextWriter即完成了針對相關內容在View上的呈現,因為在調用Render方法的時候,作為該參數的是當前。

View引擎的核心是一個ViewEngine對象,它實現了IViewEngine接口。如下麵的代碼片斷所示,IViewEngine定義了兩個FindView和FindPartialView方法根據指定的Controller上下文、View名稱和布局文件名稱去獲取對應的View和Partial View,兩個方法中具有一個布爾類型的參數useCache表示是否啟用緩存。另一個方法ReleaseView用於釋放View對象。

   1: public interface IViewEngine
   2: {    
   3:     ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
   4:     ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
   5:     void ReleaseView(ControllerContext controllerContext, IView view);
   6: }

FindView和FindPartialView方法返回的並不是實現了IView接口的類型的對象,而是一個類型為System.Web.Mvc.ViewEngineResult對象。如下麵的代碼片斷所示,ViewEngineResult的隻讀屬性View和ViewEngine屬性表示找到的View對象和表示自身的ViewEngine對象。在成功獲取到對應View的情況下這兩個屬性會通過構造函數進行初始化。如果沒有找到相應的View,則將一個搜尋位置列表傳入另一個構造函數創建一個ViewEngineResult,而隻讀屬性SearchedLocations表示的就是這麼一個搜尋位置列表。

   1: public class ViewEngineResult
   2: {    
   3:     public ViewEngineResult(IEnumerable<string> searchedLocations);
   4:     public ViewEngineResult(IView view, IViewEngine viewEngine);
   5:    
   6:     public IEnumerable<string> SearchedLocations { get; }
   7:     public IView               View { get; }
   8:     public IViewEngine         ViewEngine { get; }
   9: }

如果返回的ViewEngineResult包含一個具體的View,那麼這個View將會最終被呈現出來。反之,如果ViewEngineResult僅僅包含一個通過SearchedLocations屬性表示的在獲取目標View過程中使用的搜索位置列表,那麼最終呈現出來的就是如下圖所示的包含該列表的錯誤頁麵。

image

我們可以通過一個簡單的實例來驗證這一點。在通過Viual Studio的ASP.NET MVC項目模板創建的空Web應用中,我們定義了如下一個HomeController。在默認的Action方法Index中,我們通過ViewEngines的靜態隻讀屬性Engines得到一個全局ViewEngine列表,並調用其FindView方法試圖去尋找一個根本不存在View(“NonExistentView”)。最後我們將得到的ViewEngineResult對象的SearchedLocations屬性表示的搜尋位置列表呈現出來。

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, "NonExistentView", null);
   6:         foreach (string location in result.SearchedLocations)
   7:         {
   8:             Response.Write(location + "<br/>");
   9:         }
  10:     }
  11: }

運行我們的程序後表示在獲取目標View中采用的搜尋位置列表會如下圖所示的方式呈現出來,而這個列表與上圖是完全一致的。

image

在上麵實例演示中涉及到了一個重要的靜態類型ViewEngines,它通過如下定義的隻讀屬性Engines維護一個全局ViewEngine列表。從給出的定義可以看出,兩個原生的ViewEngine在初始化的時候就被添加到了該列表中,它們的類型就是分別代表Web Form和Razor引擎的WebFormViewEngineRazorViewEngine如果我們創建了一個自定義View引擎,相應的ViewEngine也可以通過ViewEngines進行注冊。

   1: public static class ViewEngines
   2: {
   3:     private static readonly ViewEngineCollection _engines = new ViewEngineCollection { new WebFormViewEngine(), new RazorViewEngine() };
   4:    
   5:     public static ViewEngineCollection Engines
   6:     {
   7:         get { return _engines;}
   8:     }
   9: }
  10:  
  11: public class ViewEngineCollection : Collection<IViewEngine>
  12: {
  13:     //其他成員
  14:     public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName);
  15:     public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName);
  16: }

ViewEngines的靜態隻讀屬性Engines的類型是ViewEngineCollection,它是一個元素類型為IViewEngine的集合。ViewEngineCollection同樣定義了FindView/FindPartialView這兩個方法用於獲取指定名稱的View和分部View,在方法內部它會遍曆集合中 的ViewEngine對象並調用它們的同名方法直到找的一個具體的View或者Partial View。由於WebFormViewEngine排在RazorViewEngine之前,所以前者會被優先使用,這可以從上麵兩張截圖所示的搜尋位置列表看出來(先搜索.aspx和.ascx,再搜索.cshtml和.vbhtml)。

對於ViewEngineCollection的FindView/FindPartialView方法來說,不知道讀者是否注意到了它們沒有一個表示是否采用緩存的useCache參數。實際上當這兩個方法被調用的時候,會先采用緩存的方式調用相應的ViewEngine,如果返回為Null,則以不采用緩存的方式再次調用它們。

View引擎對View的獲取以及對View的呈現最初是通過ViewResult觸發的,那麼兩者是如何銜接的呢?這是本小節著重討論的問題,在這之前我們不妨先來看看ViewResult的定義。如下麵的代碼片斷所示,表示ViewResult的類型ViewResult是抽象類ViewResultBase的子類。

   1: public class ViewResult : ViewResultBase
   2: {    
   3:     protected override ViewEngineResult FindView(ControllerContext context);
   4:     public string MasterName { get; set; }
   5: }
   6:  
   7: public abstract class ViewResultBase : ActionResult
   8: {   
   9:     public override void ExecuteResult(ControllerContext context);
  10:     protected abstract ViewEngineResult FindView(ControllerContext context);
  11:   
  12:     public object                     Model { get; }
  13:     public TempDataDictionary         TempData { get; set; }    
  14:     [Dynamic]
  15:     public object                     ViewBag { [return: Dynamic] get; }
  16:     public ViewDataDictionary         ViewData { get; set; }   
  17:     public string                     ViewName { get; set; }
  18:     public ViewEngineCollection       ViewEngineCollection { get; set; }
  19:     public IView                      View { get; set; }
  20: }

ViewResultBase的隻讀屬性Model表示作為View的Model對象,三個表示數據狀態的屬性(ViewData、ViewBag和TempData)來源於Controller的同名屬性。View和ViewName屬性則是代表具體的View對象和View的名稱。ViewEngineCollection屬性值默認來源於ViewEngines的靜態屬性Engines代表的全局ViewEngine列表。

ViewResultBase用於獲取具體View的FindView方法在ViewResult類中被實現,後者提供了額外的屬性MasterName表示布局文件名稱。在FindView方法的內部會直接調用ViewEngineCollection屬性的FindView方法,如果返回的ViewEngineResult包含一個具體的View(View屬性不為空),則直接返回該ViewEngineResult,否則拋出一個InvalidOperation異常,並將通過ViewEngineResult的SearchedLocations屬性表示的搜尋位置列表格式化成一個字符串作為該異常的消息,所以圖8-5所示的搜尋位置列表實際上是拋出的InvalidOperation異常的消息。

ASP.NET MVC的View引擎涉及到的相關的類型/接口以及它們之間的關係可以通過如圖下所示的UML來表示。ViewResult通過靜態類型ViewEngines利用View引擎激活對應的View對象並最終將View的內容呈現出來。

image

與除EmptyResult以外的所有ActionResult類型一樣,抽象類Conrtoller中提供了相應的方法輔助創建ViewResult。如下麵的代碼片斷所示,Controller具有如下一係列View方法幫助我們根據指定的View名稱、View對象、布局文件名稱和Model對象創建相應的ViewResult。

   1: public abstract class Controller : ControllerBase, ...
   2: {
   3:     //其他成員   
   4:     protected ViewResult View();
   5:     protected ViewResult View(object model);
   6:     protected ViewResult View(string viewName);
   7:     protected ViewResult View(IView view);
   8:     protected ViewResult View(string viewName, object model);
   9:     protected ViewResult View(string viewName, string masterName);
  10:     protected virtual ViewResult View(IView view, object model);
  11:     protected virtual ViewResult View(string viewName, string masterName, object model);
  12: }

ViewResult與View引擎的交互體現在用於執行執行ActionView的ExecuteResult上。如下麵的代碼片斷所示,如果View屬性為Null,會調用FindView方法得到一個用於封裝指定名稱(如果沒有執行則采用當前的Action名稱作為View名稱)的View的ViewEngineResult對象,並將其View屬性作為自身的View。然後創建View上下文,並將該上下文和當前HttpResponse的Output屬性代表的TextWriter對象作為參數調用View對象的Render方法實現對View的最終呈現。View呈現完成之後,通過ViewEngineResult得到對應的ViewEngine,並調用其Release對象對View進行回收操作。

   1: public abstract class ViewResultBase : ActionResult
   2: {
   3:     //其他成員
   4:     public override void ExecuteResult(ControllerContext context)
   5:     {   
   6:         //其他操作     
   7:         if (string.IsNullOrEmpty(this.ViewName))
   8:         {
   9:             this.ViewName = context.RouteData.GetRequiredString("action");
  10:         }
  11:         ViewEngineResult result = null;
  12:         if (this.View == null)
  13:         {
  14:             result = this.FindView(context);
  15:             this.View = result.View;
  16:         }
  17:         TextWriter output = context.HttpContext.Response.Output;
  18:         ViewContext viewContext = new ViewContext(context, this.View, this.ViewData, this.TempData, output);
  19:         this.View.Render(viewContext, output);
  20:         if (result != null)
  21:         {
  22:             result.ViewEngine.ReleaseView(context, this.View);
  23:         }
  24:     }
  25: }

ViewResult為們提供了一種與View引擎交互的手段,其實在進行View的獲取和呈現的時候完全可以拋開ViewResult,直接利用View引擎來完成,如下兩種Action方法的定義是完全等效的。

   1: //Action方法直接返回ViewResult
   2: public class HomeController : Controller
   3: {
   4:     public ActionResult Index()
   5:     {
   6:         return View();
   7:     }
   8: }
   9:  
  10: //Action方法直接調用View引擎
  11: public class HomeController : Controller
  12: {
  13:     public void Index()
  14:     {
  15:         string viewName = ControllerContext.RouteData.GetRequiredString("action");
  16:         ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, viewName, null);
  17:         if (null == result.View)
  18:         { 
  19:             throw new InvalidOperationException(FormatErrorMessage(viewName,result.SearchedLocations));
  20:         }
  21:         try
  22:         {
  23:             ViewContext viewContext = new ViewContext(ControllerContext, result.View, this.ViewData, this.TempData, Response.Output);
  24:             result.View.Render(viewContext, viewContext.Writer);
  25:         }
  26:         finally
  27:         {
  28:             result.ViewEngine.ReleaseView(ControllerContext, result.View);
  29:         }
  30:     }
  31:  
  32:     private string FormatErrorMessage(string viewName, IEnumerable<string> searchedLocations)
  33:     {
  34:         string format = "The view '{0}' or its master was not found or no view engine supports the searched locations. The following locations were searched:{1}";
  35:         StringBuilder builder = new StringBuilder();
  36:         foreach (string str in searchedLocations)
  37:         {
  38:             builder.AppendLine();
  39:             builder.Append(str);
  40:         }
  41:         return string.Format(CultureInfo.CurrentCulture, format, viewName, builder);
  42:     }
  43: }

上麵我們僅僅介紹了ViewResult利用View引擎進行View的獲取和呈現,其實當我們調用HtmlHelper的擴展方法Partial將指定的Partial View的HTML呈現出來時,內部調用View引擎的方式與之類

ASP.NET MVC的View是如何被呈現出來的?[設計篇]
ASP.NET MVC的View是如何被呈現出來的?[實例篇]


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 16:34:01

  上一篇:go  阿裏雲支撐馬來西亞數字自由貿易區落地 幫助馬來西亞中小企業參與全球貿易
  下一篇:go  ASP.NET MVC的View是如何呈現出來的[實例篇]