MahApps.Metro in Verbindung mit der PRISM-Bibliothek (Teil 2: Regions und RegionAdapter)

Im ersten Artikel MahApps.Metro in Verbindung mit der PRISM-Bibliothek (Teil 1: Erstellung der Solution) wurde beschrieben wie man vorgehen muss um die Basisanwendung zu erstellen. In diesem Artikel wird nun erklärt wie man den Dialog in sogenannte Regionen unterteilt und die Anwendung mit Hilfe von Modulen erweitert (unter einem PRSIM-Modul versteht man eine lose gekoppelte Komponente, die sich nahtlos in die Applikation integrieren lässt). In größeren Anwendungen ist es oft so, dass das Aussehen einer Anwendung durch die enthaltenen Module bestimmt wird und hier bietet PRISM die sogenannten Regionen. Mit Regionen definiert man das Aussehen des Dialogs und legt fest wo einzelne Views (im Normalfall innerhalb des Hauptdialogs) angezeigt werden sollen. Diese Regionen können von den einzelnen Modulen genutzt werden um die dort implementierten Views anzuzeigen. Dabei haben die Module keinerlei Wissen darüber, wo die Views innerhalb des Hauptdialogs angezeigt werden. Die Module teilen der Shell lediglich mit zeige View A in Region C.

Hinweis: Dieser Artikel wird demnächst noch einmal überarbeitet da hier einige grundlegende Änderungen im Quellcode vorgenommen wurden. Bei den vorgenommen Änderungen handelt es sich hauptsächlich um Verbesserungen bzgl. der PRISM-Architektur (Hinweise kamen von Brian Lagunas und dafür nochmal ein Dankeschön von meiner Seite)! Siehe dazu das aktuelle Github-Repository!

Einige Grundlagen zu PRISM-Regionen

PRISM-Regionen werden vom sogenannten RegionManager erstellt und verwaltet. Eine Region ist ein Host für ein oder mehrere Controls. Für die Verwaltung benötigt der RegionManager einen sogenannten RegionAdapter. Ein solcher RegionAdapter ist immer spezifisch für einen bestimmten Typ eines Controls (z.B. TabControl, usw.). Mit Hilfe eines RegionAdapters werden die Controls dann mit der entsprechenden Region „verbunden“. Das folgende Schaubild soll die Zusammenhänge verdeutlichen:

PrismMahAppsSample_07
Zusammenhang Region, RegionAdapter und Controls (Quelle [1])

Mit dem RegionManager können Regionen entweder direkt innerhalb des XAML-Codes oder in der CodeBehind-Datei erzeugt werden. Die AttachedProperty „RegionManager.RegionName“ kann dazu genutzt werden eine Region direkt im XAML-Code zu erzeugen. Dafür wird beim entsprechenden Host-Control einfache diese Eigenschaft gesetzt. Der RegionManager besitzt auch eine eigene DataContext-Eigenschaft, die es ermöglicht Daten zwischen verschiedenen Regionen auszutauschen.

Hier mal ein kleines Beispiel wie das in der Praxis aussehen könnte:

PrismMahAppsSample_08

In dem oben gezeigten Beispiel sind insgesamt vier Regionen definiert:

  • In der Titelzeile gibt es insgesamt zwei Regionen: Left-/RightWindowCommandsRegion. Diese Region kann dazu genutzt werden um verschiedene Buttons anzuzeigen
  • Die FlyoutRegion zur Anzeige von sogenannten Flyouts
  • Die MainRegion zur Anzeige beliebiger Controls, z.B. Kacheln

Wie man diese Regionen definiert wird im nächsten Kapitel erklärt.

Definition von PRISM-Regionen

Wie schon geschrieben können PRISM können Regionen direkt im XAML-Code definiert werden. Die oben gezeigten Regionen werden direkt in der MainWindow.xaml definiert. Dafür wird das AttachedProperty RegionManager.RegionName genutzt:

