《Pro ASP.NET MVC 3 Framework》學習筆記之二十七【視圖1】


在前面很多的章節里面的,最常用的action result是視圖呈現並返回給客戶端的ViewResult類型。本章會專注於視圖的原理,首先展示MVC框架是如何使用視圖引擎處理ViewResults的,包括闡釋如何創建一個視圖引擎。接着介紹使用Razor視圖引擎的一些技術。最后是關於創建和使用部分視圖,子actions,以及Razor片段,這些都是涉及高效MVC開發的本質話題。

創建一個自定義視圖引擎(Creating a Custom View Engine)

MVC框架包含了兩個內置的,功能完善,容易理解的視圖引擎:

a.Razor引擎:MVC3里面引入的一個新的視圖引擎,具有更加簡潔和優雅的語法。
b.ASPX引擎:使用的是webform里面的標簽語法"<%..%>",用於維護兼容舊的MVC程序。

這里創建一個自定義的視圖引擎的目的是為了闡釋請求管道的運行原理以及完善我們對MVC框架運行原理的認識,包含了理解視圖引擎在轉化一個ViewResult到響應客戶端的輸出有多少自主性。

視圖引擎實現了IViewEngine接口,如下所示:

View Code
namespace System.Web.Mvc 
{
public interface IViewEngine
{
ViewEngineResult FindView(ControllerContext controllerContext, string viewName,
string masterName, bool useCache);
ViewEngineResult FindPartialView(ControllerContext controllerContext,
string partialViewName, bool useCache);

void ReleaseView(ControllerContext controllerContext, IView view);
}
}

視圖引擎扮演的角色是將請求轉換成ViewEngineResult對象,前面兩個方法包含的參數分別是:描述請求,處理請求的控制器,視圖及其布局,以及是否允許從它的緩存里面取出前一次的結果。這些方法在ViewResult被處理時調用。最后一個方法當視圖不在需要時被調用。

注:MVC框架對視圖引擎的支持是通過ControllerActionInvoker類實現的,這個類是IActionInvoker接口的實現。如果我們實現的是自己的action調用者或控制器工廠的話,這樣是不會自動訪問視圖引擎的功能。

ViewEngineResult類允許視圖引擎在一個視圖被請求時響應給MVC框架,我們通過選擇兩個構造器中的一個來表達結果。

如果視圖引擎能夠提供一個針對請求的視圖,那么我們可以使用的構造器是:public ViewEngineResult(IView view, IViewEngine viewEngine)
否則,能使用的構造器為:public ViewEngineResult(IEnumerable<string> searchedLocations) ,參數提供能夠找到視圖的位置。
視圖引擎系統最后一塊是IView接口,如下:

View Code
using System.IO; 

namespace System.Web.Mvc
{
public interface IView {
void Render(ViewContext viewContext, TextWriter writer);
}
}

我們傳遞一個IView接口的實現給ViewEngineResult對象的構造器,然后從視圖引擎的方法返回。MVC框架調用Render方法,ViewContext參數包含了關於請求的信息和action方法的輸出。TextWriter參數將輸出寫到客戶端。下面會創建一個視圖引擎,只返回一種視圖,該視圖會呈現包含請求信息的結果以及由action方法產生的視圖數據。從創建視圖引擎的過程里面能讓我們了解視圖引擎運作的方式,這里是沒有涉及到解析視圖模版的。

創建一個自定義的IView接口(Creating a Custom IView)

View Code
namespace Views.Infrastructure
{
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/>");
}
}
}

創建IViewEngine實現(Creating an IViewEngine Implementation)

我要明確視圖引擎的目的是生產一個ViewEngineResult對象,該對象包含一個IView接口或者是一個包含視圖位置的列表。上面創建了IView接口,下面接着創建視圖引擎,如下:

View Code
using System.Web.Mvc;

namespace Views.Infrastructure
{
public class DebugDataViewEngine : IViewEngine
{
public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
return new ViewEngineResult(new string[] { "Debug Data View Engine" });
}

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[] { "Debug Data View Engine" });
}
}

public void ReleaseView(ControllerContext controllerContext, IView view)
{

}
}
}

這里創建的視圖引擎支持DebugData視圖,當有一個針對該視圖的請求時,則返回一個IView接口實現的實例,像這樣:return new ViewEngineResult(new DebugDataView(), this);

