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

Im dritten Teil des Tutorials entstand eine ASP.NET-Weboberfläche für den Webservice zur Flugbuchung. Nun soll eine vergleichbare Windows-Desktop-Anwendung mit WPF geschaffen werden. Dabei kommt das zunehmend beliebtere Pattern Model View ViewModel (MVVM) zum Einsatz.

In Pocket speichern vorlesen Druckansicht 4 Kommentare lesen
Lesezeit: 38 Min.
Von
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

Im dritten Teil des Tutorials entstand eine ASP.NET-Weboberfläche für den Webservice zur Flugbuchung. Nun soll eine vergleichbare Windows-Desktop-Anwendung mit WPF geschaffen werden. Dabei kommt das zunehmend beliebtere Pattern Model View ViewModel (MVVM) zum Einsatz.

Die Abbildung 1 zeigt die WPF-Applikation (Windows Presentation Foundation) als Benutzeroberfläche über Webservices, Geschäftslogik und Datenzugriffscode, die in Teil 1 und 2 des Tutorials entstanden sind. Die Anwendung (gelbe Kästen) nimmt die gleiche Position in der Gesamtarchitektur ein und soll die gleichen Funktionen wie die im dritten Teil erstellte Webanwendung erfüllen: Flugauswahl, Passagierauswahl, Flugbuchung und gegebenenfalls das Anlegen eines Passagiers.

Gesamtarchitektur mit der WPF-Anwendung (Abb. 1)

Erste Ansicht der zu erstellenden WPF-Anwendung (Abb. 2)

In den Abbildungen 2 und 3 sieht man das Ergebnis der entwickelten WPF-Anwendung. Wie bei der Webanwendung gibt es zwei Masken: eine für die Buchung und eine für das Erfassen neuer Passagiere. Die Buchungsmaske besteht aus drei Bereichen. Im oberen sucht und wählt man einen Flug, in der Mitte einen Passagier, und unten löst man für den gewählten Flug und Passagier eine Buchung aus. Anders als bei der Webanwendung gibt es aber nur ein Fenster. Die beiden Masken sind fallweise eingeblendete User Controls. Über allem steht ein Ribbon-Steuerelement mit zwei Ribbon Tabs. Das erste Ribbon gehört zur Buchungsmaske, das zweite zur Passagierverwaltung. Mit dem Klick auf das entsprechende Ribbon wird auch der Inhaltsbereich umgeschaltet.

Zweite Ansicht der zu erstellenden WPF-Anwendung (Abb. 3)

Für WPF-Anwendungen gibt es in Visual Studio 2010 für .NET 4.0 drei Projektvorlagen: "WPF Browser Application", "WPF Application" und "WPF Ribbon Application". Die erste Projektart läuft dann allerdings nur im Internet Explorer oder Firefox. Zum Erstellen einer Windows-Desktop-Anwendung wählt man "WPF Application" oder "WPF Ribbon Application". Hier soll Letztere zum Einsatz kommen, in der bereits das Ribbon-Steuerelement angelegt ist.

Das Projekt soll WWWings_WPF heißen. Die WPF-Anwendung benötigt Referenzen auf die in Teil 1 und 2 angelegten Projekte WWWings_GO und WWWings_ServiceProxies sowie die Systembibliothek System.ServiceModel. Außerdem muss der Programmierer die Dienstkonfiguration übernehmen. Dafür kann er die app.config-Datei aus WWWings_TestKonsole im Visual Studio Server Explorer per Drag & Drop nach WWWings_WPF kopieren.

Mehr Infos

Von der Datenbank bis zur Oberfläche mit .NET

Um das Beispiel mitzuprogrammieren, benötigt der Entwickler Grundkenntnisse in der C#-Syntax und der Handhabung von Visual Studio 2010. Das Tutorial verwendet die englische Version von Visual Studio, weil es in der deutschen Ausgabe einen Fehler gibt, durch den sich das Beispiel nicht ohne viel manuelle Arbeit zum Laufen bringen lässt.

WWWings_WPF erhält durch die Vorlage die Datei MainWindows.xaml mit einem Hauptfenster, das die Grundstruktur eines Ribbons enthält. Das Fenster ist vom vordefinierten Typ RibbonWindow. Das Ribbon wird durch Tags wie Ribbon, RibbonTab, RibbonGroup und RibbonButton strukturiert. Die Oberflächenbeschreibung in WPF erfolgt in XAML (Extensible Application Markup Language), einer von Microsoft geschaffenen XML-Sprache, die wesentlich mächtiger als HTML ist.

Neben der Oberflächenbeschreibung in der XAML-Datei gibt es eine Code-Behind-Datei (.xaml.cs). Das entspricht der Struktur von ASP.NET-Webseiten (.aspx und .aspx.cs, vgl. Teil 3 des Tutorials). Die ebenfalls automatisch angelegte app.xaml-Datei repräsentiert den Einsprungpunkt in die Anwendung; in ihr ist nichts mehr als der Verweis auf MainWindow.xaml enthalten.

Nun gilt es erst mal, die Grundstruktur der WPF-Anwendung anzulegen. Für die beiden User Controls werden ein Ordner "Ansichten" im Projekt und darin zwei Projektelemente vom Typ "User Control (WPF)" mit Namen "BuchungsView" und "NeuerPassagierView" angelegt. Es entstehen jeweils eine .xaml- und eine .xaml.cs-Datei. In die .xaml-Datei kann man für einen ersten Test zwischen die <Grid>-Tags einen Text in der Form <TextBlock>Buchung</TextBlock> beziehungsweise <TextBlock>Neuer Passagier</TextBlock> schreiben. Die Designeroberfläche sollte den Text dann anzeigen.

