zurück zum Artikel

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

Dr. Holger Schwichtenberg

Im vierten Teil des .NET-Tutorials entstand eine mit Windows Presentation Foundation erstellte Desktop-Anwendung. Diese soll nun in eine Silverlight-Anwendung überführt werden, die ebenso wie WPF die Auszeichnungssprache XAML verwendet. Silverlight-Programme laufen wahlweise in Webbrowsern oder auf dem Windows-Desktop.

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

Im vierten Teil des .NET-Tutorials entstand eine mit Windows Presentation Foundation erstellte Desktop-Anwendung. Diese soll nun in eine Silverlight-Anwendung überführt werden, die ebenso wie WPF die Auszeichnungssprache XAML verwendet. Silverlight-Programme laufen wahlweise in Webbrowsern oder auf dem Windows-Desktop.

Während die erste Version von Silverlight noch auf JavaScript beruhte, kommt seit Version 2.0 eine reduzierte Variante des .NET Framework zum Einsatz. Der Entwickler kann daher C# und Visual Basic .NET sowie viele der aus .NET bekannten Klassen verwenden. Silverlight ist in den Versionen 1.0 bis 5.1 in relativ kurzer Versionsfolge in den Jahren 2007 bis 2012 erschienen. Microsoft unterstützt mit Silverlight nicht nur Internet Explorer (ab 7.0), sondern auch Firefox (ab 3.6), Chrome (ab 12.0) sowie Safari (ab 4.0) auf Mac OS X.

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 [5] gibt, durch den sich das Beispiel nicht ohne viel manuelle Arbeit zum Laufen bringen lässt.

Den Quellcode zum Artikel findet man hier [6].

In den letzten Monaten wurde wiederholt berichtet, dass Microsoft nun seine Begeisterung für Silverlight verloren habe und nun Oberflächen mit HTML5 und JavaScript favorisiere. Die Anwendergemeinde vermutet, dass Microsoft nach Silverlight 5 keine neue Hauptversion mit weiteren signifikanten neuen Features mehr produzieren werde. Das letzte Wort ist aus Redmond dazu aber noch nicht gesprochen, auch wenn es Anzeichen gibt, dass Microsoft sich selbst bei eigenen Produkten (z. B. dem Windows Azure Management Portal und Visual Studio LightSwitch) von Silverlight verabschiedet.

Dennoch hat Silverlight (zumindest vorerst) weiterhin einen Platz im Portfolio der Webentwickler, denn (noch) ist die Arbeit mit Silverlight wesentlich produktiver als mit HTML5 und JavaScript. Die hohe Produktivität von Silverlight beruht vor allem auf der Verwendung der klassenbasierten, streng typisierten Sprachen C# und Visual Basic, der reichhaltigen Silverlight-Klassenbibliothek sowie der mächtigen Entwicklungsumgebung Visual Studio mit viel Eingabeunterstützung und Validierung zur Kompilierzeit.

Zudem gibt es bei Silverlight keine Herausforderungen mit der Browserkompatibilität. In jedem Browser, in dem die Silverlight-Laufzeitumgebung als Plug-in installiert ist, läuft Silverlight gleich. Das Problem ist freilich, dass bei weitem weniger Benutzer die Silverlight- als die Flash-Laufzeitumgebung in ihrem Browser installiert haben [7]. Silverlight ist daher vor allem eine gute Wahl für Intranets und gegebenenfalls Extranets, in denen man die Installation des Silverlight-Plug-ins durchführen beziehungsweise durchsetzen kann, aber keine gute Wahl für das öffentliche World Wide Web. Es gab eine Portierung von Silverlight auf Unix/Linux unter dem Namen Moonlight, die aber inzwischen eingestellt [8] ist.

Zum Entwickeln mit Silverlight 5.1 muss man neben Visual Studio (Variante Professional, Premium, Ultimate oder Visual Web Developer Express) zunächst die Silverlight Tools for Visual Studio und das Silverlight Toolkit installieren, das zusätzliche Steuerelemente enthält. Es gibt andere Erweiterungspakete für Silverlight, zum Beispiel Silverlight Contrib [9], die aber hier nicht zum Einsatz kommen.

Dann ist eine neue "Silverlight Application" mit Namen "WWWings_SL" in der bestehenden Projektmappe "WWWings.sln" anzulegen. Beim Anlegen erscheint der Dialog in Abbildung 1.

