沒有 Web Form 的 Web 應用程式

本文是根據 ASP.NET MVC Framework 的搶鮮版所撰寫,因此文中的資訊有可能變更。     
本文探討:       
  • 模型檢視控制器 (Model View Controller) 模式           
  • 建立控制器 (Controller) 與檢視 (View)           
  • 建置表單 (Form) 及回傳           
  • 控制器工廠 (Controller Factory) 與其他擴充點           
本文使用技術:       
ASP.NET       
下載程式碼位址:  MVCFramework2008_03.exe     (189 KB) 
Browse the Code Online
          我 擔任專業開發人員至今已有 15 年左右,在這之前則至少有 10 年是軟體開發的熱衷者。就像當時大多數人一樣,我最先接觸的是 8 位元電腦,然後再進入 PC 平台。我所使用的電腦越來越複雜,並開始撰寫具備各種功能的應用程式,從簡單的遊戲、個人資料管理,甚至還有可控制外部硬體的應用程式等等。
不過,在整個經歷的前半段,我寫的所有軟體都有一個共同點:它們都是在使用者桌上型電腦上執行的本機應用程式。1990 年代初,我開始耳聞全球資訊網 (World Wide Web) 這個新鮮的玩意。我看到一個建置 Web 應用程式的機會,這個應用程式能夠讓我不必親自從工作現場趕回辨公室,就能輸入打卡資訊。
整個經驗一言以蔽之,就是大受挫折。想盡辦法處理無狀態的 Web 就是和我的桌上型電腦導向心態不合。再加上缺乏優良的偵錯功能、我沒有根目錄存取權的 UNIX 伺服器,以及一些奇奇怪怪的角括弧,年輕的心性很快地就讓我垂頭喪氣地一頭鑽回桌上型電腦應用程式的開發,這一來又是好幾年。
我不再嘗試 Web 開發;Web 無疑是很重要的,但是我當時並未真正了解它的程式設計模型。之後 Microsoft ® .NET Framework 與 ASP.NET 問世了。終於有一個架構可以讓我開發 Web 應用程式,而且這個架構幾乎就像桌上型電腦應用程式的程式設計。我可以建置視窗 (頁面)、結合控制項與事件,而且設計工具還貼心地幫我免除了處理那些角括弧的麻煩。最棒的是,ASP.NET 能夠以檢視狀態自動幫我處理 Web 的無狀態性質!我又再次成為快樂的程式設計師 ... 至少有那麼一陣子啦。
隨著我經驗的累積,設計的選擇也多了起來。我學到處理桌上型電腦應用程式時所套用的許多最佳作法。其中兩個是:
  • 分離問題:不要將 UI 邏輯與基礎行為混在一起。
  • 自動化單元測試:撰寫驗證程式碼行為是否合乎預期的自動化測試。
