程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 使用WPF構建復合應用程序的模式

使用WPF構建復合應用程序的模式

編輯:關於.NET

本文將介紹以下內容:

復合應用程序基礎知識

引導程序和模塊初始化

區域和 RegionManager

視圖、命令和事件 本文使用了以下技術:

WPF 復合應用程序指南

內容

問題:單一應用程序

復合應用程序

復合應用程序指南

引導程序和容器

模塊初始化

使用引導程序

模塊和服務

區域和 RegionManager

本地作用區域

視圖

單獨的表示

命令

事件

結束語

Windows ® Presentation Foundation (WPF) 和 Silverlight™ 等技術為開發人員提供了一種簡單的聲明性方法,使他們可以快速輕松地開發出具有豐富用戶體驗的應用程序。但是,盡管這些技術有助於進一步將表示層從邏輯層中分離出來,但它們無法解決如何構建易於維護的應用程序這一老問題。

對於一些較小的項目,具備一定經驗的開發人員應該能夠設計和構建出便於進行維護和擴展的應用程序,此要求並不過分。但是,隨著移動部件的數量(以及使用這些部件的人員)的不斷增加,對項目實施控制的難度開始呈指數級增長。

復合應用程序是專門針對此問題提出的解決方案。在本文中,我將對復合應用程序的定義進行解釋,並說明如何才能構建一個利用 WPF 功能的復合應用程序。隨後,我還會為您介紹 Microsoft 模式和實施方案小組提供的全新 WPF 復合應用程序指南(以前的代號為 "Prism")。

問題:單一應用程序

讓我們通過一個示例來了解復合應用程序的需求。Contoso Financial Investments 提供了一個應用程序用來管理股票投資組合。借助此應用程序,用戶可以查看當前的投資以及與這些投資相關的新項目,還可以將新項目添加到觀察列表以及執行購買/銷售交易。

如果將其構建成具有用戶控件的傳統 WPF 應用程序,首先應構建一個頂層窗口並針對上述各個功能添加用戶控件。在這種情況下,您需要添加以下用戶控件:PositionGrid、PositionSummary、TrendLine 和 WatchList(參見圖 1)。在設計過程中,各個用戶控件都通過在 XAML 中手動操作或使用設計器(如 Expression Blend™)等方式排列在主窗口中。

圖 1 單一應用程序中的用戶控件

然後,使用 RoutedEvents、RoutedCommands 和數據綁定將所有內容連接起來。有關此主題的詳細信息,請參閱 Brian Noyes 在本期撰寫的文章“了解 WPF 中的路由事件和命令” (msdn.microsoft.com/magazine/cc785480)。Position­Grid 有一個相關聯的 RoutedCommand 可供選擇。在命令的 Execute 處理程序中,只要選擇了某個位置就會發生 TickerSymbol­Selected 事件。TrendLine 和 NewsReader 被連接在一起,以偵聽 TickerSymbolSelected 事件並根據所選的股票代號呈現相應的內容。

在這種情況下,應用程序與每個控件都緊密耦合在一起。UI 中存在大量用於協調各個部分的邏輯。控件之間還存在著相互依賴關系。

由於存在這些依賴關系,因此無法通過某種簡單的方法將應用程序分解成可在其中分別開發各個不同部分的窗體。雖然可以將所有用戶控件都放在一個單獨的程序集中以提高可維護性,但這種做法只是將問題從主應用程序轉移到了控件程序集,治標不治本。在這種模型中,進行重大更改或引入新功能都非常困難。

現在,讓我們增加兩個新業務需求以使問題變得更復雜一些。第一個需求是添加一個基金債券屏幕,當雙擊某個基金時它會顯示有關所選基金的個人債券。第二個需求是添加一個新屏幕,顯示與所選基金相關的超鏈接列表。由於時間有限,這些功能必須由不同的團隊並行開發。

每個團隊都開發單獨的控件:FundNotes 和 FundLinks。要將這兩個控件添加到相同的控件程序集,必須將它們添加到控件項目中。更重要的是,必須將其添加到主窗體中,這意味著每個控件中對代碼和 XAML 的更改都必須合並到主窗體中。此類操作可能會非常脆弱,尤其是對已有的應用程序。

如何將所有更改都應用到主應用程序中?要完成此任務,您可能需要花費大量的時間在源控件中執行合並和拆分操作。如果在應用變更時出錯或意外覆蓋了某些內容,應用程序就會遭到破壞。補救方法是重新考慮應用程序設計。

復合應用程序

復合應用程序由運行時動態發現和構成的松散耦合模塊組成。模塊包含代表系統的不同垂直片段的可視和非可視組件(參見圖 2)。可視組件(視圖)被組合在一個常規外殼中,可用作應用程序所有內容的宿主。復合應用程序可提供各種服務,將這些模塊級組件結合在一起。模塊可提供與應用程序的特定功能相關的其他服務。

圖 2 復合應用程序的組件

從較高的層次來看,復合應用程序是“復合視圖”設計模式的實現,此模式可描述包含子項的視圖的遞歸 UI 結構,這些子項本身也是視圖。這些視圖然後通過某種機制組合起來 — 通常是在運行時而非設計時靜態組合。

為了說明此模式的優點,讓我們以其中具有多個訂單實例的訂單輸入系統為例。每個實例都可能非常復雜,需要顯示標題、詳細信息、運輸和收據等信息。隨著系統的發展變化,它可能還需要顯示更多信息。並且還要考慮根據訂單類型的不同而顯示訂單的不同部分。

