# 18 WPF-Containerelemente Nachdem Sie nun alle wesentlichen Grundlagen von XAML erfahren haben, wollen wir uns in den beiden nächsten Kapiteln mit den Elementen beschäftigen, die der Gestaltung der grafischen Benutzerschnittstelle dienen und somit einen wichtigen Beitrag zur optischen Darstellung als auch zur Interaktivität mit dem Anwender leisten. In diesem Kapitel behandeln wir die Elemente Window und Page sowie die unerlässlichen Layoutcontainer. In Kapitel 19 wenden wir uns anschließend den Steuerelementen zu. ## 18.1 WPF-Fenster Die WPF unterstützt grundsätzlich zwei unterschiedliche Fenstertypen: Window NavigationWindow Bei Window handelt es sich um die klassische Darstellungsweise, die Sie auch vielleicht schon unter der WinForm-API kennengelernt haben. Dabei werden die »Inhalte« in separaten Fenstern angezeigt. Der Anwender navigiert durch die Anwendung, indem er immer wieder neue Fenster öffnet und schließt. Solche Anwendungen setzen sich daher in der Regel aus vielen einzelnen Fenstern zusammen. Die Folge ist, dass zur Laufzeit eines Programms oft viele unterschiedliche Fenster der Anwendung geöffnet sind. Das Konzept der Fenster vom Typ NavigationWindow erinnert dagegen an das Konzept der Internet-Browser, bei denen unterschiedliche »Inhalte« in einem gemeinsamen Fenster dargestellt werden. Setzen Sie diese Idee mit NavigationWindow in einer WPF-Anwendung um, werden die Inhalte, die ansonsten in mehreren Fenstern angeboten würden, auf mehrere Seiten aufgeteilt, die untereinander durch Links verbunden sind. Der Anwender navigiert durch die Anwendung, ohne gleichzeitig mehrere Fenster geöffnet zu haben. Zur Navigation stehen eine Vor- und Zurück-Schaltfläche zur Verfügung, ähnlich wie es auch bei Internet-Browsern üblich ist. Wahrscheinlich haben Sie mit solchen Fenstern bereits Bekanntschaft gemacht, denn spätestens seit Vista wird diese Fenstertechnik auch vom Betriebssystem vermehrt eingesetzt. Ein typisches Beispiel ist das Fenster Systemsteuerung (siehe Abbildung 18.1). Die beiden Fenstertypen Window und NavigationWindow unterscheiden sich hauptsächlich durch die Art und Weise, wie der Anwender durch die Anwendung navigiert. Beide Typen können Sie selbstverständlich innerhalb einer Anwendung gleichberechtigt einsetzen. Hier klicken, um das Bild zu vergrößern Abbildung 18.1 Die Systemsteuerung von Windows 7 ### 18.1.1 Nachrichtenfenster mit »MessageBox« Ehe wir die beiden Fenstertypen Window und NavigationWindow im Detail betrachten, wollen wir uns mit einem Fenster beschäftigen, das bereits vordefiniert vom .NET Framework bereitgestellt wird: dem Nachrichtenfenster, mit dem eine Anwendung oder das System den Benutzer über Zustände informiert oder um Bestätigung bzw. Ablehnung eingeleiteter Vorgänge bittet. Sicherlich sind Ihnen Dialoge wie der in Abbildung 18.2 gezeigte geläufig. Hier klicken, um das Bild zu vergrößern Abbildung 18.2 Ein typischesNachrichtenfenster Das Nachrichtenfenster wird modal angezeigt. Das heißt, es kann kein anderes Fenster der Anwendung aktiviert werden, bis das modale Fenster geschlossen wird. Weil modale Dialogfenster sehr häufig zur einfachen Interaktion mit dem Anwender oder auch nur zur Bereitstellung von Informationen in Applikationen eingesetzt werden, ist im Namespace System.Windows eine Klasse vordefiniert, mit der die meisten Anforderungen erfüllt werden können. Es handelt sich um MessageBox. Der abgebildete Dialog beruht auf der folgenden Anweisung:
    MessageBox.Show("Wollen Sie die Anwendung schließen?",
                    "Beenden",
                    MessageBoxButton.YesNo,
                    MessageBoxImage.Question,
                    MessageBoxResult.No);
Man braucht eigentlich kaum erklärende Worte zu verlieren, denn der Code beschreibt sich nahezu von selbst: Im ersten Argument wird der Meldungstext übergeben, im zweiten die Beschriftung der Titelleiste, im dritten die anzuzeigenden Schaltflächen. Das vierte Argument beschreibt das Symbol im Clientbereich des Meldungsfensters, und das fünfte und letzte Argument gibt vor, welche Schaltfläche nach dem Start der Anzeige fokussiert werden soll. ### 18.1.2 Die Methode »MessageBox.Show« MessageBox verfügt nur über eine typspezifische Methode: Es ist die vielfach überladene statische Methode Show, die mit insgesamt 12 Überladungen aufwartet. Die einfachste ist die, die nur die Zeichenfolge des Meldungstextes entgegennimmt:
    MessageBox.Show("Visual C# macht Spaß");
