
目錄
我 從事專業(yè)開發(fā)迄今為止已有 15 年,在此之前,我利用業(yè)余時間從事開發(fā)至少也有 10 年了。與我這一代的大多數(shù)人一樣,我是從 8 位計(jì)算機(jī)起步,然后轉(zhuǎn)用 PC 平臺的。隨著計(jì)算機(jī)的復(fù)雜性日益增加,我編寫的應(yīng)用程序涵蓋了從小型游戲到個人數(shù)據(jù)管理再到控制外部硬件的各項(xiàng)功能。
不過,在我職業(yè)生涯的前半段,我編寫的所有軟件都有一個共同點(diǎn):即,都是運(yùn)行在用戶桌面上的本地應(yīng)用程序。我最早是在 90 年代初期聽說萬維網(wǎng)這件新生事物。那時我發(fā)現(xiàn),通過構(gòu)建 Web 應(yīng)用程序,可以讓我輸入我的考勤卡信息而不必再費(fèi)時費(fèi)力從工作場所趕回辦公室。
一言以蔽之,我感覺很是困惑。我當(dāng)時滿腦子是面向桌面的理念,很難接納這種無狀態(tài)的 Web。要添加很多讓人頭疼的調(diào)試、我沒有 UNIX 服務(wù)器的超級用戶訪問權(quán)限,再加上這個奇怪的角括號,這些因素使年輕時的我止步不前,又重返桌面開發(fā)渡過了幾年時光。
我遠(yuǎn)離了 Web 開發(fā)領(lǐng)域,雖然這領(lǐng)域顯然很重要,但我并沒有真正理解其編程模型。然后,Microsoft® .NET Framework 和 ASP.NET 發(fā)行了。盡管它與桌面應(yīng)用程序編程有許多相似之處,但終于有了可以讓我從事 Web 應(yīng)用程序編程的框架。我可以構(gòu)建窗口(頁面),將控件與事件掛鉤,而設(shè)計(jì)器使我不必處理那些討厭的角括號。最妙的是,ASP.NET 會通過查看狀態(tài)自動為我處理 Web 的無狀態(tài)性質(zhì)!我又重新找回了程序員的快樂 ... 至少在一段時間內(nèi)是如此。
隨著經(jīng)驗(yàn)的增加,我的設(shè)計(jì)內(nèi)容也隨之豐富。我早已掌握了幾種最佳實(shí)踐,并將其應(yīng)用到桌面應(yīng)用程序編程。其中的兩種就是:
- 分離關(guān)注點(diǎn):不要將 UI 邏輯與基礎(chǔ)行為混合在一起。
- 自動單元測試:編寫自動測試以驗(yàn)證您的代碼是否按預(yù)期執(zhí)行。
這些是適用于任何技術(shù)的基本原則。分離關(guān)注點(diǎn)是一項(xiàng)可幫助您處理復(fù)雜問題的基本原則。在同一個對象內(nèi)混合多種責(zé)任(如計(jì)算剩余的工時、設(shè)置數(shù)據(jù)格式并繪圖)會給維護(hù)帶來很大的負(fù)擔(dān)。而自動測試對于獲得生產(chǎn)質(zhì)量的代碼同時仍保持條理性至關(guān)重要,尤其是當(dāng)您更新現(xiàn)有項(xiàng)目時更是如此。
ASP.NET Web 窗體使入門變得非常簡單,但另一方面,要將我的設(shè)計(jì)理念應(yīng)用到 Web 應(yīng)用程序卻并非易事。Web 窗體堅(jiān)持以 UI 為側(cè)重點(diǎn);其基本單位為頁面。首先設(shè)計(jì) UI 并拖曳控件。只需將應(yīng)用程序邏輯融入頁面的事件處理程序(與為 Windows® 應(yīng)用程序啟用的 Visual Basic® 非常相似)就萬事大吉,這一點(diǎn)非常吸引人。
但進(jìn)一步的頁面單元測試常常有很大困難。您必須先啟動所有 ASP.NET,然后才能在“頁面”對象的生命周期內(nèi)運(yùn)行該對象。盡管可以通過發(fā)送 HTTP請求到服務(wù)器或自動化瀏覽器來測試 Web 應(yīng)用程序,但這類測試非常脆弱(更換一個控制 ID 測試就會中斷)、難以設(shè)置(您必須以完全相同的方式在每位開發(fā)人員的計(jì)算機(jī)上設(shè)置該服務(wù)器)并且運(yùn)行緩慢。
當(dāng)我開始構(gòu)建更復(fù)雜的 Web 應(yīng)用程序時,Web 窗體提供的抽象概念(如控件、視圖狀態(tài)和頁面生命周期)就開始添亂而不是幫忙了。我需要花越來越多的時間來配置數(shù)據(jù)綁定(并編寫大量的事件處理程序?qū)ζ溥M(jìn)行正確配置)。我不得不想辦法縮減視圖狀態(tài)的大小以便更快加載我的頁面。Web 窗體要求每個 URL 均存在物理文件,這對于動態(tài)站點(diǎn)(例如 wiki)非常困難。而成功編寫一個自定義的 WebControl 是一個非常復(fù)雜的過程,需要全面了解頁面生命周期和 Visual Studio® 設(shè)計(jì)器。
自從在 Microsoft 工作開始,我就一直與其他人分享關(guān)于各種 .NET 難題的體驗(yàn)并希望可以解決一些難題。最近,作為開發(fā)人員參加有關(guān)模式與實(shí)踐的 Web 客戶端軟件工廠項(xiàng)目 (
codeplex.com/websf) 時,我遇到了一個這樣的機(jī)會。特別是,模式與實(shí)踐交付的內(nèi)容之一就是自動單元測試。在 Web 客戶端軟件工廠中,我們建議使用 Model View Presenter (MVP) 模式構(gòu)建可測試的 Web 窗體。
簡而言之,MVP 并非將您的邏輯放入頁面中,而是讓您構(gòu)建自己的頁面,頁面 (View) 只需調(diào)用單獨(dú)的對象,即 Presenter。Presenter 對象隨即執(zhí)行響應(yīng)視圖上活動必需的任何邏輯,通常通過使用其它對象 (Model) 訪問數(shù)據(jù)庫、執(zhí)行業(yè)務(wù)邏輯等。一旦這些步驟完成后,Presenter 會更新視圖。這種方法提供了可測試性,因?yàn)楸硎酒鲝?ASP.NET 管道中隔離出來;它與視圖通過界面進(jìn)行通信并可脫離頁面獨(dú)立進(jìn)行測試。
MVP 的這種功能實(shí)現(xiàn)有點(diǎn)笨;您需要單獨(dú)的視圖界面,并且您必須在源代碼文件中編寫許多事件轉(zhuǎn)發(fā)函數(shù)。但如果您想要在 Web 窗體應(yīng)用程序中得到可測試的 UI,這差不多是最佳途徑。任何改進(jìn)均需要在基礎(chǔ)平臺中做出更改。
模型視圖控制器模式
幸運(yùn)的是,ASP.NET 團(tuán)隊(duì)聽取了象我這樣的開發(fā)人員的意見,并且已經(jīng)著手開發(fā)一種新的 Web 應(yīng)用程序框架,該框架與您所熟知并喜愛的 Web 窗體處于同一層級,但采用一組完全不同的設(shè)計(jì)目標(biāo):
- 使用 HTTP 和 HTML—不隱藏。
- 可測試性貫穿整個框架之內(nèi)。
- 幾乎在每個點(diǎn)均可擴(kuò)展。
- 對輸出進(jìn)行總體控制。
由于此新框架基于模型視圖控制器 (MVC) 模式,因此其名稱為 ASP.NET MVC。MVC 模式最初在 70 年代發(fā)明,是 Smalltalk 技術(shù)的一部分。正如我將在本文中所展示的,它實(shí)際上非常適合 Web 的性質(zhì)。MVC 將您的 UI 分為三種不同的對象:用于接收和處理輸入的控制器;包含您域邏輯的模型;以及用于生成輸出的視圖。在 Web 環(huán)境中,輸入為 HTTP 請求,而請求流程與圖 1 類似。
Figure 1 MVC 模式請求流程 (單擊該圖像獲得較大視圖)
這實(shí)際上與 Web 窗體中的過程完全不同。在 Web 窗體模型中,輸入進(jìn)入頁面(視圖),然后視圖負(fù)責(zé)處理輸入并生成輸出。而 MVC 中這些責(zé)任是分開的。
因此,您可能立即會產(chǎn)生以下一種想法:“嘿,這太好了。我應(yīng)該如何使用它?”或“為什么我要編寫這些對象,以前只需要編寫一個對象?”這兩個問題都問得很好,最好通過示例來進(jìn)行解釋。因此,我將使用 MVC Framework 編寫一個小型 Web 應(yīng)用程序以說明其優(yōu)點(diǎn)。
創(chuàng)建控制器
要繼續(xù)進(jìn)行,您將需要安裝 Visual Studio 2008 并獲得 MVC Framework 的副本。在撰寫本文時,ASP.NET 擴(kuò)展的 2007 年 12 月社區(qū)技術(shù)預(yù)覽 (CTP) 中已提供了這些內(nèi)容 (asp.net/downloads/3.5-extensions)。您可能想要獲取擴(kuò)展 CTP 和 MVC 工具包,其中包括一些非常有用的幫助程序?qū)ο?。一旦下載并安裝 CTP 后,您將在“新建項(xiàng)目”對話框中獲得名為“ASP.NET MVC Web 應(yīng)用程序”的新項(xiàng)目類型。
選擇“MVC Web 應(yīng)用程序”項(xiàng)目后,會為您提供一個與常用網(wǎng)站或應(yīng)用程序稍有不同的解決方案。該解決方案模板會創(chuàng)建一個帶有一些新目錄的 Web 應(yīng)用程序(如圖 2 中所示)。特別是 Controllers 目錄包含各種控制器類,而 Views 目錄(及其所有子目錄)包含了各種視圖。
Figure 2 MVC 項(xiàng)目結(jié)構(gòu)
我將會編寫一個非常簡單的控制器,返回 URL 中傳遞的名稱。右鍵單擊 Controllers 文件夾并選擇“添加項(xiàng)目”以顯示常用的“添加項(xiàng)目”對話框以及一些新增加的內(nèi)容,包括 MVC 控制器類和幾個 MVC 視圖組件。在此例中,我將添加一個非常富有想象力、名為 HelloController 的類:
using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Controllers
{
public class HelloController : Controller
{
[ControllerAction]
public void Index()
{
...
}
}
}
控制器類比頁面簡單得多。實(shí)際上,唯一真正必需做的就是從 System.Web.Mvc.Controller 中衍生并將 [ControllerAction] 屬性置于您的操作方法中。操作是調(diào)用以響應(yīng)特定 URL 請求的一種方法。操作負(fù)責(zé)執(zhí)行所需的一切處理,然后呈現(xiàn)一個視圖。我將通過編寫一個將名稱傳遞到視圖的簡單操作著手,如下所示:
[ControllerAction]
public void HiThere(string id)
{
ViewData["Name"] = id;
RenderView("HiThere");
}
操作方法會通過 ID 參數(shù)從 URL 接收該名稱(稍后會介紹方法),將其存儲在 ViewData 集合中,然后呈現(xiàn)名為 HiThere 的視圖。
在討論如何調(diào)用此方法,或該視圖的顯示內(nèi)容之前,我希望說一說可測試性。還記得我之前關(guān)于測試 Web 窗體頁面類有多難的評論嗎?控制器的測試簡單得多。實(shí)際上,控制器可以直接實(shí)例化,而調(diào)用操作方法無需任何附加的基礎(chǔ)結(jié)構(gòu)。您不需要 HTTP 上下文,也不需要服務(wù)器,只要測試工具即可。作為示例,我在圖 3 中為此類包括了 Visual Studio Team System (VSTS) 測試單元。