如果以靜態方式構建此類屏幕,則最終可能會需要大量用於顯示訂單不同部分的條件邏輯。並且,添加新功能時也會增大使現有邏輯遭到破壞的可能性。但是,如果將其作為復合視圖來實現,則只需動態組合相關片段的訂單屏幕即可。這意味著我們可以不使用條件顯示邏輯,而且無需修改訂單視圖本身即可添加新的子屏幕。

模塊會影響在其中創建主復合視圖(也稱為外殼)的視圖。模塊永遠不會相互直接引用,也不會直接引用外殼。相反,它們會利用服務在彼此之間以及與外殼之間進行通信,以響應用戶操作。

使用模塊來組成系統有很多好處。模塊可聚合來自同一應用程序中不同後端系統的數據。此外,系統可隨著時間的推移更加方便地發展演變。在系統需求發生變化而需要向系統中添加新模塊時,與非模塊化系統相比,模塊化系統面臨的沖突要少很多。而且還可以對現有模塊進行獨立性更強的改進,從而改善可測試性。最後,模塊可由不同的團隊開發、測試和維護。

復合應用程序指南

Microsoft 模式和實施方案小組最近發布了第一個版本的“WPF 復合應用程序指南”(網址為 microsoft.com/CompositeWPF)。這一新指南旨在充分利用 WPF 的功能和編程模型。同時,團隊還根據內部產品團隊、客戶以及 .NET 社區的反饋,在之前復合應用程序指南的設計基礎上進行了完善。

“WPF 復合應用程序指南”包括一個參考實現(之前討論的 Stock Trader 應用程序)、一個復合應用程序庫 (CAL)、快速入門應用程序以及設計和技術文檔。

CAL 提供了用於構建復合應用程序的服務和探測功能。它使用的組合模型允許逐個使用或作為 CAL 設計的應用程序的一部分同時使用它的每個服務。並且,無需重新編譯 CAL 即可輕松替換任何一個服務。例如,CAL 隨附了一個擴展,它可以使用 Unity Application Block 實現依賴注入,但也允許您將其替換為自己的依賴注入服務。

快速入門提供了一些小型的專用應用程序,以展示每個不同 CAL 組件的使用方法。它們的設計目的在於提綱挈領,幫您快速了解主要概念,而不是面面俱到,試圖馬上就掌握所有知識。

在本文的剩余內容中,我將介紹 Stock Trader 參考實現中所出現的一些復合應用程序的多種技術概念。本文的所有代碼均可從 MSDN® 的“WPF 復合應用程序指南”下載部分獲得,網址為 msdn.microsoft.com/library/cc707819。

引導程序和容器

使用 CAL 構建復合應用程序時,首先必須初始化幾個核心復合服務。這就引入了引導程序。它可以執行發生復合所需的全部功能(如圖 3 所示)。在許多方面,它都類似於 CAL 應用程序的 Main 方法。

圖 3 引導程序初始化任務

首先,初始化容器。對於容器,我指的是控制反轉 (IoC) 容器/依賴關系注入 (DI) 容器。如果不太熟悉這一術語,請參閱 James Kovacs 撰寫的《MSDN 雜志》文章“通過理順軟件的依賴關系提高應用程序靈活性”(msdn.microsoft.com/magazine/cc337885)。

容器在 CAL 應用程序中起著關鍵作用。容器存儲著復合中使用的所有應用程序服務。它負責在需要的位置注入這些服務。默認情況下,CAL 包括一個抽象 UnityBootstrapper,它使用模式和實施方案小組提供的 Unity 框架作為容器。但是,構建 CAL 的目的是為了使用其他容器(如 Windsor、Structure Map 和 Sprint.NET)。CAL 中的任何類(除了 Unity 擴展)都不依賴於某個特定的容器。

在配置容器的同時,還會自動注冊幾個用於復合的核心服務(包括記錄程序和事件聚合器),基本的引導程序允許您覆蓋其中的任何服務。例如,自動注冊 ImoduleLoader 服務。如果在引導程序中覆蓋 ConfigureContainer 方法,即可注冊自己的模塊加載程序。

protected override void ConfigureContainer() {
 Container.RegisterType<IModuleLoader, MyModuleLoader>();
 base.ConfigureContainer();
}

如果不希望默認注冊服務,也可以關閉此功能。只需針對引導程序調用 Run 方法重載,為 useDefaultConfiguration 參數傳遞一個 false 值即可。

接下來,配置區域適配器。區域在 UI 中是指一個特定位置(通常它是一個容器,如面板),模塊可以在其中注入 UI 元素。區域適配器負責連接將被訪問的不同區域類型。這些適配器被映射到容器中的 RegionAdapterMappings 單例實例中。

現在創建外殼。外殼是指頂層窗口,可在其中定義區域。我們沒有在 App.Xaml 中聲明它,而是在應用程序特定的引導程序中通過 CreateShell 方法進行創建。這可以確保在外殼顯示出來之前引導程序的初始化即已完成。

當您發現應用程序中實際並不需要外殼時,您可能會感到很吃驚。例如,您可能有一個現有的 WPF 應用程序,而您希望向其中添加一些 CAL 功能。您可能不想讓 CAL 控制整個屏幕,而是希望添加一個面板作為頂層區域。在這種情況下,您就不需要定義外殼。如果不定義外殼,引導程序不會顯示它,直接將其忽略。

模塊初始化

