ASP.NET MVC: 构建不带 Web 窗体的 Web 应用程序(转载)
生活随笔
收集整理的這篇文章主要介紹了
ASP.NET MVC: 构建不带 Web 窗体的 Web 应用程序(转载)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
我 從事專業開發迄今為止已有 15 年,在此之前,我利用業余時間從事開發至少也有 10 年了。與我這一代的大多數人一樣,我是從 8 位計算機起步,然后轉用 PC 平臺的。隨著計算機的復雜性日益增加,我編寫的應用程序涵蓋了從小型游戲到個人數據管理再到控制外部硬件的各項功能。 不過,在我職業生涯的前半段,我編寫的所有軟件都有一個共同點:即,都是運行在用戶桌面 上的本地應用程序。我最早是在 90 年代初期聽說萬維網這件新生事物。那時我發現,通過構建 Web 應用程序,可以讓我輸入我的考勤卡信息而不必再費時費力從工作場所趕回辦公室。 一言以蔽之,我感覺很是困惑。我當時滿腦子是面向桌面的理念,很難接納這種無狀態的 Web。要添加很多讓人頭疼的調試、我沒有 UNIX 服務器的超級用戶訪問權限,再加上這個奇怪的角括號,這些因素使年輕時的我止步不前,又重返桌面開發渡過了幾年時光。 我遠離了 Web 開發領域,雖然這領域顯然很重要,但我并沒有真正理解其編程模型。然后,Microsoft? .NET Framework 和 ASP.NET 發行了。盡管它與桌面應用程序編程有許多相似之處,但終于有了可以讓我從事 Web 應用程序編程的框架。我可以構建窗口(頁面),將控件與事件掛鉤,而設計器使我不必處理那些討厭的角括號。最妙的是,ASP.NET 會通過查看狀態自動為我處理 Web 的無狀態性質!我又重新找回了程序員的快樂 ... 至少在一段時間內是如此。 隨著經驗的增加,我的設計內容也隨之豐富。我早已掌握了幾種最佳實踐,并將其應用到桌面 應用程序編程。其中的兩種就是:
- 分離關注點:不要將 UI 邏輯與基礎行為混合在一起。
- 自動單元測試:編寫自動測試以驗證您的代碼是否按預期執行。
模型視圖控制器模式 幸運的是,ASP.NET 團隊聽取了象我這樣的開發人員的意見,并且已經著手開發一種新的 Web 應用程序框架,該框架與您所熟知并喜愛的 Web 窗體處于同一層級,但采用一組完全不同的設計目標:
- 使用 HTTP 和 HTML—不隱藏。
- 可測試性貫穿整個框架之內。
- 幾乎在每個點均可擴展。
- 對輸出進行總體控制。
創建控制器 要繼續進行,您將需要安裝 Visual Studio 2008 并獲得 MVC Framework 的副本。在撰寫本文時,ASP.NET 擴展的 2007 年 12 月社區技術預覽 (CTP) 中已提供了這些內容 (asp.net/downloads/3.5-extensions)。您可能想要獲取擴展 CTP 和 MVC 工具包,其中包括一些非常有用的幫助程序對象。一旦下載并安裝 CTP 后,您將在“新建項目”對話框中獲得名為“ASP.NET MVC Web 應用程序”的新項目類型。 選擇“MVC Web 應用程序”項目后,會為您提供一個與常用網站或應用程序稍有不同的解決方案。該解決方案模板會創建一個帶有一些新目錄的 Web 應用程序(如圖 2 中所示)。特別是 Controllers 目錄包含各種控制器類,而 Views 目錄(及其所有子目錄)包含了各種視圖。 Figure 2?MVC 項目結構? 我將會編寫一個非常簡單的控制器,返回 URL 中傳遞的名稱。右鍵單擊 Controllers 文件夾并選擇“添加項目”以顯示常用的“添加項目”對話框以及一些新增加的內容,包括 MVC 控制器類和幾個 MVC 視圖組件。在此例中,我將添加一個非常富有想象力、名為 HelloController 的類: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Controllers
{
public class HelloController : Controller
{
[ControllerAction]
public void Index()
{
...
}
}
}
控制器類比頁面簡單得多。實際上,唯一真正必需做的就是從 System.Web.Mvc.Controller 中衍生并將 [ControllerAction] 屬性置于您的操作方法中。操作是調用以響應特定 URL 請求的一種方法。操作負責執行所需的一切處理,然后呈現一個視圖。我將通過編寫一個將名稱傳遞到視圖的簡單操作著手,如下所示: 復制代碼 [ControllerAction]
public void HiThere(string id)
{
ViewData["Name"] = id;
RenderView("HiThere");
}
操作方法會通過 ID 參數從 URL 接收該名稱(稍后會介紹方法),將其存儲在 ViewData 集合中,然后呈現名為 HiThere 的視圖。 在討論如何調用此方法,或該視圖的顯示內容之前,我希望說一說可測試性。還記得我之前關 于測試 Web 窗體頁面類有多難的評論嗎?控制器的測試簡單得多。實際上,控制器可以直接實例化,而調用操作方法無需任何附加的基礎結構。您不需要 HTTP 上下文,也不需要服務器,只要測試工具即可。作為示例,我在圖 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"];
}
}
}
下面將進行幾項操作。實際的測試相當簡單:實例化該控制器,使用預期的數據調用該方法, 然后檢查呈現的視圖是否正確。我通過創建測試專用的子類覆蓋 RenderView 方法進行檢查。這可以縮短實際創建 HTML 的時間。我只關心是否將正確的數據發送到視圖以及是否呈現了正確的視圖。我不關心此測試視圖本身的底層詳細信息。
創建視圖 當然,最終我必須生成一些 HTML,因此,讓我們創建該 HiThere 視圖。要進行此操作,首先,我將在解決方案中的 Views 文件夾下創建名為 Hello 的新文件夾。默認情況下,控制器將在 Views\<控制器前綴> 文件夾(控制器前綴為控制器類的名稱去掉 "Controller" 字樣)中查找視圖。因此,對于 HelloController 呈現的視圖,它會在 Views\Hello 中查找。解決方案的查找結果如圖 4 所示。 Figure 4?將視圖添加到項目中?(單 擊該圖像獲得較大視圖) 視圖的 HTML 如下所示: 復制代碼 <html >
<head runat="server">
<title>Hi There!</title>
</head>
<body>
<div>
<h1>Hello, <%= ViewData["Name"] %></h1>
</div>
</body>
</html>
應注意以下幾件事。沒有 runat="server" 標記。沒有 form 標記。沒有控件聲明。實際上,這看起來更象傳統的 ASP 而不是 ASP.NET。請注意,MVC 視圖僅負責生成輸出,因此其不需要任何 Web 窗體頁面所需的事件處理或復雜控件。 MVC Framework 借用了 .aspx 文件格式作為一種有用的文本模板語言。如果需要,甚至可以使用源代碼,但默認情況下,源代碼文件如下所示: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Views.Hello
{
public partial class HiThere : ViewPage
{
}
}
沒有頁面初始化或加載方法,沒有事件處理程序,除了基類聲明以外沒有任何內容,基類聲明 為 ViewPage 而不是 Page。這就是 MVC 視圖所需的一切。運行該應用程序,導航至 http://localhost:<端口>/Hello/HiThere/Chris,您將看到如圖 5 所示的內容。 Figure 5?成功的 MVC 視圖?(單 擊該圖像獲得較大視圖) 如果您看到的并非如圖 5 所示,而是難以理解的意外情況,請不要驚慌。如果您將 HiThere.aspx 文件設置為 Visual Studio 中的活動文檔,則當按 F5 后,Visual Studio 將嘗試直接訪問 .aspx 文件。由于 MVC 視圖要求控制器在顯示前運行,因此嘗試直接導航至該頁面將不起作用。只需將該 URL 編輯為與圖 5 中所示的內容相匹配,即可正常工作。 MVC Framework 如何知道調用我的操作方法?該 URL 甚至沒有文件擴展名。答案是 URL 路由。如果您仔細查看 global.asax.cs 文件,則會看到如圖 6 所示的代碼段。全局 RouteTable 會存儲 Route 對象的集合。每個 Route 說明一個 URL 窗體以及對其進行何種操作。默認情況下,會向該表中添加兩個路由。第一個是該方法的內容。它說明每個 URL 在服務器名后均由三部分組成,第一部分應為控制器名,第二部分為操作名稱,而第三部分為 ID 參數。 ?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]"
此默認路由是能讓我的 HiThere 方法得以調用的路由。請記住此 URL:http://localhost/Hello/HiThere/Chris?此路由將 Hello 與控制器、HiThere 與操作以及 Chris 與 ID 一一對應。MVC Framework 隨即創建 HelloController 實例,調用 HiThere 方法,然后將 Chris 作為 ID 參數的值傳遞。 此默認路由為您提供了許多功能,但您也可以添加自己的路由。例如,我想要一個真正友好的 站點,好友們只需輸入他們的姓名即可獲得個性化的問候。如果我在路由表的頂部添加以下路由 復制代碼 RouteTable.Routes.Add(new Route
{
Url = "[id]",
Defaults = new {
controller = "Hello",
action = "HiThere" },
RouteHandler = typeof(MvcRouteHandler)
});
隨后,我只需訪問 ,我的操作仍處于調用狀態,而我將會看到熟悉的友好問候。 系統如何知道調用哪個控制器和操作?答案是 Defaults 參數。它利用新的 C# 3.0 匿名類型語法來創建一個偽詞典。Route 上的 Defaults 對象可包含任意附加的信息,對于 MVC,它還可以包含一些眾所周知的條目:即控制器和操作。如果 URL 中沒有指定控制器或操作,則其將使用 Defaults 中的名稱。這就是為什么即使我在 URL 中忽略它們,但仍可以將我的請求映射到正確的控制器和操作。 還有一件事需要注意:還記得我說過“添加到表格的頂部”嗎?如果您將其置于底部,將會出 現錯誤。路由根據先到先得的原則進行工作。當處理 URL 時,路由系統會自上至下瀏覽表格,并且使用第一個匹配的路由。在本例中,默認路由 "[controller]/[action]/[id]" 匹配,因為它們是操作和 ID 的默認值。這樣,它會繼續查找 ChrisController,但我沒有控制器,因此會出現錯誤。
稍大的示例 現在,我已經說明了 MVC Framework 的基礎知識,將為您展示一個更大的示例,實現比僅顯示字符串更多的功能。wiki 是一種可以在瀏覽器中進行編輯的網站。可以輕松地添加或編輯頁面。我使用 MVC Framework 編寫了一個小型的示例 wiki。“編輯此頁面”屏幕如圖 7 所示。 Figure 7?編輯主頁?(單擊該 圖像獲得較大視圖) 您可以檢查本文的代碼下載以查看如何實現底層 wiki 邏輯。現在我想重點說明 MVC Framework 如何使 Web 上的 wiki 獲取變得簡單。讓我們先設計 URL 結構。我想要以下各項:
- /[pagename] 顯示該名稱的頁面。
- /[pagename]?version=n 顯示頁面的請求版本,其中 0 = 當前版本,1 = 以前的版本,以此類推。
- /Edit/[pagename] 打開該頁的編輯屏幕。
- /CreateNewVersion/[pagename] 是為提交編輯而傳入的 URL。
{
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
});
}
}
我前面的示例說明了一種將數據從控制器傳遞到視圖的方法:即 ViewData 詞典。詞典非常方便,但也很危險。它們幾乎包含一切內容,您不能獲取內容的任何 IntelliSense?,并且由于 ViewData 詞典屬于 Dictionary<string, object> 類型,它將消耗內容,您必須計算所有一切。 當您了解在視圖中將需要什么數據后,就可以傳遞強類型化的 ViewData 對象。在我的示例中,我創建了一個簡單的對象 (WikiPageViewData),如圖 9 中所示。此對象將 wiki 頁面信息帶到視圖,同時還攜帶了一些實用工具方法,執行獲取 wiki 標記的 HTML 版本這類任務。 ?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); }
}
}
現在,我已經定義了視圖數據,那么,我如何使用它呢?在 ShowPage.aspx.cs 中,您將看到以下內容: 復制代碼 namespace MiniWiki.Views.WikiPage {
public partial class ShowPage : ViewPage<WikiPageViewData>
{
}
}
請注意,我將基類類型定義為 ViewPage<WikiPageViewData>。這意味著頁面的 ViewData 屬性為 WikiPageViewData 類型,而不是象以前示例中的“Dictionary”。 .aspx 文件中的實際標記非常簡單: 復制代碼 <%@ 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>
請注意,當引用 ViewData 時,我沒有使用索引操作符 []。由于我現在有強類型化的 ViewData,我可以直接訪問該屬性。不需要進行任何計算,而 Visual Studio 會提供 IntelliSense。 目光敏銳的讀者將會注意到此文件中的 <asp:Content> 標記。沒錯,“母版頁”確實可以與 MVC 視圖配合使用。并且“母版頁”還可以成為視圖。讓我們看看“母版頁”的源代碼: 復制代碼 namespace MiniWiki.Views.Layouts
{
public partial class Site :
System.Web.Mvc.ViewMasterPage<WikiPageViewData>
{
}
}
相關標記如圖 10 中所示。現在,“母版頁”將獲得與視圖完全相同的 ViewData 對象。我已經將“母版頁”的基類聲明為 ViewMasterPage<WikiPageViewData>,因此,我擁有了正確類型的 ViewData。我會在那里設置各種 DIV 標記以對頁面進行布局,填寫版本列表,然后以常用內容占位符收尾。 ?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://http://www.cnblogs.com/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 的調用。以下是呈現幫助程序的一個示例。各種視圖類均具有兩種屬性,Html 和 Url。每種均有輸出 HTML 代碼塊的有用方法。在本例中,Html.ActionLink 獲取一個對象(此處為匿名類型)并通過路由系統將其返回。這將會生成一個 URL,該 URL 將路由至我指定的控制器和操作。這樣一來,無論我如何更改路由,“編輯此頁面”鏈接將始終指向正確的位置。 您可能還注意到,我還不得不依靠手動構建鏈接(到先前頁面版本的鏈接)。遺憾的是,當前 的路由系統在涉及查詢字符串時生成 URL 的功能不是十分完善。這應會在框架的后續版本中得到修復。
創建表單和回發 現在,讓我們看看控制器上的 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 });
}
同樣,該操作所做的不多—它只是呈現指定頁面的視圖。視圖中的內容變得更加有趣,如圖 11 中所示。此文件構建了一個 HTML 表單,但沒有出現 Runat="server"。Url.Action helper 用于生成表單回發的 URL。其中還使用了幾種不同的 HTML 幫助程序(如 TextBox、TextArea 和 SubmitButton)。它們會出色完成您的預期目標:為各種輸入字段生成 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 編程最頭疼的事情之一就是表單中的錯誤。更確切地說,您想要顯示錯誤信息,但同時想要保留原來輸入的數據。我們都有過那種經歷,在填寫一張有 35 個字段的表單時出現一個錯誤,程序卻只是提供一堆錯誤信息和一張新的空白表單。MVC Framework 使用 TempData 存儲以前輸入信息,以便可以重新填入表單。這是 ViewState 實際上在 Web 窗體中變得非常簡單的原因,因為保存控件的內容幾乎是自動的。 我想在 MVC 中如法炮制,因此引入了 TempData。TempData 是一種詞典,與非類型化的 ViewData 很相似。不過,TempData 的內容僅針對單一請求存在,隨后就會被刪除。要了解如何使用此方法,請參閱圖 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 }); } 首先,它創建一個 NewVersionPostData 對象。這是另一個幫助程序對象,具有存儲記入的內容和進行某些驗證的屬性和方法。為加載 postData 對象,我將使用 MVC 工具包的幫助程序。UpdateFrom 實際上是工具包提供的擴展方法,它使用反射將表單字段的名稱與我的對象中屬性的名稱相對映。最終結果是,所有字段值均載入到我的 postData 對象中。不過,UpdateFrom 使用起來確實有缺點,由于它直接從 HttpRequest 獲取表單數據,使單元測試變得更為困難。 NewVersion 檢查的第一項是 SubmitAction。如果用戶單擊“確定”按鈕并確實想要發布編輯的頁面,則此項檢查將通過。如果此處有任何其它值,操作會重定向回 ShowPage,只是重新顯示原來的頁面。 如果用戶確實單擊了“確定”,則檢查 postData.Errors 屬性。這將在記入內容上運行一些簡單的驗證。如果沒有任何錯誤,我會將新版本的頁面重新寫入 wiki。不過,如果出現錯誤,情況會變得饒有趣味。 如果出現錯誤,我會設置 TempData 詞典的各個字段,以便其包含 PostData 的內容。然后,我會重定向回“編輯”頁面。現在,由于已設置 TempData,頁面將重新顯示以用戶上次記入的值初始化的表單。 處理記入、驗證和 TempData 的這個過程現在變得有些煩瑣,并且需要多做一些手動工作。將來發行的版本應包括至少會將一些 TempData 檢查自動化的幫助程序方法。關于 TempData 的最后一個注意事項是:TempData 的內容存儲在用戶的服務器端會話中。如果您關閉會話,TempData 將無法正常工作。
創建控制器 現在,wiki 的基礎已在發揮功效,但繼續進行之前,我想要明確實現中的以下幾個要點。例如,Repository 屬性用于分離 wiki 的邏輯與物理存儲。您可以提供在文件系統(正如我所做的那樣)、數據庫或您想要的任何位置中存儲內容的存儲庫。遺憾的是,我需要解決兩個問題。 首先,我的控制器類與具體的 FileBasedSpaceRepository 類緊密地連在一起。我需要一個默認值,以便在屬性沒有設置時,也能合理使用。更糟的是,磁盤上文件的路徑在這里也是硬編碼的。最起碼,它應取自配置。 其次,我的對象必須依賴存儲庫,否則無法運行。對于良好的設計,存儲庫實際應為構造函數 參數,而不是屬性。但我無法將其添加到構造函數中,因為 MVC Framework 要求控制器上的構造函數不能有參數。 幸運的是,我可以通過一個擴展性掛接擺脫此限制:即控制器工廠。控制器工廠的功能正如其 名稱所指:它創建 Controller 實例。您只需要創建一個類實現 IControllerFactory 接口并向 MVC 系統注冊即可。您可以為所有控制器或僅為指定的類型注冊控制器工廠。圖 13 所示為 WikiPageController 的控制器工廠,其現在將存儲庫作為構造函數參數傳遞。在這種情況下,實現非常煩瑣,但它可以創建能使用更強大工具(特別是依賴關系注入容器)的控制器。 無論如何,現在我擁有了將控制器依賴關系分離到對象中(易于管理和維護)的所有詳細信息。 ?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 類可進行此操作,方法是將以下行添加到 Application_Start 方法中的 Global.asax.cs(路由前后均可): 復制代碼 ControllerBuilder.Current.SetControllerFactory(
typeof(WikiPageController), typeof(WiliPageControllerFactory));
這將注冊 WikiPageController 的工廠。如果此項目中有其他控制器,它們不會使用此工廠,因為此工廠僅針對 WikiPageController 類型進行了注冊。如果您想要將工廠設置為供所有控制器使用,還可以調用 SetDefaultControllerFactory。
其他擴展點 控制器工廠只是框架擴展性的起點。本文中無法詳述所有的細節,因此我將僅僅說明要點。首 先,如果您想要輸出的內容不是 HTML,或想要使用其他模板引擎而不是 Web 窗體,可將控制器的 ViewFactory 設為其他項。您可以實現 IviewFactory 界面,然后即可完全控制如何生成輸出。這對于生成 RSS、XML 或圖形非常有用。 正如您所見到的,路由系統非常靈活。但路由系統中沒有任何內容是 MVC 專用的。每個路由均有一個 RouteHandler 屬性;目前為止,我始終將其設為 MvcRouteHandler。但可以實現 IRouteHandler 界面并將路由系統與其他 Web 技術掛接。將來推出的框架將附帶 WebFormsRouteHandler,并且其他技術也會在將來利用通用路由系統的優勢。 控制器并非必須從 System.Web.Mvc.Controller 衍生。控制器需要做的僅僅是實現 IController 界面,該界面只有稱為 Execute 的一種方法。您可以從中進行任何操作。另一方面,如果您想將 Controller 基類的幾種行為組合在一起,您可以覆蓋 Controller 的許多虛擬函數:
- OnPreAction、OnPostAction 和 OnError 可讓您將每個已執行操作上的預處理和后處理連接起來。OnError 為您提供在控制器內處理錯誤的機制。
- 當 URL 路由到控制器但控制器沒有實現路由中請求的操作時,會調用 HandleUnknownAction。默認情況下,此方法會拋出一個異常,但您可以用所需的操作覆蓋默認值。
- InvokeAction 是一種方法,它負責解決調用何種操作方法并進行調用。如果您想要自定義過程(例如,除去 [ControllerAction] 屬性的要求),應使用該方法。
要告別 Web 窗體嗎? 現在您可能在想:“Web 窗體會面臨怎樣的命運?MVC 會取代它嗎?”答案是否定的!Web 窗體是一種普及技術,Microsoft 將繼續支持并改進它。它在許多應用程序中發揮著重要的作用;例如,可使用 Web 窗體創建典型的 Intranet 數據庫報表應用程序,所花的時間比使用 MVC 編寫短得多。此外,Web 窗體支持大量的控件,許多控件均具備非常先進的功能,可以大大提高效率。 那么,什么時候應該選擇 MVC 呢?這主要取決于您的要求和喜好。您是否正在為獲得想要的 URL 格式而煩惱?您是否想要對 UI 進行單元測試?以上情況均需要依靠 MVC。反之,如果您要顯示許多數據,提供可編輯的網格和優良的樹形視圖控件?那么,您暫時最好還是使用 Web 窗體。 今后,MVC Framework 很可能在 UI 控制部分有所改進,但在便利性上,它可能始終不及 Web 窗體,因為后者具備大量拖曳功能。同時,ASP.NET MVC Framework 為 Web 開發人員提供了一種在 Microsoft .NET Framework 中構建 Web 應用程序的新方法。Framework 針對可測試性設計、推倡使用 HTTP 并且幾乎在每個點均可擴展。對于那些想要完全控制其 Web 應用程序的開發人員來說,這是一個對 Web 窗體的誘人補充。
Chris Tavares 是 Microsoft 模式和實施方案小組的一名開發人員,他致力于幫助開發社區了解在 Microsoft 平臺上構建系統的最佳實踐。他還是 ASP.NET MVC 小組的虛擬成員,幫助您設計新的框架。可以通過 cct@tavaresstudios.com 與 Chris 取得聯系。 我 從事專業開發迄今為止已有 15 年,在此之前,我利用業余時間從事開發至少也有 10 年了。與我這一代的大多數人一樣,我是從 8 位計算機起步,然后轉用 PC 平臺的。隨著計算機的復雜性日益增加,我編寫的應用程序涵蓋了從小型游戲到個人數據管理再到控制外部硬件的各項功能。 不過,在我職業生涯的前半段,我編寫的所有軟件都有一個共同點:即,都是運行在用戶桌面 上的本地應用程序。我最早是在 90 年代初期聽說萬維網這件新生事物。那時我發現,通過構建 Web 應用程序,可以讓我輸入我的考勤卡信息而不必再費時費力從工作場所趕回辦公室。 一言以蔽之,我感覺很是困惑。我當時滿腦子是面向桌面的理念,很難接納這種無狀態的 Web。要添加很多讓人頭疼的調試、我沒有 UNIX 服務器的超級用戶訪問權限,再加上這個奇怪的角括號,這些因素使年輕時的我止步不前,又重返桌面開發渡過了幾年時光。 我遠離了 Web 開發領域,雖然這領域顯然很重要,但我并沒有真正理解其編程模型。然后,Microsoft? .NET Framework 和 ASP.NET 發行了。盡管它與桌面應用程序編程有許多相似之處,但終于有了可以讓我從事 Web 應用程序編程的框架。我可以構建窗口(頁面),將控件與事件掛鉤,而設計器使我不必處理那些討厭的角括號。最妙的是,ASP.NET 會通過查看狀態自動為我處理 Web 的無狀態性質!我又重新找回了程序員的快樂 ... 至少在一段時間內是如此。 隨著經驗的增加,我的設計內容也隨之豐富。我早已掌握了幾種最佳實踐,并將其應用到桌面 應用程序編程。其中的兩種就是:
- 分離關注點:不要將 UI 邏輯與基礎行為混合在一起。
- 自動單元測試:編寫自動測試以驗證您的代碼是否按預期執行。
模型視圖控制器模式 幸運的是,ASP.NET 團隊聽取了象我這樣的開發人員的意見,并且已經著手開發一種新的 Web 應用程序框架,該框架與您所熟知并喜愛的 Web 窗體處于同一層級,但采用一組完全不同的設計目標:
- 使用 HTTP 和 HTML—不隱藏。
- 可測試性貫穿整個框架之內。
- 幾乎在每個點均可擴展。
- 對輸出進行總體控制。
創建控制器 要繼續進行,您將需要安裝 Visual Studio 2008 并獲得 MVC Framework 的副本。在撰寫本文時,ASP.NET 擴展的 2007 年 12 月社區技術預覽 (CTP) 中已提供了這些內容 (asp.net/downloads/3.5-extensions)。您可能想要獲取擴展 CTP 和 MVC 工具包,其中包括一些非常有用的幫助程序對象。一旦下載并安裝 CTP 后,您將在“新建項目”對話框中獲得名為“ASP.NET MVC Web 應用程序”的新項目類型。 選擇“MVC Web 應用程序”項目后,會為您提供一個與常用網站或應用程序稍有不同的解決方案。該解決方案模板會創建一個帶有一些新目錄的 Web 應用程序(如圖 2 中所示)。特別是 Controllers 目錄包含各種控制器類,而 Views 目錄(及其所有子目錄)包含了各種視圖。 Figure 2?MVC 項目結構? 我將會編寫一個非常簡單的控制器,返回 URL 中傳遞的名稱。右鍵單擊 Controllers 文件夾并選擇“添加項目”以顯示常用的“添加項目”對話框以及一些新增加的內容,包括 MVC 控制器類和幾個 MVC 視圖組件。在此例中,我將添加一個非常富有想象力、名為 HelloController 的類: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Controllers
{
public class HelloController : Controller
{
[ControllerAction]
public void Index()
{
...
}
}
}
控制器類比頁面簡單得多。實際上,唯一真正必需做的就是從 System.Web.Mvc.Controller 中衍生并將 [ControllerAction] 屬性置于您的操作方法中。操作是調用以響應特定 URL 請求的一種方法。操作負責執行所需的一切處理,然后呈現一個視圖。我將通過編寫一個將名稱傳遞到視圖的簡單操作著手,如下所示: 復制代碼 [ControllerAction]
public void HiThere(string id)
{
ViewData["Name"] = id;
RenderView("HiThere");
}
操作方法會通過 ID 參數從 URL 接收該名稱(稍后會介紹方法),將其存儲在 ViewData 集合中,然后呈現名為 HiThere 的視圖。 在討論如何調用此方法,或該視圖的顯示內容之前,我希望說一說可測試性。還記得我之前關 于測試 Web 窗體頁面類有多難的評論嗎?控制器的測試簡單得多。實際上,控制器可以直接實例化,而調用操作方法無需任何附加的基礎結構。您不需要 HTTP 上下文,也不需要服務器,只要測試工具即可。作為示例,我在圖 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"];
}
}
}
下面將進行幾項操作。實際的測試相當簡單:實例化該控制器,使用預期的數據調用該方法, 然后檢查呈現的視圖是否正確。我通過創建測試專用的子類覆蓋 RenderView 方法進行檢查。這可以縮短實際創建 HTML 的時間。我只關心是否將正確的數據發送到視圖以及是否呈現了正確的視圖。我不關心此測試視圖本身的底層詳細信息。
創建視圖 當然,最終我必須生成一些 HTML,因此,讓我們創建該 HiThere 視圖。要進行此操作,首先,我將在解決方案中的 Views 文件夾下創建名為 Hello 的新文件夾。默認情況下,控制器將在 Views\<控制器前綴> 文件夾(控制器前綴為控制器類的名稱去掉 "Controller" 字樣)中查找視圖。因此,對于 HelloController 呈現的視圖,它會在 Views\Hello 中查找。解決方案的查找結果如圖 4 所示。 Figure 4?將視圖添加到項目中?(單 擊該圖像獲得較大視圖) 視圖的 HTML 如下所示: 復制代碼 <html >
<head runat="server">
<title>Hi There!</title>
</head>
<body>
<div>
<h1>Hello, <%= ViewData["Name"] %></h1>
</div>
</body>
</html>
應注意以下幾件事。沒有 runat="server" 標記。沒有 form 標記。沒有控件聲明。實際上,這看起來更象傳統的 ASP 而不是 ASP.NET。請注意,MVC 視圖僅負責生成輸出,因此其不需要任何 Web 窗體頁面所需的事件處理或復雜控件。 MVC Framework 借用了 .aspx 文件格式作為一種有用的文本模板語言。如果需要,甚至可以使用源代碼,但默認情況下,源代碼文件如下所示: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Views.Hello
{
public partial class HiThere : ViewPage
{
}
}
沒有頁面初始化或加載方法,沒有事件處理程序,除了基類聲明以外沒有任何內容,基類聲明 為 ViewPage 而不是 Page。這就是 MVC 視圖所需的一切。運行該應用程序,導航至 http://localhost:<端口>/Hello/HiThere/Chris,您將看到如圖 5 所示的內容。 Figure 5?成功的 MVC 視圖?(單 擊該圖像獲得較大視圖) 如果您看到的并非如圖 5 所示,而是難以理解的意外情況,請不要驚慌。如果您將 HiThere.aspx 文件設置為 Visual Studio 中的活動文檔,則當按 F5 后,Visual Studio 將嘗試直接訪問 .aspx 文件。由于 MVC 視圖要求控制器在顯示前運行,因此嘗試直接導航至該頁面將不起作用。只需將該 URL 編輯為與圖 5 中所示的內容相匹配,即可正常工作。 MVC Framework 如何知道調用我的操作方法?該 URL 甚至沒有文件擴展名。答案是 URL 路由。如果您仔細查看 global.asax.cs 文件,則會看到如圖 6 所示的代碼段。全局 RouteTable 會存儲 Route 對象的集合。每個 Route 說明一個 URL 窗體以及對其進行何種操作。默認情況下,會向該表中添加兩個路由。第一個是該方法的內容。它說明每個 URL 在服務器名后均由三部分組成,第一部分應為控制器名,第二部分為操作名稱,而第三部分為 ID 參數。 ?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]"
此默認路由是能讓我的 HiThere 方法得以調用的路由。請記住此 URL:http://localhost/Hello/HiThere/Chris?此路由將 Hello 與控制器、HiThere 與操作以及 Chris 與 ID 一一對應。MVC Framework 隨即創建 HelloController 實例,調用 HiThere 方法,然后將 Chris 作為 ID 參數的值傳遞。 此默認路由為您提供了許多功能,但您也可以添加自己的路由。例如,我想要一個真正友好的 站點,好友們只需輸入他們的姓名即可獲得個性化的問候。如果我在路由表的頂部添加以下路由 復制代碼 RouteTable.Routes.Add(new Route
{
Url = "[id]",
Defaults = new {
controller = "Hello",
action = "HiThere" },
RouteHandler = typeof(MvcRouteHandler)
});
隨后,我只需訪問 ,我的操作仍處于調用狀態,而我將會看到熟悉的友好問候。 系統如何知道調用哪個控制器和操作?答案是 Defaults 參數。它利用新的 C# 3.0 匿名類型語法來創建一個偽詞典。Route 上的 Defaults 對象可包含任意附加的信息,對于 MVC,它還可以包含一些眾所周知的條目:即控制器和操作。如果 URL 中沒有指定控制器或操作,則其將使用 Defaults 中的名稱。這就是為什么即使我在 URL 中忽略它們,但仍可以將我的請求映射到正確的控制器和操作。 還有一件事需要注意:還記得我說過“添加到表格的頂部”嗎?如果您將其置于底部,將會出 現錯誤。路由根據先到先得的原則進行工作。當處理 URL 時,路由系統會自上至下瀏覽表格,并且使用第一個匹配的路由。在本例中,默認路由 "[controller]/[action]/[id]" 匹配,因為它們是操作和 ID 的默認值。這樣,它會繼續查找 ChrisController,但我沒有控制器,因此會出現錯誤。
稍大的示例 現在,我已經說明了 MVC Framework 的基礎知識,將為您展示一個更大的示例,實現比僅顯示字符串更多的功能。wiki 是一種可以在瀏覽器中進行編輯的網站。可以輕松地添加或編輯頁面。我使用 MVC Framework 編寫了一個小型的示例 wiki。“編輯此頁面”屏幕如圖 7 所示。 Figure 7?編輯主頁?(單擊該 圖像獲得較大視圖) 您可以檢查本文的代碼下載以查看如何實現底層 wiki 邏輯。現在我想重點說明 MVC Framework 如何使 Web 上的 wiki 獲取變得簡單。讓我們先設計 URL 結構。我想要以下各項:
- /[pagename] 顯示該名稱的頁面。
- /[pagename]?version=n 顯示頁面的請求版本,其中 0 = 當前版本,1 = 以前的版本,以此類推。
- /Edit/[pagename] 打開該頁的編輯屏幕。
- /CreateNewVersion/[pagename] 是為提交編輯而傳入的 URL。
{
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
});
}
}
我前面的示例說明了一種將數據從控制器傳遞到視圖的方法:即 ViewData 詞典。詞典非常方便,但也很危險。它們幾乎包含一切內容,您不能獲取內容的任何 IntelliSense?,并且由于 ViewData 詞典屬于 Dictionary<string, object> 類型,它將消耗內容,您必須計算所有一切。 當您了解在視圖中將需要什么數據后,就可以傳遞強類型化的 ViewData 對象。在我的示例中,我創建了一個簡單的對象 (WikiPageViewData),如圖 9 中所示。此對象將 wiki 頁面信息帶到視圖,同時還攜帶了一些實用工具方法,執行獲取 wiki 標記的 HTML 版本這類任務。 ?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); }
}
}
現在,我已經定義了視圖數據,那么,我如何使用它呢?在 ShowPage.aspx.cs 中,您將看到以下內容: 復制代碼 namespace MiniWiki.Views.WikiPage {
public partial class ShowPage : ViewPage<WikiPageViewData>
{
}
}
請注意,我將基類類型定義為 ViewPage<WikiPageViewData>。這意味著頁面的 ViewData 屬性為 WikiPageViewData 類型,而不是象以前示例中的“Dictionary”。 .aspx 文件中的實際標記非常簡單: 復制代碼 <%@ 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>
請注意,當引用 ViewData 時,我沒有使用索引操作符 []。由于我現在有強類型化的 ViewData,我可以直接訪問該屬性。不需要進行任何計算,而 Visual Studio 會提供 IntelliSense。 目光敏銳的讀者將會注意到此文件中的 <asp:Content> 標記。沒錯,“母版頁”確實可以與 MVC 視圖配合使用。并且“母版頁”還可以成為視圖。讓我們看看“母版頁”的源代碼: 復制代碼 namespace MiniWiki.Views.Layouts
{
public partial class Site :
System.Web.Mvc.ViewMasterPage<WikiPageViewData>
{
}
}
相關標記如圖 10 中所示。現在,“母版頁”將獲得與視圖完全相同的 ViewData 對象。我已經將“母版頁”的基類聲明為 ViewMasterPage<WikiPageViewData>,因此,我擁有了正確類型的 ViewData。我會在那里設置各種 DIV 標記以對頁面進行布局,填寫版本列表,然后以常用內容占位符收尾。 ?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://http://www.cnblogs.com/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 的調用。以下是呈現幫助程序的一個示例。各種視圖類均具有兩種屬性,Html 和 Url。每種均有輸出 HTML 代碼塊的有用方法。在本例中,Html.ActionLink 獲取一個對象(此處為匿名類型)并通過路由系統將其返回。這將會生成一個 URL,該 URL 將路由至我指定的控制器和操作。這樣一來,無論我如何更改路由,“編輯此頁面”鏈接將始終指向正確的位置。 您可能還注意到,我還不得不依靠手動構建鏈接(到先前頁面版本的鏈接)。遺憾的是,當前 的路由系統在涉及查詢字符串時生成 URL 的功能不是十分完善。這應會在框架的后續版本中得到修復。
創建表單和回發 現在,讓我們看看控制器上的 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 });
}
同樣,該操作所做的不多—它只是呈現指定頁面的視圖。視圖中的內容變得更加有趣,如圖 11 中所示。此文件構建了一個 HTML 表單,但沒有出現 Runat="server"。Url.Action helper 用于生成表單回發的 URL。其中還使用了幾種不同的 HTML 幫助程序(如 TextBox、TextArea 和 SubmitButton)。它們會出色完成您的預期目標:為各種輸入字段生成 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 編程最頭疼的事情之一就是表單中的錯誤。更確切地說,您想要顯示錯誤信息,但同時想要保留原來輸入的數據。我們都有過那種經歷,在填寫一張有 35 個字段的表單時出現一個錯誤,程序卻只是提供一堆錯誤信息和一張新的空白表單。MVC Framework 使用 TempData 存儲以前輸入信息,以便可以重新填入表單。這是 ViewState 實際上在 Web 窗體中變得非常簡單的原因,因為保存控件的內容幾乎是自動的。 我想在 MVC 中如法炮制,因此引入了 TempData。TempData 是一種詞典,與非類型化的 ViewData 很相似。不過,TempData 的內容僅針對單一請求存在,隨后就會被刪除。要了解如何使用此方法,請參閱圖 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 }); } 首先,它創建一個 NewVersionPostData 對象。這是另一個幫助程序對象,具有存儲記入的內容和進行某些驗證的屬性和方法。為加載 postData 對象,我將使用 MVC 工具包的幫助程序。UpdateFrom 實際上是工具包提供的擴展方法,它使用反射將表單字段的名稱與我的對象中屬性的名稱相對映。最終結果是,所有字段值均載入到我的 postData 對象中。不過,UpdateFrom 使用起來確實有缺點,由于它直接從 HttpRequest 獲取表單數據,使單元測試變得更為困難。 NewVersion 檢查的第一項是 SubmitAction。如果用戶單擊“確定”按鈕并確實想要發布編輯的頁面,則此項檢查將通過。如果此處有任何其它值,操作會重定向回 ShowPage,只是重新顯示原來的頁面。 如果用戶確實單擊了“確定”,則檢查 postData.Errors 屬性。這將在記入內容上運行一些簡單的驗證。如果沒有任何錯誤,我會將新版本的頁面重新寫入 wiki。不過,如果出現錯誤,情況會變得饒有趣味。 如果出現錯誤,我會設置 TempData 詞典的各個字段,以便其包含 PostData 的內容。然后,我會重定向回“編輯”頁面。現在,由于已設置 TempData,頁面將重新顯示以用戶上次記入的值初始化的表單。 處理記入、驗證和 TempData 的這個過程現在變得有些煩瑣,并且需要多做一些手動工作。將來發行的版本應包括至少會將一些 TempData 檢查自動化的幫助程序方法。關于 TempData 的最后一個注意事項是:TempData 的內容存儲在用戶的服務器端會話中。如果您關閉會話,TempData 將無法正常工作。
創建控制器 現在,wiki 的基礎已在發揮功效,但繼續進行之前,我想要明確實現中的以下幾個要點。例如,Repository 屬性用于分離 wiki 的邏輯與物理存儲。您可以提供在文件系統(正如我所做的那樣)、數據庫或您想要的任何位置中存儲內容的存儲庫。遺憾的是,我需要解決兩個問題。 首先,我的控制器類與具體的 FileBasedSpaceRepository 類緊密地連在一起。我需要一個默認值,以便在屬性沒有設置時,也能合理使用。更糟的是,磁盤上文件的路徑在這里也是硬編碼的。最起碼,它應取自配置。 其次,我的對象必須依賴存儲庫,否則無法運行。對于良好的設計,存儲庫實際應為構造函數 參數,而不是屬性。但我無法將其添加到構造函數中,因為 MVC Framework 要求控制器上的構造函數不能有參數。 幸運的是,我可以通過一個擴展性掛接擺脫此限制:即控制器工廠。控制器工廠的功能正如其 名稱所指:它創建 Controller 實例。您只需要創建一個類實現 IControllerFactory 接口并向 MVC 系統注冊即可。您可以為所有控制器或僅為指定的類型注冊控制器工廠。圖 13 所示為 WikiPageController 的控制器工廠,其現在將存儲庫作為構造函數參數傳遞。在這種情況下,實現非常煩瑣,但它可以創建能使用更強大工具(特別是依賴關系注入容器)的控制器。 無論如何,現在我擁有了將控制器依賴關系分離到對象中(易于管理和維護)的所有詳細信息。 ?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 類可進行此操作,方法是將以下行添加到 Application_Start 方法中的 Global.asax.cs(路由前后均可): 復制代碼 ControllerBuilder.Current.SetControllerFactory(
typeof(WikiPageController), typeof(WiliPageControllerFactory));
這將注冊 WikiPageController 的工廠。如果此項目中有其他控制器,它們不會使用此工廠,因為此工廠僅針對 WikiPageController 類型進行了注冊。如果您想要將工廠設置為供所有控制器使用,還可以調用 SetDefaultControllerFactory。
其他擴展點 控制器工廠只是框架擴展性的起點。本文中無法詳述所有的細節,因此我將僅僅說明要點。首 先,如果您想要輸出的內容不是 HTML,或想要使用其他模板引擎而不是 Web 窗體,可將控制器的 ViewFactory 設為其他項。您可以實現 IviewFactory 界面,然后即可完全控制如何生成輸出。這對于生成 RSS、XML 或圖形非常有用。 正如您所見到的,路由系統非常靈活。但路由系統中沒有任何內容是 MVC 專用的。每個路由均有一個 RouteHandler 屬性;目前為止,我始終將其設為 MvcRouteHandler。但可以實現 IRouteHandler 界面并將路由系統與其他 Web 技術掛接。將來推出的框架將附帶 WebFormsRouteHandler,并且其他技術也會在將來利用通用路由系統的優勢。 控制器并非必須從 System.Web.Mvc.Controller 衍生。控制器需要做的僅僅是實現 IController 界面,該界面只有稱為 Execute 的一種方法。您可以從中進行任何操作。另一方面,如果您想將 Controller 基類的幾種行為組合在一起,您可以覆蓋 Controller 的許多虛擬函數:
- OnPreAction、OnPostAction 和 OnError 可讓您將每個已執行操作上的預處理和后處理連接起來。OnError 為您提供在控制器內處理錯誤的機制。
- 當 URL 路由到控制器但控制器沒有實現路由中請求的操作時,會調用 HandleUnknownAction。默認情況下,此方法會拋出一個異常,但您可以用所需的操作覆蓋默認值。
- InvokeAction 是一種方法,它負責解決調用何種操作方法并進行調用。如果您想要自定義過程(例如,除去 [ControllerAction] 屬性的要求),應使用該方法。
要告別 Web 窗體嗎? 現在您可能在想:“Web 窗體會面臨怎樣的命運?MVC 會取代它嗎?”答案是否定的!Web 窗體是一種普及技術,Microsoft 將繼續支持并改進它。它在許多應用程序中發揮著重要的作用;例如,可使用 Web 窗體創建典型的 Intranet 數據庫報表應用程序,所花的時間比使用 MVC 編寫短得多。此外,Web 窗體支持大量的控件,許多控件均具備非常先進的功能,可以大大提高效率。 那么,什么時候應該選擇 MVC 呢?這主要取決于您的要求和喜好。您是否正在為獲得想要的 URL 格式而煩惱?您是否想要對 UI 進行單元測試?以上情況均需要依靠 MVC。反之,如果您要顯示許多數據,提供可編輯的網格和優良的樹形視圖控件?那么,您暫時最好還是使用 Web 窗體。 今后,MVC Framework 很可能在 UI 控制部分有所改進,但在便利性上,它可能始終不及 Web 窗體,因為后者具備大量拖曳功能。同時,ASP.NET MVC Framework 為 Web 開發人員提供了一種在 Microsoft .NET Framework 中構建 Web 應用程序的新方法。Framework 針對可測試性設計、推倡使用 HTTP 并且幾乎在每個點均可擴展。對于那些想要完全控制其 Web 應用程序的開發人員來說,這是一個對 Web 窗體的誘人補充。
Chris Tavares 是 Microsoft 模式和實施方案小組的一名開發人員,他致力于幫助開發社區了解在 Microsoft 平臺上構建系統的最佳實踐。他還是 ASP.NET MVC 小組的虛擬成員,幫助您設計新的框架。可以通過 cct@tavaresstudios.com 與 Chris 取得聯系。
轉載于:https://www.cnblogs.com/bndy/archive/2010/07/19/1780491.html
總結
以上是生活随笔為你收集整理的ASP.NET MVC: 构建不带 Web 窗体的 Web 应用程序(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Windows 7解决GAC错误
- 下一篇: 使用代理时服务变量的变化