Ein solches Meldungsfenster verfügt nur über eine OK-Schaltfläche, die Titelleiste ist leer. Klickt der Anwender auf die Schaltfläche, wird das Meldungsfenster automatisch geschlossen. Optisch wirkt eine leere Titelleiste stümperhaft. Deshalb werden Sie bestimmt in allen Fällen einen Text vorsehen. Häufig verwendet man dazu den Namen der Anwendung. In einem dritten Argument können Sie die im Meldungsfenster angezeigten Schaltflächen festlegen. Dazu übergeben Sie eine der in der Enumeration MessageBoxButton vordefinierten Konstanten (siehe Tabelle 18.1).

Tabelle 18.1 Konstanten der Enumeration »MessageBoxButton«

Konstante | Beschreibung
OK | Das Meldungsfeld enthält die OK-Schaltfläche.
OKCancel | Das Meldungsfeld enthält die Schaltflächen OK und Abbrechen.
YesNoCancel | Das Meldungsfeld enthält die Schaltflächen Ja, Nein und Abbrechen.
YesNo | Das Meldungsfeld enthält die Schaltflächen Ja und Nein.
In welcher Sprache die Schaltflächen beschriftet sind, hängt von der Sprachversion des Betriebssystems ab. Um im linken Bereich des Meldungsfensters ein Symbol anzuzeigen, wählen Sie eine Konstante aus Tabelle 18.2 aus.

Tabelle 18.2 Konstanten der Enumeration »MessageBoxImage«

Konstante | Beschreibung
None | Zeigt kein Symbol an.
Hand | Zeigt ein Handsymbol.
Question | Zeigt ein Fragezeichen.
Exclamation | Zeigt ein Ausrufungszeichen.
Asterisk | Zeigt ein Sternchen.
Stop | Zeigt ein Stoppsymbol.
Error | Zeigt ein Fehlersymbol.
Warning | Zeigt ein Warnsymbol.
Information | Zeigt ein Informationssymbol.
Erzeugen Sie ein Meldungsfenster mit mehreren Schaltflächen, kommt der Wahl der vorfokussierten Schaltfläche eine besondere Bedeutung zu. Drückt nämlich der Anwender die -Taste, entspricht das dem Klicken auf die vorfokussierte Schaltfläche. Daher sollte immer die Schaltfläche vorfokussiert werden, bei deren Klicken keine Nachteile entstehen, zum Beispiel durch den Verlust eingegebener, aber nicht gespeicherter Daten. Die Vorfokussierung erfolgt mithilfe der Enumeration MessageBoxResult.

Tabelle 18.3 Konstanten der Enumeration »MessageBoxResult«

Konstante | Beschreibung
None | Gibt keinen Wert zurück.
OK | Der Anwender hat auf die Schaltfläche OK geklickt.
Cancel | Der Anwender hat auf die Schaltfläche Cancel geklickt.
Yes | Der Anwender hat auf die Schaltfläche Yes geklickt.
No | Der Anwender hat auf die Schaltfläche No geklickt.
Diese Aufzählung dient gleichzeitig als Rückgabewert der Show-Methode, denn schließlich muss nach dem Aufruf und dem Schließen des Nachrichtenfensters per Code geprüft werden, welche der angebotenen Schaltflächen der Anwender geklickt hat. Nur für den Fall, dass das Meldungsfenster nur einen OK-Button hat, erübrigt sich die Auswertung. Im folgenden Ereignishandler einer Schaltfläche wird ein Meldungsfenster angezeigt, das den Anwender um die Bestätigung bittet, ob er die Anwendung schließen möchte oder nicht. Der Code reagiert in Abhängigkeit von der im Meldungsfenster gewählten Schaltfläche.
    string message = "Möchten Sie die Daten speichern?";
    MessageBoxResult result= MessageBox.Show(message,
                                             "Meine Anwendung",
                                              MessageBoxButton.OKCancel,
                                              MessageBoxImage.Question,
                                              MessageBoxResult.OK);
     if(result == MessageBoxResult.OK)
       // Daten speichern
     else
       // andere Anweisungen
In einer if-Anweisung wird der Rückgabewert überprüft. Hat der Anwender OK angeklickt, werden die Daten gespeichert, ansonsten kann eine andere Reaktion erfolgen. Wahrscheinlich wird in diesem Fall keine Reaktion erforderlich sein. Alternativ könnte der gesamte if-Block auch durch switch-case ersetzt werden:
    switch( result) {
      case MessageBoxResult.OK:
        // Daten speichern
        break;
      case MessageBoxResult.Cancel:
        // andere Anweisungen
        break;
    }