Zumindest für die beiden User Controls soll die Entwicklung nach dem Model View ViewModell (MVVM) Pattern erfolgen. Darunter versteht man die Adaption des Model View Controller (MVC) Pattern auf WPF und Silverlight. Model und View entsprechen dabei der Bedeutung im klassischen MVC-Entwurfsmuster. Das ViewModel lässt sich als eine spezielle Implementierungsform eines Controllers mit der Aufgabe ansehen, die im Modell enthaltenen Informationen dergestalt aufzubereiten, dass man die Datenbindungstechniken in WPF und Silverlight direkt nutzen kann. Das ViewModel hat außerdem die Aufgabe, sogenannte Commands zu implementieren, die an Ereignisse der Benutzeroberfläche gebunden werden. Das ViewModel kennt die View nicht, und das Model weder das ViewModel noch die View, wie Abbildung 4 veranschaulicht.

MVVM-Entwurfsmuster (Abb. 4)

Das oberste Ziel von MVVM ist es, die Kompetenztrennung, das heißt die Arbeit von Grafikern/Designern (Views erstellen) und die der Entwickler (Model und ViewModel erstellen) zu trennen. Dadurch lässt sich eine Benutzeroberfläche später leicht austauschen. Darüber hinaus sorgt die strikte Trennung für eine Verbesserung der automatisierten Testbarkeit.

Im konkreten Fall heißt das, dass im Ordner "Ansichten" jeweils noch die C#-Klassendateien
BuchungViewModel.cs und NeuerPassagierViewModel.cs anzulegen sind. Beide müssen die Schnittstelle INotifyPropertyChanged implementieren, die dazu dient, die View über Änderungen im ViewModel zu informieren (siehe folgenden Quellcode). Mit konkreten Befehlen in Form von Command Objekten und Properties zur Datenbinding werden diese beiden Klassen erst später gefüllt, denn zunächst soll man nur das Hauptfenster mit der Umschaltung zwischen den beiden Ansichten realisieren. Doch vorab die Grundstruktur des ViewModel für die Buchung-Ansicht:

using System.ComponentModel;
namespace WWWings_WPF.Ansichten
{
public partial class BuchungViewModel : INotifyPropertyChanged
{


/// ########### INotifyPropertyChanged ###########

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}

Gefolgt von der Grundstruktur des ViewModel für die NeuePassagier-Ansicht:

using System.ComponentModel;
namespace WWWings_WPF.Ansichten
{
public partial class NeuerPassagierViewModel : INotifyPropertyChanged
{

/// ########### INotifyPropertyChanged ###########

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion

}
}

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.

Listing 3 zeigt das ViewModel für die Buchungsansicht. Darin enthalten sind drei Command-Objekte für FlugSuchen, PassagierSuchen und Buchen. Die mit der ActionCommand-Klasse implementierten Command-Objekte werden im Konstruktor instanziiert und mit drei Methoden verbunden, die von der Logik denen ähnlich sind, die der Autor in Teil 3 des Tutorials für ASP.NET geschrieben hat. Aber anders als im ASP.NET-Beispiel gibt es keine direkten Bezugnahmen auf Steuerelemente. Vielmehr lädt der Konstruktor die Liste der Flughäfen nicht direkt in ein Auswahlsteuerelement, sondern in eine Liste von Zeichenketten (Flughaefen), die als öffentliche Property (mit INotityPropertyChanged-Mechanismus) der Klasse definiert ist.

Auch die Aktionsroutinen PassagierSuchen() und FlugSuchen() beziehen die Eingaben des Benutzers aus solchen Properties der Klasse (FlugNummer, Abflugort, Zielort, PassagierID und PassagierName) und legen die vom Webservice geladenen Listen wieder in Properties (Fluege und Passagiere) ab. Gleichwohl greift Buchen() auf die Variablen Flug und Passagier (die die aktuelle Auswahl enthalten werden) zu, um damit die Webservice-Operation CreateBuchung() aufzurufen. Konsequenterweise wird das Ergebnis in der Property BuchenErgebnis gespeichert. Auch die Farbe der Anzeige des Ergebnisses wandert in eine Property (BuchenErgebnisFarbe). Das Beispiel verdeutlicht einen Vorteil von MVVM: Die ViewModel-Klasse ist leicht durch Unit-Tests automatisiert testbar. Dabei instanziiert man im Unit-Test das ViewModel, setzt die Properties und ruft die Command-Objekte durch ihre Execute()-Methode auf.

Jede Property der Klasse löst im Setter OnPropertyChanged() aus, damit die View von Veränderungen erfährt, die der Programmcode der ViewModel-Klasse an einer Property vorgenommen hat. Neben den Properties der Klasse kann der ViewModel-Code die IsEnabled-Eigenschaft der Command-Objekte verändern. In Listing 3 sieht man, dass der Konstruktor IsEnabled von BuchenCommand auf "false" gesetzt ist. Bei jedem Zugriff auf Flug und Passagier prüft der Setter, ob beide Properties ungleich null sind. Nur wenn sowohl Flug als auch Passagier ungleich null sind, setzt der Programmcode BuchenCommand.IsEnabled auf "true".

Listing 4 zeigt die zugehörige View. Sie ist deutlich komplexer als die MainWindow.xaml-Datei, denn hier strukturiert das oberste Grid-Steuerelement den Bildschirm in elf Zeilen und drei Spalten. Die oberste und unterste Zeile sowie die erste und dritte Spalte dienen dazu, einen leeren Bereich zum Fensterrand von zehn Pixeln um den eigentlichen Inhalt herum zu definieren. Innerhalb der Zellen sind dann wieder Grid-Steuerelemente dafür verantwortlich, die Eingabesteuerelemente (TextBox und ComboBox) und die Schaltflächen (Button) in Spalten anzuordnen. Für die Ausgabe der gefundenen Objekte kommen DataGrid-Steuerelemente zum Einsatz. Die Ergebnisausgabe nach dem Buchen erfordert ein Label-Steuerelement. Die Kästen mit abgerundeten Ecken benötigen Border-Steuerelemente mit CornerRadius="4".

Bei den Eingabesteuerelementen und dem Label-Steuerelement sind die Texteigenschaften mit einem Datenbindungsausdruck (z. B. {Binding FlugNummer}) versehen. Die ComboBox-Steuerelemente haben direkt zwei Datenbindungsausdrücke: Neben Text ist hier die ItemSource an die Liste Flughaefen gebunden. Auch die DataGrid-Steuerelemente haben zwei Datenbindungsausdrücke: einen für die anzuzeigenden Daten in ItemSource und einen für das Übergeben des ausgewählten Steuerelements von SelectedItem an eine Property. Innerhalb der DataGrid-Steuerelemente nehmen die einzelnen DataGridTextColumn-Elemente dann per Datenbindungsausdruck Bezug auf die einzelnen Properties der gebundenen Flug- und Passagierobjekte. Die drei Button-Steuerelemente haben jeweils einen Datenbindungsausdruck auf der Command-Eigenschaft, der sich auf eines der Command-Objekte bezieht. Bei einzelnen Bindungsausdrücken sieht man Formatierungsanweisungen, zum Beispiel:

<DataGridTextColumn Header="Geburtsdatum" Binding="{Binding 
Geburtsdatum, StringFormat={}{0:dd.MM.yyy}}"/>