這些基本原則適用於所有的技術。分離問題是可幫助您應付複雜性的基本原則。在同一個物件中混合不同的責任 (例如,計算剩餘的工時、格式化資料及繪圖) 只會造成維護上的問題。而自動化測試對於可盡快得到實際執行程式碼 (同時還要避免您手動測試到抓狂) 則非常重要,尤其是在更新現有的專案時。
ASP.NET Web Form 降低了門檻,不過若要嘗試將我的設計原則套用到 Web 應用程式則很難。Web Form 很殘酷地以 UI 為中心;基本元素是頁面。您首先要設計 UI 和拖曳控制項。只要隨意地將應用程式邏輯放入頁面的事件處理常式中 (很像使用 Visual Basic ® 為 Windows ® 應用程式提供的功能),這是很吸引人的。
此外,頁面的單元測試往往很難進行。不啟動完整的 ASP.NET 就無法執行整個 Page 物件週期。雖然可以傳送 HTTP 要求至伺服器或讓瀏覽器自動執行來測試 Web 應用程式,但是這樣的測試很容易發生問題 (變更一個控制項 ID,測試就會中斷)、很難設定 (每一個開發人員的電腦上都必須以完全相同的方式設定伺服器),而且執行速度很慢。
當我開始建置較複雜的 Web 應用程式時,Web Form 提供的抽象概念 (例如控制項、檢視狀態和頁面週期) 便開始弊大於利。我開始花費越來越多的時間在設定資料繫結 (以及撰寫無數的事件處理常式以保持正確的設定)。我必須想出如何縮減檢視狀態大小,好加快我的頁面載入速度。Web Form 要求每個 URL 都要有實際檔案存在,這在動態的站台上 (例如 Wiki) 是很難達到的。而且自訂 WebControl 的撰寫是一個非常複雜的程序,需要充份了解網頁週期和 Visual Studio ® 設計工具。
來到 Microsoft 工作之後,我才有機會分享出我對 .NET 各種痛苦點的了解,也希望能藉此幫助大家減輕這方面的痛苦。最近由於參與模式和實例的 Web 用戶端軟體工廠 (Web Client Software Factory) 專案 ( codeplex.com/websf) 擔任開發人員,因而又有這樣的機會。尤其是,模式和實例的遞交項目中內建的功能,就包含自動化單元測試。在 Web 用戶端軟體工廠中,我們建議使用模型檢視呈現器 (Model View Presenter,MVP) 模式建置可測試的 Web Form。
簡而言之,MVP 不是將您的邏輯放在頁面中,而是讓您建置單純呼叫另一個物件 (Presenter) 的頁面 (View)。然後呈現器 (Presenter) 物件再執行回應檢視活動所需的任何邏輯,通常是使用其他物件 (Model) 來存取資料庫、執行商務邏輯...等等。這些步驟完成之後,呈現器 (Presenter) 就會更新檢視 (View)。這種方法會為您提供可測試性,因為呈現器與 ASP.NET 管線分開;它會透過介面與檢視通訊,而且可以脫離頁面單獨測試。
MVP 有效,但是實作稍嫌不便;您需要另一個檢視介面,而且必須在程式碼後置檔案中撰寫許多事件轉送函式。不過您若是希望 Web Form 應用程式中的 UI 能夠測試,這應該就是最佳的技巧。若想要進一步的改進,就需要變更基礎平台。

模型檢視控制器 (Model View Controller) 模式
幸好,ASP.NET 團隊有聽進像我這樣的開發人員的心聲,並已著手開發新的 Web 應用程式架構,這個架構會和您已熟悉並且愛用的 Web Form 並存,只是設計目標明顯不同:
  • 運用 HTTP 與 HTML -- 不要隱藏。
  • 從頭到尾都內建可測試性。
  • 幾乎可在每一點進行延伸。
  • 讓您完全控制輸出。
這個新架構是以模型檢視控制器 (Model View Controller,MVC) 模式為基礎,因此命名為 ASP.NET MVC。MVC 模式最初是在 1970 年代發明的,屬於 Smalltalk 的一部分。我會在本文中示範,它其實非常適合 Web 的性質。MVC 會將您的 UI 劃分成三種不同的物件:控制器,負責接收及處理輸入;模型,包含網域邏輯;以及檢視,負責產生輸出。在 Web 的內容中,輸入是 HTTP 要求,要求流程如 [圖 1] 所示。
Figure 1 MVC 模式的要求流程 (按影像可放大)
這實際上與 Web Form 的程序大不相同。在 Web Form 模型中,輸入會進入頁面 (View),而且檢視 (View) 會負責處理輸入及產生輸出。而在 MVC 中,這兩個責任是分開的。
您現在的想法可能是下列兩者中的一種。可能是「太棒了。要怎麼使用呢?」,或者是「以前只需要撰寫一個物件,現在為什麼要撰寫三個呢?」。這兩個都是很好的問題,最佳的回答就是使用範例來說明。所以我要使用 MVC Framework 撰寫一個小的 Web 應用程式,來證明它的優勢。