### 18.1.3 Fenster vom Typ »Window« Mit der Klasse Window wird ein Fenster beschrieben, also das wohl wichtigste Element einer klassischen Windows-Anwendung. Ein Window ist einfach gesagt ein Container für alle darin enthaltenen Steuerelemente. Da Window direkt von ContentControl abgeleitet ist, besitzt diese Klasse eine Content-Eigenschaft. Im nächsten Kapitel werden wir noch genauer auf diese Eigenschaft eingehen. An dieser Stelle sei aber bereits erklärt, dass die Content-Eigenschaft es ermöglicht, andere WPF-Komponenten einzubetten. Das wird im Fall des Window eigentlich grundsätzlich gemacht, und zwar durch Angabe eines oder auch mehrerer Layoutcontainer, die für die exakte Ausrichtung der enthaltenen Steuerelemente sorgen. Mit der Eigenschaft Title wird der Inhalt der Titelleiste beschrieben, mit den Eigenschaften Height und Width die Breite und die Höhe des Fensters. Diese drei Eigenschaften werden direkt im XAML-Code angeboten, nachdem Sie eine neue WPF-Anwendung gestartet oder ein zusätzliches Window der Anwendung hinzugefügt haben. Damit sind zumindest die wahrscheinlich drei wichtigsten Eigenschaften schon festgelegt. Neben den genannten können Sie mit vielen weiteren Eigenschaften das Aussehen und das Verhalten eines Window-Objekts verändern. Einen Überblick der vermutlich wichtigsten Eigenschaften können Sie Tabelle 18.4 entnehmen.

Tabelle 18.4 Eigenschaften der Klasse »Window« (Auszug)

Eigenschaft | Beschreibung
Icon | Per Vorgabe wird als Symbol ein Standard-Icon verwendet. Wollen Sie dieses durch ein anwendungsspezifisches austauschen, geben Sie der Eigenschaft Icon eine ICO-Datei an.
ResizeMode | ResizeMode gibt an, ob und wie sich die Größe des betreffenden Window-Elements ändern kann. Die Eigenschaft lässt die Einstellungen NoResize, CanMinimize, CanResize und CanResizeWithGrip zu. Je nach Wahl der Einstellung werden die Schaltflächen zum Minimieren und Maximieren in der Titelleiste angezeigt. Der Standard ist CanResize.
ShowInTaskBar | Die Eigenschaft legt fest, ob das minimierte Fenster in der Taskleiste angezeigt wird. Der Vorgabewert ist true.
SizeToContent | Legt fest, ob die Größe eines Fensters automatisch an die Größe des Inhalts angepasst wird. Die Standardeinstellung Manual bedeutet, dass sich die Größe aus den Einstellungen Height und Width des Fensters ergibt. Die Eigenschaft selbst lässt darüber hinaus auch die Einstellungen Width, Height und WidthAndHeight zu. Bei letztgenannter Einstellung werden die angezeigte Breite und Höhe des Fensters automatisch an die Breite und Höhe des Inhalts angepasst.
Topmost | Wird diese Eigenschaft auf true eingestellt, erscheint dieses Fenster immer über allen anderen Fenstern der Anwendung.
WindowStartupLocation | Legt die Position des Fensters fest, wenn es zum ersten Mal angezeigt wird. Die möglichen Einstellungen sind Manual, CenterScreen und CenterOwner. Manual ist der Standard und wird durch die Eigenschaften Left und Top des Fensters beschrieben. Mit CenterScreen wird das Fenster in der Bildschirmmitte angezeigt und mit CenterOwner mittig bezüglich eines anderen Fensters, aus dem heraus das aktuelle Fenster aufgerufen wurde.
WindowState | Diese Eigenschaft beschreibt die drei Fensterzustände Normal, Minimized und Maximized.
WindowStyle | Gibt den Rahmentyp für das Fenster an. Die möglichen Einstellungen hier lauten None (weder Rahmen noch Titelleiste werden angezeigt), SingleBorderWindow (das ist der Standard), ThreeDBorderWindow (Fenster mit 3D-Rahmen) und ToolWindow (verankertes Toolfenster mit minimierten Fensterrändern).
#### Mehrere Fenster Enthält eine WPF-Anwendung nur ein Fenster, wird die Anwendung mit dem Schließen des Fensters beendet. Anwendungen mit nur einem Fenster stellen aber wohl eher die Ausnahme dar; die meisten Anwendungen weisen mehr oder weniger viele Fenster auf. Entwickeln Sie eine Anwendung mit mehreren Fenstern, können Sie das gewünschte Startfenster im Wurzelelement Application der Datei App.xaml angeben. Dazu ändern Sie einfach nur die Angabe des Attributs StartupUri passend ab. Beendet wird eine WPF-Anwendung mit dem Schließen des letzten Fensters. Jedes neu zu öffnende Fenster muss zuerst als Objekt vorliegen. Dazu instanziieren Sie die Klasse und rufen die Methode Show auf.
    Window1 frm = new Window1();
    frm.Show();