最後,初始化模塊。在 CAL 應用程序中,模塊是復合應用程序的分離單位,可將其部署為單獨的程序集(盡管並非必需)。在 CAL 應用程序中,模塊包含了大部分的功能。

加載模塊分為兩個步驟,涉及以下兩個服務:IModuleEnumerator 和 ImoduleLoader。枚舉器負責定位可用的模塊。它將返回幾個包含模塊元數據的 ModuleInfo 對象的集合。UnityBootstrapper 包含一個 GetModuleEnumerator,必須將其覆蓋才能返回正確的枚舉器;否則,運行時會拋出異常。CAL 包括一些枚舉器,可從目錄掃描和配置中靜態定位模塊。

對於加載,CAL 包括一個默認由 UnityBootstrapper 使用的 ModuleLoader。它加載每個模塊程序集(如果尚未加載)然後初始化它們。模塊可指定與其他模塊的依賴關系。ModuleLoader 將構建依賴關系樹並根據這些規范以正確的順序初始化模塊。

使用引導程序

由於 UnityBootstrapper 是一個抽象類,因此 StockTraderRIBootstrapper 會覆蓋它(參見圖 4)。引導程序有多個受保護的虛擬方法,可利用它們來插入您自己的特定於應用程序的功能。

圖 4 Stock Trader 引導程序

public class StockTraderRIBootstrapper : UnityBootstrapper {
 private readonly EntLibLoggerAdapter _logger = new EntLibLoggerAdapter();
 protected override IModuleEnumerator GetModuleEnumerator() {
  return new StaticModuleEnumerator()
  .AddModule(typeof(NewsModule))
  .AddModule(typeof(MarketModule))
  .AddModule(typeof(WatchModule), "MarketModule")
  .AddModule(typeof(PositionModule), "MarketModule", "NewsModule");
 }
 protected override ILoggerFacade LoggerFacade {
  get { return _logger; }
 }
 protected override void ConfigureContainer() {
  Container.RegisterType<IShellView, Shell>();
  base.ConfigureContainer();
 }
 protected override DependencyObject CreateShell() {
  ShellPresenter presenter = Container.Resolve<ShellPresenter>();
  IShellView view = presenter.View;
  view.ShowView();
  return view as DependencyObject;
 }
}

必須要注意到,EntlibLoggerAdapter 是在 _logger 變量中定義和存儲的。然後,代碼將覆蓋 LoggerFacade 屬性以返回此記錄程序(它將實現 IloggerFacade)。在本例中,我使用的是 Enterprise Library 的記錄程序,但您也可以方便地替換為自己的適配器。

接下來,覆蓋 GetModuleEnumerator 方法以返回 StaticModuleEnumerator,它已使用四個參考實現模塊進行了預填充。參考實現使用靜態模塊加載,但也可以通過一些其他方法來枚舉模塊,包括目錄查找和配置。要使用其他枚舉方法,只需更改此方法來實例化一個不同的枚舉器即可。

然後,覆蓋 ConfigureContainer 以注冊外殼。此時,也可以根據需要通過編程方式注冊其他服務。最後,使用特定的邏輯覆蓋 CreateShell 以創建外殼。在此例中,代碼實現的是 Model View Presenter 模式,因此外殼具有相關聯的表示器。

圖 4 所示的引導程序展示了從頭開始構建 CAL 應用程序的常見模式,它將創建一個特定於應用程序的引導程序。此方法的主要好處是特定於應用程序的引導程序可增強應用程序的可測試性。除了 DependencyObject 以外,引導程序與 WPF 不存在任何依賴關系。例如,您可以創建一個繼承自特定於應用程序引導程序的測試引導程序,並覆蓋 CreateContainer 方法以返回 AutoMocking 容器,從而模仿出所有服務。

此外,由於引導程序可提供用於初始化復合應用程序的單個入口點,並且由於 CAL 並不依靠應用程序中框架類的繼承關系,因此在將 CAL 集成到現有應用程序時所面臨的沖突要比之前的框架少很多。請注意,CAL 本身完全不依賴於引導程序,因此如果引導程序不適合您的需求,您可以棄用它。

模塊和服務

正如我之前提到的,在使用 CAL 構建的復合應用程序中,大部分應用程序邏輯都位於模塊內。Stock Trader 參考實現包括以下四個模塊:

NewsModule,提供與所選的每項基金相關的新聞源。

MarketModule,提供所選基金的趨勢數據以及實時市場數據。

WatchModule,提供一個觀察列表,顯示所監視的基金的列表。

PositionModule,顯示已投資基金的列表並允許執行購買/銷售交易。

在 CAL 中,模塊是實現 IModule 接口的類。此接口僅包含一個方法,稱為 Initialize。如果把引導程序看作應用程序的 Main 方法,那麼 Initialize 方法就是每個模塊的 Main。例如,以下顯示的是 WatchModule 的 Initialize 方法:

public void Initialize() {
 RegisterViewsAndServices();
 IWatchListPresentationModel watchListPresentationModel =
  _container.Resolve<IWatchListPresentationModel>();
 _regionManager.Regions["WatchRegion"].Add(watchListPresentationModel.View);
 IAddWatchPresenter addWatchPresenter =
  _container.Resolve<IAddWatchPresenter>();
 _regionManager.Regions["MainToolbarRegion"].Add(addWatchPresenter.View);
}

在深入研究模塊細節之前,有兩件事值得討論一下,那就是對 _container 和 _region­Manager 的引用。如果接口並未定義它們,那它們究竟從何而來?我是否要將邏輯硬編碼到模塊中以找出這些依賴關系?

