Von der Datenbank bis zur Oberfläche mit .NET, Teil 4: Desktop-Entwicklung mit WPF und MVVM

Seite 2: Hauptfenster

Inhaltsverzeichnis

Nun wendet sich der Autor der MainWindow.xaml-Datei zu, deren XAML-Code in Listing 1 zu sehen ist. Die Standardeinträge im Wurzel-Tag der Datei wurden um einen Verweis auf den Namensraum WWWings_WPF.Ansichten ergänzt, in dem man Views und ViewModels der Ansichten findet. Danach folgt der Window.Resources-Teil, der für die beiden Ansichten die Verbindung zwischen Views und ViewModels herstellt. Allein die Namensähnlichkeit reicht für die Verbindung nicht aus.

Mit einem <Grid>-Element wird der Bildschirm in zwei Bereiche geteilt: den ersten für das Ribbon (Größe "Auto" bedeutet, dass das Ribbon über seine Größe selbst entscheiden darf) und den zweiten (Größe "*" entspricht dem Rest des Bildschirms) für das jeweils aktuelle User Control. Darauf folgt der Aufbau des Ribbons aus zwei RibbonTab-Elementen mit jeweils mehreren RibbonGroup-Elementen, die RibbonButton-Elemente enthalten. Die RibbonTab-Steuerelemente binden ihre IsSelected-Eigenschaften an Properties mit Namen Tab1Aktiv beziehungsweise Tab2Aktiv. Dabei kommt ein Datenbindungsausdruck in Form einer sogenannten XAML Markup Extension in geschweiften Klammern zum Einsatz, zum Beispiel IsSelected="{Binding Path=Tab1Aktiv}". Hier ist Path= optional. Die RibbonButton-Steuerelemente nehmen ebenfalls mit WPF-Datenbindungsausdrücken Bezug auf Properties, die Command-Objekte enthalten müssen.

Am Schluss findet man ContentControl als Platzhalter fĂĽr die jeweils anzeigende User Control. Sie bindet den Inhalt an eine Property mit Namen ActiveAnsichtViewModel:

<ContentControl Content="{Binding Path=ActiveAnsichtViewModel}" 
Grid.Row="1"></ContentControl>

Sowohl Ribbon als auch ContentControl platzieren sich mit Grid.Row="..." in einen der beiden Bereiche des Bildschirms. Das <Grid>-Element ist eines von mehreren Layout-Steuerelementen in WPF. Mit ihm teilt der Entwickler den Bildschirm tabellarisch in Zeilen und Spalten. Optional kann er dem Fenster und den Ribbon-Elementen ein Bild als Symbol zuweisen. Das Bild muss er dem WPF-Projekt als Datei hinzufĂĽgen und in den Dateieigenschaften als "Resource" kennzeichnen. Hier wurde das Bild im Ordner /Images abgelegt, der durch die Projektvorlage automatisch entstanden ist.

Die Anwendung lässt sich nun starten: Man sieht oben das Ribbon und kann zwischen den beiden Ribbon Tags wechseln. Allerdings wird noch keines der beiden User Controls geladen. Es fehlt der Steuerungscode für das Laden und das Wechseln der Ansichten. Den könnte man nun in der Code-Behind-Datei MainWindow.xaml.cs in klassischer Vorgehensweise mit direkter Bezugnahme auf die einzelnen Steuerelemente hinterlegen. Nach dem MVVM-Prinzip bleibt diese Datei aber fast unangetastet. Der Programmierer schreibt ein ViewModel, das gar nicht von der Fensterklasse RibbonWindow oder irgendeinem Steuerelement abhängig ist. Für diese Vorgehensweise ist die View (MainWindows.xaml) durch die Datenbindungsausdrücke ja vorbereitet.

Das unabhängige MainWindowViewModel zeigt Listing 2. Das ViewModel definiert ein Command (EndeCommand), das mit der Hilfsklasse ActionCommand implementiert ist.

using System;
using System.Windows.Input;

namespace WWWings_WPF
{

/// <summary>
/// Hilfsklasse fĂĽr MVVM-Commands
/// </summary>
public class ActionCommand : ICommand
{
public ActionCommand(Action action)
{
this.action = action;
this.IsEnabled = true;
}

private bool isEnabled;

public bool IsEnabled
{
get { return isEnabled; }
set
{
isEnabled = value;
if (CanExecuteChanged != null) CanExecuteChanged(this,
EventArgs.Empty);
}
}
private Action action;

public bool CanExecute(object parameter)
{
return IsEnabled;
}

public event EventHandler CanExecuteChanged;

public void Execute(object parameter)
{
action();
}

}
}