Das Fenster wird nicht-modal geöffnet. Das heißt, der Benutzer kann ein anderes Fenster der laufenden Anwendung aktivieren. Weiter oben haben Sie bereits erfahren, dass sich Fenster auch modal öffnen lassen. Zur Erinnerung: Modal geöffnete Fenster erlauben dem Anwender nicht, ein anderes Fenster der aktuellen Anwendung zu aktivieren. Das modale Fenster muss erst wieder geschlossen werden. Üblicherweise werden modale Fenster auch als Dialogfenster bezeichnet. Eingesetzt werden sie da, wo der Anwender Angaben machen muss, die von der Anwendung sofort in irgendeiner Form umgesetzt werden. Um ein Fenster als Dialogfenster modal zu öffnen, rufen Sie die Methode ShowDialog auf. Im Gegensatz zu Show liefert die Methode ShowDialog einen Rückgabewert vom Typ Boolean?. Die Rückgabe kann also true, false oder null sein, der Standardwert ist false. Von einem modalen Fenster wird erwartet, dass es nach dem Anklicken einer der Schaltflächen geschlossen wird. Deshalb sollten Sie bei der Schaltfläche Abbrechen die Eigenschaft IsCancel auf true einstellen. Damit wird das Fenster geschlossen, was übrigens auch mit Drücken der -Taste erfolgen kann. Bei der OK-Schaltfläche eines modalen Fensters wird stattdessen im Click-Ereignishandler die Eigenschaft DialogResult des WPF-Fensters auf true festgelegt. Damit ist auch gleichzeitig das Schließen des Dialogs verbunden. Im aufrufenden Fenster kommt es dann darauf an, auszuwerten, auf welche der Schaltflächen der Anwender geklickt hat. Dazu wird der Rückgabewert der Methode ShowDialog ausgewertet und darauf entsprechend reagiert. Sehen wir uns die Programmierung eines Dialogs nun im Zusammenhang an, und beginnen wir mit dem Codefragment, mit dem ein Fenster modal geöffnet und anschließend ausgewertet wird:
    // im aufrufenden Fenster: Dialogfenster öffnen
    private void ButtonOpenDialog_Click(object sender, RoutedEventArgs e){
      DialogWindow frm = new DialogWindow();
      if (frm.ShowDialog() == true)
        // Anweisungen, wenn die Schaltfläche OK angeklickt wird
    }
Das Dialogfenster enthält die zwei Schaltflächen OK und Cancel. Die Eigenschaft IsCancel ist für die Abbrechen-Schaltfläche im XAML-Code auf True gesetzt.
    <Button Content="OK" Click="ButtonOk_Click" ... />
    <Button Content="Cancel" IsCancel="True" ... />
Die OK-Schaltfläche signalisiert dem aufrufenden Fenster, dass der Anwender seine Eingaben im Dialog bestätigt. Damit der Dialog geschlossen und gleichzeitig der Rückgabewert true an den Aufrufer zurückgeliefert wird, muss die Eigenschaft DialogResult des Dialogs auf true gesetzt werden. Üblicherweise wird das im Click-Ereignis der Schaltfläche codiert.
    // Click-Ereignishandler der OK-Schaltfläche im Dialogfenster
    private void ButtonOk_Click(object sender, RoutedEventArgs e) {
      this.DialogResult = true;
    }
Sie können durch das Festlegen der Eigenschaft IsDefault=true der OK-Schaltfläche dem Dialog mitteilen, dass es sich bei dieser Schaltfläche um die Standardschaltfläche handelt, die der Anwender durch Drücken der -Taste erreichen kann. Das bedeutet mit anderen Worten: Unabhängig davon, welche Schaltfläche im Fenster aktuell den Fokus hat, wird beim Drücken der -Taste das Click-Ereignis der OK-Schaltfläche ausgelöst und der entsprechende Ereignishandler abgearbeitet. #### Fenster schließen Um ein Fenster zu schließen, stehen Ihnen zwei Möglichkeiten zur Verfügung: die Methoden Hide und Close. Mit Hide wird das Fenster nur unsichtbar geschaltet, bleibt aber weiterhin im Speicher. Sie können das Fenster durch den erneuten Aufruf der Methode Show sichtbar machen, eine Neuinstanziierung ist nicht notwendig. Das Fenster wird in dem Zustand geöffnet, den es vor dem Aufruf von Hide hatte. Mit Close wird das Fenster geschlossen, und die beanspruchten Ressourcen werden freigegeben. Das bringt natürlich Ressourcenvorteile im Vergleich zum Verstecken des Fensters. Benötigen Sie das Fenster erneut, müssen Sie die zugrunde liegende Klasse instanziieren. Beim Schließen eines Fensters treten hintereinander die beiden Ereignisse Closing und Closed auf. Beide unterscheiden sich in der Programmierung dahingehend, dass im Ereignishandler von Closing der eigentlich eingeleitete Schließvorgang im letzten Augenblick noch abgebrochen werden kann. Dazu setzen Sie die Eigenschaft Cancel des EventArgs-Parameters auf true, zum Beispiel:
    private void Window_Closing(object sender, CancelEventArgs e){
      MessageBoxResult result = MessageBox.Show("Schließen?", "Beenden",
                                MessageBoxButton.YesNo,
                                MessageBoxImage.Question,
                                MessageBoxResult.No);
      if (result == MessageBoxResult.No)
        e.Cancel = true;
    }