建立控制器 (Controller)
如果要跟著做,您必須安裝 Visual Studio 2008 並取得 MVC Framework。在撰寫本文時,它是包含在 ASP.NET Extensions (asp.net/downloads/3.5-extensions) 的 December 2007 Community Technology Preview (CTP) 中。您必須取得 CTP 和 MVC Toolkit 擴充功能,這兩者當中包含一些非常有用的協助程式物件。下載並安裝 CTP 之後,您的 [新增專案 (New Project)] 對話方塊中會有一個新專案類型,名為 [ASP.NET MVC Web Application]。
選取 [MVC Web Application] 專案,會得到一個外觀與一般網站或應用程式稍有不同的方案。方案範本會建立一個包含一些新目錄的 Web 應用程式 (如 [圖 2] 所示)。其中,[Controllers] 目錄會包含控制器 (Controller) 類別,而 Views 目錄 (及其全部子目錄) 則會包含檢視 (View)。
Figure 2 MVC 專案結構
我將要撰寫一個非常簡單的控制器,它會傳回在 URL 中傳入的名稱。在 [Controllers] 資料夾上按一下滑鼠右鍵,再選擇 [加入項目 (Add Item)],就會顯示平常的 [加入新項目 (Add New Item)] 對話方塊,其中有一些新增項目,包括一個 MVC Controller Class 和許多 MVC View 元件。在這個範例中,我要新增一個名為 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 Form 頁面類別有多困難的評語嗎?但是控制器的測試就容易多了。事實上,控制器可以直接具現化,而且不需要其他任何基礎結構就能呼叫動作方法。您不需要 HTTP 內容,也不需要伺服器,只需要測試控管。我在 [圖 3] 中有提供這個類別的 Visual Studio Team System (VSTS) 單元測試做為範例。
複製程式碼            
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"];
        }
    }

}

這裡有許多動作。實際的測試非常簡單:具現化控制器、使用預期的資料來呼叫方法,然後確認呈現的檢視正確無誤。我檢查的方式是建立一個 Test 專用子類別,這個子類別會覆寫 RenderView 方法。如此我就能略過 HTML 的實際建立。我只需在乎是否傳送了正確的資料到檢視,而且也呈現了正確的檢視。在此一測試中,我不需要關心檢視本身的基礎詳細資料。

建立檢視 (View)
當然,我終究還是需要產生一些 HTML,所以現在就來建立這個 HiThere 檢視。首先,我要在方案中的 Views 資料夾內,建立一個名為 [Hello] 的新資料夾。根據預設,控制器會在 Views\<ControllerPrefix> 資料夾中尋找檢視 (控制器前置詞是控制器類別的名稱減去「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 Form 頁面具有的任何事件處理或複雜的控制項。
但是 MVC Framework 確實有借用 .aspx 檔案格式,因為這是好用的文字樣板化語言。如果您想要,甚至可以使用程式碼後置,不過預設的程式碼後置檔案看起來像這樣:
                複製程式碼            
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Views.Hello
{
    public partial class HiThere : ViewPage
    {
    }
}
沒有頁面 Init 或 load 方法、沒有事件處理常式,除了基底類別的宣告以外沒有其他東西,基底類別不是 Page,而是 ViewPage。MVC 檢視所需要的只有這些。執行應用程式,巡覽至 http://localhost:<port>/Hello/HiThere/Chris,您就會看到類似 [圖 5] 的內容。
Figure 5 成功的 MVC 檢視 (按影像可放大)
如果您看到的不是 [圖 5],而是討厭的例外狀況,別緊張。如果您在 Visual Studio 中設定按 F5 時的使用中文件是 HiThere.aspx 檔案,Visual Studio 會嘗試直接存取 .aspx 檔案。因為 MVC 檢視在顯示之前,必須已經先執行控制器,所以嘗試直接巡覽至頁面並不會正常運作。只要編輯 URL,使其和 [圖 5] 中顯示的一樣,應該就能正常運作。
MVC Framework 如何知道要呼叫我的動作方法呢?該 URL 甚至連副檔名都沒有。答案在於 URL 路由。如果您查看 global.asax.cs 檔案的內容,就會看到如 [圖 6] 中的一段程式碼。全域 RouteTable 會儲存一組 Route 物件。每個 Route 則描述一個 URL 表單,以及如何處理該表單。根據預設,會在表格中加入兩個路由。發揮魔力的是第一個。它會指明每個 URL 在伺服器名稱後面都有三個部分,第一部分應做為控制器名稱,第二部分應做為動作名稱,而第三部分應做為 ID 參數:
複製程式碼            
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)
  });