Hinweis: In Zeile 32 wird die MainRegion mit Hilfe des AttachedProperties „RegionManager.RegionName“ definiert. Hierfür wird dieser Eigenschaft einfach ein entsprechender Name zugewiesen. Diesen Namen kann man entweder direkt angeben oder, wie im oben gezeigten Beispiel, eine eigene Konstanten dafür definieren. Dies hat den großen Vorteil, dass man später in den ViewModels oder im CodeBehind einfach diese Konstante wiederverwenden kann. Für die anderen Regionen muss man leider einen anderen Weg einschlagen.

Grund: Hier funktioniert die XAML-Deklaration (über die attached property RegionManager.RegionName) nicht, da diese Regionen außerhalb des LogicalTree definiert sind und der RegionManager sie aus diesem Grund nicht finden kann (LogicalTreeHelper.FindParent). Daher können diese Regionen nicht automatisch durch PRISM zugewiesen werden und man muss sich selbst darum kümmern. (Hinweis kam von Brian Lagunas -> nochmal ein Dankeschön von mir an dieser Stelle!) Aus diesem Grund werden den Controls hier einfach Namen zugewiesen (Attribut x:Name) und die Regionen im CodeBehind definiert. Hier werden dann ebenfalls Konstanten für die Namen der einzelnen Regionen verwendet. Das sieht dann wie folgt aus:

Damit wären die Regionen definiert. Aktuell ist aber nur die MainRegion nutzbar!

Hintergrund: Damit UI-Controls als Region genutzt werden können muss es für den Typ des UI-Controls einen RegionAdapter geben. Der RegionAdapter ist für die Erzeugung der Region und die Zuweisung der Controls zur Region verantwortlich. Jedes UI-Control benötigt also seinen eigenen RegionAdapter. Out-of-the-box bietet die PRISM-Biblothek die folgenden RegionAdapter:

  • ContentControlRegionAdapter – dieser Adapter ist für Controls des Typs System.Windows.Controls.ContentControl und davon abgeleitete Klassen
  • SelectorRegionAdapter – dieser Adapter ist für Controls, die von System.Windows.Controls.Primitives.Selector ableiten (z.B. das System.Windows.Controls.TabControl)
  • ItemsControlRegionAdapter – dieser Adapter ist für Controls des Typs System.Windows.Controls.ItemsControl und davon abgeleitete Klassen

Für die MainRegion wird ein Standard ItemsControl verwendet. Für dieses Control bringt die PRISM-Bibliothek schon out-of-the-box einen entsprechenden RegionAdapter mit. Den anderen Regionen wurden spezielle Controls aus der MahApps.Metro-Bibliothek zugewiesen und für diese gibt es noch keine RegionAdapter. Daher muss für diese Regionen noch ein entsprechender RegionAdapter implementiert werden. Wie das geht wird im nächsten Kapitel beschrieben.

Erstellung eines RegionAdapters für die MahApps.Metro-Controls

Hinweis: Dieser Punkt ist für das gezeigte Beispiel obsolet und wird hier nur der Vollständigkeit halber aufgeführt! Siehe dazu den aktuellen Quellcode im Github-Repository (Verwendung der Methode RegionManager.RegisterViewRegion)

RegionAdapter müssen das Interface IRegionAdapter-Interface implementieren. Dieses Interface enthält lediglich eine Intialize-Methode, die als Parameter das entsprechende Control erhält und eine neue Region mit dem zugewiesenen Control zurückliefert. Das Interface ist wie folgt definiert:

Die folgenden Schritte beschreiben nun die Vorgehensweise zur Implementierung des IRegionAdapter-Interface:

  1. Zunächst muss eine neue Klasse angelegt werden, z.B. FlyoutRegionAdapter.cs. Diese Klassen lege ich im Normalfall im Shell-Projekt anPrismMahAppsSample_09
  2. Danach benötigen wir die folgenden using-Statements
  3. Danach ändern wir die Signatur der Klasse, so dass diese von der RegionAdapterBase Basisklasse erbt, wobei T den Typ des Controls definiert. Diese Basisklasse implementiert das Interface IRegionAdapter und stellt gleichzeitig ein paar Methoden bereit, die dann nur noch überschrieben werden müssen. Das sieht dann wie folgt aus (T ist vom Typ unseres Zielcontrols):
  4. Danach muss die CreateRegion-Methode implementiert werden. Das ist eine abstrakte Methode aus der Basisklasse RegionAdapterBase und sollte eine Region-Instanz (ein Objekt, das das IRegion-Interface implementiert) zurückliefern. Die PRISM-Library bietet out-of-the-box die folgenden Implementierungen:
    • Region – diese Region erlaubt mehrere aktive Views. Wird im Normalfall für Controls, die von der Selector-Klasse erben, eingesetzt
    • SingleActiveRegion – diese Region erlaubt nur einen aktiven View zur gleichen Zeit (Region für ContentControls)
    • AllActiveRegion – diese Region hält alle darin enthaltenen Views aktiv (Deaktivierung von Views ist nicht erlaubt). Wird im Normalfall für ItemsControls verwendet

    Für den benötigten FlyoutRegionAdapter würde das dann so aussehen:

  5. Danach muss die abstrakte Adapt-Methode überschrieben werden. Diese Methode übernimmt 2-Argumente: zum einen die Region mit welcher das Control verbunden werden soll und zum anderen das Control selbst. Die Implementierung für die FlyoutRegion sieht wie folgt aus:

Dies reicht im Normalfall auch schon aus und die Implementierung des Adapters wäre fertig. Damit der RegionAdapter verwendet werden kann muss dieser noch registriert werden.

Registrierung eines RegionAdapters

Die Registrierung des RegionAdapters erfolgt im Bootstrapper (wie schon im ersten Artikel beschrieben übernimmt der Bootstrapper die Initialisierung einer PRISM-Applikation). Dafür muss im Boostrapper die Methode ConfigureRegionAdapterMappings überschrieben werden:

Damit wäre der Dialog vorbereitet für die Verwendung der Regionen, d.h. jetzt können Views mit der Region verbunden und angezeigt werden. Die geschieht über den RegionManager:

Der RegionManager stellt eine Methode namens AddToRegion bereit, welche einer Region einen View hinzufügt. Dabei haben die hinzugefügten Views keinerlei Kenntnis darüber wo sie innerhalb des Dialogs angezeigt werden. Damit könnte ein Dialog in der Praxis wie folgt aussehen:

Regionen innerhalb des Hautpdialogs

Regionen innerhalb des Hautpdialogs

Das Hinzufügen von Views kann innerhalb der Anwendung von überall erfolgen. Somit können PRISM-Module das Hauptfenster dynamisch erweitern. Wie man solche PRISM-Module anlegt und dann entsprechende Views mit dem RegionManager registriert zeige ich im nächsten Artikel.

Hinweis zu den Flyouts

In der Flyout-Region können ja beliebig viele Flyouts registriert werden und damit diese Flyouts auch identifiziert werden können wurde das Interface IFlyoutView erstellt. Dieses Interface besitzt lediglich eine Eigenschaft und ist wie folgt aufgebaut:

Jedes Flyout muss nun dieses Interface implementieren. Die Aktivierung kann dann z.B. über ein Command erfolgen, das in der Basisklasse für die ViewModels implementiert ist. Dieses Command schaut einfach in der Flyout-Region, ob ein View vom Typ IFlyoutView und dem entsprechenden Namen registriert ist. Wurde ein View gefunden wird dieser geöffnet. Das Command ist aktuell wie folgt implementiert:

Das Command bekommt als Parameter einfach den Namen des Flyouts mitgegeben. Die Namen der Flyouts sind im Beispiel global definiert, d.h. es gibt eine statische Klasse namens FlyoutNames im Projekt PrismMahAppsSample.Infrastructure (innerhalb des Projektmappen-Ordners Constants). In dieser Klasse werden dann die Namen aller Flyouts hinterlegt (gleiches gilt für die Regionen). Somit kann dann in den ViewsModels und im XAML-Code einfach die jeweilige Konstante verwendet werden.

Github-Repository

Der aktuellste Quellcode ist in folgendem Github-Repository verfügbar: https://github.com/steve600/PrismMahAppsSample

Download Quellcode

[wpdm_package id=’722′]

 

Literaturverzeichnis und Weblinks

Abk.Quelle
[1]Composing the User Interface Using the Prism Library 5.0 for WPF
Fork me on GitHub