Diese Chance gibt es nicht mehr, wenn das Ereignis Closed ausgelöst wird. Im Zweifelsfall sind jetzt noch die Benutzereingaben zu speichern, ansonsten sind sie unwiederbringlich verloren. ### 18.1.4 Fenster vom Typ »NavigationWindow« Weiter oben wurde schon angedeutet, dass ein Fenster basierend auf der Klasse NavigationWindow dem Benutzer eine Navigation anbietet, die der eines Browsers ähnelt. Die Umsetzung der Navigation macht mit NavigationWindow keine Schwierigkeiten, da alle notwendigen Verhaltensweisen bereits zur Verfügung gestellt werden. Die Klasse NavigationWindow ist von der Klasse Window abgeleitet und weist daher dieselben Eigenschaften und Methoden auf. Allerdings repräsentiert NavigationWindow nur einen Rahmen als Grundstruktur, der weder Programmlogik noch Steuerelemente enthalten sollte. Dabei ist die Navigationsleiste, mit der der Anwender durch die Seiten navigiert, bereits vordefiniert. Über die vom Anwender aufgerufenen Seiten müssen Sie sich als Entwickler keine Gedanken machen, denn das NavigationWindow protokolliert die aufgerufenen Seiten in einem Journal. Innerhalb des NavigationWindow werden die Inhalte durch Page-Objekte beschrieben. Für jeden darzustellenden Inhalt müssen Sie ein Page-Objekt erstellen, das die Logik und die Steuerelemente des Inhalts beschreibt. Page ähnelt zwar in vielerlei Hinsicht einem Window-Objekt, kann aber nicht selbstständig angezeigt werden. Daher werden Sie auch keine Eigenschaften vorfinden, die die Größe und Position eines Page-Objekts betreffen. NavigationWindow wird nicht als entsprechende Vorlage angeboten. Möchten Sie Ihrer Anwendung ein NavigationWindow hinzufügen, bleibt Ihnen nichts anderes übrig, als zunächst einmal ein Window-Element als Grundlage zu benutzen. Ändern Sie in der XAML-Datei den Elementbezeichner von Window in NavigationWindow ab, und vergessen Sie nicht, in der Codebehind-Datei die Basisklassenangabe ebenfalls anzupassen.
    <NavigationWindow x:Class="WpfApplication1.MainWindow"
       xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
       xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
       Title="MainWindow" Height="350" Width="525">
    </NavigationWindow>
    public partial class MainWindow : NavigationWindow {
      public MainWindow() {
        InitializeComponent();
      }
    }
Im folgenden Codefragment sehen Sie, wie ein NavigationWindow mit Code erzeugt wird. Um in diesem Fenster auch sofort einen Inhalt anzuzeigen, wird ein neues Page-Objekt erzeugt und dessen Referenz der Eigenschaft Content übergeben.
    NavigationWindow navWindow = new NavigationWindow();
    navWindow.Content = new Page();
    navWindow.Width = 300;
    navWindow.Height = 200;
    navWindow.Show();
In Abbildung 18.3 sehen Sie die Ausgabe eines NavigationWindow-Fensters ohne Inhalt. In der Entwicklungsumgebung sind die Navigationsschaltflächen nicht zu sehen. Hier klicken, um das Bild zu vergrößern Abbildung 18.3 Ein »NavigationWindow« ohne Inhalt zur Laufzeit ### 18.1.5 Das »Page«-Element Nachdem Sie das Element NavigationWindow kennengelernt haben, ist es an der Zeit, sich mit der Darstellung der Inhalte in einem Page-Objekt zu beschäftigen. Dazu werden Page-Objekte benutzt. Page wird als Vorlage angeboten, sodass Sie eine Page über das Kontextmenü des Projekteintrags im Projektmappen-Explorer der Anwendung hinzufügen können (Vorlage: Seite). Dabei wird der folgende XAML-Code erzeugt:
    <Page x:Class="WpfApplication1.Page1"
          xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
          xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
          ...
          Title="Page1">
    </Page>
Welche Page des Projekts das NavigationWindow direkt beim Start anzeigen soll, teilen Sie der Eigenschaft Source mit, zum Beispiel:
    <NavigationWindow ...
      Source="Page1.xaml">
    </NavigationWindow>
