本文章是由機器翻譯。

技術最前線

MVP 模式改進 Web Form

Dino Esposito

模型-視圖-控制器 (MVC) 模式的出現是軟體發展方面的一個重要里程碑。該模式表明,如果在設計應用程式時,注意將關注點分離開來,會對開發流程和最終應用程式均有改善作用。此外,該模式還提供了一種可再現的方法來將其應用於實踐。

但是 MVC 並不完美,因此多年來出現了若干種變化版本。

由於它是在 20 世紀 80 年代設計的,浮現出來的一個問題是 MVC 不能直接適應 Web 開發。改造 MVC 使之適應 Web 需要,又花費了數年時間,這也導致開發了更多特定的 MVC 模式,例如 Model2。(Model2 是由 Castle MonoRail 和 ASP.NET MVC 實現的 MVC 實際風格。)

在更常規的環境中,模型-視圖-表示器 (MVP) 模式是 MVC 的一種演變,通過在中間加入控制器作為調節器將視圖與模型巧妙地分開。图 1 描述了使用 MVP 模式設計的應用程式的行為。

图 1 使用 MVP 模式

在本文中,將首先介紹對於 ASP.NET Web 表單來說,可能(並且相對標準)的 MVP 模式實現,然後討論該模式的應用,為團隊帶來的益處,以及將該模式與 ASP.NET MVC 和模式-視圖-視圖模型 (MVVM) 進行對比(假定已在 Windows Presentation Foundation (WPF) 和 Silverlight 中實現該模式)。

MVP 概覽

MVP 是 Taligent(現在歸屬於 IBM)在 20 世紀 90 年代開發的原始 MVC 模式的一種衍生模式。有關 MVP 及其背後概念的介紹,可從 wildcrest.com/Potel/Portfolio/mvp.pdf 下載相關文章。

MVP 的創建者將模型(在視圖中處理的資料)與視圖/控制器對巧妙地分開。他們還將控制器重命名為表示器,以強化一種概念,即在該模式中,控制器的角色就是使用者與應用程式之間的調節器的角色。表示器是向使用者“呈現”UI 並接受使用者所發出命令的元件。表示器包含大多數表示邏輯,知道如何處理視圖和系統的其餘部分,包括後端服務和資料層。

MVP 中的一項重要創新是,視圖的詳細資訊抽象為介面(或基類)。表示器與視圖的抽象層交互,使表示器自身成為一個再使用性和可測試性均很高的類。這樣可實現兩種有意思的方案。

首先,表示邏輯獨立于將要使用的 UI 技術。因此,可在 Windows 和 Web 展示層中重用同一控制器。最後,針對某一介面為表示器編碼,該表示器可以與公開該介面的任何物件進行交互,而無論該物件是 Windows 表單物件、ASP.NET 頁面物件還是 WPF 視窗物件。

其次,同一表示器可以處理同一應用程式的不同視圖。這是關於軟體即服務 (SaaS) 方案的一個重要成就,在這些方案中,應用程式託管在 Web 伺服器上,並作為服務提供給每一個要求使用自訂 UI 的客戶。

毫無疑問,這兩項優點都不一定適用于所有情況。這些優點是否會給您帶來益處,很大程度上取決於您期望在 Windows 和 Web 前端中使用的應用程式和導航邏輯。但是,當邏輯相同時,您可以通過 MVP 模型進行重用。

運行中的 MVP

實現 MVP 模式時,第一步是為每個需要的視圖定義抽象。ASP.NET 應用程式中的每個頁面和 Windows(或 WPF/Silverlight)應用程式中的每個表單均有其各自的介面,可與展示層的其餘部分交互。介面標識視圖支援的資料模型。無論哪種平臺,每個邏輯上等效的視圖都具有相同的介面。

視圖抽象包含視圖可識別和處理的模型,並且可以使用一些有用的特殊方法和事件來擴展模型,説明表示器與視圖之間實現流暢交互。图 2 顯示了圖 3 中所呈現的視圖的可能抽象,該抽象正被一個簡單的待辦事項清單應用程式使用。