幸運的是,後一問題的答案是“否”。這時候,IoC 容器就派上用場了。加載模塊時,它從容器中被解析出來,同時會將所有指定的依賴關系注入到模塊的構造函數中:

public WatchModule(IUnityContainer container,
 IRegionManager regionManager) {
 _container = container;
 _regionManager = regionManager;
}

在這裡您可以看到容器本身被注入到模塊中。這可能是因為引導程序在其 ConfigureContainer 方法中注冊了容器:

Container.RegisterInstance<IUnityContainer>(Container);

通過讓模塊直接訪問容器,可以允許模塊以一種強制性方式在容器中注冊和解析依賴關系。

您無需執行這一強制性注冊。相反,您可將所有服務放在全局配置中。這樣做意味著必須在最初創建容器時注冊所有服務。但是,大多數模塊都擁有特定於模塊的服務。而且,通過將注冊環節放在模塊中,可使那些特定於模塊的服務僅在模塊加載時才會被注冊。

對於之前向您展示的模塊,首先調用的是 Register­ViewsAndServices。在此方法中,WatchModule 的每個特定視圖都是在容器中注冊的,同時注冊的還有一個接口:

protected void RegisterViewsAndServices() {
 _container.RegisterType<IWatchListService, WatchListService>(
  new ContainerControlledLifetimeManager());
 _container.RegisterType<IWatchListView, WatchListView>();
 _container.RegisterType<IWatchListPresentationModel,
  WatchListPresentationModel>();
 _container.RegisterType<IAddWatchView, AddWatchView>();
 _container.RegisterType<IAddWatchPresenter, AddWatchPresenter>();
}

通過要求必須指定接口可以幫助分散關注點,允許系統中的其他模塊無需直接引用即可與視圖進行交互。通過將所有內容都放入容器中可允許自動注入不同對象的各種依賴關系。例如,WatchListView 永遠不會在代碼中直接實例化 — 相反,它會作為一種依賴關系在 WatchListPresentationModel 構造函數中加載:

public WatchListPresentationModel(IWatchListView view...)

除了視圖外,WatchModule 還會注冊 WatchListService,其中包含列表數據,可用來添加新項。待注冊的特定視圖包括觀察列表以及觀察列表工具欄。注冊完畢後即可使用區域管理器,並且剛剛注冊的兩個視圖會被添加到 Watch­Region 和 ToolbarRegion 中。

區域和 RegionManager

模塊本身並不會讓人太感興趣,除非它們可以將內容呈現給 UI。在上一部分中,您看到了 Watch 模塊使用一個區域來添加它的兩個視圖。使用區域後,模塊將不再需要擁有對 UI 的特定引用,也無需再了解注入的視圖的布局和顯示方式。例如,圖 5 展示了 WatchModule 要注入的區域。

圖 5 將模塊注入應用程序

CAL 包括一個 Region 類,大體說來,此類就是涵蓋這些位置的一個句柄。Region 類包含一個 Views 屬性,它是要在區域中進行顯示的視圖的只讀集合。視圖通過調用區域的 add 方法被添加到區域中。Views 屬性實際包含對象的泛型集合;它並非局限於僅包含 UIElement。此集合將實現 InotifyPropertyCollectionChanged,以使與區域相關聯的 UIElement 能夠與之綁定並觀察其變更。

您可能想知道為什麼 Views 集合為弱類型而非 UIElement 類型。由於 WPF 可提供豐富的模板支持,因此您可以將模型直接添加到區域。然後,將會為該模型定義一個相關聯的 Data­Template,它會定義模型的呈現方式。如果添加的項目是 UIElement 或用戶控件,則 WPF 會按原樣呈現它。這意味著如果某個區域是未結訂單的選項卡,則只需將 OrderModel 或 OrderPresentation­Model 添加到區域,然後定義一個自定義 DataTemplate 即可控制顯示,而並非必須創建一個自定義的 OrderView 用戶控件。

區域可通過以下兩種方式進行注冊。第一種是在 XAML 中通過使用附帶屬性的 RegionName 來注釋 UIElement 進行定義。例如,定義 MainToolbarRegion 的 XAML 如下所示:

<ItemsControl Grid.Row="1" Grid.Column="1"
 x:Name="MainToolbar"
 cal:RegionManager.RegionName="MainToolbarRegion">
 <ItemsControl.ItemsPanel>
  <ItemsPanelTemplate>
   <WrapPanel />
  </ItemsPanelTemplate>
 </ItemsControl.ItemsPanel>
</ItemsControl>

通過 XAML 定義了區域後,它會在運行時自動注冊 RegionManager,這是由引導程序注冊的一個復合服務。RegionManager 實質上是一個 Dictionary,其中關鍵字為區域的名稱,值為 IRegion 接口的實例。RegionManager 附加的屬性使用 RegionAdapter 來創建此實例。

但是要注意,如果使用附加屬性不起作用,或需要動態注冊其他區域,您可以手動創建 Region 類或派生類的實例,並將其添加到 RegionManager 的 Regions 集合中。

請注意,在 XAML 代碼段中,MainToolbarRegion 是 ItemsControl。CAL 隨附了由引導程序注冊的三個區域適配器 — ContentControlRegionAdapter、ItemsControlRegionAdapter 和 SelectorRegionAdapter。這些適配器已注冊到 RegionAdapterMappings 類。所有適配器都從實現 I­RegionAdapter 接口的 RegionAdapterBase 繼承而來。

