Von der Datenbank bis zur Oberfläche mit .NET, Teil 5: Desktop- und Browseranwendung mit Silverlight

Seite 2: WPF-Nähe

Inhaltsverzeichnis

Es soll versucht werden, die WPF-Oberfläche mit Views und ViewModel aus Teil 4 nach Silverlight zu übernehmen – mit möglichst wenig Änderungsaufwand. Dafür werden zunächst der Ordner "Ansichten" und die Klasse ActionCommand.cs aus dem WPF- in das Siliverlight-Projekt kopiert. Beim Kompilieren des Projekts erhält man dann zahlreiche Fehler, weil in Silverlight einige Dinge anders sind.

Zunächst einmal sollte man im kompletten Programmcode alle Namensräume WWWings_WPF durch WWWings_SL sowie WWWings_Dienstproxies durch WWWings_SLDienstproxies ersetzen. Das dient einerseits der Namenskonsistenz, andererseits aber auch dem Auffinden der speziell für Silverlight erstellten Client-Proxies.

Die erste große Hürde sind nun die Steuerelemente: Label und DataGrid gehören nicht zum Standardlieferumfang von Silverlight 5.1, sondern zum optionalen Silverlight Toolkit. Das sollte zwar laut Anweisung eingangs des Beitrags installiert sein, aber die Steuerelemente werden dennoch nicht gefunden, da die Assemblies nicht automatisch in das Silverlight-Projekt eingebunden sind. Es gilt also, nun eine Referenz auf System.Windows.Controls.Data.Input (hier liegt das Label-Steuerelement) und System.Windows.Controls.Data (hier liegt das DataGrid-Steuerelement) zu setzen. Das reicht aber noch nicht, denn auch die Namensräume der Steuerelemente sind anders. Der Namensraum des Silverlight Toolkit ist nun im Tag <UserControl> zu ergänzen:

<UserControl x:Class="WWWings_SL.Ansichten.BuchungView"
...
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/
xaml/presentation/sdk"
...>

Anschließend sind alle Vorkommnisse der beiden Steuerelemente mit dem Präfix sdk: zu versehen, aus <Label> wird also <sdk:Label> und aus <sdk:DataGrid> wird <DataGrid>. Gleiches gilt für die Unterelemente von DataGrid, zum Beispiel DataGridTextColumn.

Grundsätzlich funktioniert das Pattern Model View ViewModel (MVVM) zur Trennung von Layout und Programmcode in Silverlight genauso wie in WPF, also über Command-Objekte und Properties, die den INotifyPropertyChanged-Mechanismus bereitstellen (vgl. Teil 4 des Tutorials). Im Detail gibt es aber doch (syntaktische) Unterschiede zwischen WPF und Silverlight. Der XAML-Compiler in Visual Studio beschwert sich: "The property 'Text' was not found in type 'ComboBox'”. In Silverlight gibt es diese Eigenschaft bei der ComboBox nicht; man muss stattdessen auf SelectedValue zurückgreifen und eine Zwei-Wege Bindung verwenden:

<ComboBox x:Name="C_Zielort" Grid.Row="0" Grid.Column="7" 
ItemsSource="{Binding Flughaefen}" SelectedValue="
{Binding Zielort, Mode=TwoWay}" DisplayMemberPath=""
Margin="5,0,0,0" VerticalAlignment="Center"...>

Im Fall der Bindung einfacher Zeichenketten ließe sich alternativ zu SelectedValue auch SelectedItem synonym verwenden.

Eine Zwei-Wege-Bindung ist zudem notwendig für alle anderen Datenbindungsausdrücke, bei denen die View Daten an das ViewModel liefern soll, zum Beispiel bei der Bindung der Eigenschaft Text im Textbox Steuerelement und der Eigenschaft SelectedItem im DataGrid-Steuerelement. Während in WPF hier ein SelectedItem="{Binding Path=Passagier"} reichte, braucht man in Silverlight unbedingt SelectedItem= {Binding Path=Passagier, Mode=TwoWay}", da sonst die Passagier-Property im ViewModel bei Änderungen der Auswahl im DataGrid nicht befüllt wird.

Auch bei den Formatierungsausdrücken in der Datenbindung gibt es Ärger. Eine geschweifte Klammer leitet in XAML eine Markup Extension ein. Wenn man eine geschweifte Klammer in einem Formatierungsausdruck verwenden will, ist in WPF ein {} voranzustellen:

Binding="{Binding Datum, StringFormat={}{0:dd.MM.yyy}}"

Allerdings mag Silverlight das {} nicht und quittiert das mit "Unexpected Token after end of Markup Extension". Dort muss man schreiben:

Binding="{Binding Datum, StringFormat=\{0:dd.MM.yyy\}}"

Die Syntax wäre auch in WPF möglich gewesen. Im konkreten Fall wäre ein weiterer gemeinsamer Nenner einfach folgender Ausdruck:

Binding="{Binding Datum, StringFormat=dd.MM.yyy}" 

Damit scheinen alle XAML-Probleme gelöst, denn die Fehlerliste zeigt jetzt nur noch welche im ViewModel. Leider trügt der Schein: Wenn man die Anwendung jetzt starten würde, käme es noch zu einem Laufzeitfehler, das der XAML-Compiler von Visual Studio nicht erkannt hat: "Das Festlegen von Eigenschaft 'System.Windows.Controls.ComboBox.IsEditable' hat eine Ausnahme ausgelöst." Ursache ist hier wieder die ComboBox, die zwar genau wie in Silverlight eine Eigenschaft IsEditable ist, diese steht aber immer auf "false" und darf man nicht setzen. IsEditable="True" ist also aus den <ComboBox>-Tags zu entfernen.

<ComboBox x:Name="C_Abflugort" Grid.Row="0"  Grid.Column="4" 
ItemsSource="{Binding Flughaefen}" DisplayMemberPath=""
Margin="5,0,0,0" VerticalAlignment="Center"
SelectedItem="{Binding Abflugort, Mode=TwoWay}" />

Weiterhin wird man einen funktionalen Unterschied bei der ComboBox in der View NeuerPassagier feststellen: Egal was man als Passagierstatus auswählt, die Silverlight-Anwendung speichert im Gegensatz zur WPF-Anwendung nicht den gewählten Status A, B oder C, sondern den Namen der Klasse System.Windows.Controls.ComboBoxItem im Passagierstatus. Das Problem liegt in der fehlenden Eigenschaft Text, die aus einem ComboxItem den Inhalt herauszieht. Sie wurde ja oben durch SelectedValue ersetzt. SelectedItem hingegen liefert ein Objekt vom Typ ComboxItem, da in XAML die Auswahloptionen der ComboBox so definiert sind:

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

Das ComboBoxItem konvertiert dann Silverlight durch Aufruf der Standardmethode ToString() in einen Text. Nun hat aber Microsoft sogar eine so primitive Funktion wie ComboxItem.ToString() unterschiedlich in WPF und Silverlight realisiert. Das sieht man auch an folgender C#-Befehlsfolge:

var i = new System.Windows.Controls.ComboBoxItem();
i.Content = "A";
var s = i.ToString();

Sie liefert in WPF s = "System.Windows.Controls.ComboBoxItem: A"; in Silverlight aber nur s = "System.Windows.Controls.ComboBoxItem". In Silverlight muss man also nun für die Auswahl des Passagierstatus einen anderen Weg gehen. Eine Lösung besteht darin, im XAML in der NeuerPassagierView die ComboBox-Inhalte statt mit ComboBoxItem-Tags <ComboBoxItem Content="A" /> mit einfachen Zeichenketten in Form von <sys:String>A</sys:String> zu befüllen:

<ComboBox  Name="C_Passagierstatus" Grid.Row="3" Grid.Column="1" 
Height="23" HorizontalAlignment="Left" VerticalAlignment="Top"
Width="50" SelectedItem="{Binding Passagier.PassagierStatus,
Mode=TwoWay}" >
<sys:String>A</sys:String>
<sys:String>B</sys:String>
<sys:String>C</sys:String>
</ComboBox>

Dann liefert SelectedItem direkt eine Zeichenkette, und alles läuft wieder. Voraussetzung für <sys:String> ist, dass der Entwickler vorher den Namensraum sys auch im Wurzel-Tag der View unter Angabe der Assembly deklariert hat:

<UserControl x:Class="WWWings_SL.Ansichten.NeuerPassagierView"
xmlns:sys="clr-namespace:System;assembly=mscorlib" ... >

Das wäre auch in WPF möglich gewesen. Listing 3 zeigt die komplette Silverlight-Variante von NeuerPassagierView.

Nach diesen Änderungen auf XAML-Ebene sieht man aber immer noch eine lange Fehlerliste, in der sich Visual Studio beschwert, dass die in den ViewModels verwendete Dienstproxy-Klasse BuchungsServiceClient nicht die Methoden GetFlug(), GetPassagier(), CreateBuchung() etc. und nicht mal Close() kennt. Schaut man sich die Klasse BuchungsServiceClient an, findet man dort stattdessen GetFlugAsync(), GetPassagierAsync(), CreateBuchungAsync() etc., denn Silverlight unterstützt nur asynchrone Aufrufe.

Zu jeder Async-Methode gibt es dann ein Completed-Ereignis. Das bedeutet: Beim Aufruf der Async-Methode werden die Kontrolle sofort an den Aufrufer zurückgegeben und die Webservice-Operation in einem Hintergrund-Thread gestartet. Wenn dieser fertig ist (erfolgreich oder mit Fehler), wird die Ereignisbehandlungsroutine aufgerufen, die dem Completed-Ereignis zugewiesen wurde. Wenn der Aufruf erfolgreich war, enthält die Property Result des zweiten Ereignishandlungsroutinenparameters das Rückgabeobjekt. Im Fehlerfall liefert das Property Error das Exception-Objekt.

Das bedeutet nun, dass die ViewModels erheblich umzustellen sind, denn es ist für jeden Webservice-Aufruf eine Ereignisbehandlungsroutine als Methode zu realisieren. Diese Ereignisbehandlungsmethode kann explizit oder anonym sein. Listing 2 zeigt zu Anschauungszwecken bewusst beide möglichen Syntaxformen. Wenn man in Visual Studio nach der Eingabe von += hinter einem Ereignis zweimal die Tabulator-Taste drückt, erstellt die Entwicklungsumgebung automatisch eine explizite Ereignisbehandlungsmethode. Ein Beispiel sieht man in Listing 1 in der Methode FlugSuchen() beim Aufruf der Webservice-Operation GetFlugAsync(). Kurz darunter in FlugSuchen() erfolgt der Aufruf von GetFluegeAsync() mit einer anonymen Methode, die "inline" in geschweiften Klammern innerhalb von FlugSuchen() realisiert ist. Dieser Inline-Code, bei dessen Anlegen Visual Studio nicht explizit mithilft, wird beim Eintreten des Ereignisses abgearbeitet. Die umgebende Routine ist davon aber nicht mehr berührt, sofern man nicht explizit auf eine Variable der umgebenden Routine zugreift. Das ist möglich, und man spricht dann von einer sogenannten Closure. Die anonymen Methoden sind also etwas mächtiger. Für welche der beiden Syntaxformen man sich im vorliegenden Fall entscheidet, ist aber reine Geschmackssache. Listing 4 zeigt die Silverlight-Variante des NeuerPassagierViewModel, in dem ebenfalls die asynchronen Aufrufe realisiert sind.