图 2 视图抽象的示例

public interface IMemoFormView {
  String Title { get; set; }
  String Summary { get; set; }
  String Location { get; set; }
  String Tags { get; set; }
  DateTime BeginWithin { get; set; }
  DateTime DueBy { get; set; }
  String Message { get; set; }

  Int32 GetSelectedPriorityValue();
  void FillPriorityList(Int32 selectedIndex);
  Boolean Confirm(String message, String title);
  void SetErrorMessage(String controlName);
}

圖 3 中,您還會看到介面中的成員如何與表單中的可視元素匹配。

圖 3 將介面的成員綁定到可視元素

基本要點是表示器與 UI 之間的任何交互都必須通過視圖的約定進行。任何按鈕按一下、任何選擇和任何鍵入都必須轉發到表示器,並由其進行處理。如果表示器需要查詢視圖中的某些資料,或者將資料向下傳遞到視圖,介面中應該有方法負責執行該操作。

實現視圖約定

表示視圖的介面必須由表示視圖自身的類實現。如前所述,視圖類是 ASP.NET 中的頁面、Windows 表單中的表單、WPF 中的視窗和 Silverlight 中的使用者控制項。图 4顯示了一個 Windows 表單示例。

圖 4 視圖類的可能實現

public partial class MemoForm : Form, IMemoFormView {
  public string Title {
    get { return memoForm_Text.Text; }
    set { memoForm_Text.Text = value; }
    ...
}

  public DateTime DueBy {
    get { return memoForm_DueBy.Value; }
    set { memoForm_DueBy.Value = value; }
  }

  public int GetSelectedPriorityValue() {
    var priority = 
      memoForm_Priority.SelectedItem as PriorityItem;
    if (priority == null)
      return PriorityItem.Default;
    return priority.Value;
  }

  public void FillPriorityList(int selectedIndex) {
    memoForm_Priority.DataSource = 
      PriorityItem.GetStandardList();
    memoForm_Priority.ValueMember = "Value";
    memoForm_Priority.DisplayMember = "Text";
    memoForm_Priority.SelectedIndex = selectedIndex;
  }

  public void SetErrorMessage(string controlName) {
    var control = this.GetControlFromId(controlName);
    if (control == null)
      throw new NullReferenceException(
        "Unexpected null reference for a form control."); 

    memoForm_ErrorManager.SetError(control, 
      ErrorMessages.RequiredField);
  }

  ...
}

如您所見,屬性是以可視控制項上某些屬性的包裝形式實現的。 例如,Title 屬性是 TextBox 控制項的 Text 屬性的包裝。 同樣,DueBy 屬性是 DatePicker 控制項的 Value 屬性的包裝。 更重要的是,介面為表示器類遮罩給定平臺的 UI 詳細資訊。 對於所創建的用於與 IMemoFormView 介面交互的相同表示器類,該類可以處理實現該介面的任何物件,很好地忽略底層控制項的程式設計介面的詳細資訊。

如何處理需要資料集合的 UI 元素,例如下拉清單? 是否應使用資料綁定(如圖 4 所示),或應選用使視圖保持被動狀態並且沒有任何表示邏輯的更簡單方法?

這將由您來決定。 為了解決這些類型的問題,MVP 模式被分成兩種單獨模式 - Supervising Controller 和 Passive View,兩者的主要區別僅僅在於視圖中的代碼數量。 如果使用資料綁定填充 UI(請參閱圖 4),將會向視圖中添加某些表示邏輯,使其獲得 supervising controller 的風格。

視圖中包含的邏輯越多,測試時應該愈小心。 測試 UI 不是一件可輕鬆自動完成的任務。 轉向使用 supervising controller 還是選擇更精細和更固定的視圖,都僅僅是一種主觀判斷。

表示器類

視圖中的控制項捕獲任何使用者操作並觸發視圖中的事件,例如按鈕按一下或索引選擇更改。 視圖包含簡單的事件處理常式,向負責視圖的表示器分派調用。 首次載入視圖時,它會創建其表示器類的一個實例,並在內部將其保存為一個私有成員。 图 5 顯示了 Windows 表單的典型構造函數。