圖 6 顯示了 ItemsControlRegionAdapter 的實現。適配器本身的實現方式完全取決於它所應用到的 UIElement 的類型。對於 ItemsControlRegionAdapter,它的實現主要位於 Adapt 方法中。Adapt 方法接受兩個參數。第一個參數是 Region­Manager 創建的 Region 類本身的實例。第二個參數是代表區域的 UIElement。Adapt 方法將執行相關探測以確保區域可與元素協同工作。

圖 6 ItemsControlRegionAdapter

public class ItemsControlRegionAdapter : RegionAdapterBase<ItemsControl> {
 protected override void Adapt(IRegion region, ItemsControl regionTarget) {
  if (regionTarget.ItemsSource != null ||
   (BindingOperations.GetBinding(regionTarget,
   ItemsControl.ItemsSourceProperty) != null))
   throw new InvalidOperationException(
    Resources.ItemsControlHasItemsSourceException);
  if (regionTarget.Items.Count > 0) {
   foreach (object childItem in regionTarget.Items) {
    region.Add(childItem);
   }
   regionTarget.Items.Clear();
  }
  regionTarget.ItemsSource = region.Views;
 }
 protected override IRegion CreateRegion() {
  return new AllActiveRegion();
 }
}

對於 ItemsControl,適配器將從 ItemControl 本身自動刪除所有子項,然後將其添加到區域。接下來,區域的 Views 集合被綁定到控件的 ItemsSource。

第二種覆蓋方法是 CreateRegion,它會返回新的 AllActiveRegion 實例。區域可包含處於活動狀態或非活動狀態的視圖。對於 ItemsControl,它的所有項目始終都處於活動狀態,因為它沒有選擇意識。但是,對於其他類型的區域(如 Selector),一次僅選擇一項。視圖可實現 IActiveAware 接口,以便能夠從其區域處得到已被選中的通知。只要視圖被選中,它就會將其 IsSelected 屬性設為 true。

在復合應用程序的整個開發過程中,您可能不得不創建一些附加的區域和區域適配器,例如需要用來適應第三方供應商所提供的控件的適配器。要注冊新的區域適配器,需覆蓋引導程序中的 ConfigureRegionAdapter­Mappings 方法。完成後,添加類似如下所示的代碼:

protected override RegionAdapterMappings
 ConfigureRegionAdapterMappings() {
 RegionAdapterMappings regionAdapterMappings =
  base.ConfigureRegionAdapterMappings();
 regionAdapterMappings.RegisterMapping(typeof(Selector),
  new MyWizBangRegionAdapter());
 return regionAdapterMappings;
}

定義完區域後,可通過控制 Region­Manager 服務來從應用程序中的任意類訪問它。在 CAL 應用程序中執行此操作時,常見方法是讓依賴關系注入容器將 RegionManager 注入到需要它的類的構造函數中。要將視圖或模型添加到區域中,只需調用區域的 Add 方法即可。添加視圖時,可傳遞一個可選名稱:

_regionManager.Regions["MainRegion"].Add(
 somePresentationModel, "SomeView");

隨後,可通過區域的 GetView 方法使用該名稱從區域中檢索視圖。

本地作用區域

默認情況下,應用程序中只有一個 RegionManager 實例,因而使得每個區域的作用范圍都全局有效。這適用於很多情況,但有時您可能希望定義僅在特定范圍內有效的區域。如果應用程序有一個員工詳細信息視圖,其中可同時顯示視圖的多個實例,這就是此情形的一個示例。如果這些視圖非常復雜,其行為方式類似於迷你外殼或 CompositeView。在這些情形下,您可能希望每個視圖都像外殼一樣擁有其自己的區域。CAL 允許您為視圖定義本地 RegionManager,這樣在其中或其子視圖中定義的任何區域都會在該本地區域中自動注冊。

指南中包括的“UI 復合”快速入門描述了這種員工情形(參見圖 7)。快速入門中有一個員工列表。單擊每位員工時,您會看到相關聯的員工詳細信息。每次選擇員工時,都會為該員工創建一個新的 EmployeeDetailsView 並將其添加到 DetailsRegion(參見圖 8)。此視圖包含一個本地 TabRegion,EmployeesController 在其 OnEmployeeSelected 方法中會將 ProjectListView 注入其中。

圖 7 通過 RegionManager 實現 UI 復合

圖 8. 創建新的員工視圖

public virtual void
 OnEmployeeSelected(BusinessEntities.Employee employee) {
 IRegion detailsRegion =
  regionManager.Regions[RegionNames.DetailsRegion];
 object existingView = detailsRegion.GetView(
  employee.EmployeeId.ToString(CultureInfo.InvariantCulture));
 if (existingView == null) {
  IProjectsListPresenter projectsListPresenter =
  this.container.Resolve<IProjectsListPresenter>();
  projectsListPresenter.SetProjects(employee.EmployeeId);
  IEmployeesDetailsPresenter detailsPresenter =
   this.container.Resolve<IEmployeesDetailsPresenter>();
  detailsPresenter.SetSelectedEmployee(employee);
  IRegionManager detailsRegionManager =
   detailsRegion.Add(detailsPresenter.View,
   employee.EmployeeId.ToString(CultureInfo.InvariantCulture), true);
  IRegion region = detailsRegionManager.Regions[RegionNames.TabRegion];
  region.Add(projectsListPresenter.View, "CurrentProjectsView");
  detailsRegion.Activate(detailsPresenter.View);
 }
 else {
  detailsRegion.Activate(existingView);
 }
}