In der Titelleiste des NavigationWindow wird jedoch nicht der Text angezeigt, den Sie der Eigenschaft Title des Page-Objekts zugewiesen haben. Um einen spezifischen Text in der Titelleiste anzuzeigen, verwenden Sie die Eigenschaft WindowTitle. Die Eigenschaft Title dient nur als Angabe in der Navigationsliste der Vor- und Zurück-Schaltflächen des Seitencontainers. Um die Darstellungsgröße des NavigationWindow an den Inhalt der Page anzupassen, legen Sie die Eigenschaften WindowHeight und WindowWidth der Seite fest. Möchten Sie mit Links oder beispielsweise Buttons selbst die Navigation in die Hand nehmen, können Sie mit ShowNavigationUI=false die Navigationssteuerelemente im Container ausblenden. Es werden nur sehr wenige spezifische Seiteninfomationen gespeichert, wenn eine Page zur Laufzeit der Anwendung verlassen wird. Viele Einstellungen der Seite gehen dadurch verloren. Dieses Verhalten dient dazu, Speicherressourcen zu schonen, und es kann problematisch sein, wenn die Seite Objekte erzeugt, die später noch einmal verwendet werden müssen. Mit der Einstellung KeepAlive=true lässt sich das Standardverhalten ändern (die Standardvorgabe lautet false). Die Seite wird dann komplett im Speicher gehalten. #### Navigation zwischen den Seiten Um dem Benutzer die Navigation zwischen den Seiten der Anwendung zu ermöglichen, werden Ihnen mit HyperLink und NavigationService zwei Varianten angeboten. Verwenden Sie einen Hyperlink, müssen Sie sowohl die Zieladresse angeben als auch einen Text, auf den der Benutzer klicken soll.
    <TextBlock>
      <Hyperlink NavigateUri="Page2.xaml">Zur Seite 2</Hyperlink>
    </TextBlock>
Das HyperLink-Element muss sich entweder in einem TextBlock oder einem FlowDocument befinden. Das Ziel des Hyperlinks wird an die Eigenschaft NavigateUri übergeben. Dabei müssen Sie beachten, dass die XAML-Datei genannt wird und nicht der Klassenbezeichner. Anstelle einer Zeichenfolge lassen sich, wie auch in HTML-Seiten, auch Images verlinken.
    <TextBlock>
      <Hyperlink NavigateUri="/Testordner/Page2.xaml">
        <Image Source="Mein.jpg" />
      </Hyperlink>
    </TextBlock>
Hinweis
Befindet sich die verlinkte XAML-Datei im aktuellen Verzeichnis, genügt die einfache Dateiangabe. In WPF lässt sich aber auch eine Verzeichnisstruktur festlegen, beispielsweise um die Ressourcen eindeutig benennen zu können. Angenommen, die Page mit dem Namen Page1.xaml würde sich im Ordner CollectedPages befinden, müssten Sie die Seite wie folgt adressieren:
    NavigateUri="../CollectedPages/Page1.xaml"
Es handelt sich dabei aber nicht um ein Verzeichnis, das nach dem Kompilieren im Ausgabeverzeichnis des Kompilats zu finden ist.
Das HyperLink-Element eignet sich insbesondere zur Angabe im XAML-Code. Um mittels Programmcode von einer Seite zu einer anderen zu navigieren, benutzen Sie das vom Container der Page zur Verfügung gestellte Objekt vom Typ NavigationService. An dessen Methode Navigate übergeben Sie entweder ein Uri-Objekt mit der Angabe der gewünschten Seite oder ein neues Page-Objekt. In einem Ereignishandler für das Click-Ereignis eines Buttons könnte der Zugriff auf die zum Projekt gehörende Page2 wie folgt codiert werden:
    private void Button1_Click(object sender, RoutedEventArgs e) {
      NavigationService.Navigate(new Uri("Page2.xaml", UriKind.Relative));
    }
oder alternativ:
    private void Button1_Click(object sender, RoutedEventArgs e) {
      NavigationService.Navigate(new Page2());
    }
Diese beiden Anweisungen sowie auch die Variante mit dem Hyperlink erzeugen jeweils ein neues Page-Objekt. Das ist insofern problematisch, als dadurch eventuell vorhandene Informationen der Seite, von der die Navigation angestoßen wird, verloren gehen. Sie sollten daher die Eigenschaft KeepAlive der betreffenden Seite auf true eingestellt haben. Im Programmcode müssen Sie ein wenig anders vorgehen. Sie können dazu ein vorhandenes Page-Objekt an die Navigate-Methode übergeben oder alternativ die beiden Methoden GoForward oder GoBack benutzen. In den beiden letztgenannten Fällen müssen Sie vorher allerdings mit CanGoBback und CanGoForward überprüfen, ob das möglich ist. Eine weitere Variante zur Navigation bietet das Ereignis RequestNavigate des HyperLink-Objekts. Sehen wir uns das am besten an einem Beispiel an. Beginnen wir mit dem XAML-Code, in dem an das Ereignis RequestNavigate ein Ereignishandler gebunden wird.
    <TextBlock>
      <Hyperlink NavigateUri="Page2.xaml"
                 RequestNavigate="Hyperlink_RequestNavigate">
          Zur Seite 2
      </Hyperlink>
    </TextBlock>