圖 5 創建一個 MVP 表單

public partial class Form1 : 
  Form, ICustomerDetailsView {

  private MemoFormPresenter presenter;

  public Form1() {
    // Framework initialization stuff
    InitializeComponent();
    // Instantiate the presenter
    presenter = new MemoFormPresenter(this);
    // Attach event handlers
    ...
}

  private void Form1_Load(
    object sender, EventArgs e) {

    presenter.Initialize();
  }
  ...
}

表示器類通常通過其構造函數接收對視圖的引用。 視圖保留對表示器的引用,表示器保留對視圖的引用。 但是,表示器僅通過約定瞭解視圖。 表示器的工作是將其接收到的任何視圖物件與其約定的視圖介面分開。 图 6 顯示出表示器類的基本內容。

圖 6 一個表示器類示例

public class MemoFormPresenter {
  private readonly IMemoFormView view;

  public MemoFormPresenter(IMemoFormView theView) {
    view = theView;
    context = AppContext.Navigator.Argument 
      as MemoFormContext;
    if (_context == null)
      return;
  }
 
  public void Initialize() {
    InitializeInternal();
  }

  private void InitializeInternal() {
    int priorityIndex = _context.Memo.Priority;
    if (priorityIndex >= 1 && priorityIndex <= 5)
      priorityIndex--;
    else
      priorityIndex = 2;

    if (_context.Memo.BeginDate.HasValue)
      _view.BeginWithin = _context.Memo.BeginDate.Value;
    if (_context.Memo.EndDate.HasValue)
      _view.DueBy = _context.Memo.EndDate.Value;
      _view.FillPriorityList(priorityIndex);
      _view.Title = _context.Memo.Title;
      _view.Summary = _context.Memo.Summary;
      _view.Tags = _context.Memo.Tags;
      _view.MemoLocation = _context.Memo.Location;
  }
  ...
}

構造函數接收並緩存對視圖的引用,使用約定所表示的公共介面初始化視圖。 您看到的圖 6 代碼中使用的上下文物件,是表示器為了初始化視圖而需要從調用方接收的任何輸入資料。 並非在所有情況下都需要此資訊,但是當您使用表單編輯某些資料或者用對話方塊顯示某些資訊時,事實證明此資訊是必需的。

初始化視圖與向類的成員賦值一樣簡單,只不過現在的任何賦值操作都會導致 UI 更新。

表示器類還包含大量方法,執行這些方法可回應來自 UI 的任何請求。 任何按一下或使用者操作都綁定到表示器類的方法:

private void memoForm_OK_Click(
  object sender, EventArgs e) {
  presenter.Ok();
}

表示器方法使用視圖引用訪問輸入值,並按同樣方式更新 UI。

MVP 導航

表示器也負責在應用程式中導航。尤其是,表示器負責對下一個視圖啟用(或禁用)子視圖和命令導航。

子視圖本質上是視圖的一個子集。子視圖通常是一個可以根據上下文展開或折疊的面板,或者是一個子視窗(強制回應視窗或無強制回應視窗)。表示器通過視圖介面的成員(大多數是布林值成員)控制子視圖的可見性。

將控制項傳送到其他視圖(和表示器)會如何呢?創建一個表示應用程式控制器的靜態類,即保留所有邏輯來確定下一個視圖的中心主控台。图 7 顯示出應用程式控制器圖。

图 7 应用程序控制器

應用程式控制器類表示表示器調用以導航到其他位置的 Shell。這個類將用 NavigateTo 方法實現工作流,以確定下一個視圖或者僅移動到指定的視圖。工作流可以是任意形式 – 可以與實際工作流一樣複雜,也可以與一個 IF 語句序列那樣簡單。在應用程式控制器中可按靜態方式對工作流邏輯進行編碼,也可以從外部可插入元件導入工作流邏輯(請參閱 圖 8)。

圖 8 應用程式控制器的實現