Alle Bindungen beziehen sich auf den gleichen Datenkontext. Anders als bei MainWindows.xaml wird er aber nicht in der Code-Behind-Datei gesetzt. Sie bleibt unangetastet, nur das automatisch von Visual Studio erzeugte InitializeComponent() ist dort zu finden (daher verzichtet der Autor hier auf die Wiedergabe der Datei). Das Setzen des Datenkontexts erledigt MainWindow über das zuvor beschriebene Data Template.

Dass Visual Studio MVVM noch nicht optimal unterstützt, merkt man deutlich beim Anlegen der View: Weder für die Property-Namen noch die Command-Objekte gibt es eine IntelliSense-Eingabeunterstützung. Der Entwickler muss diese frei eingeben. Wenn er sich vertippt, führt das nicht zu einem Kompilierfehler, denn die Datenbindungen werden erst zur Laufzeit vollzogen. Beim Debugging innerhalb von Visual Studio sieht man im Ausgabefenster Meldungen wie diese:

System.Windows.Data Error: 40 : BindingExpression path error: 'Tippfehler' property not found on 'object' ''Flug' (HashCode=3364521)'. BindingExpression:Path=Tippefehler; DataItem='Flug' (HashCode=3364521); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')

Zuvor sollte man die Ausgabe im Ausgabefenster auf "Program Output" (und ggf. noch "Exceptions") beschränkt haben, sonst gehen die Datenbindungswarnungen in den vielen Hinweisen auf das Laden von Codemodulen unter. Grundsätzlich kann man mehr Unterstützung im Editor erhalten, wenn das Model auch zur Entwicklungszeit nutzbar ist beziehungsweise man dafür Dummy-Klassen erzeugt (z. B. mit Expression Blend).

Falls man später vorhat, die Namen von Properties oder Command-Objekten zu ändern, ist zu beachten, dass die Refactoring-Funktion in Visual Studio leider nicht für die Datenbindungsausdrücke und die Zeichenketten in den OnPropertyChanged()-Aufrufen greift. Wieder muss der Entwickler händisch die Namen anpassen, sonst bleibt der Bildschirm leer und die Schaltflächen ohne Funktion und Werte aktualisieren sich nicht. Die Fehlersuche kann komplex werden, weil falsche OnPropertyChanged()-Aufrufe nicht zu Ausgaben im Ausgabefenster führen.

Nun gilt es noch, die Eingabemaske für neue Passagiere zu gestalten. Wie in der Webmaske gibt es vier Eingabefelder (Vorname, Name, Geburtsdatum und Passagierstatus) und zwei Aktionen (Speichern und Abbrechen), vergleiche Abbildung 2, die hier noch mal gebracht sei.

Erste Ansicht der zu erstellenden WPF-Anwendung (Abb. 2)

Die vier Eingabefelder werden im ViewModel NeuerPassagierViewModel.cs als eine einzige Property (mit INotifyPropertyChanged-Benachrichtigung) vom Typ Passagier erfasst. Die Aktionen sind als Command-Objekte mit der Klasse ActionCommand (siehe Listing 5) realisiert. Zusätzlich ist ein Ereignis NeuerPassagierViewModelExit umgesetzt, das NeuerPassagierViewModel auslöst, wenn entweder nach einem Speichern oder Abbrechen MainWindow die Information erhalten soll, dass wieder auf die Buchungsansicht zurückzuschalten ist. In der Webanwendung übernahm die Funktion Response.Redirect().

Die Speichern()-Routine in NeuerPassagierViewModel hat eine gewisse Ähnlichkeit mit NeuerPassagierManager in der Webanwendung. Allerdings kümmert sich die Speichern()-Routine in NeuerPassagierViewModel selbst um die Konstruktion des Passagier-Objekts aus den Eingabefeldern, denn eine komplett mit der ObjectDataSource aus ASP.NET vergleichbare Funktion gibt es nicht in WPF. Nach der Konstruktion des Objekts erfolgt der Aufruf der Webservice-Operation SavePassagierSet(), aus deren Ergebnis das endgültige Passagier-Objekt inklusive Primärschlüssel ermittelt wird. Bei Erfolg löst das ViewModel das Ereignis NeuerPassagierViewModelExit mit dem neuen Passagier-Objekt als Parameter aus. Bei Misserfolg oder Abbruch ist dieser Parameter null, sodass der Ereignisempfänger erkennen kann, dass kein gültiger neuer Passagier existiert.

Listing 6 zeigt die zugehörige View (NeuerPassagierView.xaml) mit den Datenbindungen für das Passagier-Objekt und zwei Command-Objekten. Die Datenbindungen müssen dabei den Pfad zum "Inneren" des Passagierobjekts beschreiben (z. B. {Binding Passagier.Vorname}), da ja eine Property des Objekts gesetzt werden soll, nicht das Objekt an sich. Die Passagier-Klasse ist eine Entitätsklasse des ADO.NET Entity Framework. Standardmäßig realisieren alle Properties einer Entitätsklasse INotifyPropertyChange, sodass das Zusammenspiel mit WPF gewährleistet ist.

Die Strukturierung des Bildschirms übernimmt auch in NeuerPassagierView ein <Grid>-Element mit sechs gleich hohen Zeilen und zwei Spalten. Innerhalb der Spalte 2 der Zeile 6 ordnet ein <StackPanel> die beiden Befehlsschaltflächen horizontal an.

Wenn der Leser nun die Anwendung startet, wird er feststellen, dass die Passagiereingabemaske zwar die erfassten Passagiere speichert, aber nicht zurück zur Buchungsmaske schaltet. Der Grund dafür liegt darin, dass MainWindowViewModel nicht auf das NeuerPassagierViewModelExit-Ereignis reagiert. Hier ist im Setter der Property Tab2Aktiv die bisherige Zeile

this.ActiveAnsichtViewModel = new 
WWWings_WPF.Ansichten.NeuerPassagierViewModel();

durch folgende Zeilen zu ersetzen:

      //// Später mit NeuePassagierViewModel, der Event auslöst:
var pvm = new WWWings_WPF.Ansichten.NeuerPassagierViewModel();
this.ActiveAnsichtViewModel = pvm;
pvm.NeuerPassagierViewModelExit += delegate(Passagier passagier)
{
Tab1Aktiv = true; // RibbonTab umschalten
this.ActiveAnsichtViewModel =
new WWWings_WPF.Ansichten.BuchungViewModel(passagier);
// ViewModel umschalten und Passagier übergeben
};

Der neue Code bindet das NeuerPassagierViewModelExit-Ereignis an eine zweizeilige Ereignisbehandlungsroutine, die zunächst das RibbonTab wieder und dann das BuchungViewModel unter Übergabe des vom NeuerPassagierViewModelExit-Ereignis gelieferten Passagier-Objekts umschaltet.

Damit das Projekt wieder kompiliert, muss nun der Konstruktor von BuchungViewModel ebenfalls zwei kleine Änderungen erfahren.

  1. Der Konstruktor erhält einen optionalen Parameter für ein Passagier-Objekt: public BuchungViewModel(Passagier passagier = null).
  2. Im Konstruktor ist – insofern ein Passagier-Objekt übergeben wurde – dieses Objekt in die Passagiere-Liste einzuhängen. Außerdem soll das übergebene Objekt das aktuelle Objekt werden. Damit erzielt man den gleichen Effekt wie in der Webanwendung: Ein neu angelegter Passagier erscheint sofort in der Passagierliste und ist auch ausgewählt.
    if (passagier != null)
    {
    Passagiere = new ObservableCollection<WWWings_GO.Passagier>() { passagier };
    Passagier = passagier;
    }

Der Programmcode der Klassen MainWindowViewModel und BuchungViewModel nach diesen Änderungen ist hier nicht mehr wiedergegeben, befindet sich aber im Rahmen der fertigen Gesamtlösung auf dem FTP-Server.

Damit soll die WPF-Anwendung fertig sein. Sicherlich könnte man hie und da die Oberfläche noch verschönern und den Zugriff auf die Webservices durch "Try ... Catch"-Blöcke absichern. Aber auch jetzt ist der Programmcode für eine Tutorialfolge umfangreich geraten.

Dieser Teil des Tutorials hat gezeigt, wie man auf Basis bestehender Webservices eine WPF-Anwendung im modernen Ribbon-Design unter Anwendung des MVVM-Entwurfsmusters realisiert. Auf den ersten Blick besitzt die WPF-Anwendung deutlich mehr Programmcodezeilen als die ASP.NET-Anwendung aus Teil 3. Die Ursache dafür liegt in der aufwendigeren Architektur der Benutzerschnittstelle, präziser gesagt, in der Tatsache, dass zusätzliche Properties und Command-Objekte für die Realisierung des MVVM-Entwurfsmusters zu schreiben waren. Bei Verzicht auf MVVM wären die Menge und der Aufbau des Programmcodes in WPF vergleichbar mit ASP.NET gewesen. Nur dass an die Stelle von HTML und ASP.NET-Steuerelement-Tags in WPF die Oberflächenbeschreibungssprache XAML tritt.

Im fünften und letzten Teil des Tutorials soll erneut XAML zum Einsatz kommen. Die WPF- wird in eine Silverlight-Anwendung umgewandelt, die im Browser läuft. Es wird interessant sein zu sehen, wie viel ein Entwickler dafür an Oberflächenbeschreibung und Benutzerschnittstellensteuerung ändern muss.

Holger Schwichtenberg
leitet das Expertenteam von www.IT-Visions.de, das Beratung und Schulungen im Umfeld von .NET und Microsofts Serverprodukten anbietet. Er hält Vorträge auf Fachkonferenzen und ist Autor zahlreicher Fachbücher.

  • Holger Schwichtenberg; .NET 4.0 Crashkurs; Microsoft Press, 4. Auflage 2010
  • Bernd Marquardt; WPF Crashkurs; Microsoft Press, 2. Auflage 2011
<ribbon:RibbonWindow   
x:Class="WWWings_WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ribbon="clr-namespace:Microsoft.Windows.
Controls.Ribbon;assembly=RibbonControlsLibrary"
Title="World Wide Wings-Flugbuchung"
x:Name="RibbonWindow"
xmlns:Ansichten="clr-namespace:WWWings_WPF.Ansichten"
Width="650" Height="775"
Icon="/WWWings_WPF;component/Images/Logo.jpg">

<!--############# Hier werden die Views und die ViewModel
durch ein DataTemplate zusammengebunden-->
<Window.Resources>
<DataTemplate DataType="{x:Type Ansichten:BuchungViewModel}">
<Ansichten:BuchungView></Ansichten:BuchungView>
</DataTemplate>
<DataTemplate DataType="{x:Type Ansichten:NeuerPassagierViewModel}">
<Ansichten:NeuerPassagierView></Ansichten:NeuerPassagierView>
</DataTemplate>
</Window.Resources>

<!--############# Grundaufteilung des Bildschirms-->
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<!--############# Bereich 1 des Grids: Ribbon-->
<ribbon:Ribbon x:Name="Ribbon">
<ribbon:Ribbon.ApplicationMenu>
<ribbon:RibbonApplicationMenu SmallImageSource="Images\Logo.jpg">
<ribbon:RibbonApplicationMenuItem Header="Hello _Ribbon"
x:Name="MenuItem1" ImageSource="Images\Logo.jpg"/>
</ribbon:RibbonApplicationMenu>
</ribbon:Ribbon.ApplicationMenu>

<!-- ############# Ribbon Tab 1-->
<ribbon:RibbonTab x:Name="Buchung"
IsSelected="{Binding Path=Tab1Aktiv}" Header="Buchung">
<ribbon:RibbonGroup Header="Anwendung">
<ribbon:RibbonButton LargeImageSource="Images\Logo.jpg"
Label="Ende" Command="{Binding EndeCommand}" />
</ribbon:RibbonGroup>
<ribbon:RibbonGroup Header="Suchen">
<ribbon:RibbonButton x:Name="FlugSuchen"
SmallImageSource="Images\Logo.jpg" Label="Flug suchen"
Command="{Binding Path=ActiveAnsichtViewModel.
FlugSuchenCommand}" />
<ribbon:RibbonButton x:Name="PassagierSuchen"
SmallImageSource="Images\Logo.jpg" Label="Passagier suchen"
Command="{Binding Path=ActiveAnsichtViewModel.
PassagierSuchenCommand}" />
</ribbon:RibbonGroup>

<ribbon:RibbonGroup Header="Aktionen">
<ribbon:RibbonButton x:Name="Buchen"
SmallImageSource="Images\Logo.jpg" Label="Buchen"
Command="{Binding Path=ActiveAnsichtViewModel.
BuchenCommand}" />
</ribbon:RibbonGroup>

</ribbon:RibbonTab>
<!-- ############# Ribbon Tab 2-->
<ribbon:RibbonTab x:Name="NeuerPassagier" IsSelected="
{Binding Path=Tab2Aktiv}" Header="NeuerPassagier">
<ribbon:RibbonGroup Header="Anwendung">
<ribbon:RibbonButton LargeImageSource="Images\Logo.jpg"
Label="Ende" Command="{Binding EndeCommand}" />
</ribbon:RibbonGroup>

<ribbon:RibbonGroup Header="Aktionen">
<ribbon:RibbonButton x:Name="Speichern"
SmallImageSource="Images\Logo.jpg" Label="Speichern"
Command="{Binding Path=ActiveAnsichtViewModel.
SpeichernCommand}" />
<ribbon:RibbonButton x:Name="Abbrechen"
SmallImageSource="Images\Logo.jpg" Label="Abbrechen"
Command="{Binding Path=ActiveAnsichtViewModel.
SpeichernCommand}" />
</ribbon:RibbonGroup>
</ribbon:RibbonTab>

</ribbon:Ribbon>

<!-- ############# Bereich 2 des Grid: Platzhalter für User Control
--> <ContentControl Content="{Binding Path=ActiveAnsichtViewModel}"
Grid.Row="1"></ContentControl>

</Grid>
</ribbon:RibbonWindow>
using System.ComponentModel;
using System.Windows;
namespace WWWings_WPF
{
public partial class MainWindowViewModel : INotifyPropertyChanged
{

/// ############# Befehle #############
public ActionCommand EndeCommand { get; set; }

private void Ende()
{
Application.Current.MainWindow.Close();
}


/// ############# Daten #############
private bool tab1Aktiv;


/// <summary>
/// Ist Ribbon Tab 1 ausgewählt?
/// </summary>
public bool Tab1Aktiv
{
get { return tab1Aktiv; }
set
{
tab1Aktiv = value;

if (tab1Aktiv)
{
this.ActiveAnsichtViewModel =
new WWWings_WPF.Ansichten.BuchungViewModel();
}

OnPropertyChanged("Tab1Aktiv");
}
}

private bool tab2Aktiv;

/// <summary>
/// Ist Ribbon Tab 2 ausgewählt?
/// </summary>
public bool Tab2Aktiv
{
get { return tab2Aktiv; }
set
{
tab2Aktiv = value;

if (tab2Aktiv)
{
this.ActiveAnsichtViewModel =
new WWWings_WPF.Ansichten.NeuerPassagierViewModel();
}

OnPropertyChanged("Tab2Aktiv");
}
}


private INotifyPropertyChanged activeAnsichtViewModel;

/// <summary>
/// Welche ist das aktive ViewModel
/// </summary>
public INotifyPropertyChanged ActiveAnsichtViewModel
{
get { return activeAnsichtViewModel; }
set { activeAnsichtViewModel = value;
OnPropertyChanged("ActiveAnsichtViewModel"); }
}


/// ############# INotifyPropertyChanged #############
#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;


protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}


/// ############# Konstruktur #############
public MainWindowViewModel()
{

EndeCommand = new ActionCommand(Ende);
// Erste Viewmodel
this.ActiveAnsichtViewModel =
new WWWings_WPF.Ansichten.BuchungViewModel();

}

#endregion


}
}
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;
using System.Windows.Media;
using WWWings_Dienstproxies.WWWingsServer;
using WWWings_GO;

namespace WWWings_WPF.Ansichten
{
public partial class BuchungViewModel : INotifyPropertyChanged
{
/// ############# Befehle #############
public ActionCommand FlugSuchenCommand { get; set; }
public ActionCommand PassagierSuchenCommand { get; set; }
public ActionCommand BuchenCommand { get; set; }

/// ############# Daten (Properties) #############
ObservableCollection<string> flughaefen;
public ObservableCollection<string> Flughaefen
// Liste der Orte für Abflug- und Zielauswahl
{
get { return flughaefen; }
set { flughaefen = value; OnPropertyChanged("Flughaefen"); }
}

private ObservableCollection<Flug> fluege { get; set; }
public ObservableCollection<Flug> Fluege // gefundene Flüge
{
get { return fluege; }
set { fluege = value; this.OnPropertyChanged("Fluege"); }
}

private ObservableCollection<Passagier> passagiere;
public ObservableCollection<Passagier> Passagiere
// gefundene Passagiere
{
get { return passagiere; }
set { passagiere = value; this.OnPropertyChanged("Passagiere"); }
}

Flug flug;
public Flug Flug // Gewählter Flug
{
get { return flug; }
set { flug = value; OnPropertyChanged("Flug");
BuchenCommand.IsEnabled = (Flug != null && Passagier != null);
}
}

Passagier passagier;
public Passagier Passagier // gewählter Passagier
{
get { return passagier; }
set { passagier = value; OnPropertyChanged("Passagier");
BuchenCommand.IsEnabled = (Flug != null && Passagier != null);
}
}

private string flugNummer;
public string FlugNummer // eingegebene Flugnummer
{
get { return flugNummer; }
set { flugNummer = value; OnPropertyChanged("FlugNummer"); }
}

private string abflugort;
public string Abflugort // ausgewählter Abflugort
{
get { return abflugort; }
set { abflugort = value; OnPropertyChanged("Abflugort"); }
}

private string zielort;
public string Zielort // ausgewählter Zielort
{
get { return zielort; }
set { zielort = value; OnPropertyChanged("Zielort"); }
}

private string passagierID;
public string PassagierID // eingegebene ID
{
get { return passagierID; }
set { passagierID = value; OnPropertyChanged("PassagierID"); }
}

private string passagiername;
public string PassagierName // eingegebener Name
{
get { return passagiername; }
set { passagiername = value; OnPropertyChanged("PassagierName"); }
}

private string buchenErgebnis;
public string BuchenErgebnis // Textausgabe nach Buchung
{
get { return buchenErgebnis; }
set { buchenErgebnis = value; OnPropertyChanged("BuchenErgebnis"); }
}

private SolidColorBrush buchenErgebnisFarbe;
public SolidColorBrush BuchenErgebnisFarbe
// Textfarbe der Textausgabe nach Buchung
{
get { return buchenErgebnisFarbe; }
set { buchenErgebnisFarbe = value; OnPropertyChanged
("BuchenErgebnisFarbe"); }
}

/// ############# Konstruktor #############

public BuchungViewModel()
{

FlugSuchenCommand = new ActionCommand(FlugSuchen);
PassagierSuchenCommand = new ActionCommand(PassagierSuchen);
BuchenCommand = new ActionCommand(Buchen);
BuchenCommand.IsEnabled = false;

BuchungsServiceClient client = new BuchungsServiceClient();
Flughaefen = client.GetFlughaefen();
client.Close();

// nur zum einfacheren Testen!
Abflugort = "Berlin";
PassagierName = "Müller";
}

/// ############# Aktion 1 #############

private void FlugSuchen()
{
BuchungsServiceClient client = new BuchungsServiceClient();

int FNr;
if (Int32.TryParse(FlugNummer, out FNr))
{
var Flug = client.GetFlug(FNr);
if (Flug!=null) Fluege = new ObservableCollection<Flug> { Flug };
else Fluege = new ObservableCollection<Flug> { };
}
else
{
Fluege = client.GetFluege(Abflugort, Zielort);
}

//if (Fluege != null) Fluege = new ListCollectionView(Fluege);

client.Close();
}

/// ############# Aktion 2 #############

private void PassagierSuchen()
{

BuchungsServiceClient client = new BuchungsServiceClient();
int PID;
if (Int32.TryParse(PassagierID, out PID))
{

var Passagier = client.GetPassagier(PID);

if (Passagier != null) Passagiere =
new ObservableCollection<Passagier> { Passagier };
else Passagiere = new ObservableCollection<Passagier>();
}
else
{
Passagiere = client.GetPassagiere(PassagierName);
}

if (Passagiere != null) Passagiere =
new ObservableCollection<WWWings_GO.Passagier>
(Passagiere.OrderBy(x => x.GanzerName));

client.Close();
}

/// ############# Aktion 3 #############

private void Buchen()
{
if (Flug == null || Passagier == null) return;

BuchungsServiceClient client = new BuchungsServiceClient();

try
{
string ergebnis = client.CreateBuchung(Flug.ID, Passagier.ID);
if (ergebnis == "OK")
{
BuchenErgebnis = "Buchung erfolgreich!";
BuchenErgebnisFarbe = new SolidColorBrush(Colors.Green);
}
else
{
BuchenErgebnis = ergebnis;
BuchenErgebnisFarbe = new SolidColorBrush(Colors.Red);
}
}
catch (Exception ex)
{
BuchenErgebnis = "Fehler beim Aufruf der Buchungsfunktion: " +
ex.Message;
BuchenErgebnisFarbe = new SolidColorBrush(Colors.Red);
}

client.Close();
}

/// ############# INotifyPropertyChanged #############

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;


protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
<UserControl x:Class="WWWings_WPF.Ansichten.BuchungView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WWWings_WPF.Ansichten"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="500">


<!--############# Grundaufteilung des Bildschirms-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="*" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="*" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>

<!-- ### Bereich 1: Flugsuche ############# -->
<Border Grid.Row="1" Grid.Column="1" Padding="10"
BorderBrush="#FF3B5383" BorderThickness="1" CornerRadius="4"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="42" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="74" />
<ColumnDefinition />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="74" />
<ColumnDefinition />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="70" />
</Grid.ColumnDefinitions>

<TextBlock Grid.Row="0" Grid.Column="0" Text="Flug ID:"
VerticalAlignment="Center"/>
<TextBox x:Name="C_Flugnummer" Grid.Row="0" Grid.Column="1"
Text="{Binding FlugNummer}" Margin="5,0,0,0"
VerticalAlignment="Center" />

<TextBlock Grid.Row="0" Grid.Column="3" Text="Abflughafen:"
VerticalAlignment="Center"/>
<ComboBox x:Name="C_Abflugort" Grid.Row="0" ItemsSource=
"{Binding Flughaefen}" DisplayMemberPath="" Margin="5,0,0,0"
VerticalAlignment="Center" IsEditable="True"
Text="{Binding Abflugort}" />

<TextBlock Grid.Row="0" Grid.Column="6" Text="Zielflughafen:"
VerticalAlignment="Center"/>
<ComboBox x:Name="C_Zielort" Grid.Row="0" Grid.Column="7"
ItemsSource="{Binding Flughaefen}" Text="
{Binding Zielort}" DisplayMemberPath=""
Margin="5,0,0,0" VerticalAlignment="Center"
IsEditable="True" />


<Button x:Name="C_FlugSuchen" Grid.Row="0" Grid.Column="9"
Content="Suchen" Command="{Binding
Path=FlugSuchenCommand}" />
</Grid>
</Border>

<!-- ### Flugliste ############# -->
<DataGrid x:Name="C_GefundeneFluege" Grid.Row="3"
Grid.Column="1" AutoGenerateColumns="False"
ItemsSource="{Binding Path=Fluege}"
SelectedItem="{Binding Path=Flug}">
<DataGrid.Columns>
<DataGridTextColumn Header="Abflugort"
Binding="{Binding Tippefehler}"/>
<DataGridTextColumn Header="Zielort" Binding="{Binding Zielort}"/>
<DataGridTextColumn Header="Datum"
Binding="{Binding Datum, StringFormat={}{0:dd.MM.yyy}}"/>
<DataGridTextColumn Header="Plätze" Binding="{Binding Plaetze}"/>
<DataGridTextColumn Header="Freie Plätze"
Binding="{Binding FreiePlaetze}"/>
</DataGrid.Columns>
</DataGrid>

<!-- ### Bereich 2: Passagiersuche ############# -->
<Border Grid.Row="5" Grid.Column="1" Padding="10"
BorderBrush="#FF3B5383" BorderThickness="1"
CornerRadius="4">
<Grid x:Name="detailsGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="4" />
<RowDefinition Height="Auto" />
<RowDefinition Height="4" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<ColumnDefinition />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="70" />
<ColumnDefinition />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="70" />
</Grid.ColumnDefinitions>

<TextBlock Grid.Row="2" Text="Passagier ID:"
VerticalAlignment="Center"/>
<TextBox x:Name="C_PassagierID" Grid.Row="2"
Grid.Column="1" Text= {Binding PassagierID}" Margin="5,0,0,0"
VerticalAlignment="Center" />

<TextBlock Grid.Row="2" Grid.Column="3" Text="Nachname:"
VerticalAlignment="Center"/>
<TextBox x:Name="C_Passagiername" Grid.Row="2"
Grid.Column="4" Text="{Binding PassagierName}" Margin="5,0,0,0"
VerticalAlignment="Center"
/>

<Button x:Name="C_PassagierSuchen" Grid.Row="2"
Grid.Column="6" Content="Suchen"
Command="{Binding PassagierSuchenCommand}" />


</Grid>
</Border>

<!-- ### Passagierliste ############# -->
<DataGrid x:Name="C_GefundenePassagiere" Grid.Row="7"
Grid.Column="1" AutoGenerateColumns="False"
ItemsSource="{Binding Path=Passagiere}"
SelectedItem="{Binding Path=Passagier}">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding ID}"/>
<DataGridTextColumn Header="Vorname" Binding="{Binding Vorname}"/>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Geburtsdatum"
Binding="{Binding Geburtsdatum, StringFormat=
{}{0:dd.MM.yyy}}"/>
</DataGrid.Columns>
</DataGrid>

<!-- ### Bereich 3: Buchen-Aktion ############# -->
<Border Grid.Row="9" Grid.Column="1" Padding="10"
BorderBrush="#FF3B5383" BorderThickness="1" CornerRadius="4"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="70" />
</Grid.ColumnDefinitions>

<TextBlock Name="C_BuchenErgebnis" Grid.Row="0"
Grid.Column="0" Text="{Binding BuchenErgebnis}"
Foreground={Binding BuchenErgebnisFarbe}"
Margin="5,0,0,0" VerticalAlignment="Center"/>

<Button Grid.Row="0" Grid.Column="2" Content="Buchen"
x:Name="C_Buchen" Command="{Binding BuchenCommand}" />

</Grid>
</Border>

</Grid>
</UserControl>
using System.ComponentModel;
using System.Collections.ObjectModel;
using WWWings_GO;
using WWWings_Dienstproxies.WWWingsServer;
using System;

namespace WWWings_WPF.Ansichten
{
public partial class NeuerPassagierViewModel : INotifyPropertyChanged
{

/// ############# Befehle #############
public ActionCommand SpeichernCommand { get; set; }
public ActionCommand AbbrechenCommand { get; set; }

/// ############# Ereignisse #############
public event Action<Passagier> NeuerPassagierViewModelExit;

/// ############# Daten (Properties) #############
public Passagier passagier;
public Passagier Passagier // Das neue Passagier-OBjejt
{
get { return passagier; }
set { passagier = value; OnPropertyChanged("Passagier"); }
}

public string fehlerMeldung;
public string FehlerMeldung // StatusMeldung nach Speichern
{
get { return fehlerMeldung; }
set { fehlerMeldung = value; OnPropertyChanged("FehlerMeldung"); }
}

/// ############# Konstruktor #############

public NeuerPassagierViewModel()
{
SpeichernCommand = new ActionCommand(Speichern);
AbbrechenCommand = new ActionCommand(Abbrechen);

// Passagier instanziieren
Passagier = new Passagier();

// Nur zum Test
Passagier.Vorname = "Max";
Passagier.Name = "Mustermann";
Passagier.Geburtsdatum = DateTime.Now.Date;
Passagier.PassagierStatus = "A";
}

/// ############# Aktion 1 #############

private void Speichern()
{

try
{
// Neuen Passagier speichern
ObservableCollection<Passagier> GeändertePassagiere =
new ObservableCollection<Passagier>() { Passagier };
string Statistik;

BuchungsServiceClient client = new BuchungsServiceClient();
var antwort = client.SavePassagierSet(GeändertePassagiere,
out Statistik);
client.Close();

if (antwort.Count == 1)
{
// Der erste neue Passagier muss der angelegte sein, der
// nun auch die ID enthält!
Passagier = antwort[0];
FehlerMeldung = "";
if (NeuerPassagierViewModelExit != null)
NeuerPassagierViewModelExit(Passagier);
}
}
catch (Exception ex)
{
FehlerMeldung = ex.Message;
if (NeuerPassagierViewModelExit != null)
NeuerPassagierViewModelExit(null);
}


}

/// ############# Aktion 2 #############


private void Abbrechen()
{
if (NeuerPassagierViewModelExit != null)
NeuerPassagierViewModelExit(null)
}

/// ############# INotifyPropertyChanged #############

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;


protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
<UserControl x:Class="WWWings_WPF.Ansichten.NeuerPassagierView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<Grid>

<!--############# Grundaufteilung des Bildschirms-->

<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>
<RowDefinition Height="40"/>

</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition />
</Grid.ColumnDefinitions>


<!--############# Eingabebereich-->
<Label Name="C_LabelVorname" Grid.Row="0" Content="Vorname"
Height="28" HorizontalAlignment="Left"
VerticalAlignment="Top" />
<TextBox Name="C_Vorname" Grid.Column="1" Height="23"
HorizontalAlignment="Left" VerticalAlignment="Top"
Width="200" Text="{Binding Passagier.Vorname}" />

<Label Name="C_LabelName" Grid.Row="1" Content="Name"
Height="28" HorizontalAlignment="Left"
VerticalAlignment="Top" />
<TextBox Name="C_Name" Grid.Row="1" Grid.Column="1"
Height="23" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="200" Text="{Binding
Passagier.Name}" />

<Label Name="C_LabelGeburtsdatum" Grid.Row="2"
Content="Geburtsdatum" Height="28" HorizontalAlignment=
"Left" VerticalAlignment="Top" />
<TextBox Name="C_Geburtsdatum" Grid.Row="2" Grid.Column="1"
Height="23" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="200" Text="
{Binding Passagier.Geburtsdatum,
StringFormat={}{0:dd.MM.yyy}}" />

<Label Name="C_LabelPassagierstatus" Grid.Row="3"
Content="Passagierstatus" Height="28"
HorizontalAlignment="Left" VerticalAlignment="Top" />
<ComboBox Name="C_Passagierstatus" Grid.Row="3"
Grid.Column="1" Height="23"HorizontalAlignment="Left"
VerticalAlignment="Top" Width="50" ItemsSource="{Binding}"
Text="{Binding Passagier.PassagierStatus}" >
<ComboBoxItem Content="A" />
<ComboBoxItem Content="B" />
<ComboBoxItem Content="C" />
</ComboBox>

<!--############# Aktionsbereich mit StackPanel-Anordnung -->
<StackPanel Grid.Column="1" Grid.Row="5" Orientation="Horizontal">
<Button Name="C_Speichern" Content="Speichern"
Height="23" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="75"
Command="{Binding SpeichernCommand}" />
<Button Name="C_Abbrechen" Content="Abbrechen" Height="23"
HorizontalAlignment="Left" VerticalAlignment=
"Top" Width="75" Command="{Binding
AbbrechenCommand}"/>
<Label Name="C_FehlerMeldung" Grid.Row="5"
Grid.ColumnSpan="2" Height="28" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="400"
Foreground="Red" Content="{Binding FehlerMeldung}" />
</StackPanel>
</Grid>
</UserControl> (ane)