Optionsdialog beim Anlegen einer neuen Silverlight-Anwendung in Visual Studio (die Version 5.1 erscheint nicht explizit im Dialog) (Abb. 1).

Optionsdialog beim Anlegen einer neuen Silverlight-Anwendung in Visual Studio (die Version 5.1 erscheint nicht explizit im Dialog) (Abb. 1).

Darin gilt es, ein Webserverprojekt festzulegen, dass eine HTML-Seite bereitstellt, in die die Silverlight-Anwendung geladen wird. Man könnte dafür ein neues Webserverprojekt anlegen; hier soll aber WWWings_Web verwendet werden, das in Teil 3 des Tutorials [10] für die ASP.NET-Anwendung entstanden ist. Silverlight und ASP.NET können in einem Webprojekt koexistieren und bei Bedarf über AJAX auch interagieren. In WWWings_Web entsteht diese HTML-Seite in zwei Varianten: Als reine HTML-Seite (WWWings_SLTestPage.html) und als ASP.NET-Webseite (WWWings_SLTestPage.aspx). Einen funktionalen Unterschied gibt es zunächst nicht. Das Silverlight-Projekt wird in eine ZIP-Paket mit Dateinamenserweiterung .aspx (WWWings_SL.xap) verpackt und während des Kompiliervorgangs in das Webprojekt WWWings_Web in den Ordner "ClientBin" kopiert. Die beiden Seiten WWWings_SLTestPage.html und WWWings_SLTestPage.aspx binden das .xap-Paket über ein <object>-Tag ein:

<object data="data:application/x-silverlight-2," type="application/
x-silverlight-2" width="100%" height="100%">
<param name="source" value="ClientBin/WWWings_SL.xap"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="5.0.61118.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0"
style="text-decoration:none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft
Silverlight" style="border-style:none"/>
</a>
</object>

Außerdem findet man in den beiden Seiten ein wenig JavaScript (Routine onSilverlightError), das Fehlermeldungen von Silverlight an den Browser weiterreicht.

Nach einem Kompilieren der Lösung und dem Starten der Testseite im Browser (Kontextmenü View in Browser) sollte jeweils eine leere Webseite mit einem eingebetteten leeren Silverlight-Fenster erscheinen. Ein Klick des Anwenders auf die rechte Maustaste im Browserfenster wird durch den Eintrag "Silverlight" im Kontextmenü beweisen, dass tatsächlich Silverlight im Spiel ist. Aus kosmetischen Gründen soll hier WWWings_SLTestPage.html in SilverlightApp.html umbenannt werden. Um das Debugging von Visual Studio zu nutzen, muss man WWWings_Web als Standardprojekt (Set as StartUp Project) und die Seite SilverlightApp.html als Startseite (Set as Start Page) festlegen.

Nun müsste man wie beim ASP.NET- (Teil 3 [11]) und WPF-Client (Teil 4 [12]) die gemeinsamen Objekte (WWWings_GO) und die Dienst-Proxies (WWWings_Dienstproxies) referenzieren im Silverlight-Projekt. Allerdings geht das in diesem Fall nicht so einfach, denn aus einem Silverlight-Projekt heraus kann man nicht Projekte einbinden, die für das fertige .NET Framework kompiliert wurden. Möglich wäre das nur, wenn man ursprünglich eine "Portable Library" angelegt hätte. Das hätte aber andere Herausforderungen mit sich gebracht.

Der Entwickler muss für Silverlight nun zwei neue Bibliotheken anlegen: WWWings_SLGO und WWWings_SLDienstproxies. Vorlage ist dabei "Silverlight Class Library". Die erste Bibliothek kann dabei den gleichen Code enthalten wie WWWings_GO, allerdings nicht das Entity-Framework-Modell (.edmx-Datei). Namentlich zu übernehmen sind also die generierten Entitätsklassendateien (Flug.cs, Passagier.cs, Person.cs und Pilot.cs), die manuellen Erweiterungen der Entitätsklassen (EntitaetsklassenErweiterungen.cs) und die Dienstdefinition (Dienstschnittstellen.cs und Nachrichtenklassen.cs). Statt die Dateien zu kopieren, sollte der Entwickler sie verknüpfen, damit sie bei Änderungen auch automatisch wirken. Eine Verknüpfung erstellt er in Visual Studio mit Add existing Item und dann Add as link. Verknüpfte Dateien erkennt er an einem kleinen Pfeil im Dateisymbol im Projektmappen-Explorer von Visual Studio. Die WWWings_SLDienstproxies muss er neu erzeugen, denn die Dienst-Proxies des .NET Framework sind nicht kompatibel mit Silverlight. Der Entwickler legt also ein Projekt WWWings_SLDienstproxies an und erzeugt über Add Service Reference die Proxies neu, so wie in Teil 2 des Tutorials [13] beschrieben. Anders als bei WWWings_Dienstproxies landet die XML-Konfiguration nicht in der Datei app.config, sondern in ServiceReferences.ClientConfig.