就只需前往 http://localhost/Chris,而且一樣會叫用我的動作,然後我會看到熟悉親切的問候語。
系統怎麼知道要叫用哪個控制器和動作呢?答案在 Defaults 參數中。它使用新的 C# 3.0 匿名型別語法,建立了一個虛擬字典。Route 的 Defaults 物件可以包含任意的額外資訊,不過在 MVC 中,它也可以包含一些已知的項目:控制器與動作。如果 URL 中沒有指定控制器或動作,它會使用 Defaults 中的名稱。因此我才能在 URL 中省略控制器或動作,而一樣能讓要求對應到正確的控制器與動作。
還有一點要注意的:記得我說過「加入表格的最上方」嗎?如果加入到最下方,將會發生錯誤。路由的工作方式是先到先處理。處理 URL 時,路由系統會從表格的最上方往下處理,並使用第一個符合的路由。在此案例中,預設的路由「[controller]/[action]/[id]」符合,因為動作與 ID 都有預設值。因此,它會尋找 ChrisController,但是因為我沒有控制器,所以會發生錯誤。

大一點的範例
我已經示範過 MVC Framework 的基本功能,接下來要示範一個大一點的範例,除了顯示字串以外,還要做點別的。Wiki 是可以在瀏覽器中編輯的網站。很容易就能新增或編輯它的網頁。我使用 MVC Framework 寫了一個小小的範例 Wiki。 [圖 7] 中顯示 [編輯這個頁面 (Edit This Page)] 畫面。
Figure 7 編輯首頁 (按影像可放大)
您可以檢查本文的程式碼下載,看看基本 Wiki 邏輯的實作情形。現在我要集中討論 MVC Framework 如何讓 Wiki 很容易在 Web 上運作。首先要設計我的 URL 結構。我希望有這樣的結構:
  • /[pagename] 顯示這個名稱的頁面。
  • /[pagename]?version=n 顯示要求的頁面版本,0 = 目前版本、1 = 前一版...依此類推。
  • /Edit/[pagename] 開啟該頁面的編輯畫面。
  • /CreateNewVersion/[pagename] 張貼以便提交編輯結果的目標 URL。
首先讓我們從 Wiki 頁面的基本顯示開始。為此我建立了一個新類別,名為 WikiPageController。然後加入名為 ShowPage 的動作。WikiPageController 一開始如 [圖 8] 所示。ShowPage 方法很簡單。WikiSpace 與 WikiPage 類別分別代表一組 Wiki 頁面和特定的頁面 (及其修訂版)。這個動作只會載入模型及呼叫 RenderView。但是「new WikiPageViewData」這一行有什麼作用呢?
複製程式碼            
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 
      });
  }
}

前面的範例示範了從控制器傳遞資料到檢視的一種方法:ViewData 字典。字典很方便,不過也很危險。字典可能包含任何東西,內容不會有任何 IntelliSense ®,而且因為 ViewData 字典的型別是 Dictionary<string, object>,若要使用它的內容,就必須為一切進行轉型。
當您知道檢視中需要什麼資料時,就可以改成傳遞強型別的 ViewData 物件。在我的案例中,我建立了一個簡單的物件 WikiPageViewData,如 [圖 9] 所示。這個物件會將 Wiki 頁面資訊傳送到檢視,同時傳送的還有幾個公用程式方法,以執行諸如取得 Wiki 標記之 HTML 版本等工作。
複製程式碼            
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 標記,以配置頁面、填寫版本清單,再以平常的內容預留位置結尾。
複製程式碼            
<%@ 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 的呼叫。這是呈現協助程式的例子。各個檢視類別都有兩個屬性,Html 和 Url。每一個都有一些實用的方法可輸出 HTML 段落。在這個案例中,Html.ActionLink 會接受一個物件 (屬於匿名型別),然後透過路由系統執行。這會產生一個 URL,以路由到我指定的控制器和動作。如此一來,不論我如何變更路由,「編輯這個頁面 (Edit This Page)」連結永遠會指向正確的位置。
您可能也注意到,我還必須時常手動建置連結 (到舊版頁面的連結)。可惜的是,目前的路由系統在產生有關查詢字串的 URL 方面不是很好用。這在未來的架構版本裡應該會改進。