Figure 3 Controller Unit Test
namespace HelloFromMVC.Tests
{
[TestClass]
public class HelloControllerFixture
{
[TestMethod]
public void HiThereShouldRenderCorrectView()
{
TestableHelloController controller = new
TestableHelloController();
controller.HiThere("Chris");
Assert.AreEqual("Chris", controller.Name);
Assert.AreEqual("HiThere", controller.ViewName);
}
}
class TestableHelloController : HelloController
{
public string Name;
public string ViewName;
protected override void RenderView(
string viewName, string master, object data)
{
this.ViewName = viewName;
this.Name = (string)ViewData["Name"];
}
}
}
下面將進(jìn)行幾項(xiàng)操作。實(shí)際的測試相當(dāng)簡單:實(shí)例化該控制器,使用預(yù)期的數(shù)據(jù)調(diào)用該方法,然后檢查呈現(xiàn)的視圖是否正確。我通過創(chuàng)建測試專用的子類覆蓋 RenderView 方法進(jìn)行檢查。這可以縮短實(shí)際創(chuàng)建 HTML 的時間。我只關(guān)心是否將正確的數(shù)據(jù)發(fā)送到視圖以及是否呈現(xiàn)了正確的視圖。我不關(guān)心此測試視圖本身的底層詳細(xì)信息。
創(chuàng)建視圖
當(dāng)然,最終我必須生成一些 HTML,因此,讓我們創(chuàng)建該 HiThere 視圖。要進(jìn)行此操作,首先,我將在解決方案中的 Views 文件夾下創(chuàng)建名為 Hello 的新文件夾。默認(rèn)情況下,控制器將在 Views\<控制器前綴> 文件夾(控制器前綴為控制器類的名稱去掉 "Controller" 字樣)中查找視圖。因此,對于 HelloController 呈現(xiàn)的視圖,它會在 Views\Hello 中查找。解決方案的查找結(jié)果如圖 4 所示。
Figure 4 將視圖添加到項(xiàng)目中 (單擊該圖像獲得較大視圖)
視圖的 HTML 如下所示:
<html >
<head runat="server">
<title>Hi There!</title>
</head>
<body>
<div>
<h1>Hello, <%= ViewData["Name"] %></h1>
</div>
</body>
</html>
應(yīng)注意以下幾件事。沒有 runat="server" 標(biāo)記。沒有 form 標(biāo)記。沒有控件聲明。實(shí)際上,這看起來更象傳統(tǒng)的 ASP 而不是 ASP.NET。請注意,MVC 視圖僅負(fù)責(zé)生成輸出,因此其不需要任何 Web 窗體頁面所需的事件處理或復(fù)雜控件。
MVC Framework 借用了 .aspx 文件格式作為一種有用的文本模板語言。如果需要,甚至可以使用源代碼,但默認(rèn)情況下,源代碼文件如下所示:
using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Views.Hello
{
public partial class HiThere : ViewPage
{
}
}
沒有頁面初始化或加載方法,沒有事件處理程序,除了基類聲明以外沒有任何內(nèi)容,基類聲明為 ViewPage 而不是 Page。這就是 MVC 視圖所需的一切。運(yùn)行該應(yīng)用程序,導(dǎo)航至 http://localhost:<端口>/Hello/HiThere/Chris,您將看到如圖 5 所示的內(nèi)容。
Figure 5 成功的 MVC 視圖 (單擊該圖像獲得較大視圖)
如果您看到的并非如圖 5 所示,而是難以理解的意外情況,請不要驚慌。如果您將 HiThere.aspx 文件設(shè)置為 Visual Studio 中的活動文檔,則當(dāng)按 F5 后,Visual Studio 將嘗試直接訪問 .aspx 文件。由于 MVC 視圖要求控制器在顯示前運(yùn)行,因此嘗試直接導(dǎo)航至該頁面將不起作用。只需將該 URL 編輯為與圖 5 中所示的內(nèi)容相匹配,即可正常工作。
MVC Framework 如何知道調(diào)用我的操作方法?該 URL 甚至沒有文件擴(kuò)展名。答案是 URL 路由。如果您仔細(xì)查看 global.asax.cs 文件,則會看到如圖 6 所示的代碼段。全局 RouteTable 會存儲 Route 對象的集合。每個 Route 說明一個 URL 窗體以及對其進(jìn)行何種操作。默認(rèn)情況下,會向該表中添加兩個路由。第一個是該方法的內(nèi)容。它說明每個 URL 在服務(wù)器名后均由三部分組成,第一部分應(yīng)為控制器名,第二部分為操作名稱,而第三部分為 ID 參數(shù)。