Nach dem Erstellen der beiden Bibliotheken legt der Entwickler aus dem Silverlight-Projekt WWWings_SL Verweise auf die Projekte WWWings_SLDienstproxies und WWWings_SLGO-Bibliothek sowie die System.ServiceModel-Bibliothek an. Zudem muss er die Datei ServiceReferences.ClientConfig aus dem Projekt WWWings_SLDienstproxies in das Projekt WWWings_SL kopieren, da dort Adresse und Konfigurationsinformationen für den Zugriff auf die Webservices abgelegt sind. Die Datei ServiceReferences.ClientConfig entspricht dem in Teil 3 und 4 erfolgten Übernehmen der app.config-Datei.

Nun reicht das aber immer noch nicht. Man muss in das Projekt "WWWings_Server", das die Webservices bereitstellt, noch die Datei crossdomain.xml mit dem folgenden Inhalt im Wurzelverzeichnis ablegen.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/
dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

Der Grund dafür ist eine Standardsicherheitsbeschränkung: Die Silverlight-Anwendung wird über das Projekt WWWings_Web vom Port 82 geladen. Die Webservices werden über WWWings_Server aber auf Port 81 gehostet. Wenn crossdomain.xml nicht erlaubt, dass die Webservices von fremden Websites aufgerufen werden, kommt es beim Aufruf zum Fehler: "Fehler beim Senden einer Anforderung an den URI "http://localhost:81/BuchungsService.svc". Ursache ist möglicherweise, dass ohne die entsprechende domänenübergreifende Richtlinie oder mit einer nicht für SOAP-Dienste geeigneten Richtlinie domänenübergreifend auf einen Dienst zugegriffen wurde."

Endlich ist die Architektur fertig aufgebaut, und es lässt sich mit dem Realisieren der Fenster beginnen. Die Architektur stellt Abbildung 2 grafisch dar, wobei hier auch die Views und di e ViewModels eingezeichnet sind, die im Folgenden entstehen.

Gesamtarchitektur der Silverlight-Anwendung (Abb. 2)

Gesamtarchitektur der Silverlight-Anwendung (Abb. 2)

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 [14] 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 [15] 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 [16] 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 [17] zeigt die Silverlight-Variante des NeuerPassagierViewModel, in dem ebenfalls die asynchronen Aufrufe realisiert sind.

Das Hauptfenster besteht in der WPF-Anwendung aus MainWindow.xaml, MainWindows.xaml.cs und MainWindowViewModel.cs. Die drei Dateien lassen sich aus dem WPF-Projekt übernehmen; es sind aber auch hierbei einige Änderungen notwendig.

Die Startdatei in Silverlight heißt im Standard MainPage.xaml statt MainWindows.xaml. Möchte der Entwickler den gleichen Datei- und Klassennamen wie in WPF verwenden, muss er in der App.xaml.cs-Datei die Routine Application_Startup verändern: MainPage ist zu ersetzen durch MainWindow.

  private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new MainWindow();
}

Nun sind erst mal die Änderungen in MainWindow.xaml an der Reihe. Das Wurzelelement für eine Silverlight-Anwendung ist <UserControl>. <RibbonWindow>, das in WPF zum Einsatz kam, ist auszutauschen.