建立表單 (Form) 及回傳
現在來看看控制器上的 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 協助程式來產生表單回傳的 URL。另外還使用了多個 HTML 協助程式,例如 TextBox、TextArea 及 SubmitButton。它們執行的動作與預期一致:產生各個輸入欄位的 HTML。
複製程式碼            
<%@ 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 來儲存先前輸入的資訊,以便重新填入表單。這在 Web Form 中使用 ViewState 很容易就能做到,因為控制項的內容全都會自動儲存。
我也想要在 MVC 中這麼做,因此要使用 TempData。TempData 是一個字典,很像不具型別的 ViewData。不過,TempData 的內容只會在單一要求中存活,之後就會刪除。若要了解它的用法,請參閱 [圖 12],NewVersion 動作。
複製程式碼            
[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 物件。這是另一個協助程式物件,具有一些屬性和方法可儲存張貼和一些驗證的內容。我使用 MVC Toolkit 中的一個協助程式函式來載入 postData 物件。UpdateFrom 實際上是工具組提供的一個擴充方法,其中會使用反映來對應表單欄位名稱與我物件之屬性的名稱。結果便是所有的欄位值都會載入到我的 postData 物件中。但是,使用 UpdateFrom 有一個缺點,就是會直接從 HttpRequest 取得表單資料,致使單元測試更加困難。
NewVersion 最先檢查的是 SubmitAction。如果使用者按一下 [確定 (OK)] 按鈕,而且真正想要張貼編輯過的頁面,就不會有什麼問題。如果在這裡碰到其他值,動作將會重新導向回到 ShowPage,而這只會重新顯示原始頁面。
如果使用者確實有按一下 [確定 (OK)],那麼接著我會檢查 postData.Errors 屬性。這會簡單地驗證張貼內容。如果沒有任何錯誤,我再執行將新版頁面寫回 Wiki 的處理。但是,萬一發生錯誤,事情就好玩了。
如果發生錯誤,我就要設定 TempData 字典的各個欄位,使其包含 PostData 的內容。然後重新導向回到編輯頁面。接下來,由於已設定 TempData,頁面會重新顯示,且表單會以使用者上次張貼的值初始化。
處理張貼、驗證及 TempData 的這整個程序目前還有點麻煩,需要較多手動作業,而事實上應該可以不需要這麼多手動作業。未來的版本應該會包含一些協助程式方法,以自動化一些 TempData 的檢查。最後一點有關 TempData 的注意事項:TempData 的內容會儲存在使用者的伺服器端工作階段。如果關閉工作階段,TempData 將無法正常作用。

建立控制器 (Controller)
Wiki 的基本功能現在已經可以正常運作,但是在繼續下一步之前,實作中還有幾點必須先整理好。例如,儲存機制 (Repository) 屬性會用來解除 Wiki 的邏輯和實體儲存體的聯繫性。您可以提供存放庫,將內容儲存在檔案系統上 (就像我的作法)、資料庫中,或者您要的其他任何位置。但是,有兩個問題必須解決。
第一,我的控制器類別與實體的 FileBasedSpaceRepository 類別有緊密聯繫。我需要有預設值,以防一旦未設定屬性,我還有其他適當的內容可以使用。更糟的是,磁碟上的檔案路徑在這裡是使用硬式編碼。此內容應該來自組態才對。
第二,儲存機制其實是必要的相依性,我的物件沒有它就執行不了。良好的設計會讓儲存機制是建構函式參數,而非屬性。但是我不能將它加入建構函式,因為 MVC Framework 的控制器需要無引數的建構函式。
幸好有一個可以讓我脫離此一困境的擴充性攔截程序:控制器工廠 (Controller Factory)。控制器工廠的功能正如其名:它會建立控制器 (Controller) 執行個體。您只需要建立一個實作 IControllerFactory 介面的類別,並在 MVC 系統中登錄。您可以登錄所有控制器或特定型別控制器的控制器工廠。 [圖 13] 顯示 WikiPageController 的控制器工廠,使儲存機制當成建構函式參數傳遞。
複製程式碼            
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 Form 以外的樣板化引擎,可以將控制器的 ViewFactory 設定為其他內容。您可以實作 IViewFactory 介面,然後就能完全控制產生輸出的方式。這在產生 RSS、XML,甚至是圖形時很有用。
您已經看到,路由系統非常有彈性。但是路由系統中並沒有 MVC 特定的部分。每個路由都有一個 RouteHandler 屬性;到目前為止,我都是將它設定為 MvcRouteHandler。但是也可以實作 IRouteHandler 介面,並使路由系統結合其他 Web 技術。未來的架構版本將包含 WebFormsRouteHandler,而其他技術未來將可利用泛型路由系統。
控制器 (Controller) 將不必從 System.Web.Mvc.Controller 衍生。控制器只需要實作 IController 介面,它只有一個名為 Execute 的方法。如此一來,您就可以執行自己想要的任何作業。另一方面,如果您只想調整基底控制器 (Controller) 類別的幾個行為,則控制器 (Controller) 有許多可讓您覆寫的虛擬函式:
  • OnPreAction、OnPostAction 及 OnError 可讓您在執行的每個動作上攔截泛型的前置與後置處理程序。OnError 則會提供整個控制器的錯誤處理機制。
  • URL 路由到控制器但控制器並未實作路由中要求的動作時,就會呼叫 HandleUnknownAction。根據預設,這個方法會擲回例外狀況,不過您可以加以覆寫,以執行自己想要的任何動作。
  • InvokeAction 這個方法會指出要呼叫的動作方法,並呼叫該方法。如果您想要自訂程序 (例如,免除 [ControllerAction] 屬性的需求),就是在這裡自訂。
控制器 (Controller) 還有其他許多虛擬方法,不過主要是做為測試的攔截點,而非延伸點。例如,RedirectToAction 是虛擬的,以便讓您建立一個不會實際重新導向的衍生類別。這可以讓您在不需要執行完整 Web 伺服器的情形下,測試重新導向的動作。

Web Form 要走入歷史了嗎?
您現在可能會感到困惑,「Web Form 究竟怎麼了?會被 MVC 取代嗎?」。答案是否定的!Web Form 是大家熟悉的技術,Microsoft 將會繼續支援和加強此 Web Form。Web Form 在許多應用程式上表現良好;例如,典型的內部網路資料庫報告應用程式若使用 Web Form 建立,所花的時間就遠少於使用 MVC 撰寫。再加上 Web Form 支援大量的控制項,其中有許多非常複雜,並可節省大量的工作。
那麼,什麼時候應該優先選擇 MVC,而不是 Web Form 呢?這往往取決於您的需求和喜好。您是否面臨讓 URL 格式合乎自己期望的困擾?您是否想要執行 UI 的單元測試?如果是上述任一種情況,可能就需要 MVC。另一方面,您是否需要顯示許多資料,且要有可編輯的資料格和別緻的樹狀檢視控制項?如果是,那麼 Web Form 會是較理想的選擇。
MVC Framework 以後可能會加強 UI 控制項方面,不過很可能永遠無法像 Web Form 那麼容易上手,因為 Web Form 裡有大量的功能只要拖放就能完成。然而 ASP.NET MVC Framework 為 Web 開發人員提供了在 Microsoft .NET Framework 中建置 Web 應用程式的嶄新方法。Framework 是針對可測試性所設計,也會充分運用 HTTP 而不是想辨法隱藏它,而且幾乎每個點都能夠延伸。對於想要完全駕馭 Web 應用程式的開發人員而言,它是 Web Form 的最佳輔助利器。

Chris Tavares 是 Microsoft 模式和實例團隊的開發人員,負責幫助開發社群了解在 Microsoft 平台上建置系統的最佳作法。他也是 ASP.NET MVC 團隊的虛擬成員,協助設計新架構。Chris 的連絡方式為: cct@tavaresstudios.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值