Figure 6 Route Table
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// Change Url= to Url="[controller].mvc/[action]/[id]"
// to enable automatic support on IIS6
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "Default.aspx",
Defaults = new {
controller = "Home",
action = "Index",
id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
}
}
Url = "[controller]/[action]/[id]"
此默認(rèn)路由是能讓我的 HiThere 方法得以調(diào)用的路由。請記住此 URL:http://localhost/Hello/HiThere/Chris?此路由將 Hello 與控制器、HiThere 與操作以及 Chris 與 ID 一一對應(yīng)。MVC Framework 隨即創(chuàng)建 HelloController 實(shí)例,調(diào)用 HiThere 方法,然后將 Chris 作為 ID 參數(shù)的值傳遞。
此默認(rèn)路由為您提供了許多功能,但您也可以添加自己的路由。例如,我想要一個真正友好的站點(diǎn),好友們只需輸入他們的姓名即可獲得個性化的問候。如果我在路由表的頂部添加以下路由
RouteTable.Routes.Add(new Route
{
Url = "[id]",
Defaults = new {
controller = "Hello",
action = "HiThere" },
RouteHandler = typeof(MvcRouteHandler)
});
隨后,我只需訪問 ,我的操作仍處于調(diào)用狀態(tài),而我將會看到熟悉的友好問候。
系統(tǒng)如何知道調(diào)用哪個控制器和操作?答案是 Defaults 參數(shù)。它利用新的 C# 3.0 匿名類型語法來創(chuàng)建一個偽詞典。Route 上的 Defaults 對象可包含任意附加的信息,對于 MVC,它還可以包含一些眾所周知的條目:即控制器和操作。如果 URL 中沒有指定控制器或操作,則其將使用 Defaults 中的名稱。這就是為什么即使我在 URL 中忽略它們,但仍可以將我的請求映射到正確的控制器和操作。
還有一件事需要注意:還記得我說過“添加到表格的頂部”嗎?如果您將其置于底部,將會出現(xiàn)錯誤。路由根據(jù)先到先得的原則進(jìn)行工作。當(dāng)處理 URL 時,路由系統(tǒng)會自上至下瀏覽表格,并且使用第一個匹配的路由。在本例中,默認(rèn)路由 "[controller]/[action]/[id]" 匹配,因?yàn)樗鼈兪遣僮骱?ID 的默認(rèn)值。這樣,它會繼續(xù)查找 ChrisController,但我沒有控制器,因此會出現(xiàn)錯誤。
稍大的示例
現(xiàn)在,我已經(jīng)說明了 MVC Framework 的基礎(chǔ)知識,將為您展示一個更大的示例,實(shí)現(xiàn)比僅顯示字符串更多的功能。wiki 是一種可以在瀏覽器中進(jìn)行編輯的網(wǎng)站??梢暂p松地添加或編輯頁面。我使用 MVC Framework 編寫了一個小型的示例 wiki。“編輯此頁面”屏幕如圖 7 所示。
Figure 7 編輯主頁 (單擊該圖像獲得較大視圖)
您可以檢查本文的代碼下載以查看如何實(shí)現(xiàn)底層 wiki 邏輯?,F(xiàn)在我想重點(diǎn)說明 MVC Framework 如何使 Web 上的 wiki 獲取變得簡單。讓我們先設(shè)計(jì) URL 結(jié)構(gòu)。我想要以下各項(xiàng):
- /[pagename] 顯示該名稱的頁面。
- /[pagename]?version=n 顯示頁面的請求版本,其中 0 = 當(dāng)前版本,1 = 以前的版本,以此類推。
- /Edit/[pagename] 打開該頁的編輯屏幕。
- /CreateNewVersion/[pagename] 是為提交編輯而傳入的 URL。
讓我們從 wiki 頁面的基本顯示開始。我為它創(chuàng)建了一個名為 WikiPageController 的新類。接下來,我會添加一個名為 ShowPage 的操作。啟動的 WikiPageController 如圖 8 所示。ShowPage 方法相當(dāng)簡單。WikiSpace 和 WikiPage 類分別表示一組 wiki 頁面和特定的頁面(及其修訂)。此操作只需加載模型并調(diào)用 RenderView。但此處的 "new WikiPageViewData" 行是什么意思?