Für das Hauptfenster gilt es zu beachten, dass es in Silverlight kein offizielles Ribbon-Steuerelement von Microsoft gibt, dafür aber auf Codeplex ein Beispiel [18], das aber für Silverlight 3 ist und seitdem nicht weiterentwickelt wurde. Darüber hinaus gibt es kommerzielle Anbieter solcher Steuerelemente (z. B. Sandribbon von divelements [19], RibbonView von Telerik [20], xamribbon von Infragistics [21] und das Silverlight Ribbon von Devexpress [22]. Das Tutorial soll aber ohne weitere Zusatzkosten lauffähig sein, sodass die Silverlight-Lösung auf das Ribbon verzichtet. An dessen Stelle soll eine einfache Befehlsleiste mit den Schaltflächen "Neuer Passagier" und "Ende" treten (siehe Abb. 3 und 4). Die Befehlsleiste wird realisiert durch ein <StackPanel>-Element mit einem <Image>- und zwei <Button>-Elementen. Alle anderen Aktionen sind ja innerhalb der beiden Views "BuchungsView" und "NeuerPassagierView" bereits als Schaltflächen hinterlegt.

Buchungsmaske in der Silverlight-Lösung mit einfacher Schaltflächenleiste statt Ribbon-Steuerelement (Abb. 3)

Buchungsmaske in der Silverlight-Lösung mit einfacher Schaltflächenleiste statt Ribbon-Steuerelement (Abb. 3)
Neue Passagier-Ansicht in der Silverlight-Lösung (Abb. 4)

Neue Passagier-Ansicht in der Silverlight-Lösung (Abb. 4)

Als Nächstes schlagen wieder syntaktische Unterschiede beim WPF- und Silverlight-XAML zu: Visual Studio beschwert sich in den Datenvorlagen über <DataTemplate DataType="{x:Type Ansichten:BuchungViewModel}">. In Silverlight ist die Syntax anders und einfacher: <DataTemplate DataType="Ansichten:BuchungViewModel">. Leider würde das in WPF nicht funktionieren. Listing 5 [23] zeigt MainWindows.xaml mit verändertem Wurzelelement, Ersetzung des Ribbon-Steuerelement durch eine eigene Schaltflächenleisten und den Änderungen in den Datenvorlagen.

Während die MainWindow.xaml.cs so bleiben kann wie in WPF, muss das MainWindowViewModel.cs etwas anders aufgebaut sein (siehe Listing 6 [24]). Es gibt keine Ribbon-Registerkarten mehr, und folglich braucht man dafür auch keine Properties im ViewModel. Stattdessen wird zum Umschalten zwischen den beiden Views nun eine Schaltfläche verwendet, die an das zusätzliche Command-Objekt NeuerPassagierCommand gebunden ist.

Bei der Realisierung von Ende() muss man etwas ändern, denn Application.Current.MainWindow.Close() ist nur verfügbar, wenn die Silverlight-Anwendung außerhalb des Browser läuft. Innerhalb des Browsers kann die Silverlight-Anwendung den Browser nicht eigenständig schließen, sondern nur das umgebende Browser-Fenster "höflich" dazu auffordern. Das Fenster wird aber typischerweise erst den Benutzer fragen, ob er damit einverstanden ist. Daher prüft Ende(), ob Application.Current.IsRunningOutOfBrowser "true" oder "false" ist.

Eingangs des Beitrags wurde erwähnt, dass Silverlight-Anwendungen außerhalb des Webbrowsers laufen können. Die Standardeinstellung ist der Start im Webbrowser. Der Entwickler einer Silverlight-Anwendung kann aber festlegen, dass diese außerhalb des Browser betreibbar ist. Das stellt der Entwickler im Silverlight-Projekt in den Projekteigenschaft auf der Registerkarte "Silverlight" ein, indem er Enable running application out of browser wählt und dann die "Out-of-Browser Settings" festlegt (siehe Abb. 5).

Einstellungen für den Betrieb einer Silverlight-Anwendung außerhalb des Webbrowsers (Abb. 5).

Einstellungen für den Betrieb einer Silverlight-Anwendung außerhalb des Webbrowsers (Abb. 5).

Mit den Einstellungen startet die Anwendung im Debugger nun als eigenständiges Windows-Fenster (siehe Abb. 6), wobei in Wirklichkeit hier Teile des Internet Explorer im Spiel sind; nur dessen Menüleisten sind ausgeblendet. Die Funktionen sind weiterhin gewährleistet, solange die Webservices verfügbar sind.

Silverlight-Anwendung außerhalb des Webbrowsers (Abb. 6)

Silverlight-Anwendung außerhalb des Webbrowsers (Abb. 6)

Die Anwendung kann man alternativ wie bisher über die Webseite http://localhost:82/SilverlightApp.html aufrufen, und dort läuft sie weiterhin im Browser. Der Benutzer hat aber im Kontextmenü der Silverlight Anwendung unter dem Punkt "Silverlight" die zusätzliche Option WWWings-Buchung auf diesem Computer installieren. Danach sieht der Anwender ein Installationsfenster, in dem er festlegen kann, ob er im Startmenü und/oder auf dem Windows-Desktop eine Verknüpfung zur Anwendung möchte, um diese dann zukünftig direkt ohne den vorherigen Browseraufruf zu starten. Die OK-Schaltfläche ist erst nach drei Sekunden Bedenkzeit verfügbar. Die Deinstallation der Anwendung ist später sowohl auf der Webseite im Kontextmenü (Diese Anwendung entfernen) als auch in der Windows Systemsteuerung (Programme | Software) möglich.

Im Unternehmenseinsatz ist eine solche Selbstinstallation der Silverlight-Anwendung durch jeden einzelnen Benutzer nicht der gewünschte Weg. Dort ist eine Verteilung mit herkömmlichen Methoden wie Batch-Datei/Skript, Setup/MSI oder Click-Once-Deployment [25] auf Windows-Systemen geboten. Mit etwas Hintergrundwissen kann man diese Wünsche realisieren. Das wichtigste Instrument ist SLLauncher.exe. Das Programm wird mit dem Silverlight-Plug-in unter Windows installiert. Wenn ein Benutzer eine Silverlight-Anwendung lokal installiert, entsteht eine Verknüpfung zur Anwendung im Startmenü mit der App-ID der installierten Anwendung als Parameter. SLLauncher ermöglicht aber auch, mit dem Parameter /installer den Installationsvorgang anzustoßen. Der folgende Quellcode zeigt, wie man die vom Projekt WWWings_SL erzeugte WWWings_SL.xap-Datei per Windows Batch in das lokale Dateisystem kopieren, dort installiert und startet. Den Trick für den Installationsweg über das Click-Once-Deployment beschreibt ein Blog-Eintrag [26].

echo "Kopieren"
copy "\\Server\Install\WWWings_SL.xap" "c:\SLApps\WWWings_SL.xap"
echo "Installieren"
"C:\Program Files (x86)\Microsoft Silverlight\sllauncher.exe"
/install:'c:\ SLApps\WWWings_SL.xap' /origin:"http://localhost:82/
ClientBin/WWWings_SL.xap"
/shortcut:desktop+startmenu /overwrite
echo "Starten"
"C:\Program Files (x86)\Microsoft Silverlight\sllauncher.exe"
/emulate:"c:\ SLApps\WWWings_SL.xap" /origin:"http://localhost:82/
ClientBin/WWWings_SL.xap" /overwrite
echo "Läuft!"

Silverlight-Anwendungen laufen im Standard in einer Sandbox, das heißt, sie sind vom Betriebssystem, Hardwareressourcen und anderen Anwendungen abgeschottet. Im Dateisystem kann jede Silverlight Anwendung nur in einem speziell ihr zugewiesenen Teil des Benutzerprofils Dateien erzeugen oder lesen (Isolated Storage). So können Silverlight-Anwendung nicht ausspionieren und keinen Schaden anrichten. Es gibt aber auch die Möglichkeit, Silverlight-Anwendungen unter erhöhten Rechten zu betreiben (man spricht von Elevated Trust oder Trusted Application). Dann können diese Silverlight-Anwendungen das ganze Dateisystem verwenden und über das Component Object Model (COM) mit den Betriebssystemkomponenten (z. B. dem Active Directory) oder anderen Anwendungen (z. B. Office-Produkten) interagieren. Im konkreten Fall könnte die Flugbuchungs-Silverlight-Anwendung zum Beispiel die Flugliste nach Excel importieren, eine Buchungsbestätigung als Word-Dokument erzeugen und dem Passagier per E-Mail zustellen. Das soll im Tutorial aber nicht mehr behandelt werden, denn auch die ASP.NET- und WPF-Anwendung in Teil 3 und 4 des Tutorials hatten nicht die Fähigkeiten.

Die Umstellung der WPF-Anwendung auf Silverlight hat gezeigt: Die beiden Oberflächenframeworks sind sich so nah und dann in Details doch so fern. Wenn man zu Beginn der Entwicklung weiß, dass man auch Silverlight bedienen will, kann man sicherlich an einigen Stellen auf dem kleinsten gemeinsamen Nenner programmieren. Aber an anderen Stellen gibt es diesen gemeinsamen Nenner nicht, sodass man mit einer XAML-Codebasis kaum erreichen kann, beide Oberflächen gleichzeitig zu bedienen. Definitiv ist der Weg von WPF zu Silverlight ein Vielfaches leichter und schneller als von WPF zu HTML5/JavaScript, denn dort ist sowohl die Oberflächenbeschreibungssprache als auch die Sprache der ViewModel ganz anders.

Dr. Holger Schwichtenberg
leitet das Expertenteam von www.IT-Visions.de [27], 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.

Hinweis: Ein deutsches Buch zu Silverlight 5 ist bisher nicht erschienen.

<UserControl x:Class="WWWings_SL.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_SL.Ansichten"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/
presentation/sdk"
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, Mode=TwoWay}" 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" Grid.Column="4"
ItemsSource="{Binding Flughaefen}" DisplayMemberPath=""
Margin="5,0,0,0" VerticalAlignment="Center"
SelectedItem="{Binding Abflugort, Mode=TwoWay}"
/>

