169
技術社區[雲棲]
ASP.NET MVC的Razor引擎:RazorView
Razor引擎具有兩個核心的類型,一個是表示View本身的類型RazorView,另一個則是獲取和創建它的RazorViewEngine,我們將用兩篇文章對它們分別進行剖析。Razor引擎下的View通過類型RazorView表示,它與表示Web Form引擎View的類型WebFormView都是BuildManagerCompiledView的子類。[本文已經同步到《How ASP.NET MVC Works?》中]
目錄
一、BuildManagerCompiledView
二、RazorView
三、實例演示:自定義View模擬RazorView的View呈現機製
為了能夠清楚地說明實現在BuildManagerCompiledView中的View激活與呈現機製,我們列出了BuildManagerCompiledView中與此相關的內部和受保護的成員。
1: public abstract class BuildManagerCompiledView : IView
2: {
3: internal IViewPageActivator ViewPageActivator;
4:
5: protected BuildManagerCompiledView(ControllerContext controllerContext, string viewPath);
6: protected BuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator);
7: internal BuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator, IDependencyResolver dependencyResolver);
8:
9: public void Render(ViewContext viewContext, TextWriter writer);
10: protected abstract void RenderView(ViewContext viewContext, TextWriter writer, object instance);
11:
12: internal IBuildManager BuildManager { get; set; }
13: public string ViewPath { get; protected set; }
14: }
通過《View編譯原理》的介紹我們知道采用Razor引擎的View文件(.cshtml或者.vbhtml)最終都會編譯成一個WebViewPage類型,所以通過RazorView/WebFormView體現的View的呈現機製最終體現在對WebViewPage對象的激活。我們可以利用BuildManager根據View文件的虛擬路徑得到編譯後的類型。從名稱也可以看出來,BuildManagerCompiledView內部就是利用了BuildManager根據指定的View文件虛擬路徑完成對WebViewPage對象激活。
BuildManagerCompiledView的屬性ViewPath表示的就是View文件的虛擬路徑,該屬性在構造函數中被初始化。BuildManagerCompiledView具有三個構造函數,對象本身的構造邏輯體現在內部構造函數上。如上麵的代碼片斷所示,除了將當前ControllerContext和View文件虛擬路徑作為構造函數的參數之外,該構造函數還具有額外兩個參數,其類型分別是IViewPageActivator和IDependencyResolver。
1: public interface IViewPageActivator
2: {
3: object Create(ControllerContext controllerContext, Type type);
4: }
上麵的代碼片斷體現了接口IViewPageActivator的定義。顧名思義,該接口旨在實現對WebViewPage對象的激活,基於類型的對象激活機製實現在Create方法中。BuildManagerCompiledView的構造函數中指定的ViewPageActivator被用於初始化內部字段ViewPageActivator,如果沒有通過構造函數顯式指定ViewPageActivator對象,默認采用的是一個DefaultViewPageActivator對象。
DefaultViewPageActivator是一個具有如下定義的內部類型,我們可以看到它實際上依賴於一個DependencyResolver對象完成針對WebViewPage對象的激活。這個DependencyResolver對象可以通過構造函數進行顯式設置,而默認使用的DependencyResolver對象來源於DependencyResolver類型的靜態屬性Current。
1: internal class DefaultViewPageActivator : IViewPageActivator
2: {
3: private Func<IDependencyResolver> _resolverThunk;
4: public DefaultViewPageActivator() : this(null)
5: {}
6:
7: public DefaultViewPageActivator(IDependencyResolver resolver)
8: {
9: Func<IDependencyResolver> func = null;
10: if (resolver == null)
11: {
12: this._resolverThunk = () => DependencyResolver.Current;
13: }
14: else
15: {
16: if (func == null)
17: {
18: func = () => resolver;
19: }
20: this._resolverThunk = func;
21: }
22: }
23:
24: public object Create(ControllerContext controllerContext, Type type)
25: {
26: return (this._resolverThunk().GetService(type) ?? Activator.CreateInstance(type));
27: }
28: }
如果我們在構造BuildManagerCompiledView的時候沒有指定具體的ViewPageActivator,那麼ASP.NET MVC會根據指定的DependencyResolver來創建默認的DefaultViewPageActivator。如果我們隻是根據ControllerContext和View文件虛擬路徑來構建BuildManagerCompiledView,最終用於激活WebPageView的實際上就是當前的DependencyResolver。換句話說,,接下來我們會演示相關的實例。
BuildManagerCompiledView對View的呈現機製其實很簡單。。BuildManagerCompiledView將利用激活的WebPageView對象呈現View的邏輯定義在抽象方法RenderView中,而Render方法僅僅實現了根據View文件虛擬路徑對WebPageView的激活,具體的實現可以通過如下的代碼片斷來體現。
1: public abstract class BuildManagerCompiledView : IView
2: {
3: //其他成員
4: public void Render(ViewContext viewContext, TextWriter writer)
5: {
6: Type viewType = BuildManager.GetCompiledType(ViewPath);
7: object instance = null;
8: if (null != viewType)
9: {
10: //controllerContext字段表示在構造函數中指定的ControllerContext
11: instance = this.ViewPageActivator.Create(controllerContext, viewType);
12: }
13: this.RenderView(viewContext, writer, instance);
14: }
15: protected abstract void RenderView(ViewContext viewContext, TextWriter writer, object instance);
16: }
表示Razor引擎下的View的類型RazorView直接繼承BuildManagerCompiledView。如下麵的代碼片斷所示,它具有額外的三個隻讀屬性屬性。LayoutPath表示View使用的布局文件的虛擬路徑,而RunViewStartPages和ViewStartFileExtensions屬性與通過“_ViewStart.cshtml”或“_ViewStart.vbhtml”文件定義的開始頁麵有關,前者表示是否需要執行開始頁麵,後者表示開始頁麵文件的擴展名。對於Razor引擎默認創建的RazorView,RunViewStartPages屬性為True(意味著總是會執行開始頁麵)。ViewStartFileExtensions屬性表示的字符串集合包含兩個元素“cshtml”和“vbhtml”。
1: public class RazorView : BuildManagerCompiledView
2: {
3: public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions);
4: public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator);
5:
6: protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance);
7:
8: public string LayoutPath { get; }
9: public bool RunViewStartPages { get; }
10: public IEnumerable<string> ViewStartFileExtensions { get; }
11: }
RazorView通過實現RenderView方法最終完成了對View的呈現。方法傳入參數instance是通過BuildManagerCompiledView激活的View對象,通過上麵的介紹我們知道這是一個空的WebViewPage<TModel>對象(默認情況下是通過默認構造函數創建的)。RazorView在RenderView方法中對其進行初始後調用ExecutePageHierarchy方法將整個頁麵內容呈現出來。RazorView實現RenderView方法的邏輯基本上可以通過如下的代碼片斷來表示。
1: public class RazorView : BuildManagerCompiledView
2: {
3: //其他成員
4: protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
5: {
6: WebViewPage page = instance as WebViewPage;
7: //初始化WebViewPage
8: Initialize(page);
9:
10: //得到表示開啟頁麵的WebPageRenderingBase對象
11: WebPageRenderingBase startPage;
12: if (this.RunViewStartPages)
13: {
14: startPage = StartPage.GetStartPage(page,"_ViewStart",this.ViewStartFileExtensions);
15: }
16: HttpContextBase httpContext = viewContext.HttpContext;
17: page.ExecutePageHierarchy(new WebPageContext(viewContext.HttpContext, null, null), writer, startPage);
18: }
19: }
為了讓讀者了解RazorView實現 View呈現的本質,我們按照其實現原理自定義一個簡單的RazorView類型。我們在一個ASP.NET MVCWeb應用中定義了如下一個表示自定義RazorView的SimpleRazorView類型。SimpleRazorView直接實現了IView接口,在構造函數中初始化的屬性ViewPath表示View文件的虛擬路徑。
1: public class SimpleRazorView: IView
2: {
3: public string ViewPath { get; private set; }
4:
5: public SimpleRazorView(string viewPath)
6: {
7: this.ViewPath = viewPath;
8: }
9:
10: public void Render(ViewContext viewContext, TextWriter writer)
11: {
12: Type viewType = BuildManager.GetCompiledType(this.ViewPath);
13: object instance = Activator.CreateInstance(viewType);
14: WebViewPage page = (WebViewPage)instance as WebViewPage;
15:
16: page.VirtualPath = this.ViewPath;
17: page.ViewContext = viewContext;
18: page.ViewData = viewContext.ViewData;
19: page.InitHelpers();
20:
21: WebPageContext pageContext = new WebPageContext(viewContext.HttpContext, null, null);
22: WebPageRenderingBase startPage = StartPage.GetStartPage(page,"_ViewStart",new string[]{"cshtml","vbhtml"});
23: page.ExecutePageHierarchy(pageContext, writer, startPage);
24: }
25: }
在用於呈現View的Render方法中,我們利用BuildManager根據當前View文件的虛擬路徑得到動態編譯後的類型,然後利用該類型以反射的方式創建一個WebViewPage對象。接下來我們初始化該WebViewPage對象的VirtualPath、VirewContext和ViewData屬性,並調用InitHelpers方法對HtmlHelper、UrlHelper和AjaxHelper進行初始化。
SimpleRazorView總是會執行開始頁麵,所以我們通過調用ViewStartPage的靜態方法GetStartPage根據指定的開始頁麵文件名(_ViewStart)和擴展名列表(cshtml和vbhtml)得到表示開始頁麵的WebPageRenderingBase對象。最後我們創建WebPageContext對象,並將它和表示開始頁麵的WebPageRenderingBase對象作為參數調用WebViewPage的ExecutePageHierarchy方法實現對整個頁麵的呈現。
為了驗證SimpleRazorView能夠正常完成對View內容的呈現,我們定義了如下一個HomeController。在默認的Action方法Index中,我們創建一個Contact對象作為當前ViewData的Model。然後通過指定View文件的虛擬路徑(“~/Views/Home/Index.cshtml”)創建我們自定義的SimpleRazorView對象。最後我們創建ViewContext,並將其作為參數調用SimpleRazorView的Render方法將默認的View呈現出來。
1: public class HomeController : Controller
2: {
3: public void Index()
4: {
5: ViewData.Model = new Contact {
6: Name = "張三",
7: PhoneNo = "123456789",
8: EmailAddress = "zhangsan@gmail.com" };
9: SimpleRazorView view = new SimpleRazorView("~/Views/Home/Index.cshtml");
10: ViewContext viewContext = new ViewContext(ControllerContext, view, ViewData, TempData, Response.Output);
11: view.Render(viewContext, viewContext.Writer);
12: }
13: }
14:
15: public class Contact
16: {
17: [DisplayName("姓名")]
18: public string Name { get; set; }
19:
20: [DisplayName("電話號碼")]
21: public string PhoneNo { get; set; }
22:
23: [DisplayName("電子郵箱地址")]
24: public string EmailAddress { get; set; }
25: }
我們的View很簡單,如下麵的代碼片斷所示,這是一個Model類型為Contact的強類型View,在該View中我們直接調用HtmlHelper<TModel>的擴展方法EditorForModel將作為Model的Contact對象以編輯模式呈現在一個表單之中。
1: @model Contact
2: @{
3: ViewBag.Title = Model.Name;
4: }
5:
6: @using (Html.BeginForm())
7: {
8: @Html.EditorForModel()
9: <input type="submit" value="保存" />
10: }
為了驗證我們自定義的SimpleRazorView對布局文件和_ViewStart頁麵的支持,我們在“~/Views/Shared/”目錄下定義了如下一個名為“_Layout.cshtml”的布局文件。布局文件的設置通過定義在“~/Views/”目錄下具有如下定義的“_ViewStart.cshtml”文件來指定。
1: _Layout.cshtml:
2: <html>
3: <head>
4: <title>@ViewBag.Title </title>
5: </head>
6: <body>
7: <h3>編輯聯係人信息</h3>
8: @RenderBody()
9: </body>
10: </html>
11:
12: _ViewStart.cshtml:
13: @{
14: Layout = "~/Views/Shared/_Layout.cshtml";
15: }
運行我們的程序後直接會在瀏覽器中呈現如下圖所示的效果,可以看出這和我們直接在Action方法Index方法返回一個ViewResult對象沒有本質的區別。
ASP.NET MVC的Razor引擎:View編譯原理
ASP.NET MVC的Razor引擎:RazorView
ASP.NET MVC的Razor引擎:IoC在View激活過程中的應用
ASP.NET MVC的Razor引擎:RazorViewEngine
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 16:33:54