Figure 8 WikiPageController Implementation of ShowPage
public class WikiPageController : Controller
{
ISpaceRepository repository;
public ISpaceRepository Repository
{
get {
if (repository == null)
{
repository = new FileBasedSpaceRepository(
Request.MapPath("~/WikiPages"));
}
return repository;
}
set { repository = value; }
}
[ControllerAction]
public void ShowPage(string pageName, int? version)
{
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
RenderView("showpage",
new WikiPageViewData
{
Name = pageName,
Page = page,
Version = version ?? 0
});
}
}
我前面的示例說明了一種將數(shù)據(jù)從控制器傳遞到視圖的方法:即 ViewData 詞典。詞典非常方便,但也很危險。它們幾乎包含一切內(nèi)容,您不能獲取內(nèi)容的任何 IntelliSense®,并且由于 ViewData 詞典屬于 Dictionary<string, object> 類型,它將消耗內(nèi)容,您必須計(jì)算所有一切。
當(dāng)您了解在視圖中將需要什么數(shù)據(jù)后,就可以傳遞強(qiáng)類型化的 ViewData 對象。在我的示例中,我創(chuàng)建了一個簡單的對象 (WikiPageViewData),如圖 9 中所示。此對象將 wiki 頁面信息帶到視圖,同時還攜帶了一些實(shí)用工具方法,執(zhí)行獲取 wiki 標(biāo)記的 HTML 版本這類任務(wù)。