區域作為 TabControl 呈現出來,同時包含靜態和動態內容。General 和 Location 選項卡是在 XAML 中靜態定義的。但是,Current Projects 選項卡已注入了自己的視圖。

在代碼中您會看到,從 detailsRegion.Add 方法返回了新的 RegionManager 實例。另外還要注意,我使用的是 Add 的重載來傳入視圖名稱並將 createRegionManagerScope 參數設置為 true。這樣做會創建一個本地 RegionManager 實例,它將被用於在子項中定義的任意區域。TabRegion 本身是在 EmployeeDetailsView 的 XAML 中定義的:

<TabControl AutomationProperties.AutomationId="DetailsTabControl"
 cal:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}" .../>

即使未使用實例區域,使用本地區域也會帶來另一種好處。可使用它們來定義頂層邊界,這樣模塊就不會自動向其他各方公開其區域。要實現此目的,只需將該模塊的頂層視圖添加到某個區域並將其指定為擁有自己的范圍即可。這樣做之後,即可有效地將該模塊的區域與其他各方隔離開來。訪問它們並非不可能,但要困難很多。

如果沒有視圖,復合應用程序也就沒有存在的必要。視圖是需要在復合應用程序中構建的最重要元素,因為它們是用戶使用您的應用程序所提供的各種功能的通道。

視圖通常是應用程序的屏幕。視圖可包含其他視圖,從而成為復合視圖。視圖的另一個用途是放置菜單和工具欄。例如,在 Stock Trader 中,OrdersToolbar 是一個包含 Submit、Cancel、Submit All 和 Cancel All 按鈕的視圖。

WPF 支持的視圖概念要比 Windows 窗體領域中的約定豐富得多。在 Windows 窗體中,基本上僅限於將控件用於直觀表示。在 WPF 中,此模型仍然受到支持,並且您可以創建自定義的用戶控件來代表不同的屏幕。縱觀整個 Stock Trader 應用程序,這是用於定義視圖的主要機制。

另一方法是使用模型。WPF 允許您將任意模型綁定到 UI,然後使用 DataTemplate 來呈現它。這些模板會遞歸呈現,即如果模板呈現一個綁定到某個模型屬性的元素,該屬性將使用模板(如果可用)來呈現。

對於它的工作原理,讓我們來看一看以下代碼示例。此示例實現的 UI 與“復合”快速入門相同,但它使用的完全是模型和 DataTemplate。整個項目中沒有一個用戶控件。圖 9 展示了 EmployeeDetailsView 的處理方法。此視圖現在是已在 ResourceDictionary 中定義的一組三個 DataTemplate。一切都從 Employee­DetailsPresentationModel 開始。其模板聲明它應被呈現為 TabControl。作為模板的一部分,它將 TabControl 的 ItemsSource 綁定到 EmployeeDetailsPresentationModel 的 EmployeeDetails 集合屬性。構建員工詳細信息時,此集合由兩部分信息填充而成:

public EmployeesDetailsPresentationModel() {
 EmployeeDetails = new ObservableCollection<object>();
 EmployeeDetails.Insert(0, new HeaderedEmployeeData());
 EmployeeDetails.Insert(1, new EmployeeAddressMapUrl());
 ...
}

圖 9 創建 EmployeeDetailsView

<ResourceDictionary
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:EmployeesDetailsView=
  "clr-namespace:ViewModelComposition.Modules.Employees.Views.EmployeesDetailsView">
 <DataTemplate
  DataType="{x:Type EmployeesDetailsView:HeaderedEmployeeData}">
  <Grid x:Name="GeneralGrid">
   <Grid.ColumnDefinitions>
    <ColumnDefinition></ColumnDefinition>
    <ColumnDefinition Width="5"></ColumnDefinition>
    <ColumnDefinition Width="*"></ColumnDefinition>
   </Grid.ColumnDefinitions>
   <Grid.RowDefinitions>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
    <RowDefinition></RowDefinition>
   </Grid.RowDefinitions>
   <TextBlock Text="First Name:" Grid.Column="0" Grid.Row="0">
   </TextBlock>
   <TextBlock Text="Last Name:" Grid.Column="2" Grid.Row="0">
   </TextBlock>
   <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="2"></TextBlock>
   <TextBlock Text="Email:" Grid.Column="2" Grid.Row="2"></TextBlock>
   <TextBox x:Name="FirstNameTextBox"
    Text="{Binding Path=Employee.FirstName}"
    Grid.Column="0" Grid.Row="1"></TextBox>
   <TextBox x:Name="LastNameTextBox"
    Text="{Binding Path=Employee.LastName}"
    Grid.Column="2" Grid.Row="1"></TextBox>
   <TextBox x:Name="PhoneTextBox" Text="{Binding Path=Employee.Phone}"
    Grid.Column="0" Grid.Row="3"></TextBox>
   <TextBox x:Name="EmailTextBox" Text="{Binding Path=Employee.Email}"
    Grid.Column="2" Grid.Row="3"></TextBox>
  </Grid>
 </DataTemplate>
 <DataTemplate
  DataType="{x:Type EmployeesDetailsView:EmployeeAddressMapUrl}">
  <Frame Source="{Binding AddressMapUrl}" Height="300"></Frame>
 </DataTemplate>
 <DataTemplate DataType="{x:Type
  EmployeesDetailsView:EmployeesDetailsPresentationModel}">
  <TabControl x:Name="DetailsTabControl"
   ItemsSource="{Binding EmployeeDetails}" >
   <TabControl.ItemContainerStyle>
    <Style TargetType="{x:Type TabItem}"
     BasedOn="{StaticResource RoundedTabItem}">
     <Setter Property="Header" Value="{Binding HeaderInfo}" />
    </Style>
   </TabControl.ItemContainerStyle>
  </TabControl>
 </DataTemplate>