<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}" SelectedItem="
{Binding Zielort, Mode=TwoWay}" DisplayMemberPath=""
Margin="5,0,0,0" VerticalAlignment="Center"
/>


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

<!-- ### Flugliste ########### -->
<sdk:DataGrid x:Name="C_GefundeneFluege" Grid.Row="3" Grid.Column="1"
AutoGenerateColumns="False" ItemsSource="{Binding Path=Fluege}"
SelectedItem="{Binding Path=Flug, Mode=TwoWay}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Abflugort" Binding="{Binding Abflugort}"/>
<sdk:DataGridTextColumn Header="Zielort" Binding="{Binding Zielort}"/>
<sdk:DataGridTextColumn Header="Datum" Binding="
{Binding Datum, StringFormat=dd.MM.yyy}"/>
<sdk:DataGridTextColumn Header="Plätze" Binding="{Binding Plaetze}"/>
<sdk:DataGridTextColumn Header="Freie Plätze"
Binding="{Binding FreiePlaetze}"/>
</sdk:DataGrid.Columns>
</sdk: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, Mode=TwoWay}" 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, Mode=TwoWay}" 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 ########### -->
<sdk:DataGrid x:Name="C_GefundenePassagiere" Grid.Row="7"
Grid.Column="1" AutoGenerateColumns="False"
ItemsSource="{Binding Path=Passagiere}" SelectedItem="
{Binding Path=Passagier, Mode=TwoWay}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="ID" Binding="{Binding ID}"/>
<sdk:DataGridTextColumn Header="Vorname" Binding="{Binding Vorname}"/>
<sdk:DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<sdk:DataGridTextColumn Header="Geburtsdatum" Binding="
{Binding Geburtsdatum, StringFormat=dd.MM.yyy}"/>
<sdk:DataGridTextColumn Header="Status" Binding="{Binding
PassagierStatus}"/>
</sdk:DataGrid.Columns>
</sdk: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;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Media;
using WWWings_SLDienstproxies.WWWingsServer;
using WWWings_GO;