Figure 9 WikiPageViewData Object
public class WikiPageViewData {
public string Name { get; set; }
public WikiPage Page { get; set; }
public int Version { get; set; }
public WikiPageViewData() {
Version = 0;
}
public string NewVersionUrl {
get {
return string.Format("/CreateNewVersion/{0}", Name);
}
}
public string Body {
get { return Page.Versions[Version].Body; }
}
public string HtmlBody {
get { return Page.Versions[Version].BodyAsHtml(); }
}
public string Creator {
get { return Page.Versions[Version].Creator; }
}
public string Tags {
get { return string.Join(",", Page.Versions[Version].Tags); }
}
}
現(xiàn)在,我已經(jīng)定義了視圖數(shù)據(jù),那么,我如何使用它呢?在 ShowPage.aspx.cs 中,您將看到以下內(nèi)容:
namespace MiniWiki.Views.WikiPage {
public partial class ShowPage : ViewPage<WikiPageViewData>
{
}
}
請注意,我將基類類型定義為 ViewPage<WikiPageViewData>。這意味著頁面的 ViewData 屬性為 WikiPageViewData 類型,而不是象以前示例中的“Dictionary”。
.aspx 文件中的實(shí)際標(biāo)記非常簡單:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs"
Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content
ID="Content1"
ContentPlaceHolderID="MainContentPlaceHolder"
runat="server">
<h1><%= ViewData.Name %></h1>
<div id="content" class="wikiContent">
<%= ViewData.HtmlBody %>
</div>
</asp:Content>
請注意,當(dāng)引用 ViewData 時,我沒有使用索引操作符 []。由于我現(xiàn)在有強(qiáng)類型化的 ViewData,我可以直接訪問該屬性。不需要進(jìn)行任何計(jì)算,而 Visual Studio 會提供 IntelliSense。
目光敏銳的讀者將會注意到此文件中的 <asp:Content> 標(biāo)記。沒錯,“母版頁”確實(shí)可以與 MVC 視圖配合使用。并且“母版頁”還可以成為視圖。讓我們看看“母版頁”的源代碼:
namespace MiniWiki.Views.Layouts
{
public partial class Site :
System.Web.Mvc.ViewMasterPage<WikiPageViewData>
{
}
}
相關(guān)標(biāo)記如圖 10 中所示?,F(xiàn)在,“母版頁”將獲得與視圖完全相同的 ViewData 對象。我已經(jīng)將“母版頁”的基類聲明為 ViewMasterPage<WikiPageViewData>,因此,我擁有了正確類型的 ViewData。我會在那里設(shè)置各種 DIV 標(biāo)記以對頁面進(jìn)行布局,填寫版本列表,然后以常用內(nèi)容占位符收尾。