</ResourceDictionary>

對集合中的每一項都會呈現一個單獨的選項卡。在第一項開始呈現時,WPF 將使用為 HeaderedEmployeeData 指定的 DataTemplate。HeaderedEmployeeData 模型包含員工姓名和聯系人信息。它的關聯模板會將模型呈現為用於顯示信息的一系列標簽。第二項將使用為 EmployeeAddressMapUrl 指定的模板進行呈現,在本例中,它會呈現一個幀,其中包含一個顯示員工所在位置的地圖的網頁。

這是一個相當典型的轉換,正如您之前所了解的,視圖僅在運行時通過模型及其關聯模板的組合而實際存在。您也可以實現兩種方法的混合體(如 Stock Trader 中所演示),其中的用戶控件也包含控件,並且隨後被綁定到通過模板呈現的模型中。

單獨的表示

在本文之前的內容中,我曾提到過構建復合應用程序的其中一個好處是它使代碼更具可維護性和可測試性。您可以在視圖中應用多個已建立的表示模式以達到這一目的。縱觀整個“WPF 復合應用程序指南”,您會看到在 UI 中使用了兩種重復模式:Presentation Model 和 Supervising Controller。

Presentation Model 模式會假定一個模型,它同時包含 UI 的行為和數據。然後,視圖將表示模型的狀態投射“到玻璃上”。

在後台,模型會與業務和域模型進行交互。模型還包括其他狀態信息,如已選擇項或某個元素是否被選中。然後,視圖被直接綁定到 Presentation Model 並進行呈現(參見圖 10)。WPF 中對於數據綁定、模板和命令的豐富支持使得 Presentation Model 模式成為一種頗具吸引力的開發方案。

圖 10 Presentation Model 模式

Stock Trader 應用程序明智地選擇了 Presentation Model,例如在位置摘要中:

public class PositionSummaryPresentationModel :
 IPositionSummaryPresentationModel, INotifyPropertyChanged {
 public PositionSummaryPresentationModel(
  IPositionSummaryView view,...) {
  ...
 }
 public IPositionSummaryView View { get; set; }
 public ObservableCollection<PositionSummaryItem>
  PositionSummaryItems {
  get; set; }
}

您可以看到 PositionSummaryPresentationModel 實現 INotifyPropertyChanged 以通知視圖所發生的任何更改。在容器中解析 PositionSummaryPresentationModel 的時候,視圖本身通過其 IPosition­SummaryView 接口被注入到構造函數中。此接口允許在單元測試中模擬視圖。Presentation Model 將公開一個可觀察的 PositionSummaryItem 的集合。這些項目被綁定到 PostionSummaryView 且被呈現出來。

在 Supervising Controller 模式中,存在著模型、視圖以及表示器;如圖 11 所示。模型是數據;在大多數情況下,它是一個業務對象。視圖是模型直接綁定的 UIElement。最後,表示器是包含 UI 邏輯的一個類。在此模式中,除了委派給表示器並響應來自表示器的回調以執行一些簡單操作(包括顯示或隱藏控件)外,視圖幾乎不包含任何邏輯。

圖 11 Supervising Controller 模式

Supervising Controller 模式也用於 Stock Trader 應用程序中一些支持 Presentation Model 的實例。其中的一個示例是趨勢線(參見圖 12)。與 PositionSummary­PresentationModel 類似,TrendLinePresenter 通過 ITrendLine­View 接口注入到 TrendLineView。表示器將公開視圖通過其委派邏輯調用的 OnTickerSymbolSelected 方法。請注意,在該方法中,表示器隨後會回調視圖,以調用其 UpdateLineChart 和 SetChartTitle 方法。

圖 12 顯示趨勢線

public class TrendLinePresenter : ITrendLinePresenter {
 IMarketHistoryService _marketHistoryService;
 public TrendLinePresenter(ITrendLineView view,
  IMarketHistoryService marketHistoryService) {
  this.View = view;
  this._marketHistoryService = marketHistoryService;
 }
 public ITrendLineView View { get; set; }
 public void OnTickerSymbolSelected(string tickerSymbol) {
  MarketHistoryCollection historyCollection =
   _marketHistoryService.GetPriceHistory(tickerSymbol);
  View.UpdateLineChart(historyCollection);
  View.SetChartTitle(tickerSymbol);
 }
}

實現單獨的表示時的一個挑戰就是視圖與表示模型或表示器之間的通信。有多種方法可以解決此問題。經常采用的一種方法是,讓視圖中的事件處理程序或者直接調用表示模型或表示器,或者引發調用表示模型或表示器的事件。在 UI 中,通常必須根據狀態變更或權限來啟用或禁用對表示器發起調用的相同 UIElement。這要求視圖擁有可用於回調它的方法,以便禁用這些元素。

另一方法是使用 WPF 命令。這些命令提供了一種解決這些問題的簡單方法,並且不需要任何往復委派邏輯。WPF 中的元素可綁定到命令以處理執行邏輯以及啟用或禁用元素。當 UIElement 被綁定到命令時,如果命令的 Can­Execute 屬性為 false,則它會自動被禁用。在 XAML 中可通過聲明的方式綁定命令。