namespace WWWings_SL.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(Passagier passagier = null)
{
FlugSuchenCommand = new ActionCommand(FlugSuchen);
PassagierSuchenCommand = new ActionCommand(PassagierSuchen);
BuchenCommand = new ActionCommand(Buchen);
BuchenCommand.IsEnabled = false;

BuchungsServiceClient client = new BuchungsServiceClient();
client.GetFlughaefenCompleted += delegate(object sender,
GetFlughaefenCompletedEventArgs e)
{ Flughaefen = e.Result; };
client.GetFlughaefenAsync();

if (passagier != null)
{
Passagiere = new ObservableCollection<WWWings_GO.Passagier>()
{ passagier };
Passagier = passagier;
}
// 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))
{
client.GetFlugCompleted += new EventHandler<GetFlugCompletedEventArgs>
(client_GetFlugCompleted);
client.GetFlugAsync(FNr);
}
else
{
client.GetFluegeCompleted += new EventHandler<GetFluegeCompletedEventArgs>
(client_GetFluegeCompleted);
client.GetFluegeAsync(Abflugort, Zielort);
}
}

void client_GetFluegeCompleted(object sender, GetFluegeCompletedEventArgs e)
{
Fluege = e.Result;
}

void client_GetFlugCompleted(object sender, GetFlugCompletedEventArgs e)
{
var Flug = e.Result;
if (Flug != null) Fluege = new ObservableCollection<Flug> { Flug };
else Fluege = new ObservableCollection<Flug> { };
}

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

