1. 程式人生 > >ASP.NET MVC的Razor引擎:View編譯原理

ASP.NET MVC的Razor引擎:View編譯原理

通過.cshtml或者.vbhtml檔案定義的View能夠被執行,必須先被編譯成存在於某個程式集的型別,ASP.NET MVC採用動態編譯的方式對View檔案實施編譯。當我們在對ASP.NET MVC進行部署的時候,需要對.cshtml或者.vbhtml檔案進行打包。針對某個View的第一次訪問會觸發針對它的編譯,一個View對應著一個型別。我們可以對.cshtml或者.vbhtml進行修改,View檔案修改後的第一次訪問將會導致View的再一次編譯。和ASP.NET 傳統的編譯方式一樣,針對View的編譯預設是基於目錄的,也就是說同一個目錄下的多個View檔案被編譯到同一個程式集中。[本文已經同步到《

How ASP.NET MVC Works?》中]

為了讓讀者對ASP.NET MVC對View檔案的編譯機制具有一個深刻的認識,我們通過一個簡單的例項來確定View檔案最終都被編譯成什麼型別,所在的程式集又是哪一個。我們在一個ASP.NET MVC應用中為HtmlHelper定義瞭如下一個擴充套件方法ListViewAssemblies,該方法用於獲取當前被載入的包含View型別的程式集(程式集名稱以“App_Web_”為字首)。
   1: public static class HtmlHelperExtensions
   2: {
   3:
public static MvcHtmlString ListViewAssemblies(this HtmlHelper helper)
   4:     {
   5:         TagBuilder ul = new TagBuilder("ul");
   6:         foreach(var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a=>a.FullName.StartsWith("App_Web_")))
   7:
{
   8:             TagBuilder li = new TagBuilder("li");
   9:             li.InnerHtml = assembly.FullName;
  10:             ul.InnerHtml+= li.ToString();
  11:         }
  12:         return new MvcHtmlString(ul.ToString());
  13:     }
  14: }

然後我們定義瞭如下兩個Controller型別(FooController和BarController),它們之中各自定義了兩個Action方法Action1和Action2。

   1: public class FooController : Controller
   2: {
   3:     public ActionResult Action1()
   4:     {
   5:         return View();
   6:     }
   7:     public ActionResult Action2()
   8:     {
   9:         return View();
  10:     }
  11: }
  12:  
  13: public class BarController : Controller
  14: {
  15:     public ActionResult Action1()
  16:     {
  17:         return View();
  18:     }
  19:     public ActionResult Action2()
  20:     {
  21:         return View();
  22:     }
  23: }

接下來我們為定義在FooController和BarController的四個Action建立對應的View(對應檔案路為:“~/Views/Foo/Action1.cshtml”、“~/Views/Foo/Action2.cshtml”、“~/Views/Bar/Action1.cshtml”和“~/Views/Bar/Action2.cshtml”)。它們具有如下相同的定義,我們在View中顯示自身的型別和當前載入的基於View的程式集。

   1: <div>當前View型別:@this.GetType().AssemblyQualifiedName</div>
   2: <div>當前載入的View程式集:</div>
   3: @Html.ListViewAssemblies()

現在執行我們的程式並在瀏覽器中通過輸入相應的地址“依次”(“Foo/Action1”、“Foo/Action2”、“Bar/Action1”和“Bar/Action2”)訪問定義在FooController和BarController的四個Action,四次訪問得到的輸出結果下圖所示。

image

輸出結果至少可以反映三個問題:

  • ASP.NET MVC對View檔案進行動態編譯生成的型別名稱基於View檔案的虛擬路徑(比如檔案路徑為“~/Views/Foo/Action1.cshtml”的View對應的型別為“ASP._Page_Views_foo_Action1_cshtml”)。
  • ASP.NET MVC是按照目錄進行編譯的(“~/Views/Foo/”下的兩個View檔案最終都被編譯到程式集“App_Web_j04xtjsy”中)。
  • 程式集按需載入,即第一次訪問“~/View/Foo/”目錄下的View並不會載入針對“~/View/Bar/”目錄的程式集(實際上此時該程式集尚未生成)。