Figure 10 Site.Master
<%@ Master Language="C#"
AutoEventWireup="true"
CodeBehind="Site.master.cs"
Inherits="MiniWiki.Views.Layouts.Site" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<%@ Import Namespace="MiniWiki.DomainModel" %>
<%@ Import Namespace="System.Web.Mvc" %>
<html >
<head runat="server">
<title><%= ViewData.Name %></title>
<link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="inner">
<div id="top">
<div id="header">
<h1><%= ViewData.Name %></h1>
</div>
<div id="menu">
<ul>
<li><a href="http://Home">Home</a></li>
<li>
<%= Html.ActionLink("Edit this page",
new { controller = "WikiPage",
action = "EditPage",
pageName = ViewData.Name })%>
</ul>
</div>
</div>
<div id="main">
<div id="revisions">
Revision history:
<ul>
<%
int i = 0;
foreach (WikiPageVersion version in ViewData.Page.Versions)
{ %>
<li>
<a href="http://<%= ViewData.Name %>?version=<%= i %>">
<%= version.CreatedOn %>
by
<%= version.Creator %>
</a>
</li>
<% ++i;
} %>
</ul>
</div>
<div id="maincontent">
<asp:ContentPlaceHolder
ID="MainContentPlaceHolder"
runat="server">
</asp:ContentPlaceHolder>
</div>
</div>
</div>
</body>
</html>
另一件需要注意的事是對 Html.ActionLink 的調(diào)用。以下是呈現(xiàn)幫助程序的一個示例。各種視圖類均具有兩種屬性,Html 和 Url。每種均有輸出 HTML 代碼塊的有用方法。在本例中,Html.ActionLink 獲取一個對象(此處為匿名類型)并通過路由系統(tǒng)將其返回。這將會生成一個 URL,該 URL 將路由至我指定的控制器和操作。這樣一來,無論我如何更改路由,“編輯此頁面”鏈接將始終指向正確的位置。
您可能還注意到,我還不得不依靠手動構(gòu)建鏈接(到先前頁面版本的鏈接)。遺憾的是,當(dāng)前的路由系統(tǒng)在涉及查詢字符串時生成 URL 的功能不是十分完善。這應(yīng)會在框架的后續(xù)版本中得到修復(fù)。
創(chuàng)建表單和回發(fā)
現(xiàn)在,讓我們看看控制器上的 EditPage 操作:
[ControllerAction]
public void EditPage(string pageName)
{
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
RenderView("editpage",
new WikiPageViewData {
Name = pageName,
Page = page });
}
同樣,該操作所做的不多—它只是呈現(xiàn)指定頁面的視圖。視圖中的內(nèi)容變得更加有趣,如圖 11 中所示。此文件構(gòu)建了一個 HTML 表單,但沒有出現(xiàn) Runat="server"。Url.Action helper 用于生成表單回發(fā)的 URL。其中還使用了幾種不同的 HTML 幫助程序(如 TextBox、TextArea 和 SubmitButton)。它們會出色完成您的預(yù)期目標(biāo):為各種輸入字段生成 HTML。

Figure 11 EditPage.aspx
<%@ Page Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
AutoEventWireup="true"
CodeBehind="EditPage.aspx.cs"
Inherits="MiniWiki.Views.WikiPage.EditPage" %>
<%@ Import Namespace="System.Web.Mvc" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<asp:Content ID="Content1"
ContentPlaceHolderID="MainContentPlaceHolder"
runat="server">
<form action="<%= Url.Action(
new { controller = "WikiPage",
action = "NewVersion",
pageName = ViewData.Name })%>" method=post>
<%
if (ViewContext.TempData.ContainsKey("errors"))
{
%>
<div id="errorlist">
<ul>
<%
foreach (string error in
(string[])ViewContext.TempData["errors"])
{
%>
<li><%= error%></li>
<% } %>
</ul>
</div>
<% } %>
Your name: <%= Html.TextBox("Creator",
ViewContext.TempData.ContainsKey("creator") ?
(string)ViewContext.TempData["creator"] :
ViewData.Creator)%>
<br />
Please enter your updates here:<br />
<%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ?
(string)ViewContext.TempData["body"] :
ViewData.Body, 30, 65)%>
<br />
Tags: <%= Html.TextBox(
"Tags", ViewContext.TempData.ContainsKey("tags") ?
(string)ViewContext.TempData["tags"] :
ViewData.Tags)%>
<br />
<%= Html.SubmitButton("SubmitAction", "OK")%>
<%= Html.SubmitButton("SubmitAction", "Cancel")%>
</form>
</asp:Content>
處理 Web 編程最頭疼的事情之一就是表單中的錯誤。更確切地說,您想要顯示錯誤信息,但同時想要保留原來輸入的數(shù)據(jù)。我們都有過那種經(jīng)歷,在填寫一張有 35 個字段的表單時出現(xiàn)一個錯誤,程序卻只是提供一堆錯誤信息和一張新的空白表單。MVC Framework 使用 TempData 存儲以前輸入信息,以便可以重新填入表單。這是 ViewState 實(shí)際上在 Web 窗體中變得非常簡單的原因,因?yàn)楸4婵丶膬?nèi)容幾乎是自動的。
我想在 MVC 中如法炮制,因此引入了 TempData。TempData 是一種詞典,與非類型化的 ViewData 很相似。不過,TempData 的內(nèi)容僅針對單一請求存在,隨后就會被刪除。要了解如何使用此方法,請參閱圖 12,NewVersion 操作。