In der Codebehind-Datei der Seite wird ebenfalls die Methode Navigate von NavigationService aufgerufen. Als Argument wird die Eigenschaft Uri des EventArgs-Objekts weitergeleitet. Durch das Setzen von true der Eigenschaft Handled teilen wir dem Objekt mit, dass wir die Aktion selbst übernommen haben. Alle weiteren Operationen des Hyperlinks werden damit unterbunden.
    private void Hyperlink_RequestNavigate(object sender,
                                           RequestNavigateEventArgs e){
      NavigationService.Navigate(e.Uri);
      e.Handled = true;
    }
Mit dem Ereignishandler gewinnen wir ein hohes Maß an Flexibilität, die es uns gestattet, parallel zur Navigation weitere Operationen auszuführen. #### Datenübergabe zwischen den Seiten Daten von einer Seite an eine andere zu übergeben, stellt grundsätzlich kein Problem dar. Wir benötigen dazu nur einen parametrisierten Konstruktor. Allerdings müssen wir dann auch Programmcode schreiben, da der Aufruf eines parametrisierten Konstruktors aus XAML-Code heraus nicht möglich ist. Zur Vorbereitung muss nur der Konstruktor der betreffenden Seite überladen werden, z. B. so:
    public partial class Page2 : Page {
      public int Value1 { get; set; }
      public long Value2 { get; set; }
      public Page2() {
        InitializeComponent();
      }
      public Page2(int value1, long value2) : this() {
        Value1 = value1;
        Value2 = value2;
      }
    }
Sie dürfen dabei nicht vergessen, den Aufruf des parametrisierten Konstruktors an den parameterlosen weiterzuleiten, in dem die Methode InitializeComponent dafür sorgt, die Seite mit den gewünschten Steuerelementen auszustatten. Natürlich kann InitializeComponent auch direkt im parametrisierten Konstruktor angegeben werden. Vergessen Sie jedoch den Aufruf der Methode, zeigt Ihre Seite keine Steuerelemente an und bleibt leer. Die Übergabe von Daten beim Aufruf der neuen Seite bedarf nun keiner besonderen Erklärung mehr. Der Code könnte zum Beispiel so lauten:
    NavigationService.Navigate(new Page2(20, 78));
#### Datenübergabe mit der Methode »Navigate« Eine andere Alternative bietet die Methode Navigate. Wir haben diese Methode bisher immer nur in ihrer einfachsten Form benutzt, aber die Überladungen gestatten auch die Übergabe eines Werts vom Typ Object. Die aufgerufene Seite stellt den Übergabewert im EventArgs-Parameter des Ereignisses LoadCompleted zur Verfügung. Dieses Ereignis tritt auf, wenn die Page geladen wurde und das Zeichnen der Seite beginnt. Was sich im ersten Moment noch sehr einfach anhört, wird sich schnell als Hürde erweisen. Dabei ist die Problematik die Registrierung des Ereignishandlers. Der Konstruktor der Page kommt nicht in Frage, weil die Page zu diesem Zeitpunkt noch nicht vollständig erzeugt ist. Eine weitere Idee könnten die Ereignisse Initialize und Loaded der Page sein. Doch Initialize wird während des Konstruktoraufrufs ausgelöst. Zu diesem Zeitpunkt ist die Eigenschaft NavigationService noch null. Loaded hingegen erst tritt auf, nachdem zu der Seite navigiert worden ist, also nach der Auslösung von LoadCompleted. Die Lösung ist in einer zusätzlichen Methode zu finden, die von der aufgerufenen Seite bereitgestellt werden muss. Im folgenden Beispielprogramm wird das erläutert. Dieses wird aus zwei Pages gebildet, die in einem NavigationWindow angezeigt werden. In Page1 befindet sich ein Button, von dem aus zur Page2 navigiert wird. Dabei wird der Inhalt der TextBox, die sich in Page1 befindet, an die zweite Seite weitergeleitet (siehe auch Abbildung 18.4). Hier klicken, um das Bild zu vergrößern Abbildung 18.4 Das Beispielprogramm »Navigation«
    // ----------------------------------------------------------
    // Beispiel: ...\Beispiele\Kapitel 18\Navigation
    // ----------------------------------------------------------
    <!-- NavigationWindow-->
    <NavigationWindow ...
         Title="MainWindow" Height="200" Width="300"
         Source="Page1.xaml">
    </NavigationWindow>
    <!-- Page1 -->
    <Page ...
          WindowTitle="Seite 1"
       Title="Page1" WindowHeight="200">
    <StackPanel>
       <TextBox Name="TextBox1" ...></TextBox>
       <Button Name="Button1" Click="Button1_Click" ...>
         Zur Seite 2 navigieren
      </Button>
    </StackPanel>
    </Page>
    <!-- Page2 -->
    <Page ...
          WindowTitle="Seite 2"
          Title="Page2" WindowHeight="150">
    <StackPanel>
      <TextBox Name="TextBox1" ...></TextBox>
    </StackPanel>
    </Page>
    // Code in Page1
    public partial class Page1 : Page {
      private void Button1_Click(object sender, RoutedEventArgs e) {
        Page2 page = new Page2();
        page.SetLoadCompletedHandler(NavigationService);
        NavigationService.Navigate(page,TextBox1.Text);
      }
    }
    // Code in Page2
    public partial class Page2 : Page {
      public void SetLoadCompletedHandler(NavigationService ns) {
        ns.LoadCompleted += new LoadCompletedEventHandler(ns_LoadCompleted);
      }
      void ns_LoadCompleted(object sender, NavigationEventArgs e) {
        if (e.ExtraData != null && (e.ExtraData is String))
          TextBox1.Text = (string)e.ExtraData;
        this.NavigationService.LoadCompleted -= ns_LoadCompleted;
      }
    }