如果我們要實現更加正式的視圖引擎,需要利用這個時機尋找模版,並考慮布局和提供緩存設置。這里的例子僅僅需要創建一個DataDebugView類的實例,如果接收到其他的請求,會返回如:return new ViewEngineResult(new string[] { "Debug Data View Engine" }); IViewEngine接口假定視圖引擎具有它需要用來尋找視圖的位置信息,這是非常合理的假設,因為視圖是典型的存儲在項目里的模版文件。這里並不需要去找,我們返回是一個虛擬的位置。這里也沒有支持部分視圖,也沒有實現ReleaseView方法,因為沒有資源需要釋放。

注冊自定義的視圖引擎(Registering a Custom View Engine)

注冊視圖引擎的方法有很多,首選的是在Global的Application_Start方法里面添加 ViewEngines.Engines.Add(new DebugDataViewEngine());
靜態的ViewEngine.Engines集合包含了一套安裝在應用程序的視圖引擎。當一個ViewResult正被處理時,action調用者獲得已經安裝的視圖引擎的集合並輪流調用它們的FindView方法。當action調用者收到一個包含IView接口的ViewEngineResult對象時停止調用FindView方法。這意味着添加到ViewEngines.Engines里面的引擎的順序非常重要,特別是在有多個引擎服務同名的視圖時。

如果想要我們的視圖引擎優先,可以利用集合的插入方法如下:

ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
還有一種方法是使用全局依賴解析器,當應用程序啟動時,依賴解析器會請求任何可用的IViewEngine接口實現。如果使用NinjectDependencyResolver類,我們可以在AddBinds方法里注冊視圖引擎,

如:private void AddBindings()
{
    //綁定
    Bind<IViewEngine>().To<DebugDataViewEngine>();   
}

使用這種方式不能控制視圖引擎被處理的順序,如果我們要關注視圖引擎會相互競爭,最好還是選擇ViewEngines.Engines集合里面的Insert方法來實現。下面測試我們自定義的視圖引擎,創建一個HomeController類如下:

View Code
namespace Views.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["Message"] = "Hello,World";
ViewData["Time"] = DateTime.Now.ToShortTimeString();
return View("DebugData");
}
}
}

這時可以運行程序,結果如下:


出現上面的結果是因為針對我們處理的視圖的FindView被調用了。如果我們改變下Index方法以至於ViewResult請求一個視圖時沒有安裝的引擎能夠響應,如下:
return View("No_Such_View"); 這時我們運行程序,action調用者會去遍歷每一個可用的視圖引擎並調用它們的FindView方法。沒有一個能夠服務這個請求,並且會返回它們尋找過位置集合。如圖:

使用Razor引擎(Working with the Razor Engine)

在前面我們通過實現兩個接口創建了自定義的視圖引擎,當然是非常簡單的,只是為了說明其原理。視圖引擎的復雜性來自於視圖模版系統,包含了代碼分片,支持布局,可編譯來優化性能。這些在我們自定義的視圖引擎里面是沒有的,也不需要,因為Razor引擎考慮了所有的。

理解Razor視圖渲染(Understanding Razor View Rendering)

Razor視圖引擎通過編譯視圖來改善性能,視圖會被轉變為C#類,然后編譯。這也是為什么我們能夠在視圖里面包含C#代碼片段的原因。看下Razor視圖創建的源碼會非常有益的,因為它幫助我們放置了很多Razor功能在上下文Context里面。下面是一個簡單的Razor視圖,獲取了一個字符串數組作為視圖模型對象:

View Code
@model string[] 
@{
ViewBag.Title = "Index";
}

This is a list of fruit names:

@foreach (string name in Model) {
<span><b>@name</b></span>
}

在應用程序啟動時,視圖才會被編譯,也只有在這個時候才能看到由Razor創建的C#類,我們需要啟動應用程序並導航到一個action方法。然后一個actin方法都行,因為初始化請求時會觸發視圖編譯過程。非常方便的是,創建的C#類會作為C#代碼文件寫入磁盤然后編譯,這意味着我們能夠查看到C#具體的代碼。如果是Windows7系統,查看路徑為:c:\Users\yourLoginName\AppData\Local\Temp\Temporary ASP.NET Files。下面是一個示例文件:

View Code
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.Routing;