我們可以通過BuildManager型別的靜態方法GetCompiledType和GetCompiledAssembly(如下面的程式碼片斷所示)根據View檔案的虛擬路徑得到對應的型別和程式集。

   1: public sealed class BuildManager
   2: {
   3:     //其他成員
   4:     public static Type GetCompiledType(string virtualPath);
   5:     public static Assembly GetCompiledAssembly(string virtualPath);
   6: }

在現有演示例項的基礎上我們建立瞭如下一個HomeController,預設的Action方法Index中通過呼叫BuildManager的靜態方法GetCompiledType得到並呈現出四個View檔案對應的型別名稱。

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         Response.Write(BuildManager.GetCompiledType("~/Views/Foo/Action1.cshtml") + "<br/>");
   6:         Response.Write(BuildManager.GetCompiledType("~/Views/Foo/Action2.cshtml") + "<br/>");
   7:         Response.Write(BuildManager.GetCompiledType("~/Views/Bar/Action1.cshtml") + "<br/>");
   8:         Response.Write(BuildManager.GetCompiledType("~/Views/Bar/Action2.cshtml") + "<br/>");        
   9:     }
  10: }

直接執行我們的程式後會在瀏覽器中得到代表四個View檔案編譯型別名稱的字串,具體顯示效果下圖所示。與上圖顯示的View型別名稱相比較,我們會發現它們是一致的。

image

上面我們簡單地介紹ASP.NET MVC以目錄為單位的動態View編譯,有人可能會問一個問題:編譯生成的程式集存放在哪裡?在預設情況下,View檔案被動態編譯後生成的程式集被臨時存放在ASP.NET的臨時目錄“%WinDir%\Microsoft.NET\Framework\{Version No}\Temporary ASP.NET Files\”下,不過我們可以通過如下所示的配置節<system.web>/<compilation>的tempDirectory 屬性來改變動態編譯的臨時目錄。如果我們改變了這個臨時目錄,需要確保工作程序執行帳號具有訪問該目錄的許可權。
   1: <configuration>
   2:   <system.web>
   3:     <compilation tempDirectory="c:\Temporary ASP.NET Files\" .../>
   4: </configuration>

一個寄宿於IIS的Web應用會在上述的臨時目錄下建立一個與Web應用同名的子目錄,所以我們很容易地找到應用對應的編譯目錄。但是對於將Visual Studio Development Server作為宿主的Web應用都會編譯到名稱為Roor的子目錄下。如果這樣的應用太多,我們往往不太容易準確地找到基於某個應用的編譯目錄。有時候我們可以根據目錄最後的修改時間來找到它,但是我個人傾向於直接刪除整個Root目錄,然後執行我們的程式後會重新生成一個只包含該應用編譯目錄的Root目錄。

對於上面演示的例項,我將Web應用寄宿於IIS下並且命名為MvcApp,我本機的目錄“C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\mvcapp\c4ea0afa\
a83bd407”下可以找到動態編譯的生成的檔案。如下圖所示,兩個View目錄(“~/Views/Foo”和“~/Views/Bar”)編譯生成的程式集就在這個目錄下面。

image

讀者一定很好奇一個View檔案通過動態編譯最終會生成一個怎樣的型別?對應前面演示的例項,我們已經知道了四個View檔案編譯生成的型別名稱和所在的程式集,我們只需要通過Reflector開啟對應的程式集就能得到View檔案編譯型別的定義。如下所示的是View檔案“~/Views/Foo/Action.cshtml”編譯後生成的ASP._Page_Views_Foo_Action1_cshtml型別的定義。

   1: [Dynamic(new bool[] { false, true })]
   2: public class _Page_Views_Foo_Action1_cshtml : WebViewPage<object>
   3: {    
   4:     public override void Execute()
   5:     {
   6:         this.WriteLiteral("<div>當前View型別:</div>\r\n<div>");
   7:         this.Write(base.GetType().AssemblyQualifiedName);
   8:         this.WriteLiteral("</div><br/>\r\n<div>當前載入的View程式集:</div>\r\n");
   9:         this.Write(base.Html.ListViewAssemblies());
  10:     }
  11:     
  12:     protected global_asax ApplicationInstance
  13:     {
  14:         get
  15:         {
  16:             return (global_asax) this.Context.ApplicationInstance;
  17:         }
  18:     }
  19: }