public static class ApplicationController {
  private static INavigationWorkflow instance;
  private static object navigationArgument;

  public static void Register(
    INavigationWorkflow service) {
    if (service == null)
      throw new ArgumentNullException();
    instance = service;
  }

  public static void NavigateTo(string view) {
    if (instance == null)
      throw new InvalidOperationException();
    instance.NavigateTo(view);      
  }
 
  public static void NavigateTo(
    string view, object argument) { 
    if (instance == null)
      throw new InvalidOperationException();
    navigationArgument = argument;
    NavigateTo(view);
 }

 public static object Argument {
   get { return navigationArgument; }
 }
}

工作流元件中的實際導航邏輯將使用特定于平臺的解決方案來切換到不同視圖。對於 Windows 表單,它將使用方法打開和顯示表單;在 ASP.NET 中,它將使用 Response 物件的 Redirect 方法。

MVP 和 ASP.NET MVC

ASP.NET MVC 基於 MVC 模式的風格,與 MVP 具有一些共同的特徵。MVC 中的控制器是視圖與後端之間的調節器。控制器不保留對視圖的引用,但會填滿模型物件,並使用中間元件(視圖引擎)的服務將其傳遞到視圖。

在某種程度上,視圖是通過模型進行抽象的,模型的結構將反映視圖及其 UI 的特徵。導航由控制器管理,控制器通過每個操作的上下文來選擇下一個視圖。該任務使用一些內置邏輯完成。如果邏輯對於給定的控制器方法特別複雜,坦率地說,並非每天都會發生某些事情,您始終可以引入工作流元件來確定要選擇的下一個視圖。

Web 表單如何呢?Web 表單非常適用于託管 MVP 實現。但是,應該明確,您所能做的是在回發事件的上下文中添加層。不能包含回發事件之前的任何事情,以及回發事件之後發生的任何事情。擴展到包含整個生命週期的完全 MVP 實現無法在 Web 表單中完成,但是即使圍繞回發事件添加 MVP 也是一件好事,將大大提高
Web 表單頁面的可測試性水準。

MVP 和 MVVM

WPF 與 Silverlight 應用程式的上下文中的 MVP 和 MVVM 如何呢?MVVM 是 MVP 的變體,也稱為表示模型。該概念是,將視圖模型包含到表示器類中,表示器類公開視圖將進行讀寫操作的公共成員。這是通過雙向資料綁定實現的。在一天結束時,您可以調用 MVVM 作為 MVP 的特殊風格,這尤其適合宣導此功能的豐富 UI 和框架(如 WPF)。

在 MVVM 中,視圖與表示器類(視圖模型)的屬性進行了資料綁定。使用者執行任何操作都會更新表示器中的這些屬性。使用者發出的任何請求(WPF 中的命令)通過表示器類的方法進行處理。表示器方法計算出的任何結果都存儲在視圖模型中,並通過資料綁定提供給視圖。在 WPF 和 Silverlight 中,使用 MVP 模式的手動實現不會受到任何干擾。但是,事實證明 Blend 等工具可使通過資料綁定使用 MVVM 更簡單而且更有效。

回發

MVP 提供了有關如何管理視圖堆的指南,但顯然要付出代價:應用程式碼複雜性提高所產生的成本。可以想像,與簡單程式相比,大型應用程式更易於承擔這些成本。因此,MVP 並不只是適用于任何應用程式。根據表示視圖的約定,通過 MVP,設計人員和開發人員可以並行工作,這在任何開發方案中都絕對是一個福音。MVP 將表示器類保留為單獨的類,將其與視圖隔離。在 Web 表單中,MVP 代表至少增強執行回發的代碼的可測試性的唯一合理方法。

Dino Esposito 是 Microsoft Press 出版的《Programming ASP.NET MVC》一書的作者,並且是《Microsoft .NET: Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。Esposito 定居於義大利,經常在世界各地的業內活動中發表演講。您可訪問他的博客,網址為 weblogs.asp.net/despos.

衷心感謝以下技術專家對本文進行了審閱: Don Smith 和 Josh Smith