public class _Page_Views_Home_Index_cshtml : System.Web.Mvc.WebViewPage<string[]> {

public _Page_Views_Home_Index_cshtml() {
}

public override void Execute() {

WriteLiteral("\r\n");
ViewBag.Title = "Index";

WriteLiteral("\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表示model的類型,這也是能夠處理強類型視圖的原因。也可以發現視圖文件的路徑是怎樣被編碼為類名的。這也是Razor映射視圖的請求到已編譯的類的實例的原理。在Execute方法里,我們能夠看到視圖里面語句和元素是怎樣被處理的。HTML元素被WirteLiteral方法處理,這個方法可以將參數的內容寫到結果里面。這剛好跟Write方法相反,Write方法對C#變量使用並對字符串的值編碼讓它們能夠在HTML頁面安全使用。

WriteLiteral和Write方法都是將內容寫到一個TextWriter對象里面。這個對象同樣被傳遞給了IView.Render方法。一個已經編譯的Razor視圖的目的是為了創建靜態和動態的內容並通過TextWriter發送給客戶端。

添加依賴注入到Razor視圖(Adding Dependency Injection to Razor Views)

MVC請求處理管道的每一個部分都支持DI(Dependency Injection),Razor也不例外。然而所不同的是,這里擴展了類和視圖之間的邊界。下面是一個例子來說明:
創建一個接口ICalculator如下:

View Code
namespace Views.Models
{
public interface ICalculator
{
int Sum(int x, int y);
}
}

如果我們想要一個視圖能夠使用ICalculator並且具有注入的實現,那么我們需要創建一個派生自WebViewPage的抽象類,如下:

View Code
using System.Web.Mvc;
using Ninject;

namespace Views.Models.ViewClasses
{
public abstract class CalculatorView : WebViewPage
{
[Inject]
public ICalculator Calculator { get; set; }
}
}

支持DI最簡便的方式就是使用屬性注入(property injection),即:我們的DI容器注入依賴到一個屬性里面,而不是最開始時使用的構造器注入。這里使用的了Inject特性,為了在視圖里面利用它,我們使用Razor繼承元素,如下所示:

View Code
//Controller代碼
namespace Views.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
string[] fruitNames = { "Apple", "Cherry", "Pear", "Mango", "<script>do something</script>" };
return View(fruitNames);
}

public ViewResult Calculate()
{
ViewBag.X = 10;
ViewBag.Y = 20;
return View();
}
}
}

//Calculate視圖代碼,也就是這里要用@inherits元素
@inherits Views.Models.ViewClasses.CalculatorView
@{
ViewBag.Title = "Calculate";
}
<h2>
Calculate</h2>
The calculator result for @ViewBag.X and @ViewBag.Y is @Calculator.Sum(ViewBag.X, ViewBag.Y)

//Index視圖代碼
@model string[]
@{
ViewBag.Title = "Index";
}
@helper GetNow()
{
<blockquote>@DateTime.Now</blockquote>
}
this is a list of fruit names:
@foreach (string name in Model)
{
<span><b>@name</b></span>
}

我們能夠使用@inherits指定基類,這樣就能夠訪問Calculator屬性並接收ICalculator接口的實現,所有這一切都沒有創建view和ICalculator接口之間的依賴。使用了@inherits元素的效果就是改變了實體編譯創建的類的基類,如:public class _Page_Views_Home_Calculate_cshtml : Views.Models.ViewClasses.CalculatorView {...}
由於創建的類是從我們的抽象類派生的,所以能夠訪問我們自己定義的Calculator屬性。剩下的就是使用依賴解析器注冊ICalculator接口實現,以至於Ninject將被用來創建視圖類並且有機會實施屬性注入。如下:

View Code
//SimpleCalculator類,實現ICalculator接口
namespace Views.Infrastructure
{
public class SimpleCalculator : ICalculator
{
public int Sum(int x, int y)
{
return x + y;
}
}
}

//NinjectDependencyResolver,實施屬性依賴
namespace Views.Infrastructure
{
public class NinjectDependencyResolver : IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver()
{
kernel = new StandardKernel();
AddBindings();
}

public object GetService(Type serviceType)
{
return kernel.TryGet(serviceType);
}

public IEnumerable<object> GetServices(Type serviceType)
{
return kernel.GetAll(serviceType);
}

public IBindingToSyntax<T> Bind<T>()
{
return kernel.Bind<T>();
}

public IKernel Kernel
{
get { return kernel; }
}

private void AddBindings()
{
Bind<IViewEngine>().To<DebugDataViewEngine>();
Bind<ICalculator>().To<SimpleCalculator>();
}
}
}

//在galobal里面設置解析器
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
DependencyResolver.SetResolver(new NinjectDependencyResolver());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