Figure 12 NewVersion Action
[ControllerAction]
public void NewVersion(string pageName) {
NewVersionPostData postData = new NewVersionPostData();
postData.UpdateFrom(Request.Form);
if (postData.SubmitAction == "OK") {
if (postData.Errors.Length == 0) {
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
WikiPageVersion newVersion = new WikiPageVersion(
postData.Body, postData.Creator, postData.TagList);
page.Add(newVersion);
} else {
TempData["creator"] = postData.Creator;
TempData["body"] = postData.Body;
TempData["tags"] = postData.Tags;
TempData["errors"] = postData.Errors;
RedirectToAction(new {
controller = "WikiPage",
action = "EditPage",
pageName = pageName });
return;
}
}
RedirectToAction(new {
controller = "WikiPage",
action = "ShowPage",
pageName = pageName });
}
首先,它創(chuàng)建一個 NewVersionPostData 對象。這是另一個幫助程序?qū)ο螅哂写鎯τ浫氲膬?nèi)容和進(jìn)行某些驗(yàn)證的屬性和方法。為加載 postData 對象,我將使用 MVC 工具包的幫助程序。UpdateFrom 實(shí)際上是工具包提供的擴(kuò)展方法,它使用反射將表單字段的名稱與我的對象中屬性的名稱相對映。最終結(jié)果是,所有字段值均載入到我的 postData 對象中。不過,UpdateFrom 使用起來確實(shí)有缺點(diǎn),由于它直接從 HttpRequest 獲取表單數(shù)據(jù),使單元測試變得更為困難。
NewVersion 檢查的第一項(xiàng)是 SubmitAction。如果用戶單擊“確定”按鈕并確實(shí)想要發(fā)布編輯的頁面,則此項(xiàng)檢查將通過。如果此處有任何其它值,操作會重定向回 ShowPage,只是重新顯示原來的頁面。
如果用戶確實(shí)單擊了“確定”,則檢查 postData.Errors 屬性。這將在記入內(nèi)容上運(yùn)行一些簡單的驗(yàn)證。如果沒有任何錯誤,我會將新版本的頁面重新寫入 wiki。不過,如果出現(xiàn)錯誤,情況會變得饒有趣味。
如果出現(xiàn)錯誤,我會設(shè)置 TempData 詞典的各個字段,以便其包含 PostData 的內(nèi)容。然后,我會重定向回“編輯”頁面?,F(xiàn)在,由于已設(shè)置 TempData,頁面將重新顯示以用戶上次記入的值初始化的表單。
處理記入、驗(yàn)證和 TempData 的這個過程現(xiàn)在變得有些煩瑣,并且需要多做一些手動工作。將來發(fā)行的版本應(yīng)包括至少會將一些 TempData 檢查自動化的幫助程序方法。關(guān)于 TempData 的最后一個注意事項(xiàng)是:TempData 的內(nèi)容存儲在用戶的服務(wù)器端會話中。如果您關(guān)閉會話,TempData 將無法正常工作。
創(chuàng)建控制器
現(xiàn)在,wiki 的基礎(chǔ)已在發(fā)揮功效,但繼續(xù)進(jìn)行之前,我想要明確實(shí)現(xiàn)中的以下幾個要點(diǎn)。例如,Repository 屬性用于分離 wiki 的邏輯與物理存儲。您可以提供在文件系統(tǒng)(正如我所做的那樣)、數(shù)據(jù)庫或您想要的任何位置中存儲內(nèi)容的存儲庫。遺憾的是,我需要解決兩個問題。
首先,我的控制器類與具體的 FileBasedSpaceRepository 類緊密地連在一起。我需要一個默認(rèn)值,以便在屬性沒有設(shè)置時,也能合理使用。更糟的是,磁盤上文件的路徑在這里也是硬編碼的。最起碼,它應(yīng)取自配置。
其次,我的對象必須依賴存儲庫,否則無法運(yùn)行。對于良好的設(shè)計(jì),存儲庫實(shí)際應(yīng)為構(gòu)造函數(shù)參數(shù),而不是屬性。但我無法將其添加到構(gòu)造函數(shù)中,因?yàn)?MVC Framework 要求控制器上的構(gòu)造函數(shù)不能有參數(shù)。
幸運(yùn)的是,我可以通過一個擴(kuò)展性掛接擺脫此限制:即控制器工廠??刂破鞴S的功能正如其名稱所指:它創(chuàng)建 Controller 實(shí)例。您只需要創(chuàng)建一個類實(shí)現(xiàn) IControllerFactory 接口并向 MVC 系統(tǒng)注冊即可。您可以為所有控制器或僅為指定的類型注冊控制器工廠。圖 13 所示為 WikiPageController 的控制器工廠,其現(xiàn)在將存儲庫作為構(gòu)造函數(shù)參數(shù)傳遞。在這種情況下,實(shí)現(xiàn)非常煩瑣,但它可以創(chuàng)建能使用更強(qiáng)大工具(特別是依賴關(guān)系注入容器)的控制器。 無論如何,現(xiàn)在我擁有了將控制器依賴關(guān)系分離到對象中(易于管理和維護(hù))的所有詳細(xì)信息。