Leider liefert Microsoft keine Klasse in .NET mit, die das Erstellen von Command-Objekten so vereinfacht wie diese ActionCommand-Hilfsklasse. Ein großer Vorteil von Command-Objekten gegenüber der direkten Registrierung auf Ereignissen ist, dass Command-Objekte eine IsEnabled-Eigenschaft besitzen. Wenn man diese Eigenschaft auf "false" setzt, hat das zur Folge, dass alle Schaltflächen und Menüpunkte automatisch deaktiviert werden, die an das Command-Objekt gebunden sind. Das EndeCommand ist ein einfacher Befehl, weil er nur das aktive Fenster beendet. Im Konstruktor wird die Methode Ende() mit EndeCommand verbunden. In der View MainWindow.xaml werden weitere Command-Objekte referenziert, die sich aber auf Befehle in den beiden Ansichten beziehen und daher in deren ViewModel-Klassen zu realisieren sind.

Das MainWindowViewModel setzt neben dem Command-Objekt drei Attribute um:

  1. Tab1Selected: ist "true", wenn Ribbon-Tab 1 gewählt ist.
  2. Tab2Selected: ist "true", wenn Ribbon-Tab 2 gewählt ist.
  3. ActiveAnsichtViewModel: verweist auf das ViewModel der derzeit aktiven Ansicht.

Bei Attributen ist zu beachten, dass sie als Properties mit Getter (nicht Fields) realisiert sein und die gebundene View über den INotifyPropertyChanged-Mechanismus von .NET über Veränderungen informieren müssen. Deshalb ist INotifyPropertyChanged als Schnittstelle für die Klasse MainWindowViewModel vorgegeben und ebenso in jedem Setter OnPropertyChanged() mit dem Namen der Property aufzurufen. Dabei steht der Name in Anführungszeichen, das heißt, der Entwickler erhält keine Eingabeunterstützung. Bei einem Tippfehler warnt der Compiler nicht, dass die Anwendung zur Laufzeit nicht wie erwartet agiert. Leider gibt es im .NET Framework noch keinen Mechanismus, der den INotifyPropertyChanged-Mechanismus elegant kapselt. Open-Source-Frameworks (etwa Caliburn, Caliburn Micro und Microsoft Prism) bieten hierfür aber Unterstützung, auch beim Finden und Vermeiden von Schreibfehlern. Da dieses Tutorial überschaubar bleiben soll, verzichtet der Autor an der Stelle jedoch auf die Einführung zusätzlicher Frameworks.

Tab1Aktiv und Tab2 Aktiv sind ja an die entsprechenden IsSelected-Eigenschaften der beiden RibbonTab-Elemente gebunden. Durch Wechsel der Ribbon-Registerkarte ändert das Ribbon-Steuerelement automatisch die IsSelected-Eigenschaften. Im Setter der beiden Properties wird demnach auch das ActiveAnsichtViewModel-Attribut mit einer Instanz des entsprechenden ViewModel belegt. ActiveAnsichtViewModel ist an den Inhalt der Content Control gebunden. Durch das in der MainWindow.xaml-Datei definierte Data Template erfolgt dann automatisch das Laden der zum ViewModel gehörenden View. Das Template beschreibt, welcher XAML-Code für welche Datenklasse zu verwenden ist. Data Templates kommen in einzelnen Steuerelementen zum Einsatz, können aber wie hier auch ganze Ansichten steuern. Initial muss der Entwickler im Konstruktor des ViewModel die beim Start zu ladende Ansicht festlegen.

Command-Objekte und die zur Datenbindung verwendeten Properties sind übrigens als "public" zu deklarieren. Die Klassen View und ViewModel stehen ja nicht zueinander in einer Vererbungsbeziehung. Daher können sie nur über öffentliche Mitglieder kommunizieren.

Der Code-Behind-Datei MainWindows.xaml.cs

using Microsoft.Windows.Controls.Ribbon;

namespace WWWings_WPF
{
public partial class MainWindow : RibbonWindow
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
}

bleibt neben dem obligatorischen InitalizeComponent() im Konstruktor nur noch eine Aufgabe: die View in MainWindows.xaml mit MainWindowsViewModel zu verbinden. In vorstehendem Codebeispiel wird dazu der DataContext der View auf eine neue Instanz des ViewModel gesetzt.

Beim Start der Anwendung funktioniert nun das Umschalten zwischen den Ansichten über das Ribbon. Man sieht aber nur die in den beiden Ansichts-Views hinterlegten Texte. Daher gilt es nun, die ViewModels und Views der beiden Ansichten zu implementieren. Bewusst soll hier der Ausgangspunkt das ViewModel sein, um zu zeigen, dass man es tatsächlich als ViewModel ohne Kenntnisse der grafischen Darstellung in der View definieren kann.