private void PassagierSuchen()
{
BuchungsServiceClient client = new BuchungsServiceClient();
int PID;
if (Int32.TryParse(PassagierID, out PID))
{
client.GetPassagierCompleted += new EventHandler
<GetPassagierCompletedEventArgs>(client_GetPassagierCompleted);
client.GetPassagierAsync(PID);
}
else
{
client.GetPassagiereCompleted += new EventHandler
<GetPassagiereCompletedEventArgs>(client_GetPassagiereCompleted);
client.GetPassagiereAsync(PassagierName);
}
}


void client_GetPassagierCompleted(object sender,
GetPassagierCompletedEventArgs e)
{
var Passagier = e.Result;
if (Passagier != null) Passagiere = new ObservableCollection<Passagier>
{ Passagier };
else Passagiere = new ObservableCollection<Passagier>();
}

void client_GetPassagiereCompleted(object sender,
GetPassagiereCompletedEventArgs e)
{
passagiere = e.Result;

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

}

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

private void Buchen()
{
if (Flug == null || Passagier == null) return;
BuchungsServiceClient client = new BuchungsServiceClient();
client.CreateBuchungCompleted += new
EventHandler<CreateBuchungCompletedEventArgs>
(client_CreateBuchungCompleted);
client.CreateBuchungAsync(Flug.ID, Passagier.ID);
}

void client_CreateBuchungCompleted(object sender,
CreateBuchungCompletedEventArgs e)
{

if (e.Error == null)
{

string ergebnis = e.Result;

if (ergebnis == "OK")
{
BuchenErgebnis = "Buchung erfolgreich!";
BuchenErgebnisFarbe = new SolidColorBrush(Colors.Green);
}
else
{
BuchenErgebnis = ergebnis;
BuchenErgebnisFarbe = new SolidColorBrush(Colors.Red);
}

}
else
{
BuchenErgebnis = "Fehler beim Aufruf der Buchungsfunktion: "
+ e.Error.Message;
BuchenErgebnisFarbe = new SolidColorBrush(Colors.Red);
}

}

/// ################ 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_SL.Ansichten.NeuerPassagierView"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
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:sdk="http://schemas.microsoft.com/winfx/2006/xaml/
presentation/sdk"

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-->
<sdk: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, Mode=TwoWay}" />

<sdk: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, Mode=TwoWay}" />

<sdk: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,
Mode=TwoWay,StringFormat=dd.MM.yyy}" />

<sdk:Label Name="C_LabelPassagierstatus" Grid.Row="3"
Content="Passagier-status" Height="28" HorizontalAlignment="Left"
VerticalAlignment="Top" />
<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>

<!--################ 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}"/>
<sdk: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>
using System.ComponentModel;
using System.Collections.ObjectModel;
using WWWings_GO;
using WWWings_SLDienstproxies.WWWingsServer;
using System;

namespace WWWings_SL.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-Objekt
{
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()
{

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

BuchungsServiceClient client = new BuchungsServiceClient();
client.SavePassagierSetCompleted +=
new EventHandler<SavePassagierSetCompletedEventArgs>
(client_SavePassagierSetCompleted);
client.SavePassagierSetAsync(GeändertePassagiere);
}

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


private void client_SavePassagierSetCompleted(object sender,
SavePassagierSetCompletedEventArgs e)
{

if (e.Error == null)
{
var antwort = e.Result;

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);
}
}
else
{
FehlerMeldung = e.Error.Message;
if (NeuerPassagierViewModelExit != null)
NeuerPassagierViewModelExit(null);
}
}


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_SL.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Name="Main"
xmlns:Ansichten="clr-namespace:WWWings_SL.Ansichten"
d:DesignHeight="300" d:DesignWidth="400"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/
xaml/presentation/sdk">


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

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