Betrachten wir zuerst den Code in Page2. Die Seite stellt mit SetLoadCompletedHandler eine öffentliche Methode zur Verfügung, in der das Ereignis LoadCompleted registriert wird. Im Ereignishandler, der sich in Page2 befindet, erfolgt die Auswertung der übermittelten Daten, die von der Eigenschaft ExtraData des NavigationEventArgs-Objekts bereitgestellt werden. Nach der notwendigen Überprüfung, ob die Daten von null abweichen und ob es sich um Daten vom Typ String handelt, wird der Übergabewert nach vorhergehender Typkonvertierung in die TextBox der Page2 geschrieben. Danach sollte die Registrierung des Ereignishandlers in jedem Fall wieder aufgehoben werden. In Page1 wird nach der Instanziierung der Klasse Page2 die Methode SetLoadCompletedhandler aufgerufen und das NavigationService-Objekt an die Methode übergeben. Damit sind alle vorbereitenden Maßnahmen getroffen, und die Navigate-Methode kann unter Übergabe des Inhalts der TextBox in Page1 aufgerufen werden. #### Ereignisse des »NavigationService«-Objekts Das Ereignis LoadCompleted ist nicht das einzige des NavigationService-Objekts. Zumindest vier weitere sollten auch noch an dieser Stelle genannt werden: Navigating Dieses Ereignis wird kurz vor dem Seitenwechsel ausgelöst. In diesem Ereignis kann der Seitenwechsel im letzten Moment noch abgebrochen werden, indem die Eigenschaft Cancel des EventArgs-Parameters auf true gesetzt wird. Navigated Wird dieses Ereignis ausgelöst, hat die Navigation bereits begonnen. NavigationFailed Das Ereignis wird ausgelöst, wenn während des Ladevorgangs ein Fehler auftritt. Das kann beispielsweise der Fall sein, wenn die angegebene Seite nicht gefunden wird. NavigationProcess Dieses Ereignis tritt während des Ladevorgangs der Seite permanent auf. Im Ereignishandler lässt sich das EventArgs-Objekt auswerten. So können Sie die Eigenschaft MaxBytes auswerten, um zu erfahren, wie viele Daten die Seite insgesamt erfordert, während ReadBytes angibt, wie viele Daten bereits übertragen worden sind. #### Navigation im Internet Die Navigation ist nicht zwangsläufig nur auf die Seiten der aktuellen Anwendung beschränkt. Sie können dem Attribut Source des NavigationWindow auch eine Internetadresse übergeben. Gleiches gilt auch für die Methode Navigate. Allerdings müssen Sie dann auch der Methode mitteilen, dass es sich um eine Absolutadresse handelt, beispielsweise mit:
    NavigationService.Navigate(new Uri("http://www.dotnet-training.de",
                                        UriKind.Absolute));
Sollten Sie nun der Meinung sein, Sie hätten es hierbei mit einer abgespeckten Variante eines WebBrowser-Steuerelements zu tun, liegen Sie falsch. Das Navigationsverhalten des NavigationWindow protokolliert unverständlicherweise nicht die besuchten Seiten im Internet. Klicken Sie auf den Zurück-Button im Fenster, landen Sie wieder bei der Page, von der aus Sie sich die erste Webseite haben anzeigen lassen. ### 18.1.6 Das »Frame«-Steuerelement Eine Alternative zum NavigationWindow wird mit dem Steuerelement Frame angeboten. Sie können es dazu benutzen, in einem Teilbereich eines Window Navigation zu ermöglichen. Damit lässt sich die Einschränkung umgehen, dass in einem NavigationWindow keine Steuerelemente angezeigt werden können. In Abbildung 18.5 ist ein Fenster zu sehen, das in seinem rechten Bereich einen Frame aufweist, in dem die eigentlichen Inhalte angezeigt werden. Diesem liegt die folgende XAML-Struktur zugrunde:
    <Window ...>
    <Grid>
    <Grid.ColumnDefinitions>
       <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <StackPanel>
        <TextBox ...></TextBox>
        <Button>OK</Button>
      </StackPanel>
      <Frame Source="Page1.xaml" ...></Frame>
    </Grid>
    </Window>
Hier klicken, um das Bild zu vergrößern Abbildung 18.5 Ein »Window«-Element mit einem »Frame«