這時可以運行程序得到如下結果:


配置視圖搜索位置(Configuring the View Search Locations)

Razor視圖引擎遵循早期MVC版本里面尋找視圖的建立的約定,如果請求與Home控制器關聯的Index視圖,Razor按照下面的列表尋找:
•  ~/Views/Home/Index.cshtml
•  ~/Views/Home/Index.vbhtml
•  ~/Views/Shared/Index.cshtml
•  ~/Views/Shared/Index.vbhtml

Razor不是真的在磁盤上尋找視圖文件,因為視圖這個時候已經編譯成了C#類。Razor實際上是尋找的這些代表視圖的編譯后的C#類,.cshtml文件是包含C#語句的模版。我們可以通過創建RazorViewEngine的子類來改變Razor搜尋的視圖文件。它是建立在一序列的基類之上的,這些基類包含了決定哪一個視圖文件被搜尋的一套屬性。如下:


占位符{0},{1},{2}分別表示:視圖名,控制器名,區域名。
為了改變搜尋的位置,我們創建一個從RazorViewEngine派生的類並改變屬性的值,下面是一個示例:

View Code
using System.Web.Mvc;

namespace Views.Infrastructure
{
public class CustomRazorViewEngine : RazorViewEngine
{
public CustomRazorViewEngine()
{
ViewLocationFormats = new string[] {
"~/Views/{1}/{0}.cshtml","~/Views/Common/{0}.cshtml"
};
}
}
}

代碼里面我們為ViewLocationFormats設置了新的值,新的數組包含了全部的.cshtml文件。另外,還改變了公共視圖的尋找位置,不再是Views/Shared而是Views/Common。接着需要在Application_Start方法里面注冊:

View Code
        protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomRazorViewEngine());
//ViewEngines.Engines.Insert(0, new DebugDataViewEngine());
//DependencyResolver.SetResolver(new NinjectDependencyResolver());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

記住一點:action調用者輪流去每一個視圖引擎尋找視圖,等到我們添加視圖到集合里面時,它已經包含了標准的Razor視圖引擎。為了避免跟其他的視圖引擎競爭,我們調用Clear方法移除任何可能已經注冊的視圖引擎並調用Add方法注冊我們自己實現的視圖引擎。

添加動態的內容到Razor視圖(Adding Dynamic Content to a Razor View)

視圖的全部意圖就是允許我們將領域模型作為用戶接口呈現出來。為了達到這個目的,我們需要添加動態的內容,動態內容是在運行時創建的,並且每一次請求創建的內容都可以不同。這個跟靜態內容是相反的,像HTML這些靜態的內容在每次請求時都是一樣的。添加動態內容的方式有四種:內嵌代碼(Inline code),HTML輔助方法(HTML helper methods),部分視圖(Partial views),子action(Child actions).下面分別對每一種方式進行介紹:

使用內嵌代碼(Using Inline Code)

創建動態內容最簡便的方式就是使用內嵌代碼——一行或多行以@符合開始的C#代碼。這是Razor視圖引擎的核心。
內嵌代碼與分解關注點:學過ASP.NET WebForms的人可能非常想知道為什么會對內嵌代碼如此着迷,畢竟在WebForms里面的約定是盡可能的將代碼放在代碼隱藏文件里面。這很容易混淆,特別是當我們認為代碼隱藏文件常常是對分解關注點的維護時。這個分歧的出現是因為MVC和WebForms對哪一個關注點應該被分解以及分界線應該在哪有不同的概念。WebForms將陳述性的HTML標記和程序邏輯分開,ASPX包含HTML標簽,代碼隱藏文件包含邏輯。與之相對的是MVC,它是將展示邏輯和應用邏輯分開,控制器和領域模型分別負責應用邏輯和領域邏輯,視圖包含展示邏輯。所以內嵌代碼更好的適應了MVC框架。Razor句法讓我們很容易的創建可維護,可擴展的視圖。

內嵌代碼功能的靈活性很容易模糊應用程序的邊界,並且讓分解關注點的思想瓦解。作為一種紀律約束:必須通過某種方式來維護分解關注點的有效性,那就是與MVC設計模式保持一致。當我們着手使用MVC框架時,建議將關注點放在理解功能上。當我們有了更多的經驗時,會自然形成將視圖和MVC組件的職責分開的感覺。

好了,今天的筆記就到這里,關於這章的筆記在下一篇接着做完。
晚安!


免責聲明!

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



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