Figure 13 Controller Factory
public class WikiPageControllerFactory : IControllerFactory {
public IController CreateController(RequestContext context,
Type controllerType)
{
return new WikiPageController(
GetConfiguredRepository(context.HttpContext.Request));
}
private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
{
return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
}
}
此工作的最后一步是向框架注冊工廠。通過 ControllerBuilder 類可進(jìn)行此操作,方法是將以下行添加到 Application_Start 方法中的 Global.asax.cs(路由前后均可):
ControllerBuilder.Current.SetControllerFactory(
typeof(WikiPageController), typeof(WiliPageControllerFactory));
這將注冊 WikiPageController 的工廠。如果此項(xiàng)目中有其他控制器,它們不會使用此工廠,因?yàn)榇斯S僅針對 WikiPageController 類型進(jìn)行了注冊。如果您想要將工廠設(shè)置為供所有控制器使用,還可以調(diào)用 SetDefaultControllerFactory。
其他擴(kuò)展點(diǎn)
控制器工廠只是框架擴(kuò)展性的起點(diǎn)。本文中無法詳述所有的細(xì)節(jié),因此我將僅僅說明要點(diǎn)。首先,如果您想要輸出的內(nèi)容不是 HTML,或想要使用其他模板引擎而不是 Web 窗體,可將控制器的 ViewFactory 設(shè)為其他項(xiàng)。您可以實(shí)現(xiàn) IviewFactory 界面,然后即可完全控制如何生成輸出。這對于生成 RSS、XML 或圖形非常有用。
正如您所見到的,路由系統(tǒng)非常靈活。但路由系統(tǒng)中沒有任何內(nèi)容是 MVC 專用的。每個路由均有一個 RouteHandler 屬性;目前為止,我始終將其設(shè)為 MvcRouteHandler。但可以實(shí)現(xiàn) IRouteHandler 界面并將路由系統(tǒng)與其他 Web 技術(shù)掛接。將來推出的框架將附帶 WebFormsRouteHandler,并且其他技術(shù)也會在將來利用通用路由系統(tǒng)的優(yōu)勢。
控制器并非必須從 System.Web.Mvc.Controller 衍生??刂破餍枰龅膬H僅是實(shí)現(xiàn) IController 界面,該界面只有稱為 Execute 的一種方法。您可以從中進(jìn)行任何操作。另一方面,如果您想將 Controller 基類的幾種行為組合在一起,您可以覆蓋 Controller 的許多虛擬函數(shù):
- OnPreAction、OnPostAction 和 OnError 可讓您將每個已執(zhí)行操作上的預(yù)處理和后處理連接起來。OnError 為您提供在控制器內(nèi)處理錯誤的機(jī)制。
- 當(dāng) URL 路由到控制器但控制器沒有實(shí)現(xiàn)路由中請求的操作時,會調(diào)用 HandleUnknownAction。默認(rèn)情況下,此方法會拋出一個異常,但您可以用所需的操作覆蓋默認(rèn)值。
- InvokeAction 是一種方法,它負(fù)責(zé)解決調(diào)用何種操作方法并進(jìn)行調(diào)用。如果您想要自定義過程(例如,除去 [ControllerAction] 屬性的要求),應(yīng)使用該方法。
還有其他幾種針對 Controller 的虛擬方法,但這些方法主要是測試掛接而不是作為擴(kuò)展點(diǎn)。例如,RedirectToAction 是虛擬的,因此您可以創(chuàng)建實(shí)際并不進(jìn)行重定向的衍生類。這樣,您不需要完全運(yùn)行 Web 服務(wù)器就能測試重定向操作。
要告別 Web 窗體嗎?
現(xiàn)在您可能在想:“Web 窗體會面臨怎樣的命運(yùn)?MVC 會取代它嗎?”答案是否定的!Web 窗體是一種普及技術(shù),Microsoft 將繼續(xù)支持并改進(jìn)它。它在許多應(yīng)用程序中發(fā)揮著重要的作用;例如,可使用 Web 窗體創(chuàng)建典型的 Intranet 數(shù)據(jù)庫報表應(yīng)用程序,所花的時間比使用 MVC 編寫短得多。此外,Web 窗體支持大量的控件,許多控件均具備非常先進(jìn)的功能,可以大大提高效率。
那么,什么時候應(yīng)該選擇 MVC 呢?這主要取決于您的要求和喜好。您是否正在為獲得想要的 URL 格式而煩惱?您是否想要對 UI 進(jìn)行單元測試?以上情況均需要依靠 MVC。反之,如果您要顯示許多數(shù)據(jù),提供可編輯的網(wǎng)格和優(yōu)良的樹形視圖控件?那么,您暫時最好還是使用 Web 窗體。
今后,MVC Framework 很可能在 UI 控制部分有所改進(jìn),但在便利性上,它可能始終不及 Web 窗體,因?yàn)楹笳呔邆浯罅客弦饭δ?。同時,ASP.NET MVC Framework 為 Web 開發(fā)人員提供了一種在 Microsoft .NET Framework 中構(gòu)建 Web 應(yīng)用程序的新方法。Framework 針對可測試性設(shè)計(jì)、推倡使用 HTTP 并且?guī)缀踉诿總€點(diǎn)均可擴(kuò)展。對于那些想要完全控制其 Web 應(yīng)用程序的開發(fā)人員來說,這是一個對 Web 窗體的誘人補(bǔ)充。
Chris Tavares 是 Microsoft 模式和實(shí)施方案小組的一名開發(fā)人員,他致力于幫助開發(fā)社區(qū)了解在 Microsoft 平臺上構(gòu)建系統(tǒng)的最佳實(shí)踐。他還是 ASP.NET MVC 小組的虛擬成員,幫助您設(shè)計(jì)新的框架??梢酝ㄟ^
cct@tavaresstudios.com 與 Chris 取得聯(lián)系。