WPF 預設即提供了 RoutedUICommand。要使用這些命令,在視圖的源代碼中必須包含 Execute 和 Can­Execute 方法的處理程序 — 這意味著在往復通訊中仍需要修改代碼。RoutedUICommand 還有其他限制,如要求接收方必須位於 WPF 的邏輯樹中(此限制在構建復合應用程序時會出現問題)。

幸運的是,RoutedUICommand 只是命令的其中一個實現。WPF 提供了 ICommand 接口並將其綁定到實現它的所有命令。這意味著您可以創建自定義命令以滿足您的所有需求,而且無需更改源代碼。缺點是對於 SaveCommand、Submit­Command 和 CancelCommand 等自定義命令在各個位置必須都分別實現。

CAL 包括許多新命令(如 Delegate­Command<T>,它允許您為構造函數中的 Execute 和 CanExecute 方法指定兩個委派)。通過使用此命令,在連接各個視圖時,您不必再通過在視圖本身定義的方法進行委派,也不必為每個操作分別創建自定義命令。

在 Stock Trader 應用程序中,DelegateCommand 在多個位置(包括觀察列表)都被用到。WatchListService 使用此命令來向觀察列表中添加項目:

public WatchListService(IMarketFeedService marketFeedService) {
 this.marketFeedService = marketFeedService;
 WatchItems = new ObservableCollection<string>();
 AddWatchCommand = new DelegateCommand<string>(AddWatch);
}

除了在視圖和表示器或表示模型之間路由命令外,在復合應用程序中還需處理其他一些類型的通信(如事件發布)。在這些情況下,發行者與訂閱者是完全分離的。例如,模塊可能會公開一個從服務器接收通知的 Web 服務端點。接收到該通知後,它需要觸發一個事件,位於相同或不同模塊中的組件都可以訂閱此事件。

為支持此功能,CAL 有一個向容器注冊的 EventAggregator 服務。通過使用此服務(它是 Event Aggregator 模式的一個實現),發行者和訂閱者可通過松散耦合的方式進行通信。EventAggregator 服務包含一個事件存儲庫,其中的事件是抽象 EventBase 類的實例。此服務有一個用於檢索事件實例的 GetEvent<TEventType> 方法。

CAL 包括 CompositeWPFEvent<TPayload> 類,它繼承了 EventBase 並提供對 WPF 的特定支持。此類使用委派而非完整的 .NET 事件來執行發布。實質上,它使用默認起弱委派作用的 DelegateReference 類(有關弱委派的詳細信息,請參閱 msdn.microsoft.com/library/ms404247)。這將允許對訂閱者進行垃圾回收,即使在他們並未顯式取消訂閱。

CompositeWPFEvent 類包含 Publish、Subscribe 和 Unsubscribe 方法。每種方法都使用事件的泛型類型信息以確保發布者傳遞正確的參數 (TPayload) 並且 Subscriber 屬性會接收它們 (Action<TPayload>)。Subscribe 方法允許傳入 ThreadOption(可設為 PublisherThread、UIThread 或 BackgroundThread)。此選項可確定將針對哪個線程調用訂閱委派。此外,Subscribe 方法將被重載以允許傳入 Predicate<T> 過濾器,從而確保只有在滿足過濾器條件時訂閱者才會收到事件通知。

在 Stock Trader 應用程序中,EventAggregator 被用來廣播消息(當在位置屏幕中選擇了某個代號時)。News 模塊訂閱此事件並顯示該基金的新聞。以下是此功能的實現過程:

public class TickerSymbolSelectedEvent :
 CompositeWpfEvent<string> {
}

首先,在 StockTraderRI.Infrastructure 程序集中定義事件。以下是所有模塊都引用的一個共享程序集:

public void Run() {
 this.regionManager.Regions["NewsRegion"].Add(
  articlePresentationModel.View);
 eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Subscribe(
  ShowNews, ThreadOption.UIThread);
}
public void ShowNews(string companySymbol) {
 articlePresentationModel.SetTickerSymbol(companySymbol);
}

News 模塊的 NewsController 在其 Run 方法中訂閱此事件:

private void View_TickerSymbolSelected(object sender,
 DataEventArgs<string> e) {
 _trendLinePresenter.OnTickerSymbolSelected(e.Value);
 EventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(
  e.Value);
}

然後,只要選擇某個代號,PositionSummaryPresentation 模型就會觸發事件。

結束語

從 microsoft.com/compositewpf 下載指南。要運行代碼,只需安裝 .NET Framework 3.5 即可。

指南中包含的工具可幫助您初窺門徑。快速入門提供了易於理解的示例,它們主要關注的是構建復合應用程序時涉及的各種知識。參考實現為您提供了一個全面的示例,其中涵蓋了所有不同的方面。最後,在文檔中提供了一些背景信息、一組完整的特定任務操作方法以及動手練習教程。

在使用指南的同時,請將您的意見發布到 CodePlex 論壇或通過電子郵件發送到 [email protected]

Glenn Block 是 .NET Framework 4.0 中新增的“托管可擴展性框架”(MEF) 團隊的一名項目經理。在 MEF 工作之前,他是負責 Prism 以及其他客戶指南的模式和實施方案小組中的一名產品計劃員。Glenn 在內心裡是個奇客,他花費了大量時間在各種會議和小組(如 ALT.NET)中不遺余力地傳播著他的這份狂熱。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved