Prism
Muster für das Erstellen zusammengesetzter Anwendungen mit WPF
Glenn Block
Themen in diesem Artikel:
- Grundlagen zusammengesetzter Anwendungen
- Der Bootstrapper und Modulinitialisierung
- Bereiche und RegionManager
- Ansichten, Befehle und Ereignisse
|
In diesem Artikel werden folgende Technologien verwendet:
Composite Application Guidance for WPF
|

Inhalt
Technologien wie beispielsweise Windows
® Presentation Foundation (WPF) und Silverlight™ bieten Entwicklern ein einfaches, deklaratives Mittel zur schnellen und problemlosen Bereitstellung von Anwendungen mit reichhaltiger Benutzerfunktionalität. Aber obwohl diese Technologien dazu beitragen, die Darstellungsschicht noch stärker von der Logikebene zu trennen, lösen sie nicht das uralte Problem, eine einfach zu verwaltende Anwendung zu erstellen.
Bei kleineren Projekten kann davon ausgegangen werden, dass ein Entwickler mit mittlerer Erfahrung eine Anwendung entwerfen und erstellen kann, die sich leicht verwalten und erweitern lässt. Mit zunehmender Anzahl beweglicher Teile (und der Anzahl von Personen, die an diesen Teilen arbeiten) wird es jedoch exponentiell schwieriger, das Projekt unter Kontrolle zu halten.
Zusammengesetzte Anwendungen sind eine Lösung für dieses Problem. In diesem Artikel wird erläutert, was eine zusammengesetzte Anwendung ist und wie Sie eine Anwendung erstellen können, die die Funktionen von WPF nutzt. Dabei wird auch die neue Composite Application Guidance for WPF (früher bekannt unter dem Codenamen „Prism“) des patterns & practices-Teams bei Microsoft vorgestellt.
Das Problem: Monolithische Anwendungen
Betrachten wir ein Beispiel, um den Bedarf an zusammengesetzten Anwendungen zu verstehen. Contoso Financial Investments stellt eine Anwendung zum Verwalten Ihres Aktienportfolios bereit. Mithilfe der Anwendung kann sich ein Benutzer aktuelle Investitionen und Pressemeldungen für diese Investitionen anzeigen lassen, einer Überwachungsliste Elemente hinzufügen und Kauf-/Verkaufstransaktionen durchführen.
Wenn diese Anwendung als traditionelle WPF-Anwendung mit Benutzersteuerelementen erstellt wurde, würden Sie mit einem Fenster oberster Ebene beginnen und Benutzersteuerelemente für jede der erwähnten Funktionen hinzufügen. In diesem Fall hätten Sie Benutzersteuerelemente wie PositionGrid, PositionSummary, TrendLine und WatchList (siehe Abbildung 1). Jedes Benutzersteuerelement wird zur Entwurfszeit innerhalb des Hauptfensters entweder manuell in XAML oder mithilfe eines Designers wie Expression Blend™ angelegt.
Abbildung 1 Benutzersteuerelemente in einer monolithischen Anwendung (zum Vergrößern auf das Bild klicken)
Sie würden dann RoutedEvents, RoutedCommands und Datenbindung verwenden, um alles miteinander zu verknüpfen. Weitere Informationen zu diesem Thema finden Sie in dem Artikel „Routingereignisse und -befehle in WPF“ von Brian Noyes in dieser Ausgabe (
msdn.microsoft.com/magazine/cc785480). PositionGrid verfügt über ein zugeordnetes RoutedCommand für die Auswahl. Im Execute-Handler des Befehls findet jedes Mal, wenn eine Position ausgewählt wird, ein TickerSymbolSelected-Ereignis statt. TrendLine und NewsReader sind miteinander verknüpft, um das TickerSymbolSelected-Ereignis abzuhören und den Inhalt basierend auf dem ausgewählten Tickersymbol zu rendern.
In diesem Fall ist die Anwendung eng an die jeweiligen Steuerelemente gekoppelt. Die Benutzeroberfläche enthält eine bedeutende Menge an Logik zum Koordinieren der verschiedenen Teile. Zwischen den Steuerelementen bestehen auch gegenseitige Abhängigkeiten.
Aufgrund dieser Abhängigkeiten gibt es keine einfache Möglichkeit, die Anwendung so zu trennen, dass die einzelnen Teile separat entwickelt werden können. Sie könnten alle Benutzersteuerelemente in eine separate Assembly stellen, um die Verwaltbarkeit zu verbessern, aber dadurch wird das Problem lediglich von der Hauptanwendung in die Steuerelementassembly verschoben. Bei diesem Modell ist es sehr schwierig, bedeutende Änderungen vorzunehmen oder neue Funktionen einzuführen.
Wir wollen das Ganze nun noch erschweren, indem wir zwei neue Geschäftsanforderungen hinzufügen. Die erste besteht im Hinzufügen eines Bildschirms für Fondsnotizen, der beim Doppelklicken persönliche Notizen zum ausgewählten Fonds anzeigt. Die zweite besteht im Hinzufügen eines neuen Bildschirms, der eine Liste von Hyperlinks anzeigt, die sich auf den ausgewählten Fonds beziehen. Aus Zeitgründen müssen diese Features parallel von verschiedenen Teams entwickelt werden.
Jedes Team entwickelt separate Steuerelemente: FundNotes und FundLinks. Um beide Steuerelemente derselben Steuerelementassembly hinzuzufügen, müssen beide dem Steuerelementprojekt hinzugefügt werden. Außerdem müssen sie dem Hauptformular hinzugefügt werden, d. h., Code- und XAML-Änderungen jedes Steuerelements müssen mit dem Hauptformular zusammengeführt werden. Solche Vorgänge können speziell bei vorhandenen Anwendungen unglaublich empfindlich sein.
Wie werden alle diese Änderungen wieder in die Hauptanwendung eingecheckt? Wenn Sie fertig sind, haben Sie möglicherweise viel Zeit für Zusammenführungen und Vergleiche in der Quellcodeverwaltung aufgewendet. Wenn Sie beim Anwenden dieser Änderungen Fehler machen oder unbeabsichtigt etwas überschreiben, haben Sie schließlich eine beschädigte Anwendung. Die Lösung besteht darin, den Anwendungsentwurf zu überdenken.
Zusammengesetzte Anwendungen
Eine zusammengesetzte Anwendung besteht aus lose gekoppelten Modulen, die zur Laufzeit dynamisch erkannt und zusammengesetzt werden. Die Module enthalten sichtbare und nicht sichtbare Komponenten, die die verschiedenen vertikalen Bestandteile des Systems darstellen (siehe Abbildung 2). Die sichtbaren Komponenten (Ansichten) sind in einer Shell zusammengestellt, die als Host für den gesamten Inhalt der Anwendung dient. Zusammengesetzte Anwendungen bieten Dienste, die diese Komponenten auf Modulebene miteinander verknüpfen. Module können zusätzliche Dienste bieten, die sich auf die spezifische Funktionalität der Anwendung beziehen.
Abbildung 2 Komponenten einer zusammengesetzten Anwendung (zum Vergrößern auf das Bild klicken)
Eine zusammengesetzte Anwendung ist eine Implementierung des Composite View-Entwurfsmusters, das eine rekursive Benutzeroberflächenstruktur von Ansichten beschreibt, die untergeordnete Elemente enthalten, die selbst Ansichten sind. Die Ansichten werden dann (normalerweise zur Laufzeit) durch eine Methode zusammengesetzt, statt dass sie zur Entwurfszeit statisch zusammengesetzt werden.
Um die Vorteile dieses Musters zu veranschaulichen, denken Sie an ein Auftragserfassungssystem, in dem mehrere Instanzen eines Auftrags vorhanden sind. Jede Instanz kann sehr komplex sein, um Header, Details, Versand und Eingänge anzeigen zu können. Wenn sich das System weiterentwickelt, müssen möglicherweise mehr Informationen angezeigt werden. Stellen Sie sich außerdem vor, dass Teile des Auftrags in Abhängigkeit vom Auftragstyp unterschiedlich angezeigt werden.
Wenn ein solcher Bildschirm statisch erstellt wurde, kann schließlich viele bedingte Logik zum Anzeigen der verschiedenen Teile des Auftrags vorhanden sein. Zudem wird durch das Hinzufügen neuer Funktionen die Wahrscheinlichkeit erhöht, dass die vorhandene Logik beschädigt wird. Wird das Ganze jedoch als zusammengesetzte Anwendung implementiert, setzt sich der Auftragsbildschirm dynamisch nur aus den relevanten Teilen zusammen. Bedingte Anzeigelogik ist dann nicht erforderlich, und neue untergeordnete Bildschirme können hinzugefügt werden, ohne die Auftragsansicht selbst zu ändern.
Module tragen die Ansichten bei, aus denen die zusammengesetzte Hauptansicht (auch als Shell bezeichnet) erstellt wird. Module verweisen nie direkt aufeinander oder auf die Shell. Stattdessen nutzen sie Dienste zur Kommunikation miteinander und mit der Shell, um auf Benutzeraktionen zu reagieren.
Ein System, das sich aus Modulen zusammensetzt, bietet mehrere Vorteile. Module können Daten aggregieren, die aus verschiedenen Back-End-Systemen innerhalb derselben Anwendung stammen. Zudem ist das System im Laufe der Zeit entwicklungsfähiger. Wenn sich die Systemanforderungen ändern, können neue Module sehr viel reibungsloser dem System hinzugefügt werden als bei einem nichtmodularen System. Vorhandene Module lassen sich dann unabhängiger entwickeln, wodurch die Testfähigkeit verbessert wird. Schließlich können Module von verschiedenen Teams entwickelt, getestet und verwaltet werden.
Composite Application Guidance
Das patterns & practices-Team bei Microsoft hat kürzlich die erste Version von Composite Application Guidance for WPF ausgeliefert (verfügbar unter
microsoft.com/CompositeWPF). Der neue Leitfaden wurde so entworfen, dass die Funktionen und das Programmiermodell von WPF genutzt werden können. Gleichzeitig hat das Team auch das Design früherer Leitfäden für zusammengesetzte Anwendungen basierend auf dem Feedback von internen Produktteams, Kunden und der .NET-Community verbessert.
Die Composite Application Guidance for WPF umfasst eine Verweisimplementierung (die bereits diskutierte Stock Trader-Anwendung), eine Composite Application Library (CAL), Starthilfeanwendungen sowie Entwurfsdokumentation und technische Dokumentation.
Die CAL stellt die Dienste und die Grundstruktur für das Erstellen zusammengesetzter Anwendungen bereit. Sie verwendet ein Kompositionsmodell, sodass die einzelnen Dienste einzeln für sich oder zusammen als Teil einer CAL-entworfenen Anwendung verwendet werden können. Jeder Dienst lässt sich außerdem ohne Neukompilierung der CAL leicht ersetzen. Die CAL wird beispielsweise mit einer Erweiterung geliefert, die den Unity Application Block zur Abhängigkeitsinjektion verwendet, Ihnen aber ermöglicht, ihn durch einen eigenen Abhängigkeitsinjektionsdienst zu ersetzen.
Starthilfen bieten kleine, fokussierte Anwendungen, die die Verwendung der verschiedenen CAL-Komponenten veranschaulichen. Sie sollen den Benutzer in Bezug auf Konzepte schnell auf den neuesten Stand bringen, ohne dass alles auf einmal nachvollzogen werden muss.
In den übrigen Abschnitten des Artikels werden mehrere technische Konzepte zusammengesetzter Anwendungen untersucht, die in der Stock Trader-Verweisimplementierung veranschaulicht werden. Der gesamte Code für diesen Artikel steht in der Composite Application Guidance for WPF von MSDN
® unter
msdn.microsoft.com/library/cc707819 als Download zur Verfügung.
Der Bootstrapper und Container
Wenn Sie mithilfe von CAL zusammengesetzte Anwendungen erstellen, müssen zuerst mehrere Kernkompositionsdienste initialisiert werden. Hier kommt der Bootstrapper ins Spiel. Er führt alle notwendigen Funktionen aus, damit die Komposition stattfindet (siehe Abbildung 3). In vielerlei Hinsicht ist es die Main-Methode einer CAL-Anwendung.
Abbildung 3 Bootstrapper-Initialisierungsaufgaben (zum Vergrößern auf das Bild klicken)
Zuerst wird der Container initialisiert. Mit Container meine ich einen IoC/DI-Container (Inversion of Control, Steuerumkehrung; Dependency Injection, Abhängigkeitsinjektion). Wenn Sie mit diesem Begriff nicht vertraut sind, sehen Sie sich den
MSDN Magazin-Artikel, „Flexiblere Anwendungen durch Verringerung der Softwareabhängigkeiten“ von James Kovacs (
msdn.microsoft.com/magazine/cc337885) an.
Container spielen eine entscheidende Rolle in einer CAL-Anwendung. Der Container ist der Speicher aller Anwendungsdienste, die bei der Komposition verwendet werden. Er ist verantwortlich für das Injizieren dieser Dienste überall dort, wo sie gebraucht werden. Standardmäßig enthält die CAL einen abstrakten UnityBootstrapper, der das Unity-Framework aus patterns & practices als Container verwendet. CAL wurde jedoch zur Arbeit mit anderen Containern wie Windsor, Structure Map und Sprint.NET erstellt. Keine Klasse in der CAL (mit Ausnahme der Unity-Erweiterungen) ist von einem bestimmten Container abhängig.
Wenn der Container konfiguriert wird, werden mehrere Kerndienste, die zur Komposition verwendet werden, einschließlich einer Protokollierung und eines Ereignisaggregators, automatisch registriert, und der Basisbootstrapper lässt zu, dass sie außer Kraft gesetzt werden. Ein Dienst, der automatisch registriert wird, ist beispielsweise IModuleLoader. Wenn Sie die ConfigureContainer-Methode im Bootstrapper außer Kraft setzen, können Sie Ihren eigenen Modullader registrieren.
protected override void ConfigureContainer() {
Container.RegisterType<IModuleLoader, MyModuleLoader>();
base.ConfigureContainer();
}
Wenn Sie nicht wünschen, dass Dienste standardmäßig registriert werden, kann diese Funktion ebenfalls ausgeschaltet werden. Rufen Sie einfach die Run-Methodenüberladung im Bootstrapper auf, und übergeben Sie einen falschen Wert für den useDefaultConfiguration-Parameter.
Als Nächstes werden die Bereichsadapter konfiguriert. Ein Bereich ist ein bezeichneter Speicherort (zumeist ein Container, beispielsweise ein Panel) auf der Benutzeroberfläche, in dem Module Benutzeroberflächenelemente injizieren können. Bereichsadapter behandeln das Verknüpfen verschiedener Bereichstypen, auf die der Zugriff ermöglicht werden soll. Diese Adapter sind einer RegionAdapterMappings-Singletoninstanz im Container zugeordnet.
Nun wird die Shell erstellt. Die Shell ist das Fenster oberster Ebene, in dem Bereiche definiert werden. Statt sie in App.Xaml zu deklarieren, wird sie von der CreateShell-Methode in Ihrem anwendungsspezifischen Bootstrapper erstellt. Damit wird sichergestellt, dass die Initialisierung des Bootstrappers abgeschlossen ist, bevor die Shell angezeigt wird.
Vielleicht überrascht es Sie zu hören, dass eigentlich keine Shell in Ihrer Anwendung vorhanden sein muss. Sie könnten beispielsweise eine vorhandene WPF-Anwendung haben, der Sie einige CAL-Funktionen hinzufügen möchten. Statt den gesamten Bildschirm durch CAL steuern zu lassen, könnten Sie einen Bereich hinzufügen, der ein Bereich oberster Ebene ist. In diesem Fall müssen Sie keine Shell definieren. Ihr Bootstrapper kann die Anzeige der Shell einfach ignorieren, wenn sie nicht definiert ist.
Modulinitialisierung
Schließlich werden Module initialisiert. Ein Modul in einer CAL-Anwendung ist eine Trennungseinheit innerhalb einer zusammengesetzten Anwendung, die als separate Assembly bereitgestellt werden kann, obwohl dies nicht unbedingt erforderlich ist. In einer CAL-Anwendung enthält das Modul den Großteil der Funktionalität.
Das Laden von Modulen erfolgt in zwei Schritten, wobei zwei Dienste beteiligt sind: IModuleEnumerator und IModuleLoader. Der Enumerator ist verantwortlich für das Suchen nach verfügbaren Modulen. Er gibt mehrere Sammlungen von ModuleInfo-Objekten zurück, die Metadaten zu einem Modul enthalten. UnityBootstrapper enthält einen GetModuleEnumerator, der außer Kraft gesetzt werden sollte, um den richtigen Enumerator zurückzugeben. Andernfalls wird zur Laufzeit eine Ausnahme ausgelöst. Die CAL enthält Enumeratoren zur statischen Suche nach Modulen sowie zu ihrer Suche in einem Verzeichnis bzw. mithilfe der Konfiguration.
Zum Laden enthält die CAL einen ModuleLoader, der von UnityBootstrapper standardmäßig verwendet wird. Er lädt alle Modulassemblys (wenn sie nicht geladen wurden) und initialisiert sie dann. Module können Abhängigkeiten von anderen Modulen angeben. Der ModuleLoader erstellt eine Abhängigkeitsstruktur und initialisiert Module in der richtigen Reihenfolge auf Grundlage dieser Spezifikationen.
Verwenden des Bootstrappers
Da UnityBootstrapper eine abstrakte Klasse ist, setzt StockTraderRIBootstrapper sie außer Kraft (siehe Abbildung 4). Der Bootstrapper hat mehrere geschützte virtuelle Methoden, die Ihnen ermöglichen, Ihre eigene anwendungsspezifische Funktionalität einzufügen.

Abbildung 4 Stock Trader-Bootstrapper
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;
}
}
Als Erstes sollte Ihnen auffallen, dass ein EntlibLoggerAdapter definiert und in der _logger-Variablen gespeichert ist. Der Code setzt dann die LoggerFacade-Eigenschaft außer Kraft, um diese Protokollierung zurückzugeben, die ILoggerFacade implementiert. In diesem Fall verwende ich die Protokollierung der Enterprise Library, aber Sie können diese problemlos ersetzen und Ihren eigenen Adapter verwenden.
Als Nächstes wird die GetModuleEnumerator-Methode außer Kraft gesetzt, um einen StaticModuleEnumerator zurückzugeben, der bereits mit den vier Verweisimplementierungsmodulen ausgefüllt ist. Die Verweisimplementierung verwendet die statische Modulladung, aber es gibt mehrere andere Möglichkeiten zum Aufzählen von Modulen, u. a. die Verzeichnissuche und die Konfiguration. Um eine andere Enumerationsmethode zu verwenden, ändern Sie diese Methode einfach, sodass ein anderer Enumerator instanziiert wird.
ConfigureContainer wird dann außer Kraft gesetzt, um die Shell zu registrieren. Zusätzliche Dienste können bei Bedarf zu diesem Zeitpunkt ebenfalls programmgesteuert registriert werden. Schließlich wird CreateShell mit der spezifischen Logik zum Erstellen der Shell außer Kraft gesetzt. In diesem Fall implementiert der Code das Model View Presenter-Muster, sodass die Shell über einen zugeordneten Presenter verfügt.
Der in Abbildung 4 dargestellte Bootstrapper zeigt ein gebräuchliches Muster, wenn eine CAL-Anwendung von Grund auf neu erstellt wird, wobei ein anwendungsspezifischer Bootstrapper erstellt wird. Ein Hauptvorteil dieses Ansatzes besteht darin, dass ein anwendungsspezifischer Bootstrapper die Testfähigkeit Ihrer Anwendung verbessert. Der Bootstrapper hat mit Ausnahme von DependencyObject keine Abhängigkeiten bezüglich WPF. Sie können beispielsweise einen Testbootstrapper erstellen, der vom anwendungsspezifischen Bootstrapper erbt und die CreateContainer-Methode außer Kraft setzt, um einen AutoMocking-Container zurückzugeben, sodass alle Ihre Dienste simuliert werden.
Da der Bootstrapper außerdem einen einzelnen Eintrittspunkt zur Kompositionsinitialisierung bietet und die CAL nicht auf die Vererbung von Frameworkklassen in Ihrer Anwendung angewiesen ist, können Sie CAL reibungsloser in Ihre vorhandenen Anwendungen integrieren als in früheren Frameworks. Beachten Sie, dass die CAL selbst überhaupt nicht vom Bootstrapper abhängig ist. Sie müssen also keinen Bootstrapper verwenden, wenn ein solcher nicht Ihren Anforderungen entspricht.
Module und Dienste
Wie bereits erwähnt, ist der größte Teil der Anwendungslogik in einer mithilfe der CAL erstellten zusammengesetzten Anwendung in den Modulen enthalten. Die Stock Trader-Verweisimplementierung enthält vier Module:
- NewsModule bietet zugehörige Nachrichtenfeeds für jeden ausgewählten Fonds.
- MarketModule bietet Trenddaten sowie Echtzeitmarktdaten für den ausgewählten Fonds.
- WatchModule bietet eine Überwachungsliste, die eine Liste der von Ihnen überwachten Fonds anzeigt.
- PositionModule zeigt die Liste der Fonds an, in die Sie investiert haben, und ermöglicht das Durchführen von Kauf-/Verkaufstransaktionen.
In der CAL ist ein Modul eine Klasse, die die IModule-Schnittstelle implementiert. Diese Schnittstelle hat nur eine Methode, die Initialize heißt. Wenn der Bootstrapper der Main-Methode der Anwendung entspricht, ist die Initialize-Methode die Main-Methode für jedes Modul. Hier sehen Sie beispielsweise die Initialize-Methode für WatchModule:
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);
}
Bevor das Modul im Detail besprochen wird, lohnt es, hier zwei Dinge zu erörtern, und zwar die Verweise auf _container und _regionManager. Woher stammen sie, wenn sie nicht in der Schnittstelle definiert sind? Hartkodiere ich die Logik innerhalb des Moduls, um diese Abhängigkeiten zu finden?
Glücklicherweise kann diese letzte Frage verneint werden. Hier kommt ein IoC-Container zu Hilfe. Wenn ein Modul geladen wird, wird es in dem Container aufgelöst, der auch angegebene Abhängigkeiten in den Konstruktor des Moduls injiziert:
public WatchModule(IUnityContainer container,
IRegionManager regionManager) {
_container = container;
_regionManager = regionManager;
}
Hier sehen Sie, dass der Container selbst in das Modul injiziert wird. Dies ist möglich, da der Bootstrapper den Container in seiner ConfigureContainer-Methode registriert:
Container.RegisterInstance<IUnityContainer>(Container);
Wenn Module direkten Zugriff auf den Container erhalten, kann das Modul Abhängigkeiten vom Container auf imperative Weise registrieren und auflösen.
Diese imperative Registrierung ist nicht unbedingt erforderlich. Sie können stattdessen alle Dienste in eine globale Konfiguration stellen. Dies würde bedeuten, dass alle Dienste registriert werden müssen, wenn der Container erstellt wird. Die meisten Module haben jedoch modulspezifische Dienste. Durch Beibehalten der Registrierung im Modul werden diese modulspezifischen Dienste nur beim Laden des Moduls registriert.
Im Fall des Moduls, das Sie bereits gesehen haben, wird zuerst RegisterViewsAndServices aufgerufen. In dieser Methode werden die spezifischen Ansichten für WatchModule jeweils im Container zusammen mit einer Schnittstelle registriert:
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>();
}
Das Erfordernis, dass die Schnittstelle angegeben wird, fördert die Trennung der Bereiche und ermöglicht anderen Modulen im System, mit der Ansicht zu interagieren, ohne dass ein direkter Verweis erforderlich ist. Da alles in den Container gestellt wird, können alle Abhängigkeiten für die verschiedenen Objekte automatisch injiziert werden. WatchListView beispielsweise wird nie direkt im Code instanziiert. Stattdessen wird es als Abhängigkeit im WatchListPresentationModel-Konstruktor geladen:
public WatchListPresentationModel(IWatchListView view...)
Neben diesen Ansichten registriert WatchModule auch den WatchListService, der die Listendaten enthält und zum Hinzufügen neuer Elemente dient. Die spezifischen Ansichten, die registriert werden, sind die Überwachungsliste und ihre Symbolleiste. Nach der Registrierung wird der Bereichsmanager verwendet, und beide Ansichten, die gerade registriert wurden, werden zu WatchRegion und ToolbarRegion hinzugefügt.
Bereiche und RegionManager
Module an sich sind nicht besonders interessant, es sei denn, dass sie Inhalt auf der Benutzeroberfläche rendern können. Im vorhergehenden Abschnitt haben Sie gesehen, dass das Watch-Modul einen Bereich verwendet, um seine beiden Ansichten hinzuzufügen. Wenn ein Bereich verwendet wird, muss das Modul keinen spezifischen Verweis auf die Benutzeroberfläche haben und muss nicht wissen, wie die injizierten Ansichten angelegt und angezeigt werden. Als entsprechendes Beispiel zeigt Abbildung 5 die Bereiche, in die WatchModule injiziert.
Abbildung 5 Injizieren von Modulen in die Anwendung (zum Vergrößern auf das Bild klicken)
CAL enthält eine Region-Klasse, die im Grunde ein Handle ist, das diese Speicherorte abschließt. Die Region-Klasse enthält eine Views-Eigenschaft, bei der es sich um eine schreibgeschützte Sammlung der Ansichten handelt, die im Bereich angezeigt werden sollen. Ansichten werden dem Bereich hinzugefügt, indem die Add-Methode des Bereichs aufgerufen wird. Die Views-Eigenschaft enthält eine generische Sammlung von Objekten. Sie ist nicht auf UIElements beschränkt. Diese Sammlung implementiert INotifyPropertyCollectionChanged, sodass sich das dem Bereich zugeordnete UIElement daran binden und Änderungen berücksichtigen kann.
Möglicherweise fragen Sie sich, warum die Views-Sammlung schwach typisiert ist, statt zum Typ UIElement zu gehören. Dank der umfassenden Vorlagenunterstützung in WPF können Sie Modelle direkt dem Bereich hinzufügen. Für dieses Modell kann dann ein zugeordnetes DataTemplate definiert werden, das das Rendering für das Modell definiert. Wenn das hinzugefügte Element ein UIElement oder ein Benutzersteuerelement ist, wird WPF es so rendern. Wenn Sie einen Bereich haben, bei dem es sich um eine Registerkarte offener Aufträge handelt, können Sie also einfach das OrderModel oder OrderPresentationModel dem Bereich hinzufügen und dann ein benutzerdefiniertes DataTemplate zum Steuern der Anzeige definieren, statt ein benutzerdefiniertes OrderView-Benutzersteuerelement erstellen zu müssen.
Für das Registrieren von Bereichen gibt es zwei Möglichkeiten. Die erste wird in XAML durch Kommentieren eines UIElement mit einer angefügten RegionName-Eigenschaft definiert. Das XAML zum Definieren der MainToolbarRegion sieht beispielsweise folgendermaßen aus:
<ItemsControl Grid.Row="1" Grid.Column="1"
x:Name="MainToolbar"
cal:RegionManager.RegionName="MainToolbarRegion">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Nachdem ein Bereich über XAML definiert wurde, wird er automatisch zur Laufzeit mit RegionManager, einem der vom Bootstrapper registrierten Kompositionsdienste, registriert. RegionManager ist im Wesentlichen ein Dictionary, in dem der Schlüssel der Name des Bereichs und der Wert eine Instanz der IRegion-Schnittstelle ist. Die angefügte RegionManager-Eigenschaft verwendet einen RegionAdapter zum Erstellen dieser Instanz.
Beachten Sie jedoch Folgendes: Wenn die Verwendung angefügter Eigenschaften für Sie nicht das Richtige ist oder wenn Sie zusätzliche Bereiche dynamisch registrieren müssen, können Sie manuell eine Instanz der Region-Klasse oder der abgeleiteten Klasse erstellen und sie der Regions-Sammlung von RegionManager hinzufügen.
Beachten Sie im XAML-Ausschnitt, dass die MainToolbarRegion ein ItemsControl ist. CAL wird mit drei Bereichsadaptern geliefert, die vom Bootstrapper registriert werden: ContentControlRegionAdapter, ItemsControlRegionAdapter und SelectorRegionAdapter. Adapter werden mit einer RegionAdapterMappings-Klasse registriert. Alle Adapter erben von RegionAdapterBase, das die IRegionAdapter-Schnittstelle implementiert.
Abbildung 6 zeigt die Implementierung von ItemsControlRegionAdapter. Wie der Adapter selbst implementiert wird, hängt ganz vom Typ des UIElement ab, an das er angepasst wird. Im Fall von ItemsControlRegionAdapter besteht der Hauptteil seiner Implementierung in der Adapt-Methode. Die Adapt-Methode akzeptiert zwei Parameter. Der erste Parameter ist eine Instanz der Region-Klasse selbst, die von RegionManager erstellt wird. Der zweite Parameter ist das UIElement, das den Bereich darstellt. Die Adapt-Methode führt die grundlegende Arbeit durch, um sicherzustellen, dass der Bereich mit dem Element arbeitet.

Abbildung 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();
}
}
Im Fall eines ItemsControl entfernt der Adapter automatisch untergeordnete Elemente aus dem ItemControl selbst und fügt sie dann dem Bereich hinzu. Die Views-Sammlung des Bereichs wird dann an das ItemsSource des Steuerelements gebunden.
Die zweite außer Kraft gesetzte Methode ist CreateRegion, die eine neue AllActiveRegion-Instanz zurückgibt. Bereiche können Ansichten enthalten, die aktiv oder inaktiv sind. Im Fall von ItemsControl sind alle Elemente die ganze Zeit über aktiv, da es keine Auswahl kennt. Im Fall anderer Arten von Bereichen wie beispielsweise Selector wird jedoch jeweils nur ein Element ausgewählt. Eine Ansicht kann die IActiveAware-Schnittstelle implementieren, sodass sie von ihrem Bereich bezüglich ihrer Auswahl benachrichtig wird. Wenn die Ansicht ausgewählt wird, ist die IsSelected-Eigenschaft auf „true“ gesetzt.
Während der Entwicklung Ihrer zusammengesetzten Anwendung haben Sie möglicherweise zusätzliche Bereiche und Bereichsadapter erstellt, beispielsweise einen, der das Steuerelement eines Drittanbieters anpasst. Setzen Sie zum Registrieren Ihres neuen Bereichsadapters die ConfigureRegionAdapterMappings-Methode im Bootstrapper außer Kraft. Fügen Sie anschließend Code ähnlich dem folgenden hinzu:
protected override RegionAdapterMappings
ConfigureRegionAdapterMappings() {
RegionAdapterMappings regionAdapterMappings =
base.ConfigureRegionAdapterMappings();
regionAdapterMappings.RegisterMapping(typeof(Selector),
new MyWizBangRegionAdapter());
return regionAdapterMappings;
}
Nachdem der Bereich definiert wurde, kann von jeder Klasse innerhalb der Anwendung auf ihn zugegriffen werden, indem der RegionManager-Dienst aufgerufen wird. Dies erfolgt in einer CAL-Anwendung allgemein so, dass der Abhängigkeitsinjektionscontainer den RegionManager in den Konstruktor der Klasse injiziert, der ihn benötigt. Um einem Bereich eine Ansicht oder ein Modell hinzuzufügen, rufen Sie einfach die Add-Methode des Bereichs auf. Wenn Sie eine Ansicht hinzufügen, können Sie einen optionalen Namen übergeben:
_regionManager.Regions["MainRegion"].Add(
somePresentationModel, "SomeView");
Sie können diesen Namen später verwenden, um die Ansicht aus dem Bereich mithilfe der GetView-Methode des Bereichs abzurufen.
Lokal begrenzte Bereiche
Standardmäßig gibt es nur eine RegionManager-Instanz in Ihrer Anwendung, sodass jeder Bereich global vorhanden ist. Dieser Ansatz eignet sich für viele Szenarios, aber es gibt Situationen, in denen Sie möglicherweise einen Bereich definieren möchten, der lokal begrenzt ist. Sie werden dies wahrscheinlich tun wollen, wenn Ihre Anwendung eine Ansicht für Mitarbeiterdetails hat, in der mehrere Instanzen der Ansicht gleichzeitig angezeigt werden können. Wenn diese Ansichten sehr komplex sind, verhalten sie sich wie Minishells oder CompositeViews. In diesen Fällen soll jede Ansicht wahrscheinlich ihre eigenen Bereiche haben, wie dies bei der Shell der Fall ist. Die CAL ermöglicht Ihnen das Definieren eines lokalen RegionManager für eine Ansicht, sodass alle in ihr definierten Bereiche oder ihre untergeordneten Ansichten automatisch in diesem lokalen Bereich registriert werden.
Die im Leitfaden enthaltene UI Composition-Starthilfe verdeutlicht dieses Mitarbeiterszenario (siehe Abbildung 7). In der Starthilfe ist eine Mitarbeiterliste enthalten. Wenn Sie auf die einzelnen Mitarbeiter klicken, werden die zugehörigen Mitarbeiterdetails angezeigt. Bei jeder Mitarbeiterauswahl wird eine neue EmployeeDetailsView für diesen Mitarbeiter erstellt und der DetailsRegion hinzugefügt (siehe Abbildung 8). Diese Ansicht enthält eine lokale TabRegion, in die der EmployeesController eine ProjectListView in ihre OnEmployeeSelected-Methode injiziert.
Abbildung 7 UI Composition über RegionManager (zum Vergrößern auf das Bild klicken)

Abbildung 8 Erstellen einer neuen Mitarbeiteransicht
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);
}
}
Der Bereich wird als TabControl gerendert und enthält sowohl statischen als auch dynamischen Inhalt. Die Registerkarten „General“ (Allgemein) und „Location“ (Standort) sind statisch innerhalb des XAML definiert. Auf der Registerkarte „Current Projects“ (Aktuelle Projekte) werden jedoch ihre Ansichten injiziert.
Sie können im Code sehen, dass von der detailsRegion.Add-Methode eine neue RegionManager-Instanz zurückgegeben wird. Beachten Sie auch, dass die Überladung von Add verwendet wird, die einen Namen für die Ansicht übergibt und den createRegionManagerScope-Parameter auf „true“ setzt. Dadurch wird eine lokale RegionManager-Instanz erstellt, die für Bereiche verwendet wird, die in den untergeordneten Bereichen definiert sind. Die TabRegion selbst wird im XAML der EmployeeDetailsView definiert:
<TabControl AutomationProperties.AutomationId="DetailsTabControl"
cal:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}" .../>
Die Verwendung lokaler Bereiche bietet einen zusätzlichen Vorteil, selbst wenn Sie keine Instanzbereiche verwenden. Sie können sie zum Definieren einer Grenze oberster Ebene verwenden, sodass ein Modul seine Bereiche nicht automatisch global verfügbar macht. Dazu muss nur die Ansicht oberster Ebene für dieses Modul in einem Bereich hinzugefügt und angegeben werden, damit sie ihren eigenen Gültigkeitsbereich hat. Wenn dies geschehen ist, haben Sie die Bereiche des Moduls effektiv vom Rest der Welt abgeschirmt. Es ist nicht unmöglich, auf sie zuzugreifen, aber es ist sehr viel schwieriger.
Ohne Ansichten bestände kein Bedarf an einer zusammengesetzten Anwendung. Ansichten sind das wichtigste Element, das Sie innerhalb Ihrer zusammengesetzten Anwendungen erstellen werden, da sie für Ihre Benutzer den Zugang zur Welt der Funktionen darstellen, die von Ihrer Anwendung bereitgestellt werden.
Ansichten sind in der Regel die Bildschirme Ihrer Anwendung. Ansichten können andere Ansichten enthalten, wodurch sie zu zusammengesetzten Ansichten werden. Eine andere Verwendungsmöglichkeit von Ansichten sind Menüs und Symbolleisten. In Stock Trader ist beispielsweise die OrdersToolbar eine Ansicht, die Schaltflächen für „Submit“ (Senden), „Cancel“ (Abbrechen), „Submit All“ (Alle senden) und „Cancel All“ (Alle abbrechen) enthält.
WPF unterstützt einen viel umfassenderen View-Begriff als dies in der Welt von Windows Forms der Fall war. In Windows Forms waren Sie im Grunde darauf beschränkt, Steuerelemente als Ihre visuelle Darstellung zu verwenden. In WPF wird dieses Modell weiterhin unterstützt, und Sie können benutzerdefinierte Benutzersteuerelemente erstellen, die Ihre verschiedenen Bildschirme darstellen. Wenn Sie sich die Stock Trader-Anwendung näher ansehen, ist dies die primäre Methode, die zum Definieren von Ansichten eingesetzt wird.
Ein anderer Ansatz besteht in der Verwendung von Modellen. WPF ermöglicht Ihnen, ein beliebiges Modell an die Benutzeroberfläche zu binden und dann zum Rendern ein DataTemplate zu verwenden. Vorlagen werden rekursiv gerendert, d. h., wenn eine Vorlage ein Element rendert, das an eine Eigenschaft des Modells gebunden ist, wird diese Eigenschaft mithilfe einer Vorlage gerendert, wenn eine verfügbar ist.
Um zu sehen, wie dies funktioniert, wollen wir uns das folgende Codebeispiel ansehen. Dieses Beispiel implementiert dieselbe Benutzeroberfläche wie die Composition-Starthilfe, aber es werden ausschließlich Modelle und DataTemplates verwendet. Im gesamten Projekt ist kein einziges Benutzersteuerelement zu finden. Abbildung 9 zeigt, wie die EmployeeDetailsView behandelt wird. Die Ansicht ist jetzt ein Satz von drei DataTemplates, die in einem ResourceDictionary definiert wurden. Alles beginnt mit dem EmployeeDetailsPresentationModel. Seine Vorlage deklariert, dass es als TabControl gerendert werden sollte. Als Teil der Vorlage bindet es die ItemsSource von TabControl an die EmployeeDetails-Sammlungseigenschaft von EmployeeDetailsPresentationModel. Diese Sammlung ist mit zwei Informationen gefüllt, wenn die Employee-Details erstellt werden:
public EmployeesDetailsPresentationModel() {
EmployeeDetails = new ObservableCollection<object>();
EmployeeDetails.Insert(0, new HeaderedEmployeeData());
EmployeeDetails.Insert(1, new EmployeeAddressMapUrl());
...
}

Abbildung 9 Erstellen einer 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>
Für jedes Element in der Sammlung wird eine separate Registerkarte gerendert. Wenn das erste Element gerendert wird, verwendet WPF das für HeaderedEmployeeData angegebene DataTemplate. Das HeaderedEmployeeData-Modell enthält den Mitarbeiternamen sowie Kontaktinformationen. Die zugehörige Vorlage rendert das Modell als eine Reihe von Beschriftungen zum Anzeigen der Informationen. Das zweite Element wird mithilfe der Vorlage gerendert, die für EmployeeAddressMapUrl angegeben wurde. Sie rendert in diesem Fall einen Frame, der eine Webseite mit einer Karte mit den Wohnorten der Mitarbeiter enthält.
Dies ist ein wesentlicher Paradigmenwechsel, da die Ansicht, wie Sie sie aus der Vergangenheit kennen, zur Laufzeit eigentlich nur durch die Kombination aus dem Modell und seiner zugehörigen Vorlage vorhanden ist. Sie können auch eine Kombination aus beiden Ansätzen implementieren (wie in Stock Trader dargestellt), sodass Benutzersteuerelemente vorhanden sind, die selbst Steuerelemente enthalten, welche dann an Modelle gebunden werden, die durch Vorlagen gerendert werden.
Getrennte Darstellung
In diesem Artikel wurde bereits erwähnt, dass einer der Vorteile beim Erstellen einer zusammengesetzten Anwendung darin besteht, dass sich Code einfacher verwalten und testen lässt. Es gibt mehrere bewährte Darstellungsmuster, die Sie in Ihren Ansichten anwenden können, um dies zu erreichen. In der Composite Application Guidance for WPF werden Sie immer wieder auf zwei Muster stoßen, die auf der Benutzeroberfläche verwendet werden: Presentation Model und Supervising Controller.
Das Presentation Model-Muster setzt ein Modell voraus, das sowohl das Verhalten als auch die Daten für die Benutzeroberfläche enthält. Die Ansicht projiziert dann den Zustand des Präsentationsmodells „auf das Glas“.
Im Hintergrund interagiert das Modell mit Geschäfts- und Domänenmodellen. Das Modell enthält auch zusätzliche Statusinformationen, wie z. B. das ausgewählte Element, oder Informationen dazu, ob ein Element geprüft wird. Die Ansicht bindet sich dann direkt an das Presentation Model und rendert es (siehe Abbildung 10). Die umfassende Unterstützung in WPF für Datenbindung, Vorlagen und Befehle macht das Presentation Model-Muster zu einer attraktiven Option für die Entwicklung.
Abbildung 10 Das Presentation Model-Muster (zum Vergrößern auf das Bild klicken)
Die Stock Trader-Anwendung verwendet Presentation Model mit Bedacht, etwa in der Positionszusammenfassung:
public class PositionSummaryPresentationModel :
IPositionSummaryPresentationModel, INotifyPropertyChanged {
public PositionSummaryPresentationModel(
IPositionSummaryView view,...) {
...
}
public IPositionSummaryView View { get; set; }
public ObservableCollection<PositionSummaryItem>
PositionSummaryItems {
get; set; }
}
Sie sehen, dass das PositionSummaryPresentationModel INotifyPropertyChanged implementiert, um die Ansicht bezüglich Änderungen zu benachrichtigen. Die Ansicht selbst wird in den Konstruktor durch ihre IPositionSummaryView-Schnittstelle injiziert, da das PositionSummaryPresentationModel vom Container aufgelöst wird. Diese Schnittstelle ermöglicht, dass die Ansicht beim Komponententest simuliert wird. Das Presentation Model macht eine Sammlung von PositionSummaryItems verfügbar. Diese Elemente sind an die PostionSummaryView gebunden und werden gerendert.
Innerhalb des Supervising Controller-Musters sind das Modell, die Ansicht und der Presenter vorhanden. Dies ist in Abbildung 11 dargestellt. Bei dem Modell handelt es sich um die Daten. Es ist oftmals ein Geschäftsobjekt. Bei der Ansicht handelt es sich um ein UIElement, an das das Modell direkt gebunden wird. Schließlich ist der Presenter eine Klasse, die die Benutzeroberflächenlogik enthält. In diesem Muster enthält die Ansicht kaum andere Logik als das Delegieren an den Presenter und das Antworten auf Rückrufe vom Presenter, um einfache Aktionen einschließlich des Anzeigens oder Ausblendens eines Steuerelements durchzuführen.
Abbildung 11 Das Supervising Controller-Muster (zum Vergrößern auf das Bild klicken)
Das Supervising Controller-Muster wird auch in einigen Instanzen in der Stock Trader-Anwendung zugunsten von Presentation Model verwendet. Ein Beispiel ist die Trendlinie (siehe Abbildung 12). Ähnlich wie beim PositionSummaryPresentationModel wird die TrendLineView über die ITrendLineView-Schnittstelle in den TrendLinePresenter injiziert. Der Presenter macht eine OnTickerSymbolSelected-Methode verfügbar, die von der Ansicht durch ihre Delegierungslogik aufgerufen wird. Beachten Sie, dass in dieser Methode der Presenter dann Rückrufe zur Ansicht durchführt und ihre UpdateLineChart- und SetChartTitle-Methoden aufruft.

Abbildung 12 Darstellen der Trendlinie
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);
}
}
Eine Herausforderung beim Implementieren der getrennten Darstellung ist die Kommunikation zwischen der Ansicht und dem Präsentationsmodell oder Presenter. Es gibt mehrere Ansätze dafür. Ein oft implementierter Ansatz besteht darin, dass in der Ansicht Ereignishandler vorhanden sind, die Ereignisse entweder direkt aufrufen oder im Präsentationsmodell oder Presenter auslösen. Dieselben UIElements, die Aufrufe des Presenters initiieren, müssen in der Benutzeroberfläche auf Grundlage von Statusänderungen oder Berechtigungen aktiviert oder deaktiviert werden. Dazu muss die Ansicht Methoden enthalten, die verwendet werden können, um einen Rückruf durchzuführen, damit diese Elemente deaktiviert werden.
Ein anderer Ansatz besteht darin, WPF-Befehle zu verwenden. Befehle bieten eine saubere Möglichkeit zum Behandeln dieser Situationen, ohne dass dazu das Hin und Her der Delegierungslogik erforderlich ist. Elemente in WPF können eine Bindung zu Befehlen herstellen, um sowohl die Ausführungslogik als auch das Aktivieren oder Deaktivieren von Elementen zu behandeln. Wenn ein UIElement an einen Befehl gebunden ist, wird es automatisch deaktiviert, wenn die CanExecute-Eigenschaft „false“ lautet. Befehle können in XAML deklarativ gebunden werden.
Standardmäßig stellt WPF RoutedUICommands bereit. Zum Verwenden dieser Befehle ist ein Handler für die Execute- und die CanExecute-Methode innerhalb des CodeBehind der Ansicht erforderlich, d. h., eine Codeänderung ist für die Kommunikation, die vor und zurück erfolgt, noch immer erforderlich. RoutedUICommands haben noch andere Einschränkungen. So ist beispielsweise erforderlich, dass sich der Empfänger in der logischen Struktur von WPF befindet, eine Einschränkung, die für das Erstellen zusammengesetzter Anwendungen problematisch ist.
Glücklicherweise sind RoutedUICommands nur eine Möglichkeit zum Implementieren von Befehlen. WPF stellt die ICommand-Schnittstelle bereit und wird an jeden beliebigen Befehl gebunden, der sie implementiert. Sie können also benutzerdefinierte Befehle erstellen, die alle Ihre Anforderungen erfüllen, und Sie müssen sich nicht um das CodeBehind kümmern. Ein Nachteil dabei ist, dass Sie überall benutzerdefinierte Befehle implementieren müssen, z. B. SaveCommand, SubmitCommand und CancelCommand.
Die CAL enthält neue Befehle wie beispielsweise DelegateCommand<T>, das Ihnen ermöglicht, die zwei Delegaten für die Execute- und die CanExecute-Methode im Konstruktor anzugeben. Mithilfe dieses Befehls können Ansichten verbunden werden, ohne dass über Methoden delegiert werden muss, die in der Ansicht selbst definiert sind, und ohne dass benutzerdefinierte Befehle für die einzelnen Aktionen erstellt werden müssen.
In der Stock Trader-Anwendung wird DelegateCommand an mehreren Stellen, einschließlich der Überwachungsliste, verwendet. WatchListService verwendet diesen Befehl, um der Überwachungsliste Elemente hinzuzufügen:
public WatchListService(IMarketFeedService marketFeedService) {
this.marketFeedService = marketFeedService;
WatchItems = new ObservableCollection<string>();
AddWatchCommand = new DelegateCommand<string>(AddWatch);
}
Neben dem Routing von Befehlen zwischen der Ansicht und einem Presenter oder Präsentationsmodel gibt es andere Arten der Kommunikation, wie z. B. die Ereignisveröffentlichung, die in einer zusammengesetzten Anwendung behandelt werden muss. In diesen Fällen ist der Herausgeber vollständig vom Abonnenten entkoppelt. Ein Modul kann beispielsweise einen Webdienstendpunkt verfügbar machen, der Benachrichtigungen vom Server erhält. Nach Eingag der Benachrichtigung muss es ein Ereignis auslösen, das von Komponenten innerhalb desselben Moduls oder in anderen Modulen abonniert werden kann.
Um diese Funktionalität zu unterstützen, verfügt die CAL über einen EventAggregator-Dienst, der beim Container registriert ist. Mithilfe dieses Diensts, bei dem es sich um eine Implementierung des Event Aggregator-Musters handelt, können Herausgeber und Abonnenten in einer locker gekoppelten Weise kommunizieren. Der EventAggregator-Dienst enthält ein Repository von Ereignissen, bei denen es sich um Instanzen der abstrakten EventBase-Klasse handelt. Der Dienst verfügt über eine GetEvent<TEventType>-Methode zum Abrufen von Ereignisinstanzen.
Die CAL enthält die CompositeWPFEvent<TPayload>-Klasse, die EventBase erbt und spezifische Unterstützung für WPF bereitstellt. Diese Klasse verwendet zum Veröffentlichen Delegaten anstelle von vollständigen .NET-Ereignissen. Intern verwendet sie eine DelegateReference-Klasse, die standardmäßig als schwacher Delegat funktioniert (unter
msdn.microsoft.com/library/ms404247 finden Sie weitere Informationen zu schwachen Delegaten). Dadurch können Abonnenten einer Garbage Collection unterzogen werden, selbst wenn sie das Abonnement nicht explizit beenden.
Die CompositeWPFEvent-Klasse enthält Publish-, Subscribe- und Unsubscribe-Methoden. Jede verwendet die generischen Typinformationen des Ereignisses, um sicherzustellen, dass der Herausgeber die richtigen Parameter (TPayload) übergibt und die Subscriber-Eigenschaft sie richtig empfängt (Action<TPayload>). Die Subscribe-Methode ermöglicht das Einreichen einer ThreadOption, die auf PublisherThread, UIThread oder BackgroundThread eingestellt werden kann. Diese Option bestimmt, auf welchem Thread der abonnierende Delegat aufgerufen wird. Zudem wird die Subscribe-Methode überladen, damit das Übergeben eines Predicate<T>-Filters möglich ist, sodass der Abonnent bezüglich des Ereignisses nur benachrichtigt wird, wenn der Filter erfüllt ist.
In der Stock Trader-Anwendung wird der EventAggregator für Broadcasts verwendet, wenn ein Symbol im Positionsbildschirm ausgewählt wird. Das News-Modul abonniert dieses Ereignis und zeigt Nachrichten für diesen Fonds an. Und so wird die Funktionalität implementiert:
public class TickerSymbolSelectedEvent :
CompositeWpfEvent<string> {
}
Zuerst wird das Ereignis in der StockTraderRI.Infrastructure-Assembly definiert. Dies ist eine freigegebene Assembly, auf die alle Module verweisen:
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);
}
Der NewsController des News-Moduls abonniert dieses Ereignis in seiner Run-Methode:
private void View_TickerSymbolSelected(object sender,
DataEventArgs<string> e) {
_trendLinePresenter.OnTickerSymbolSelected(e.Value);
EventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(
e.Value);
}
Das PositionSummaryPresentation-Modell löst dann jedes Mal das Ereignis aus, wenn ein Symbol ausgewählt wird.
Zusammenfassung
Laden Sie den Leitfaden unter
microsoft.com/compositewpf herunter. Um den Code ausführen zu können, muss .NET Framework 3.5 installiert sein.
Der Leitfaden enthält Tools, die Sie bei den ersten Schritten unterstützen. Die Starthilfen bieten leicht verständliche Beispiele, die sich auf verschiedene Aspekte beim Erstellen zusammengesetzter Anwendungen konzentrieren. Die Verweisimplementierung enthält ein umfassendes Beispiel, das alle verschiedenen Aspekte verwendet. Schließlich bietet die Dokumentation Hintergrundinformationen, einen vollständigen Satz an Anleitungen für spezielle Aufgaben sowie eine praktische Übung.
Während Sie den Leitfaden verwenden, veröffentlichen Sie Ihre Eindrücke in die CodePlex-Foren, oder senden Sie eine E-Mail an
cafbk@microsoft.com.