<!--################ Bereich 1 des Grid: Befehlsleiste -->
<StackPanel Orientation="Horizontal">
<Image Name="image1" Stretch="Fill" Height="30" Width="100"
Source="/WWWings_SL;component/Images
/WorldWideWings_Logo_klein.jpg" Margin="20,0,20,0" />
<Button Name="NeuerPassagier" Height="30" Width="100"
Command="{Binding NeuerPassagierCommand}"
Margin="0,0,20,0" Content="Neuer Passagier"></Button>
<Button Name="Ende" Height="30" Width="100" Command="{Binding
EndeCommand}" Content="Ende"></Button>

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


</UserControl>
using System.ComponentModel;
using System.Windows;
using WWWings_GO;

namespace WWWings_SL
{
public partial class MainWindowViewModel : INotifyPropertyChanged
{
/// ################ Daten ################
private INotifyPropertyChanged activeAnsichtViewModel;

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

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

private void Ende()
{
if (Application.Current.IsRunningOutOfBrowser)
{
Application.Current.MainWindow.Close();
}
else
{
System.Windows.Browser.HtmlPage.Window.Invoke("close");
}
}

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

private void NeuerPassagier()
{
var pvm = new WWWings_SL.Ansichten.NeuerPassagierViewModel();
this.ActiveAnsichtViewModel = pvm;
pvm.NeuerPassagierViewModelExit += delegate(Passagier passagier)
{
this.ActiveAnsichtViewModel = new
WWWings_SL.Ansichten.BuchungViewModel(passagier);
// ViewModel umschalten und Passagier übergeben
};
}


/// ################ Konstruktur ################
public MainWindowViewModel()
{
NeuerPassagierCommand = new ActionCommand(NeuerPassagier);
EndeCommand = new ActionCommand(Ende);
// Erste Viewmodel
this.ActiveAnsichtViewModel = new
WWWings_SL.Ansichten.BuchungViewModel();

}


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

public event PropertyChangedEventHandler PropertyChanged;


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

#endregion

}
} (ane [28])

URL dieses Artikels:
https://www.heise.de/-1671358

Links in diesem Artikel:
[1] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-1-Datenzugriff-und-Logik-1418915.html
[2] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-2-Application-Server-und-Webservices-1446415.html
[3] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-3-Eine-Weboberflaeche-mit-ASP-NET-1520357.html
[4] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-4-Desktop-Entwicklung-mit-WPF-und-MVVM-1615881.html
[5] https://connect.microsoft.com/VisualStudio/feedback/details/557939/t4selftrackingcodegentemplatecs-german-localized-ressources-contained-quotation-marks
[6] ftp://ftp.heise.de/pub/ix/developer/schwichtenberg_nettutorial_5.rar
[7] http://www.riastats.com
[8] https://www.heise.de/news/Freier-Silverlight-Klon-Moonlight-eingestellt-1586174.html
[9] http://silverlightcontrib.codeplex.com/
[10] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-3-Eine-Weboberflaeche-mit-ASP-NET-1520357.html
[11] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-3-Eine-Weboberflaeche-mit-ASP-NET-1520357.html
[12] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-4-Desktop-Entwicklung-mit-WPF-und-MVVM-1615881.html
[13] https://www.heise.de/ratgeber/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-2-Application-Server-und-Webservices-1446415.html
[14] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html?artikelseite=7
[15] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html?artikelseite=6
[16] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html?artikelseite=5
[17] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html?artikelseite=8
[18] http://slribbon.codeplex.com/
[19] http://www.divelements.co.uk/net/controls/sandribbonsl/
[20] http://www.telerik.com/products/silverlight/controls/ribbonview.aspx
[21] http://www.infragistics.com/dotnet/netadvantage/silverlight/xam-web-ribbon.aspx#Overview
[22] http://www.devexpress.com/Products/NET/Controls/Silverlight/Ribbon/
[23] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html?artikelseite=9
[24] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html?artikelseite=10
[25] http://www.it-visions.de/glossar/alle/692/692.aspx
[26] https://www.heise.de/blog/Silverlight-Anwendungen-ausserhalb-des-Browsers-per-Batch-Setup-MSI-oder-Click-Once-verteilen-1077886.html
[27] http://www.it-visions.de/start.aspx
[28] mailto:ane@heise.de