視圖引擎與視圖
多數情況下控制器action方法返回ViewResult對象,MVC內建action調用器ControllerActionInvoker負責調用控制器action方法並調用視圖引擎處理ViewResut,由視圖引擎將ViewResult轉化為ViewEngineResult對象,ViewEngineResult對象內含實現IView接口的視圖對象,最終MVC框架調用視圖對象的Render的方法渲染輸出結果。下面還是以例子來演示這個過程,先來看看相關接口和類的定義與實現:
public interface IViewEngine { ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache); ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache); void ReleaseView(ControllerContext controllerContext, IView view); } public interface IView { void Render(ViewContext viewContext, TextWriter writer); } public class ViewEngineResult { public ViewEngineResult(IEnumerable<string> searchedLocations) { if (searchedLocations == null) { throw new ArgumentNullException("searchedLocations"); } SearchedLocations = searchedLocations; } public ViewEngineResult(IView view, IViewEngine viewEngine) { if (view == null) { throw new ArgumentNullException("view");} if (viewEngine == null) { throw new ArgumentNullException("viewEngine");} View = view; ViewEngine = viewEngine; } public IEnumerable<string> SearchedLocations { get; private set; } public IView View { get; private set; } public IViewEngine ViewEngine { get; private set; } }
IViewEngine的兩個Find方法查找請求的視圖返回ViewEngineResult對象,ViewEngineResult有兩個函數,一個接受IView和IViewEngine作為參數,另一個傳入一系列視圖文件搜索路徑列表作為參數。
先從自定義視圖類開始:
public class DebugDataView : IView { public void Render(ViewContext viewContext, TextWriter writer) { Write(writer, "---Routing Data---"); foreach (string key in viewContext.RouteData.Values.Keys) { Write(writer, "Key: {0}, Value: {1}", key, viewContext.RouteData.Values[key]); } Write(writer, "---View Data---"); foreach (string key in viewContext.ViewData.Keys) { Write(writer, "Key: {0}, Value: {1}", key, viewContext.ViewData[key]); } } private void Write(TextWriter writer, string template, params object[] values) { writer.Write(string.Format(template, values) + "<p/>"); } }
DebugDataView只是簡單的輸出一些路徑映射和視圖數據。接下來是自定義視圖引擎:
public class DebugDataViewEngine : IViewEngine { public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) { if (viewName == "DebugData") { return new ViewEngineResult(new DebugDataView(), this); } else { return new ViewEngineResult(new string[] { "No view (Debug Data View Engine)" }); } } public ViewEngineResult FindPartialView(ControllerContext controllerContext,string partialViewName, bool useCache) { return new ViewEngineResult(new string[] { "No view (Debug Data View Engine)" }); } public void ReleaseView(ControllerContext controllerContext, IView view) { // do nothing } }
DebugDataViewEngine中最主要的是FindView方法,如果當前請求的是DebugData視圖,我們直接創建一個DebugDataView並以它構建ViewEngineResult返回,其他情況下返回一個包含虛假搜索路徑的ViewEngineResult(真實實現的話我們需要搜索模板文件等)。要使用自定義的視圖引擎我們還需要在App_start中注冊:
ViewEngines.Engines.Add(new DebugDataViewEngine());
在一個應用可以注冊多個視圖引擎,action調用器依次調用這些視圖引擎的FindView方法,一旦某一個搜索引擎返回包含IView對象的ViewEngineResult結果調用停止,所以視圖引擎注冊的先后順序是有影響的,可能存在兩個視圖引擎都可以處理同一個視圖名稱。如果我們想自定義的視圖引擎優先處理可以將其插入列表首位:
ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
如果某個action方法返回DebugData視圖,比如:
return View("DebugData");
最后的結果就是調用DebugDataView.RenderData輸出結果。如果我們請求一個未實現的視圖,得到的結果就是:
錯誤顯示一系列視圖模板的搜索路徑,包含DebugDataViewEngine給出的虛假路徑"No view (Debug Data View Engine)"。結果中其他一些路徑來自於默認的Razor和ASPX視圖引擎,你可以調用ViewEngines.Engines.Clear()清除默認視圖引擎后僅注冊自定義的視圖引擎。
簡單總結上面的示例可以說視圖引擎完成從視圖名稱到視圖對象的轉換,而視圖對象則負責具體的輸出響應。
Razor視圖引擎
只有極少數情況下我們需要自定義視圖引擎,MVC已經為我們提供了Razor和ASPX引擎,Razor在MVC3中引入用以替代ASPX引擎,所以推薦使用Razor引擎。Razor引擎處理的是.cshtml視圖文件,一個簡單的Index.cshtml:
@model string[] @{
Layout = null; ViewBag.Title = "Index"; } This is a list of fruit names: @foreach (string name in Model) { <span><b>@name</b></span> }
在啟動應用程序后,Razor引擎將cshtml文件轉換為c#類的定義,我們可以在C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\root下找到這些臨時文件,比如上面的index.cshtml轉成c#的.cs文件可能是這樣的:
namespace ASP { using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Web; using System.Web.Helpers; using System.Web.Security; using System.Web.UI; using System.Web.WebPages; using System.Web.Mvc; using System.Web.Mvc.Ajax; using System.Web.Mvc.Html; using System.Web.Optimization; using System.Web.Routing; public class _Page_Views_Home_Index_cshtml : System.Web.Mvc.WebViewPage<string[]> { public _Page_Views_Home_Index_cshtml() { }
public override void Execute() { ViewBag.Title = "Index"; WriteLiteral("\r\n\r\nThis is a list of fruit names:\r\n\r\n"); foreach (string name in Model) { WriteLiteral(" <span><b>"); Write(name); WriteLiteral("</b></span>\r\n"); } } } }
它從 WebViewPage<T>擴展,T是視圖數據模型類型,上面的例子string[]。理解Razor如何處理cshtml有助於我們后續理解html幫助函數是如何工作的。
當我們請求一個視圖比如Index時,Razor引擎遵循約定規則在下列路徑查找視圖文件:
• ~/Views/Home/Index.cshtml • ~/Views/Home/Index.vbhtml • ~/Views/Shared/Index.cshtml • ~/Views/Shared/Index.vbhtml
實際上Razor並非真正的搜索這些磁盤文件,因為這些模板已經編譯為c#類。RazorViewEngine的下列屬性和模板搜索相關:
屬性 | 默認值 | |
ViewLocationFormats |
搜索視圖、部分視圖和布局文件 | ~/Views/{1}/{0}.cshtml, |
AreaViewLocationFormats |
區域搜索視圖、部分視圖和布局文件 | ~/Areas/{2}/Views/{1}/{0}.cshtml, |
這里{0}表示視圖名稱,{1}表示控制器名稱,{2}標識區域名稱。我們可以子類化RazorViewEngine后修改上述屬性更改Razor的搜索方式,當然必須注冊子類化的視圖引擎到引擎列表。
Razor模板文件
Razor模板文件混合了HTML和C#的語句,以一個例子來具體分析:
@model Razor.Models.Product @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div> @Model.Name </div> </body> </html>
第一行@model Razor.Models.Product 指定了視圖的模型對象類型,后續我們可以使用@Model來引用該對象(注意M大寫)。模型對象類型不是必須的,視圖文件中完全可以沒有這行,帶模型類型的視圖我們稱之為強類型視圖(Strong typed)。
第二行以“@{”開始一個Razor代碼塊,類似C#的代碼塊,最后也要以“}”結尾。
“Layout = null;”表示視圖不使用布局文件,布局文件存放在View\Layouts目錄下,可以為多個視圖文件共享。布局文件名一般以下划線“_”開始,比如_BasicLayout.cshtml,以下划線開頭的不會返回給用戶,這樣可以幫助我們區分哪些是支持文件。布局文件其實也是一個Razor模板文件,比如:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> </head> <body> <h1>Product Information</h1> <div style="padding: 20px; border: solid medium black; font-size: 20pt"> @RenderBody() </div> <h2>Visit <a href="http://apress.com">Apress</a></h2> </body> </html>
最重要的是這里的 @RenderBody()(后面我們知道稱為HTML幫助函數),它的作用是將視圖的渲染結果插入到這里。使用布局文件后視圖可以簡化為:
@model Razor.Models.Product
@{
ViewBag.Title = "Product Name";
Layout = "~/Views/_BasicLayout.cshtml";
}
Product Name: @Model.Name
實際上我們不需要在每個視圖文件中指定Layout,MVC會搜索一個名為 _ViewStart.cshtml的文件,它的內容會自動插入到所有視圖文件中,所以如果我們要為所有視圖文件指定布局文件可以在 _ViewStart.cshtml中定義:
@{
Layout = "~/Views/_BasicLayout.cshtml";
}
Razor語法
我們可以很方便的在視圖中插入視圖模型數據或者ViewData的數據:
@model Razor.Models.Product @{ ViewBag.Title = "DemoExpression"; } <table> <thead> <tr><th>Property</th><th>Value</th></tr> </thead> <tbody> <tr><td>Name</td><td>@Model.Name</td></tr> <tr><td>Price</td><td>@Model.Price</td></tr> <tr><td>Stock Level</td><td>@ViewBag.ProductCount</td></tr> </tbody> </table>
這些值也可以很方便的應用到標記屬性上:
<div data-discount="@ViewBag.ApplyDiscount" data-express="@ViewBag.ExpressShip" data-supplier="@ViewBag.Supplier"> The containing element has data attributes </div> Discount:<input type="checkbox" checked="@ViewBag.ApplyDiscount" /> Express:<input type="checkbox" checked="@ViewBag.ExpressShip" /> Supplier:<input type="checkbox" checked="@ViewBag.Supplier" />
可以使用條件語句:
<td> @switch ((int)ViewBag.ProductCount) { case 0: @: Out of Stock break; case 1: <b>Low Stock (@ViewBag.ProductCount)</b> break; default: @ViewBag.ProductCount break; } </td>
注意“@:Out of Stock ”一行的“@:”,它阻止Razor將后續語句解釋為代碼。上面的switch換成if:
<td> @if (ViewBag.ProductCount == 0) { @:Out of Stock } else if (ViewBag.ProductCount == 1) { <b>Low Stock (@ViewBag.ProductCount)</b> } else { @ViewBag.ProductCount } </td>
枚舉數據:
@model Razor.Models.Product[] @{ ViewBag.Title = "DemoArray"; Layout = "~/Views/_BasicLayout.cshtml"; } @if (Model.Length > 0) { <table> <thead><tr><th>Product</th><th>Price</th></tr></thead> <tbody> @foreach (Razor.Models.Product p in Model) { <tr> <td>@p.Name</td> <td>$@p.Price</td> </tr> } </tbody> </table> } else { <h2>No product data</h2> }
在引用數據類型時我們用了完整的命名空間,可以將命名空間如果c#一樣using引入:
@using Razor.Models @model Product[] @{ ViewBag.Title = "DemoArray"; Layout = "~/Views/_BasicLayout.cshtml"; } @if (Model.Length > 0) { <table> <thead><tr><th>Product</th><th>Price</th></tr></thead> <tbody> @foreach (Productp in Model) { <tr> <td>@p.Name</td> <td>$@p.Price</td> </tr> } </tbody> </table> } else { <h2>No product data</h2> }
添加動態內容到Razor視圖
除了靜態的HTML,我們可以在視圖模板中嵌入動態內容,動態內容在運行時輸出,比如上面的內聯@if、@Model等;還可以嵌入HTML幫助函數,比如布局文件中用到的@RenderBody()。除此之外我們還可以嵌入節(Sections)、分部視圖(Partial views)和子動作(Child actions )。
- 使用節的例子
@model string[] @{ ViewBag.Title = "Index"; } @section Header { <div class="view"> @foreach (string str in new [] {"Home", "List", "Edit"}) { @Html.ActionLink(str, str, null, new { style = "margin: 5px" }) } </div> } @section Body { <div class="view"> This is a list of fruit names: @foreach (string name in Model) { <span><b>@name</b></span> } </div> } @section Footer { <div class="view"> This is the footer </div> }
這里定義了Header、Body、Footer三個節,我們可以在布局文件中引用這些節:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <style type="text/css"> div.layout { background-color: lightgray;} div.view { border: thin solid black; margin: 10px 0;} </style> <title>@ViewBag.Title</title> </head> <body> @RenderSection("Header") <div class="layout"> This is part of the layout </div> @RenderSection("Body") <div class="layout"> This is part of the layout </div> @if (IsSectionDefined("Footer")) { @RenderSection("Footer") } else { <h4>This is the default footer</h4> } @RenderSection("scripts", false) <div class="layout"> This is part of the layout </div> </body> </html>
注意@RenderSection("scripts", false)多了個參數false,其作用是表示scripts節可選的,如果視圖中沒有定義scripts節則不需要輸出。如果這里不加這個false參數Razor會提示節未找到錯誤。
- 使用分部視圖的例子
分部視圖可以在添加視圖窗口中選中“Create as partial view”創建 - MyPartial.cshtml:
<div> This is the message from the partial view. @Html.ActionLink("This is a link to the Index action", "Index") </div>
我們在另一個視圖文件中引用它:
@{ ViewBag.Title = "List"; Layout = null; } <h3>This is the /Views/Common/List.cshtml View</h3> @Html.Partial("MyPartial")
分部視圖也可以是強類型的:
@model IEnumerable<string> <div> This is the message from the partial view. <ul> @foreach (string str in Model) { <li>@str</li> } </ul> </div>
在引用時傳入相應的模型對象:
@{ ViewBag.Title = "List"; Layout = null; } <h3>This is the /Views/Common/List.cshtml View</h3> @Html.Partial("MyStronglyTypedPartial", new [] {"Apple", "Orange", "Pear"})
- 使用子動作的例子
子動作調用的是一個控制的action方法,我們先定義一個這樣一個action方法:
public class HomeController : Controller { ... [ChildActionOnly] public ActionResult Time() { return PartialView(DateTime.Now); } }
注意這里使用了ChildActionOnly標識這個action方法,表示僅用於被調用而不能作為標准Action方法訪問。它對應一個分部視圖:
@model DateTime <p>The time is: @Model.ToShortTimeString()</p>
我們在視圖中引用它:
@{ ViewBag.Title = "List"; Layout = null; } <h3>This is the /Views/Common/List.cshtml View</h3> @Html.Partial("MyStronglyTypedPartial", new [] {"Apple", "Orange", "Pear"}) @Html.Action("Time")
如果要調用的子動作不在同一個控制器,我們還需要指定其控制器:
...
@Html.Action("Time", "MyController")
...
如果子動作有參數,調用時我們也可以指定參數:
...
@Html.Action("Time", new { time = DateTime.Now })
...
以上為對《Apress Pro ASP.NET MVC 4》第四版相關內容的總結,不詳之處參見原版 http://www.apress.com/9781430242369。