.NET 3.0
programmer’s
choice
Die Wahl für professionelle Programmierer und Softwareentwickler. Anerkannte Experten wie z.B. Bjarne Stroustrup, der Erfinder von C++, liefern umfassendes Fachwissen zu allen wichtigen Programmiersprachen und den neuesten Technologien, aber auch Tipps aus der Praxis. Die Reihe von Profis für Profis!
Hier eine Auswahl: Professionelle Websites Stefan Münz 1136 Seiten € 59,95 (D), € 61,70 (A) ISBN-13: 978-3-8273-2370-5 ISBN-10: 3-8273-2370-3
Wenn heute von Webdesign die Rede ist, dann immer häufiger von striktem HTML, von sauberer Trennung zwischen Layout und Inhalt, und von Beachtung der Regeln für barrierefreie Websites. Beschrieben wird hier, was der Zukunft gehört und auf immer breiterer Front Anwendung findet: strukturell sinnvolles, am Strict-Standard des W3-Konsortiums orientiertes HTML, layout-formendes, intelligent eingesetztes CSS und benutzerfreundliches, DOM-orientiertes JavaScript. Auch die Serverseite darf nicht fehlen. Immer mehr Site-Betreiber steigen auf eigene Root-Server um. Vorinstalliert ist dort meistens das beliebte LAMP-Paket, bestehend aus einem Linux-Derivat, dem Apache Webserver, dem MySQL Datenbank-System und der Scriptsprache PHP. Genau diese Technologien werden im Buch gründlich und zusammenhängend behandelt.
Visual C# 2005 Frank Eller 1104 Seiten € 49,95 (D), € 51,40 (A) ISBN-13: 978-3-8273-2288-2 ISBN-10: 3-8273-2288-X
Fortgeschrittene und Profis erhalten hier umfassendes Know-how zur Windows-Programmierung mit Visual C# in der Version 2. Nach einer Einführung ins .NET-Framework und die Entwicklungsumgebung geht der Autor ausführlich auf die Grundlagen der C#-Programmierung ein. Anhand zahlreicher Beispiele zeigt er die verschiedenen Programmiertechniken wie z.B. Anwendungsdesign, Grafikprogrammierung oder das Erstellen eigener Komponenten. Besondere Schwerpunkte liegen auf der umfangreichen .NET-Klassenbibliothek und Windows Forms sowie auf dem Datenbankzugriff mit ADO.NET.
Jürgen Kotz Rouven Haban Simon Steckermeier
.NET 3.0 WCF, WPF und WF – Ein Überblick
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ®-Symbol in diesem Buch nicht verwendet.
Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt.
10
9
08
07
8
7 6
5
4 3
2
1
ISBN 978-3-8273-2493-1
© 2007 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Lektorat: Sylvia Hasselbach,
[email protected] Herstellung: Martha Kürzl-Harrison,
[email protected] Korrektorat: Sandra Gottmann,
[email protected] Coverkonzeption und -gestaltung: Marco Lindenbeck, webwo GmbH,
[email protected] Satz: reemers publishing services gmbh, Krefeld, www.reemers.de Druck und Verarbeitung: Kösel, Krugzell (www.KoeselBuch.de) Printed in Germany
Inhalt
1
2
Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
Danksagungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
.NET 3.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
1.1 1.2 1.3 1.4 1.5
Laut gedacht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Was man zum Entwickeln braucht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .NET 3.0 als Erweiterung zu .NET 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der kleine Bruder von WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Expression-Produkte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16 17 18 20 21
Windows Presentation Foundation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
2.1
26 26 28 29 32 33 35 35 39 41 43 46 49 49 50 51 53 57 59 65 68 69 72 78 78 85 89 91 92
2.2
2.3
2.4
2.5
2.6
2.7
Hello World oder besser – best of both worlds! . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Ein erstes Fenster mit .NET-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.2 Ein Fenster mittels XAML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XAML (eXtensible Application Markup Language) . . . . . . . . . . . . . . . . . . . . . 2.2.1 Von XAML zu .NET-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2 XAML und Ereignisbehandlung in Code Behind . . . . . . . . . . . . . . . . . Layout und Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Das Grid-Steuerelement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Dependency Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Das StackPanel-Steuerelement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.4 Das DockPanel-Steuerelement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.5 Das Canvas-Steuerelement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Steuerelemente – ein Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Elemente und deren Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.2 Button, TextBox und Label . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.3 Das ListBox-Steuerelement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.4 Menüs in WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.5 Das Toolbar-Steuerelement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.6 Das MediaElement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.7 Content – einfach nur Inhalt? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lokalisierung von WPF-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.1 Lokalisierung mit Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.2 Lokalisierung mit dem LocBaml-Tool . . . . . . . . . . . . . . . . . . . . . . . . . . Vorlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6.1 Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6.2 Styles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6.3 Trigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenbindung in WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.7.1 Datenbindung zwischen zwei grafischen Elementen . . . . . . . . . . . . . .
5
Inhalt 2.8
Dokumente in Windows Presentation Foundation . . . . . . . . . . . . . . . . . . . . . . 2.8.1 Flussdokumente (Flow Documents) . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.8.2 Fixierte Dokumente (Fixed Documents) . . . . . . . . . . . . . . . . . . . . . . . . Grafiken mit WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.9.1 Geometrische Grundfiguren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.9.2 Brushes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Animationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.10.1 Timelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.10.2 Storyboards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Animationen für Entwickler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.11.1 Tools in XAML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
109 110 113 118 119 125 126 127 128 129 134 136
Windows Communication Foundation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
137
3.1
137 139 140 141 142 142 145 148 148 156 158 168 168 178 192 195 196 198 205 209 209 210 212 218 220 228 231 232 236 237 237 238 238
2.9
2.10
2.11 2.12
3
3.2
3.3
3.4
3.5
3.6
3.7
6
Einführung in WCF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Serviceorientierte Architekturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.2 WCF im Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.3 Message Exchange Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das ABC eines Endpoints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 C – Contract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 B – Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.3 A – Adresse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.4 Service Configuration Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.5 Hosting der Beispielanwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.6 Erstellen des Clients mit dem Tool svcutil . . . . . . . . . . . . . . . . . . . . . . Datenserialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Datenserialisierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Versionierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hosting von Communication Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Selfhosting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.2 IIS-Hosting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.3 Windows-Dienst Hosting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.4 WAS-Hosting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erweitertes Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.1 Programmatisches Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.2 Vordefinierte Bindings und Interoperabilität . . . . . . . . . . . . . . . . . . . . 3.5.3 Behaviors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.4 Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.5 Session . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.6 Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.7 Tracing und Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kompatibilität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.1 Integration von COM+ und COM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.2 Integration von .NET Remoting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.3 Migration von Web Services und WSE 3.0 Web Services . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Inhalt
4
Windows Workflow Foundation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 4.1 4.2
4.3
4.4
4.5
4.6
4.7 4.8 4.9 4.10 4.11
5
241 242 242 245 246 246 246 251 262 262 268 280 285 287 289 295 301 301 306 311 319 322 323 326 333 338
CardSpace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341 5.1 5.2 5.3 5.4 5.5
5.6 5.7 5.8
A
Der etwas andere Denkansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workflow Foundation-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 Projektvorlagen im Visual Studio 2005 . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.2 Aktivitäten als Grundbausteine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.3 Hosting-Möglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hello World-Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.1 Beispiel eines sequenziellen Ablaufs . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.2 Beispiel eines Statuscomputer-Workflows . . . . . . . . . . . . . . . . . . . . . . Hosting-Dienste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1 Persistence Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.2 Tracking Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.3 Scheduling Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kommunikation zwischen Host und Workflow . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.1 Schnittstelle definieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.2 Erstellung der Host-Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.3 Erstellung der Workflow-Bibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . . Workflow und Web Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.1 Workflow als Web Service publizieren . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.2 Web Service mit InvokeWebServiceActivity konsumieren . . . . . . . . . 4.6.3 Mehrere Web Service-Aufrufe innerhalb eines Workflows . . . . . . . . . Workflow und Markup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Aktivitäten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.8.1 Log-Aktivität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fehlerbehandlung in WF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kompensationsvorgänge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Authentifizierung heute – ein Chaos? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Laws of Identity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erste Vorstellung von Windows CardSpace . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rollen im Authentifizierungsprozess . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Digitale Karten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.1 Persönliche Karten erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.2 Verwaltete Karten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CardSpace als Meta-Identitätssystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Genereller Ablauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sichern und Wiederherstellen von Karten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
341 342 342 344 344 345 349 349 350 351
Projektbeschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355 A.1 A.2
Projektaufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ChatGUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.2.1 GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.2.2 Kommunikation mit dem WCF-Service . . . . . . . . . . . . . . . . . . . . . . . . .
357 358 358 365
7
Inhalt A.3
A.4
Chatserver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3.1 Datendefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3.2 ServiceContract-Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3.3 Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3.4 Instanzierung und Sessionmanagement . . . . . . . . . . . . . . . . . . . . . . . . A.3.5 ClientCallbacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3.6 Aufruf des Workflows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3.7 Hosting des Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ChatWorkflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.4.1 Registrierung als Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.4.2 Benutzer-Login als Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
366 366 367 369 370 371 373 374 375 376 377
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
8
Vorwort Als im Juli 2005 die Beta 1 des .NET Frameworks 3.0, damals noch unter dem Namen WinFX, vorgestellt wurde, haben wir schon nach dem Studium der ersten Veröffentlichungen geahnt, was alles Neues auf uns zukommen würde. Um es ganz lapidar zu sagen, was zu diesem Thema dann auf der PDC im September 2005 präsentiert wurde, das hat uns aus den Socken gehauen. Ab diesem Zeitpunkt stand eigentlich fest: Da müssen wir am Ball bleiben, denn WinFX wird sich durchsetzen. Und wir sollten recht behalten, denn wer sich heute zum Ziel nimmt, eine WebCast-Serie oder -Konferenz zu gestalten oder gar ein Buch zu schreiben, kommt an diesem Thema nicht vorbei. Nicht nur in der blanken Theorie, nein sogar in produktive Entwicklung hat .NET 3.0 Einzug gehalten, und wir haben das Glück, dass wir einige unserer Projektbetreuer schon früh vom Einsatz dieser Technologie überzeugen konnten. Die im November 2006 erschienene Endfassung des .NET Frameworks 3.0 ist auch die Grundlage für die drei großen Teile in diesem Buch, denn sowohl die Windows Presentation Foundation, die Windows Communication Foundation als auch die Workflow Foundation haben ihre Grundfesten in der Klassenbibliothek von .NET 3.0. Die Faszination, die Ende des Jahres 2005 von .NET 3.0 auf uns übergesprungen ist, wollen wir an Sie weitervermitteln. Wir haben uns dabei als Erstes Gedanken darüber gemacht, welchen der Teile wir herausgreifen könnten, um diesen Teil dann verständlich, aber trotzdem praxis-
Vorwort
nahe in einem Buch für unsere Leser aufzubereiten. Nach einigen Diskussionen, oder sollte ich es eher Wortgefechte nennen, blieb vorerst ein Konsens aus. Denn jede der einzelnen Technologien erschien uns so spannend, dass wir keine davon auslassen wollten. Wir haben uns daher entschieden, unseren Erfahrungsschatz in einem Buch der Programmers Choice-Reihe in allen drei Technologien zum Besten zu geben. Uns war es in diesem Buch ein Anliegen, Ihnen einen Überblick über alle drei großen Themengebiete von .NET 3.0 zu geben, um die Flamme an Sie weiterzureichen und mit einem Einblick in die Verwaltung von sicherheitsrelevanten Daten mit der in Vista eingebauten CardSpace-Technologie das Feuer vollständig auflodern zu lassen. Dabei haben wir versucht, sowohl den absoluten Neuling in dieser Technologie als auch den Leser anzusprechen, der sich schon vor diesem Buch mit .NET 3.0 beschäftigt hat. Um diesem Anspruch gerecht zu werden, haben wir uns immer wieder gefragt, was für den Leser, der in unserer Zielgruppe liegt, besonders von Bedeutung sein könnte. Das Ergebnis liegt Ihnen gerade vor und beinhaltet sowohl einen Überblick über die einzelnen Teile von .NET 3.0 als auch eine tiefer gehende Behandlung von anspruchsvollen Themen der einzelnen Technologien. Beide Teile haben wir mit zugehörigen Beispielen untermauert, die ihrerseits sofort in der täglichen Entwicklung eingesetzt werden können. Für Fragen, Kritik und Anregungen stehen wir Ihnen natürlich jederzeit zur Verfügung. Sie erreichen uns unter:
[email protected] [email protected] [email protected]
Kapitelübersicht 1. Kapitel: In diesem Kapitel finden Sie die einleitenden Worte zu diesem Buch. Dazu gehört in erster Linie die Definition der zu installierenden Software, die Sie benötigen, um die Programmteile in diesem Buch nachvollziehen zu können. 2. Kapitel: Das erste Kapitel behandelt die Erstellung von grafischen Benutzeroberflächen mit der Windows Presentation Foundation. Dabei wird die Interaktion zwischen .NET-Quellcode und XAML (Extensible Markup Language) eingehend besprochen. Von einfachen Oberflächen über Datenbindung und Lokalisierung bis hin zu Animationen werden die wichtigsten Merkmale und Ausprägungen von WPF dargestellt und an eingängigen Beispielen näher erläutert.
10
Vorwort
3. Kapitel: In diesem Kapitel geht es um die Windows Communication Foundation – WCF. Die Windows Communication Foundation ist ein einheitliches Programmiermodell für interoperable Kommunikation von Softwarekomponenten. Hier geht es darum, Sie mit den grundlegendsten Sachen dieser neuen Kommunikationstechnologie vertraut zu machen und Ihnen einen Ausblick zu geben, wie viele Möglichkeiten Ihnen mit WCF geboten werden. Das Endpoint-ABC sollte Ihnen nach Studium des Kapitels genauso geläufig sein wie das normale ABC. 4. Kapitel: Wollen Sie ins Reich der Automatisierung mittels der Workflow Foundation eintauchen, so bietet sich das 4. Kapitel an. Neben der Vorstellung der grundsätzlichen Mechanismen der Workflow Runtime und ihren Diensten wird anhand zahlreicher Beispiele versucht, dem Leser die Funktionsweise der gängigsten Steuerelemente beizubringen und auch das Zusammenspiel von verteilten Workflow-Anwendungen aufzuzeigen. 5. Kapitel: Ausweis bitte! So heißt es dann im 4. Kapitel mit Windows CardSpace als Hauptthema. Denn egal welche Mittel und Wege man auch wählt, um die Identität eines anderen vorzutäuschen, das Einzige, was zählt, ist die installierte Karte auf Ihrem Rechner. Wenn Sie jetzt neugierig geworden sind, starten Sie doch sofort mit Kapitel 5!
!
!
!
ACHTUNG
Aufgrund von unterschiedlichen Hardware- und Softwareinstallationen können wir leider nicht garantieren, dass alle im Buch vorgestellten Programme oder Programmteile sofort und auf Anhieb auch bei Ihnen funktionieren. Aus denselben Gründen können wir keine Haftung für irgendwelche Folgen übernehmen, die sich aus dem Benutzen des hier und auf der CD veröffentlichten Codes ergeben. Wir sind uns dennoch ziemlich sicher, dass die vorgestellten Programme auch bei Ihnen ohne größere Probleme lauffähig sein werden. Bitte beachten Sie dazu auch die Softwarevoraussetzungen, die im ersten Kapitel beschrieben werden.
11
Danksagungen Zum Schluss wollen wir uns noch bei allen recht herzlich bedanken, die bei der Erstellung dieses Buches mitgeholfen haben. Ein herzliches niederbayrisches Vergelt’s Gott geht dabei an unsere Lektorin Frau Sylvia Hasselbach für die tolle Unterstützung während des gesamten Projektes sowie an alle anderen fleißigen Hände bei Addison-Wesley. Während des Buchschreibens ist man manchmal, leider Gottes, ein bisschen gestresst, und leider nimmt man sich dann nicht die Zeit, die eigentlich der Familie zustehen sollte. Deswegen geht ein ganz dicker Kuss von Jürgen an seine Frau Ulli für das nötige Verständnis und die Unterstützung, wenn es mal wieder ganz stressig war. Auch an Julia und Lennard ein herzliches Danke für das Verständnis von Euch, wenn der Papa wieder mal keine Zeit zum Spielen hatte. Ein Dank auch an Simon und Rouven für die vorbildliche Zusammenarbeit und dafür, dass Ihr Euch wenigstens an den Zeitplan gehalten habt. Ein ganz lieber Dank von Rouven geht an seine kleine Familie, denn Mama Larissa und Junior Lennox hatten in der heißen Phase des Buchschreibens nicht allzu viel vom Papa. Vielen Dank, liebe Larissa, für die unzähligen Tassen Kaffee, das Verständnis und die Unterstützung, die Du Tag für Tag bis spät in die Nacht aufgebracht hast. Ein großes Dankeschön soll Jürgen als Initiator und Schirmherr dieses Buches gelten, der uns von seiner Erfahrung als Buchautor und Fachlektor hat profitieren lassen.
Danksagungen
Ein großer Dank auch von Simon an seine Familie für die großartige Unterstützung während der nicht immer leichten Tage als Buchautor. Besonders dabei geholfen haben einem dabei die zahlreichen Stunden im ROMA, nicht nur hinter, sondern auch vor der Theke, wo man genüsslich hat abschalten können, das ein oder andere Bierchen mit seinen Freunden getrunken hat und über die wahren Dinge des Lebens philosophieren konnte. Herzlichen Dank vor allem an meinen Bruder Johannes, der mich mit Espresso und kleinen Ausflügen bei Laune gehalten und das anfängliche Praktikum bei Prime Time Software überhaupt erst zustande gebracht hat. DANKE an dieser Stelle nochmals, ich hab’s nicht vergessen! Ach ja, da hätte ich doch den Cheffe fast vergessen. DANKE JÜRGEN!
14
1
.NET 3.0
.NET 3.0 ist da – aber was versteht man eigentlich unter .NET? Nun, falls Sie das noch nicht wissen, dann haben Sie wohl die falsche Lektüre in den Händen. Wir befassen uns in diesem Buch nicht mit den Themen, wie .NET funktioniert oder aus welchen Einheiten es sich zusammensetzt, denn das können bzw. werden Sie schon in etlichen anderen Fachbüchern oder Online-Artikeln nachgelesen haben. Nein, hier geht es ans Eingemachte, und zwar nur um die Neuerungen, die mit dem .NET Framework 3.0 einhergehen und unser Entwicklerleben weiter vereinfachen sollen. Damit Sie wissen, was Sie zum Entwickeln und Nachvollziehen unserer Projektbeispiele an Software benötigen, lesen Sie bitte zuerst dieses Kapitel hier zu Ende, bevor Sie zu einem anderen Kapitel springen. Es wird Ihnen helfen, das neue Framework besser in die bisherige .NET-Landschaft einordnen zu können, und auch einen kurzen Einblick in zukünftig geplante Produkte von Microsoft geben. Kapitel 2 wird Ihnen die Windows Presentation Foundation nahe bringen. Sie werden lernen, was XAML ist und wie einfach sich damit visuell beeindruckende Oberflächen in WPF erschaffen lassen. Tiefer in die Materie geht es in Kapitel 3. Dort wollen wir dann endlich richtig kommunizieren, und zwar über Rechnergrenzen hinweg mit der Windows Communication Foundation. Als Nachfolger der .NET Web ServiceWelle verständigt sich dieser Spezialist für verteilte Anwendungen zwar nach wie vor noch über das SOAP-
Kapitel 1
Protokoll, schützt den Entwickler jedoch vor seinen spitzen Klammern, indem er eine schützende Schicht durch den Einsatz von Klassenattributen dazwischenlegt. Nicht die technologische Umsetzung einer Netzwerkanwendung könnte jetzt eine Diskussion hervorrufen, sondern eher die Wahl der richtigen Schnittstellenmethoden, des passenden Protokolls und des optimalen Datenformats. Sie können aber auch sofort bei Kapitel 4 mit der Windows Workflow Foundation anfangen, in dem sich alles um Geschäftsprozesse und deren Visualisierung und Integration in die eigene Anwendung dreht. Last but not least bildet Windows CardSpace dann mit einem kleinen Überblick in Kapitel 5 das Schlusslicht und zeigt, wie der Datenaustausch im Internet in Zukunft sicherer gestaltet werden könnte.
1.1 Laut gedacht Würden Sie mich fragen, welche Gedankengänge mir spontan zum Thema .NET 3.0 einfallen, würde ich wohl sagen: .NET Framework 3.0 wird standardmäßig mit Windows Vista ausgeliefert Extensible Application Markup Language (XAML) Beeindruckende, animierte Oberflächen mit der Windows Presentation Foundation erstellen, die auch im Browser lauffähig sind Desktop-Anwendungen und Browseranwendungen verschmelzen Windows Communication Foundation als Generalist für verteilte, interoperable Anwendungen Einheitliche, erweiterbare Workflow-Engine für Windows-Anwendungen Geschäftsprozesse mit der Windows Workflow Foundation visualisieren, in die eigene Anwendung integrieren und so im Unternehmen manifestieren Microsoft Passport stirbt aus, CardSpace revolutioniert das Sicherheitskonzept der Internet-Kommunikation Microsoft Visual Studio 2005 als das .NET-Entwicklerwerkzeug der Wahl Microsoft Expression-Produkte sorgen für bessere und effektivere Zusammenarbeit zwischen Entwickler und Designer Kündigt Windows Forms schön langsam seinen Nachfolger Windows Presentation Foundation an …?
16
.NET 3.0
Mehr Spezialisierung im Unternehmen notwendig, um beständige Anwendungen für die Zukunft entwickeln zu können Die Zukunft …? Ich gebe zu, ein paar dieser Gedanken sind sehr gewagt, und letztendlich entscheidet doch die Mehrheit der Anwender bzw. der Kunden, welche Technologie welcher den Rang ablaufen wird, jedoch bin ich nach wie vor der festen Überzeugung, dass die Technologien des .NET Frameworks 3.0 ein paar Opfer aus seiner Vorgängerversion auf dem Gewissen haben werden.
1.2 Was man zum Entwickeln braucht Windows Vista ist mittlerweile in aller Munde, doch kann ich auch 3.0er-Anwendungen für andere Betriebssysteme entwickeln? Die Antwortet lautet Ja, denn neben Windows Vista, bei dem das .NET Framework 3.0 standardmäßig installiert ist, werden auch Windows XP Service Pack 2 und Microsoft Windows Server 2003 Service Pack 1 unterstützt. Um nun Anwendungen mit den neuen 3.0er-Framework-Komponenten zu entwickeln, muss zumindest der erste der folgenden Punkte von der Microsoft-Webseite heruntergeladen werden, die weiteren Komponenten sind optional, jedoch unbedingt für eine komfortable Entwicklung anzuraten: 1. Microsoft .NET Framework 3.0 Redistributable Package http://www.microsoft.com/downloads/details.aspx?FamilyID=10cc340b-f857-4a14-83f525634c3bf043&DisplayLang=de 2. Microsoft Visual Studio 2005 CTP Extensions for .NET Framework 3.0 (WCF & WPF) November 2006 CTP http://www.microsoft.com/downloads/details.aspx?FamilyId=F54F5537-CC86-4BF5AE44-F5A1E805680D&displaylang=en 3. Microsoft Windows SDK for .NET Framework 3.0 http://www.microsoft.com/downloads/details.aspx?FamilyId=C2B1E300-F358-4523-B479F53D234CDCCF&displaylang=en 4. Microsoft Visual Studio 2005 Extensions for Windows Workflow Foundation http://www.microsoft.com/downloads/details.aspx?displaylang=de&FamilyId=5D61409E1FA3-48CF-8023-E8F38E709BA6 Das .NET Framework 3.0 Redistributable Package benötigen Sie sowohl für die Entwicklung als auch für die Weitergabe der fertigen Anwendung an den Kunden.
17
Kapitel 1
Der zweite Punkt setzt die Installation des Microsoft Visual Studio 2005 voraus und stattet Visual Studio 2005 mit wertvollen Erweiterungen in Bezug auf Projektvorlagen und einem integrierten WPF-Designer aus.
*
*
*
TIPP
Falls Sie mit den WPF & WCF Extensions noch auf die nächste CTP warten wollen, installieren Sie ruhig die oben stehende November 2006-CTP. Denn die nächste Version dieser Tools wird nicht mehr als Erweiterung zu Visual Studio 2005 erhältlich sein, sondern vollständig integriert mit der nächsten CTP-Version des Visual Studios, Codename Orcas, ausgeliefert.
Unterschätzen Sie auch nicht die Bedeutung des Windows SDK für das .NET Framework 3.0. Man braucht diese Komponente zwar nicht unbedingt, um 3.0er-Anwendungen entwickeln zu können, dennoch möchte ich sie v. a. während meiner Einarbeitungsphase und auch jetzt nicht missen. Denn hat man dieses Development Kit einmal installiert, findet man unter PROGRAMME/MICROSOFT WINDOWS SDK zahlreiche Technologie- und Anwendungsbeispiele, ausführliche Dokumentationen und eine Reihe nützlicher Tools, unter anderem das XAML Pad, den Service Configuration Editor und den Service Trace Viewer. Bislang ist dieses Dokumentationspaket zwar nur auf Englisch erhältlich, jedoch lege ich dieses Nachschlagewerk nicht nur .NET 3.0-Entwicklern ans Herz, sondern auch allen anderen Microsoft-Programmierern, da es auch auf Windows Forms, ASP.NET und die Webentwicklung generell eingeht und sogar auf entferntere Themen wie Win32 und COM-Programmierung Bezug nimmt. Möchten Sie nun auch Workflows im Visual Studio 2005 grafisch designen und auch auf Projektvorlagen für sequenzielle und Statuscomputer-Workflows zurückgreifen können, müssen Sie die Extensions for Windows Workflow Foundation installieren, die unter Punkt 4 erwähnt sind. Einmal hatten wir bei der Installation der unter Punkt 4 erwähnten Workflow Extensions das Problem, dass bei der Installation immer der zuletzt ausgeführte Installer (z.B. des Windows SDK) aufgerufen wurde. Sollten Sie auch in diese Situation geraten, kann ich Ihnen einen kleinen Workaround anbieten, der in unserem Fall einwandfrei funktioniert hat: 1. Benennen Sie Dateiendung des Workflow Installers von *.exe in *.zip um und extrahieren Sie die dadurch entstandene *.zip Datei in einen Ordner. 2. Navigieren Sie zu dem entpackten Ordner und starten Sie die Installation der Workflow Extensions durch einen Doppelklick auf die darin enthaltene Setup.exe.
18
.NET 3.0
!
!
!
ACHTUNG
Die Visual Studio 2005 Express-Editionen werden von den Workflow Extensions for Windows Workflow Foundation nicht unterstützt.
1.3 .NET 3.0 als Erweiterung zu .NET 2.0 Falls Sie aufgrund der neuen Versionsnummer des Frameworks denken, es handle sich bei dem .NET Framework 3.0 um den Nachfolger der Vorgängerversion 2.0, so liegen Sie nicht ganz richtig: .NET Framework 3.0 = .NET Framework 2.0 + WPF + WCF + WF + CardSpace Die Common Language Runtime ist folglich noch die gleiche wie in .NET 2.0 (vgl. Abbildung 1.1), denn bei dem .NET Framework 3.0 handelt es sich eigentlich nur um eine Erweiterung der .NET Framework-Klassenbibliothek des 2.0er-Frameworks. Die nächste Versionsnummer erklärt sich wohl aus den revolutionären Neuheiten, welche die neuen Bibliotheken mit sich bringen. 2.0 Entwickler können deshalb aufatmen: Die aktuell laufenden .NET 2.0er-Anwendungen werden nach wie vor auch bei installiertem 3.0er-Framework einwandfrei und ohne Probleme laufen! Abbildung 1.1 zeigt uns mit der .NET Framework-Version 3.5 auch einen kleinen Blick in die Zukunft. ASP.NET Ajax und LINQ heißen dann zukünftige Erweiterungen, wobei ASP.NET Ajax den Webentwicklern mächtige Klassen für die JavaScript-Clientprogrammierung zur Verfügung stellt und die Language Integrated Query (LINQ) eine gemeinsame Abfragesprache darstellt, die gleichermaßen für relationale Datenbanken, XML-Dateien und .NET-Objekte verwendet werden kann.
> >
>
HINWEIS
Visual Studio Orcas ist übrigens schon als Januar-CTP in der Form eines Virtual PC Images unter http://www.microsoft.com/downloads/details.aspx?FamilyId=69055927-458b-4129-9047fcc4facae96c&displayLang=en als kostenloser Download erhältlich (). Die Größe beträgt knapp 4 GByte und enthält schon erste Teile der neuen Sprachelemente aus C# 3.0 und VB 9.0.
19
Kapitel 1
Abbildung 1.1: NET Framework-Versionen im Überblick
Abbildung 1.2: Vorschau auf Microsoft Visual Studio, Codename Orcas
20
.NET 3.0
1.4 Der kleine Bruder von WPF Richtig gelesen, es wird einen kleinen Bruder zu WPF geben. Er wird den Namen WPF/E tragen, was für Windows Presentation Foundation/Everywhere steht. Er ist ca. 1.0 MB schwer (das Browser-Plug-In), wird in mehreren Browsern und sogar auf dem Macintosh lauffähig sein, und wer sich mit JavaScript/ASP.NET Ajax und XAML schon ein bisschen auskennt, kann mit der Dezember-CTP schon sein erstes Hello World auf die WPF/E-Bühne des Browsers zaubern. Voraussetzung jedoch ist ein kleines BrowserPlug-In, das auf der Microsoft-Website kostenlos erhältlich ist: http://www.microsoft.com/downloads/details.aspx?FamilyId=A3E29817-F841-46FC-A1D2CEDC1ED5C948&displaylang=en Hat man diese Dezember-CTP installiert, kann man schon eine frühe Version von WPF/E testen. Am besten, Sie besorgen sich zudem noch das WPFE SDK unter http://www. microsoft.com/downloads/details.aspx?FamilyId=2B01EC7E-C3B8-47CC-B12A67C30191C3AA&displaylang=en, das Ihnen erste Anleitungen zum Entwickeln der ersten WPFE-Projekte zur Verfügung stellt. Unter http://channel9.msdn.com/playground/wpfe/ können Sie schon jetzt die ersten Beispiele zu WPF/E betrachten. Teilweise sind wir solche Browser-Animationen ja schon von anderen Technologien wie Macromedia Flash gewohnt, aber schauen Sie sich doch einmal das WPFEPAD unter http://www.simplegeek.com/mharsh/wpfepad/ an. Sie werden begeistert sein, welche Grafiken sich mit nur wenigen Zeilen XAML in Verbindung mit JavaScript im Browser darstellen lassen. Da die Endversion von WPF/E jedoch noch nicht in Sicht ist, beschränke ich mich hiermit auf diese wenigen Hinweise, und lasse Sie selbst in den Weiten des Internets Erfahrungen mit diesem kleinen WPF-Gefährten sammeln, denn schließlich geht es hier in diesem Buch um das .NET Framework 3.0, genauer gesagt um WPF, WCF, WF und WCS.
1.5 Die Expression-Produkte Um dem hohen Spezialisierungsgrad der einzelnen technologischen Richtungen des .NET Frameworks 3.0 gerecht werden zu können, hat Microsoft weitere Design-Werkzeuge entwickelt, deren Produktreihe die Bezeichnung Expression trägt. Die meisten davon befinden sich zwar noch im Entwicklungsstadium, können jedoch schon jetzt von der Microsoft-Website als CTP bzw. Betaversion heruntergeladen werden.
21
Kapitel 1
Microsoft Expression-Produkte
Expression Blend
http://www.microsoft.com/products/expression/en/Expression-Blend/ default.mspx
Expression Design
http://www.microsoft.com/products/expression/en/expression-design/ default.mspx
Expression Web
http://www.microsoft.com/products/expression/en/expression-web/ default.mspx
Expression Media
http://www.microsoft.com/products/expression/en/expression-media/ default.mspx
Zwei davon, nämlich Expression Blend und Expression Design, will ich kurz vorstellen, da ich denke, dass auch bei kleineren 3.0er-Projekten die Zuhilfenahme dieser Werkzeuge bedeutende Vorteile mit sich bringen könnte. Expression Blend soll als Oberflächendesign-Tool die Zusammenarbeit zwischen Designer und Entwickler erheblich verbessern (Abbildung 1.3). Musste früher der Entwickler die vom Designer entworfene Vorlage in seiner Applikation erneut zusammenbauen, so kann er sich bei Einsatz dieses Werkzeugs nun komplett auf die Geschäftslogik konzentrieren. Denn schon beim Entwurf der Oberfläche erzeugt Expression Blend automatisch XAML-Code im Hintergrund und kein Bildformat, so wie die meisten bisherigen Grafikprogramme. Selbst Animationen und kleine Codepassagen können mit diesem Werkzeug hinzugefügt werden und ersparen so dem Entwickler viel Arbeit, da er sich nun besser auf die dahinterliegende Programmebene konzentrieren kann. Expression Design, ein weiteres Produkt aus dieser Reihe, wird in der Stufe des Entwicklungsprozess einer Applikation eher vor dem Expression Blend angesiedelt werden (Abbildung 1.4). Denn wie andere Grafikprogramme kann man durch dessen Einsatz sowohl pixel- als auch vektorbasierte Grafiken erstellen und diese anschließend nach XAML exportieren. Da Oberflächenelemente in der Windows Presentation Foundation in der Regel mit XAML definiert werden, könnte man die in Expression Design erstellten Objekte z.B. direkt in Expression Blend importieren und dort deren visuelles Zusammenspiel optimal definieren.
22
.NET 3.0
Abbildung 1.3: Expression Blend Beta 1
Abbildung 1.4: Microsoft Expression Design December 2006 CTP
23
2
Windows Presentation Foundation
Windows Presentation Foundation, kurz WPF, ist Microsofts neueste Technologie, welche die Brücke zwischen Entwickler und Designern bei der Erstellung von Applikationen mit grafischer Benutzeroberfläche schlagen soll. Windows Presentation Foundation (auch unter dem Codenamen Avalon bekannt) stellt ein neues grafisches Subsystem in Windows dar und bietet einen vereinheitlichten Zugang zu Benutzeroberflächen, 2D- und 3DGrafiken, Animationen und Dokumenten. Der Weg zu diesen teils neuen, teils verbesserten Steuerelementen und Komponenten wird von Microsoft mit dem .NET Framework 3.0 und der speziell für Windows Presentation Foundation vollkommen neu erschaffenen Klassenbibliothek geebnet. Waren bisher die Entwickler auch gleichzeitig als Designer tätig und somit zuständig für die Programmierung der grafischen Teile von Windows- oder auch Webanwendungen, sollen nun mit WPF auch reine Designer stark in den Entwicklungsprozess integriert werden. Den Designern werden mächtige, von Visual Studio 2005 losgelöste Tools und eine leicht zu erlernende deklarative Programmierung basierend auf XML zur Seite gestellt. Windows Presentation Foundation führt XAML (eXtensible Application Markup Language) als XML-basierende Sprache ein. Mit XAML besteht die Möglichkeit, Objekte aus dem Windows Presentation Foundation-Objektmodell sozusagen deklarativ zu instanzieren, Eigenschaften der
Kapitel 2
Objekte zu definieren und Objekte hierarchisch untereinander anzuordnen. Damit Sie als Leser ein erstes Gespür dafür bekommen, wie die gerade erläuterten Sachverhalte in der Praxis ihre Anwendung finden, wird das allseits bekannte und beliebte Hello World als erstes Beispiel dienen. Es soll Ihnen verdeutlichen, wie XAML genauso wie reiner Quellcode in einer .NET-Sprache in analoger Art eine grafische Benutzeroberfläche auf den Bildschirm zaubert. Auch wenn eine etwas ausführlichere Besprechung der von XAML angebotenen Möglichkeiten und Limitierungen erst zu einem späteren Zeitpunkt in diesem Abschnitt folgt, werden Sie feststellen, dass dieses noch fehlende Wissen keinerlei Auswirkung auf das Verständnis des ersten XAML-Beispiels haben wird.
2.1 Hello World oder besser – best of both worlds! Wie gerade versprochen wird das erste Beispiel zeigen, wie stark .NET-Quellcode als auch XAML mit dem Windows Presentation Foundation-Objektmodell in Relation stehen. Die Vorlagen für Windows Presentation Foundation-Projekte in Visual Studio 2005 und die Tools für das Generieren und Validieren des deklarativen XAML Codes werden Ihnen im Verlauf dieses Buches noch näher vorgestellt. Für den Auftakt sollen Sie nun mit dem Inhalt des Codes und dem Markup des kleinen Beispiels vertraut gemacht werden.
2.1.1 Ein erstes Fenster mit .NET-Code Hier sei vorausgeschickt, dass die Codebeispiele in diesem Buch sich auf die Sprache C# 2.0 stützen. Keine Angst, liebe Visual Basic 2005-favorisierenden Leser, auch Sie kommen nicht zu kurz, denn alle Codebeispiele finden Sie auf der beigelegten BuchCD auch in VB. Aber jetzt soll der Startschuss in die Programmierung mit WPF fallen: using System; using System.Windows; namespace HelloWorldCode { class Program { [STAThread] static void Main(string[] args) { Window w = new Window(); w.Title = "Hello World"; w.Width = 200; w.Height = 200; w.Show(); Application app = new Application();
26
Windows Presentation Foundation app.Run(w); } } } Listing 2.1: Ein erstes Fenster in WPF
Wenn Sie sich das Beispiel in Listing 2.1 einmal etwas genauer ansehen, werden Sie feststellen, dass es sehr intuitiv zu lesen ist, und Sie werden sich fragen, wo denn der große Unterschied zu bisheriger GUI-Entwicklung zu finden sein soll. Der einzige Unterschied, der bisher festgestellt werden kann, ist, dass anstelle eines Formulars in diesem Codestück ein Window-Objekt benutzt wird. Einfach gesagt, es ist vorerst nur eine andere Klassenbibliothek, die Sie benutzen. Vielleicht ist Ihnen dabei aufgefallen, dass mit der using-Direktive der Namensraum System.Windows eingebunden wird. Dies ist der Namensraum, der alle grundlegenden Klassen, Strukturen, Interfaces und Enumerationen im Kontext der Oberflächenprogrammierung mit sich bringt. Auch die beiden benutzten Klassen Window und Application liegen in diesem Namensraum. Lassen Sie uns den Code zum Einstieg etwas genauer unter die Lupe nehmen. In jeder Applikation, deren Oberfläche mit Windows Presentation Foundation-Objekten erstellt werden soll, muss der Main-Methode das Attribut [STAThread] voranstehen. Anderenfalls wird zur Laufzeit eine Exception ausgelöst. Dieses Attribut weist das Thread-Modell des aufrufenden Threads an, als Single-Threaded-Apartment zu agieren. In der Main-Methode wird ein Objekt vom Typ Window erstellt und dessen TitleEigenschaft auf die Zeichenkette Hello World gesetzt. Window ist die grundlegende Klasse für das Erstellen und Verwalten von Standard-Win-
dows-Applikationen in der Windows Presentation Foundation. Als Letztes muss in der Main-Methode noch ein Application-Objekt für den Start der Anwendung und das In-Gang-Setzen der Windows-Nachrichtenschleife instanziert werden. Die Application-Klasse als Singleton ist das Herzstück einer jeden Windows Presentation Foundation-Anwendung und kümmert sich sowohl um den Lebenszyklus als auch um die Ressourcenverwaltung einer Anwendung. Falls Sie nun das Jucken in den Fingern spüren und Sie dieses erste Beispiel zum Laufen bringen wollen, müssen Sie einige Vorarbeit leisten. Exkurs Für die Codebeispiele in diesem Buch dienen das .NET Framework 3.0, das .NET 3.0 Windows SDK und die .NET 3.0-Erweiterungen für Visual Studio 2005 als Grundlage für die Entwicklung. Eine Erläuterung der verwendeten Komponenten finden Sie auch in der Einleitung dieses Buches.
27
Kapitel 2
Haben Sie die Installation erfolgreich hinter sich gebracht, können Sie das gerade gesehene Beispiel einfach mit der Vorlage KONSOLENANWENDUNGEN in Visual Studio 2005 implementieren. Bitte vergessen Sie nicht, Verweise auf PresentationCore, PresentationFramework und WindowBase zu setzen. Jetzt nur noch (F5) drücken, und schon sollten Sie ein Fenster auf Ihrem Bildschirm mit dem Titel Hello World sehen. Da Sie eine KONSOLENANWENDUNG als Grundlage der Applikation gewählt haben, wird sich vor dem kleinen Fenster auch die Konsole zeigen. Keine Angst, die Konsole ist kein Musskriterium. In den weiteren Beispielen werden Sie die Vorlagen für .NET 3.0-Applikationen, zum Beispiel WINDOWS APPLICATION (WPF), als Grundstock Ihrer Anwendungen wählen. Um die Konsole für das aktuelle Beispiel zu unterdrücken, können Sie in den Eigenschaften des Projektes den AUSGABETYP auf WINDOWS-ANWENDUNG umstellen.
Abbildung 2.1: Das erste Fenster in WPF. Es wurde zwar programmatisch erstellt, kann aber auch mittels XAML definiert werden.
2.1.2 Ein Fenster mittels XAML In den einleitenden Sätzen zu Windows Presentation Foundation wird angepriesen, dass mit WPF nun auch Designer stark in den Entwicklungsprozess einer Benutzerschnittstelle miteinbezogen werden können. Die Zeilen in Listing 2.2 sollen deutlich machen, dass auch ohne objektorientierte Kenntnisse und ohne .NET-Quellcode ein Fenster beschrieben werden kann. Listing 2.2: Ein einfaches Fenster mittels XAML
Alleine die Applikation, die das Fenster aus Listing 2.2 anzeigen soll, ist hier noch nicht vorgesehen. Bei der Installation des .NET 3.0 Windows SDK wird ein Tool
28
Windows Presentation Foundation
namens XAMLPad mitgeliefert, in dem Sie nach der Eingabe von Markup das Ergebnis direkt betrachten können, ohne Code für das Starten der Anwendung implementieren zu müssen.
Abbildung 2.2: Das XAMLPad als Teil des .NET 3.0 SDK kann direkt die Eingabe von XAML validieren und das Ergebnis auch sichtbar machen.
Nicht nur Fenster, also das WPF-Pendant zu den Formularen in Windows-FormsProgrammierung, sondern auch Browser-Applikationen können mit WPF erstellt werden. Das Gegenstück zum Window-Element ist dabei das Page-Element. Da Sie nun einen ersten Einblick in die Entwicklung einer Applikationsoberfläche mittels XAML bekommen haben, sollen weitere Grundlagen von XAML näher dargestellt werden.
2.2 XAML (eXtensible Application Markup Language) Wie in den einführenden Worten zu WPF beschrieben, soll mit einer Markup-Sprache den programmiertechnisch vielleicht nicht so versierten Designern der Weg geebnet werden, mit geringem Lernaufwand die Entwickler dahingehend zu unterstützen, komplizierte Benutzerschnittstellen auszuarbeiten. Über das Instanzieren von WPFObjekten hinaus bietet XAML die Möglichkeit, das Layout von Elementen aller .NETBenutzerschnittstellen wie Textfelder, Buttons, Grafiken, Listenfeldern usw. zu definieren und erlaubt es sogar, Animationen zum Leben zu erwecken. Da XAML als Grammatik zur Basis XML spezifiziert ist, müssen XAML-Dokumente wohlgeformt sein und können intuitiver erstellt und verwertet werden als objektorientierter .NET-Code. In diesem Zusammenhang gibt es für jedes XAML-Element eine
29
Kapitel 2
zugehörige und vom Namen her exakt übereinstimmende .NET-Klasse aus der Klassenbibliothek der Windows Presentation Foundation. Obwohl alle XAML-Dokumente mittels reinem .NET-Code prozedural implementiert werden können, unterliegt das deklarative XAML einigen Einschränkungen gegenüber der Implementierung in einer .NET-Programmiersprache. So können zum Beispiel Eigenschaften, die in einer WPF Klasse ohne einen set-Zweig, also als read-only definiert wurden, mittels XAML nicht angesprochen werden. XAML ist nicht limitiert auf die Instanzierung von WPF-Objekten und die Zuweisung von Eigenschaften an diese Objekte. Auch Events und deren Behandlungsmethoden können in XAML als Attribute von Objekten im Markup eingehängt werden. Dennoch ist XAML reines Markup, was zur Folge hat, dass Sie zusätzlich zur Definition der Routinen für die Ereignisbehandlung in Attributen von XAML-Elementen in den meisten Fällen dennoch per .NET-Code die wirkliche Logik der Eventbehandlung realisieren müssen. Um die Implementierung der Logik vorzunehmen, haben Sie in .NET 3.0 zwei Möglichkeiten. Die erste, aber wohl auch die seltenere Methode ist, die Logik direkt also inline in der XML-Datei abzulegen. Diese Methode findet aber keinen regen Anklang, da sie der Idee der Trennung von logischer Schicht und Präsentationsschicht widerspricht. Die Funktionalität Ihrer Applikation sollte in einer sogenannten Code-BehindDatei abgelegt werden. Wenn Sie Sich schon einmal mit ASP.NET beschäftigt haben, werden Sie mit diesem Konzept vertraut sein. Sobald Sie die ersten Steuerelemente im .NET Framework 3.0 kennengelernt haben, kommen Sie auch in den Genuss der Eventbehandlung mit Code-Behind-Dateien. Jetzt soll es erst einmal darum gehen. wie Visual Studio 2005 Sie bei der Trennung von Layout und Logik unterstützt, denn bei der Installation der Visual Studio 2005-Erweiterungen für das .NET Framework 3.0 werden mehrere Vorlagen als Hilfestellung zur Entwicklung von WPF-Applikationen im Visual Studio 2005 installiert: Windows Application (WPF) Vorlage für eine XAML-Benutzeroberfläche mit .NET-Code-Behind-Datei XAML Browser Application (WPF) Vorlage für eine XAML-Web-Benutzeroberfläche Custom Control Library Vorlage für das Erstellen benutzerdefinierter Steuerelemente Um das Konzept der Interaktion von XAML Markup und .NET-Code zu beschreibenkann die Vorlage für eine WINDOWS APPLICATION (WPF) gewählt werden.
30
Windows Presentation Foundation
Abbildung 2.3: Mit den Visual Studio 2005-Erweiterungen zu WPF werden die Vorlagen zur Erstellung von grafischen Benutzeroberflächen in dem Dialog für neue Projekte zur Verfügung gestellt.
Abbildung 2.4: Aufteilung in Visual Studio 2005 nach Auswahl der Projektvorlage für eine Windows-Applikation
31
Kapitel 2
Hierbei werden automatisch Verweise auf wichtige Namensräume gesetzt und standardmäßig ein Fenster mit einem Grid per XAML definiert. Listing 2.3: XAML-Code aus der Vorlage für eine Windows-Applikation in VS 2005
Die Logik kann nun in der partiellen Klasse Window1, die sich in der Datei Window1.xaml.cs befindet, abgelegt werden. Wichtig ist nur, dass die Klasse der CodeBehind-Datei inklusive der Namensräume im Markup auch angegeben wird. x:Class=" HelloWorld.Window1"
Dabei wird auf das Class-Element aus dem Windows Presentation FoundationNamensraum über einen qualifizierten Namen als Stellvertreter für diesen Namensraum verwiesen. Zusätzlich zu der partiellen Klasse, in der Sie Ihre Applikationslogik ablegen und somit auf Events der Benutzeroberfläche reagieren können, wird vom Compiler eine weitere partielle Klasse generiert. Jedes Element, das Sie einer XAMLDatei hinzufügen, wird als .NET-Objekt zur Laufzeit instanziert. Der XAML-Compiler entscheidet, welche Klassenbibliothek zur Validierung und Kompilierung der XAML-Datei herangezogen werden soll, indem er die Namensraumdefinitionen in den XAML-Elementen auswertet. Dem XAML-Compiler reicht dazu die Angabe des Namensraums, in dem sich die zugehörigen WPF-Klassen wieder finden lassen: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2.2.1 Von XAML zu .NET-Code XAML-Dateien werden für gewöhnlich mit der Anwendung kompiliert anstatt zur Laufzeit geparst zu werden, obwohl Sie auch diesen Weg einschlagen können. Wenn Sie den Build-Prozess für ein .NET-Projekt anstoßen, dem Sie mit einer WPF-Vorlage und zugehörigem XAML Markup Leben eingehaucht haben, generiert der Compiler eine Klasse, die in einer Codedatei auch physisch abgelegt wird. In dieser Klasse ist die Implementierung der in XAML beschriebenen WPF-Objekte verankert, und somit ist das Ergebnis der Umsetzung von Markup in wirklichen Programmcode auch sehr einfach nachzuvollziehen. Falls diese Aussicht Ihr Interesse geweckt hat, dann machen Sie sich doch einfach auf die Suche nach dieser Datei. Sie werden sehr schnell fündig, wenn Sie im Projektver-
32
Windows Presentation Foundation
zeichnis in den \obj\Debug- oder \obj\Release-Ordner wechseln. Dort müssen Sie nach einer Datei Ausschau halten, die analog zur Code-Behind-Datei benannt ist, mit dem kleinen Unterschied, dass ein ‚.g’ zwischen dem Dateinamen und dem Postfix für die Programmiersprache eingefügt wurde. In dem obigen Beispiel wäre der Pfad dann \HelloWorld\obj\Debug\Window1.g.cs. In dieser Datei ist eine Klasse implementiert, die direkt von genau der WPF-Klasse abgeleitet ist, die mit dem Wurzelelement der beigeordneten XAML-Datei in Relation steht. Die gerade erwähnte partielle Klasse ist somit von System.Windows.Window abgeleitet. Noch mehr grafische Elemente, die unter dem Wurzelelement im weiteren Verlauf abgelegt wurden, werden mit der Methode System.Windows.Application.LoadComponent erstellt und unter der Wurzel in den Elementbaum eingehängt.
2.2.2 XAML und Ereignisbehandlung in Code Behind Ungeachtet der Tatsache, dass das Button-Element aus der Windows Presentation Foundation-Klassenbibliothek noch nicht besprochen wurde, sollen Sie jetzt nicht mehr länger auf die Folter gespannt werden. Sie werden erfahren, wie es von der Definition eines Eventhandlers als Attribut des XAML Button-Elements zur wirklichen Ausführung von Code kommt. Dazu kann einfach das Beispiel aus Listing 2.3 um ein Button-Element im Markup erweitert werden. Hello Listing 2.4: Button in XAML mit Angabe eines Methodennamens für die Eventbehandlung des Click-Events
Als aufmerksamen Leser wird Ihnen nicht entgangen sein, dass für diese einfache Anforderung auf das Grid-Element, das durch die WPF-Vorlage in Visual Studio 2005 automatisch eingefügt wurde, aus Gründen der leichteren Nachvollziehbarkeit verzichtet wurde. Eine ausführliche Betrachtung des Grid-Elements erfolgt in Abschnitt 2.3 für Layout und Container. Aber nun zurück zum eigentlichen Fokus, dem Button-Element und der Ereignisbehandlung in der Code-Behind-Datei. Das Button-Element ähnelt sehr stark dem ButtonObjekt, das Sie vielleicht aus der Windows Forms-Programmierung oder aus ASP.NET kennen.
33
Kapitel 2
Die Verknüpfung zwischen dem Markup für den Button und der Implementierung der Behandlung für das Click-Ereignis ist einfach die Definition des Namens der Methode im Click-Attribut des Button-Elements. Die Methode in der Code-Behind-Datei muss sowohl vom Namen her mit der Definition im Click-Attribut des Button-Elements übereinstimmen als auch die Signatur des Delegaten, der den Typ des Click-Ereignisses bestimmt, erfüllen. Auch dieser Button wird, wie im vorangegangenen Abschnitt besprochen, in der Klasse in HelloWorld.g.cs instanziert und in den Baum der Elemente eingefügt. In der Code-Behind-Datei muss dann nur noch die Routine für die Ereignisbehandlung abgelegt werden. void buttonHello_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Hallo Welt"); }
Damit Sie einen Eindruck gewinnen, wie die Code-Behind-Datei, die zum größten Teil von der WPF-Vorlage generiert wurde, aufgebaut ist, sehen Sie in Listing 2.5 den kompletten Inhalt der Datei inklusive Ereignisbehandlung. using using using using using using using using using using using
System; System.Collections.Generic; System.Text; System.Windows; System.Windows.Controls; System.Windows.Data; System.Windows.Documents; System.Windows.Input; System.Windows.Media; System.Windows.Media.Imaging; System.Windows.Shapes;
namespace HelloWorld { public partial class Window1 : System.Windows.Window { public Window1() { InitializeComponent(); } void buttonHello_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Hallo Welt"); } } } Listing 2.5: Code-Behind-Datei für das Beispiel zu Eventbehandlung
34
Windows Presentation Foundation
Im Konstruktor wird die InitializeComponent-Methode aufgerufen, was Ihnen vielleicht auch aus der Windows Forms-Programmierung bekannt sein dürfte. Die InitializeComponent-Methode selbst wird aber in unserem Beispiel nicht über den Designer erstellt, sondern durch die Umwandlung der XAML-Datei zu einer .NETKlasse in Window1.g.cs generiert.
2.3 Layout und Container Einer der wichtigsten Ansatzpunkte in der Entwicklung von einfachen grafischen Benutzeroberflächen sind das Layout und die Positionierung von Steuerelementen auf dem anzuzeigenden Fenster. Dabei sollen die Steuerelemente für den Benutzer lesbar, oftmals gruppiert und ebenfalls intuitiv verwendbar angeordnet sein. Windows Presentation Foundation bietet Ihnen eine Reihe von Möglichkeiten an, um Inhalt und Steuerelemente hierarchisch zu gruppieren und in anderen Steuerelementen zu positionieren. In diesem Kapitel sollen Sie die angebotenen Möglichkeiten der Steuerelemente Grid, DockPanel, StackPanel und Canvas als Container für weitere Steuerelemente und deren wichtigste Eigenschaften kennenlernen. Dazu zählen unter anderem die Eigenschaften zur Positionierung von beinhalteten Elementen wie der Einzug, Padding und weitere Füllelemente, die ein exaktes Positionieren innerhalb der Container erlauben. Nicht nur die akkurate Positionierung, sondern auch die vielleicht zu erwartenden unterschiedlichen Auflösungen der Endbenutzersysteme stellen den Entwickler der visuellen Komponenten in traditioneller GUI-Programmierung vor eine große Herausforderung. XAML und Windows Presentation Foundation nehmen dieses Problem durch eine Anpassung der Größe der Elemente innerhalb des zugehörigen Containers in Angriff. Wenn Sie keine weiteren Angaben machen, füllen Elemente (außer im Canvas) immer den Container aus, in dem sie definiert wurden, und verändern ihre Größe mit dem Container. Um die Idee der Definition von Layout so einfach wie möglich für Sie zu gestalten und um nicht auf weitere Steuerelemente ohne eine ausführliche Besprechung vorzugreifen, werden für die Erklärung des Layouts nur Buttons in die Container gesteckt. In Abschnitt 2.4 werden die wichtigsten Steuerelemente besprochen und auch deren besondere Positionierungsmöglichkeiten in Containern gezeigt.
2.3.1 Das Grid-Steuerelement Das Grid-Steuerelement ist hervorragend für eine relative Positionierung von Elementen geeignet. Das Grid verhält sich vergleichbar einer HTML-Tabelle und besteht aus Zeilen und Spalten, in denen einzelne Zellen definiert sind. In diesen Zellen können dann die Inhalte des Grids untergebracht werden.
35
Kapitel 2
Standardmäßig besteht das Grid aus genau einer Zelle, und es müssen bei Bedarf weitere Zellen hinzugefügt werden. Wenn Sie sich an das erste WPF-Projekt in diesem Buch in Abschnitt 2.2 zurück entsinnen, wird Ihnen auffallen, dass die Standardvorlage für WPF-Projekte in Visual Studio automatisch ein sehr einfaches Grid mit anlegt. Um ein Grid mit Zeilen und Spalten zu definieren, müssen die einzelnen Zeilen und Spalten in ihren zugehörigen Aufzählungen definiert werden. Listing 2.6: Ein einfaches Grid mit Definitionen für Zeilen und Spalten
Ein Grid hat je eine Auflistung für die Definition von Zeilen und Spalten, die das grundsätzliche Layout und die Anzahl der Zellen im Grid beeinflussen. Das in XAML (Listing 2.6) generierte Grid beinhaltet dann neun Zellen in drei Spalten und drei Zeilen. Das Attribut ShowGridLines im Grid-Element bestimmt, ob zur Laufzeit die einzelnen Linien zwischen Spalten und Zeilen angezeigt werden sollen. Um in eine der Zellen Inhalt einzufügen, gilt es nun, innerhalb des Grid-Elements weitere Elemente zu definieren und als Attribute die Zeile und Spalte des zugehörigen Grids anzugeben. Reihe 2
Listing 2.7: Positionierung von Elementen im Grid
Die Diagonale des Grids ist nun mit Inhalt ausgefüllt und kann in Abbildung 2.5 betrachtet werden. In den einführenden Worten zu diesem Abschnitt wurde schon erwähnt, dass nicht nur die Position innerhalb eines Panels, sondern auch der Einzug relativ zur Position in einem Grid oder Panel dazu nützlich sein kann, ein Steuerelement auszurichten. Der Einzug eines Steuerelementes richtet sich immer nach dem umschließenden Element.
36
Windows Presentation Foundation
Abbildung 2.5: In einem Grid werden Elemente in Zeilen und Spalten angeordnet.
Im aktuellen Beispiel ist ein Button als Kind eines Grids direkt einer Zelle im Grid zugeordnet. Wird nun der Einzug eines Buttons verändert, so bezieht er sich direkt auf diese Zelle. Der Einzug wir durch das Margin-Property, das vom Typ Thickness ist, festgesetzt. Thickness ist eine Struktur, die es durch zwei überladene Konstruktoren erlaubt, den Einzug eines Elementes mit der Angabe von nur einem Wert gleichmäßig um das Element herum oder für jede Seite einzeln zu definieren. Soll zum Beispiel der Button in allen vier Himmelsrichtungen denselben Einzug erhalten, würde folgendes Attribut im Button-Element ausreichen:
Die Thickness des Einzugs wird somit auf 5 gestellt! Frage & Antwort Aber 5 was? Pixel, Zentimeter oder Inches? Die Antwort ist in der Klassenbibliothek zu finden und lautet wie folgt: Wenn keine weiteren Angaben durch einen qualifizierten Double-Wert (Double-Zahl gefolgt von passender Einheit) gemacht werden, so ist die Einheit standardmäßig px = Pixel (1/96 Inch pro Einheit, 1 Inch = 96 Pixel). Weitere Einheiten können bei Bedarf angegeben werden. Zentimeter als Einheit: 1 cm = (96/2.54) Pixel Point als Einheit: 1 pt = (96/72) Pixel
Soll der Einzug assymetrisch sein, kann der zweite Konstruktor der Thickness-Struktur herangezogen werden:
37
Kapitel 2
Die Wertangaben beziehen sich dabei in ihrer Reihenfolge auf den Einzug von links, von oben, von rechts und von unten. Die mittlere Zelle nimmt zur Laufzeit mit dem zugehörigen Button und dem asymmetrischen Einzug die Form an, die in Abbildung 2.6 zu sehen ist.
Abbildung 2.6: Eine der Zellen im Grid, die mit einem Einzug ausgestattet wurde
Natürlich kann dieses Erscheinungsbild auch wieder programmatisch erstellt oder verändert werden, was in Listing 2.8 nachvollzogen werden kann. Grid gridDiagonal = new Grid(); gridDiagonal.ShowGridLines = true; //Definition der Spalten ColumnDefinition colDef0 = new ColumnDefinition(); ColumnDefinition colDef1 = new ColumnDefinition(); ColumnDefinition colDef2 = new ColumnDefinition(); gridDiagonal.ColumnDefinitions.Add(colDef0); gridDiagonal.ColumnDefinitions.Add(colDef1); gridDiagonal.ColumnDefinitions.Add(colDef2); //Definition der Zeilen RowDefinition rowDef0 = new RowDefinition(); RowDefinition rowDef1 = new RowDefinition(); RowDefinition rowDef2 = new RowDefinition(); gridDiagonal.RowDefinitions.Add(rowDef0); gridDiagonal.RowDefinitions.Add(rowDef1); gridDiagonal.RowDefinitions.Add(rowDef2); Button buttonZ0R0 = new Button(); buttonZ0R0.Content = "Zeile 0, Spalte 0"; Grid.SetRow(buttonZ0R0, 0); Grid.SetColumn(buttonZ0R0, 0); buttonZ0R0.Margin = new Thickness(5); gridDiagonal.Children.Add(buttonZ0R0); Button buttonZ1R1 = new Button(); buttonZ1R1.Content = "Zeile 1, Spalte 1"; Grid.SetRow(buttonZ1R1, 1); Grid.SetColumn(buttonZ1R1, 1); buttonZ1R1.Margin = new Thickness(10, 5, 10, 5); gridDiagonal.Children.Add(buttonZ1R1);
38
Windows Presentation Foundation Button buttonZ2R2 = new Button(); buttonZ2R2.Content = "Zeile 2, Spalte 2"; Grid.SetRow(buttonZ2R2, 2); Grid.SetColumn(buttonZ2R2, 2); gridDiagonal.Children.Add(buttonZ2R2); Listing 2.8: Ein Grid kann natürlich auch programmatisch erstellt und dessen Inhalt definiert und angeordnet werden.
Wenn Sie das Markup in XAML und den C#-Quellcode miteinander vergleichen, ist es ein Einfaches zu erkennen, dass die Unterschiede nur in der Strukturierung der Inhalte zu finden sind. Um einer Kollektion von Definitionen für Zeilen oder Spalten eine tatsächliche Definition hinzuzufügen, reicht es, im Markup das Definition-Element einfach unter der zugehörigen Auflistung zu verschachteln. In .NET-Quellcode müssen Sie für das gleiche Ergebnis die Add-Funktionalität bemühen. Der Einzeiler in XAML, um den Button zu instanzieren, die Zelle im Grid anzugeben und den Inhalt des Buttons festzulegen, benötigt als C#- Code etwas mehr Programmieraufwand: Button buttonZ0R0 = new Button(); buttonZ0R0.Content = "Zeile 0, Spalte 0"; Grid.SetRow(buttonZ0R0, 0); Grid.SetColumn(buttonZ0R0, 0); buttonZ0R0.Margin = new Thickness(5); gridDiagonal.Children.Add(buttonZ0R0);
Auch wenn im einfachen Überfliegen des Codes oder Markups auf den ersten Blick alles klar zu sein scheint, stellt sich dem geschulten Auge dennoch die Frage, wie die Zeile Grid.SetColumn(buttonZ0R0, 0);
für das aktuelle Grid namens gridDiagonal das Element namens buttonZ0R0 als Inhalt der linken oberen Zelle definieren kann. Um diesen Sachverhalt ein wenig näher zu betrachten, muss ein kleiner Ausflug zu den sogenannten Dependency Properties gemacht werden. Auch wenn dieses Konzept für sehr viele Elemente in WPF seine Verwendung findet, soll es dennoch in diesem Abschnitt abgehandelt werden, da ein Dependency Property hier zum ersten Mal zur Geltung kommt.
2.3.2 Dependency Properties In der Windows Presentation Foundation werden auf die Eigenschaften von Elementen in altbewährter objektorientierter Manier über Properties zugegriffen. Zusätzlich zu dem Kontext einer gewöhnlichen Eigenschaft können in WPF Properties auch als abhängige Eigenschaften (sogenannte Dependency Properties) auftreten und somit das bekannte System der Properties erweitern.
39
Kapitel 2
Das Ziel von abhängigen Eigenschaften ist es, den Zustand einer Eigenschaft in Abhängigkeit (daher der Name Dependency Property) anderer Zustände der Applikation oder deren Benutzeroberfläche festzulegen. Abhängige Eigenschaften können von anderen Elementen aus WPF wie Steuerelementen, Styles, Themes, Datenbindung, Animationen und Ressourcen gesteuert werden. Darüber hinaus können die Werte einer abhängigen Eigenschaft auch durch die Hierarchie im Element-Baum festgesetzt werden. In dem Beispiel aus Abschnitt 2.3.1 finden sich die drei Buttons als direkte Kindelemente in einer Beziehung zu dem übergeordneten Grid wieder, das im Baum der Elemente direkt unter dem Window-Element liegt. Würde das Window-Element ein FontSize als Attribut definieren, würden die FontSize-Eigenschaften der untergeordneten Controls in Abhängigkeit zum Window-Element automatisch determiniert werden. Die einzige Ausnahme besteht darin, dass ein untergeordnetes Element die betroffene Eigenschaft schon selbst in den Attributen definiert. Die meisten abhängigen Eigenschaften sind so implementiert, dass sie als Schlüssel-Werte-Paar abgelegt werden. In Listing 2.9 sehen Sie, wie die Implementierung einer abhängigen Eigenschaft in einem WPF-Element aussehen könnte. public static DependencyProperty dependencyProperty1 = DependencyProperty.Register("Eigenschaft1", typeof(Int32), typeof(Button)); public static DependencyProperty dependencyProperty2 = DependencyProperty.Register("Eigenschaft2", typeof(String),typeof(Button)); System.Collections.Hashtable _ht = new System.Collections.Hashtable(); public object GetValue(DependencyProperty dp) { return _ht[dp]; } public void SetValue(DependencyProperty dp, object value) { _ht.Add(dp, value); } Listing 2.9: Vorlage einer Dependency Property
Die Dependency Property ist also stellvertretend für einen Schlüssel zu einem Objekt, das gesucht wird. Da die abhängige Eigenschaft auch noch als statische Eigenschaft definiert ist, kann sie auch von anderen Elementen in XAML in Attributdefinitionen verwendet werden. Handelt es sich dabei um statische abhängige Eigenschaften wird auch von einer Attached Dependency Property gesprochen. Mit diesem Wissen als
40
Windows Presentation Foundation
Hintergrund können wir jetzt auch den Rückschluss zu der Zeile finden, welche die Betrachtung der Dependency Properties erst nötig gemacht hat: Grid.SetColumn(buttonZ0R0, 0);
Die SetColumn-Methode setzt den Wert, der angibt, in welcher Spalte ein Kindelement des Grids erscheinen soll. Etwas deutlicher wird die Auswirkung einer angefügten abhängigen Eigenschaft im Markup: Zeile 1 Reihe 1
Die Attribute Grid.Row und Grid.Column existieren eigentlich nicht als Eigenschaften des Button-Elementes, sondern setzen über die Setter-Methoden der abhängigen Eigenschaft im Grid die Stelle, an welcher der Button als Kindelement angezeigt werden soll.
2.3.3 Das StackPanel-Steuerelement Mit dem StackPanel wird es sehr einfach möglich, Steuerelemente horizontal oder vertikal anzuordnen, wobei der Namensteil Stack (zu Deutsch: Stapel) schon verrät, dass die Elemente, die im StackPanel beinhaltet sind, der Reihenfolge ihres Hinzufügens nach gestapelt werden. Das StackPanel bringt wie alle anderen Elemente eine Vielzahl von Eigenschaften mit sich, aus denen Orientation und FlowDirection herausgepickt werden sollen. Mit der Orientierung lässt sich einstellen, ob die im StackPanel hinzugefügten Elemente vertikal, also übereinander, oder horizontal, also nebeneinander, angeordnet werden sollen. Dieser Abschnitt soll in Listing 2.10 ein erstes StackPanel und in einer späteren Betrachtung dessen wichtigste Eigenschaften kurz umreißen. Schwarz Rot Gold Listing 2.10: Ein StackPanel zur Anordnung von Elementen mit drei beinhalteten Buttons
Das StackPanel ist eingebettet in einem Window-Element in der Abbildung 2.7 zu bewundern. Wird bei einem StackPanel keine Orientierung angegeben, so werden die Elemente in der Reihenfolge ihrer Definition im Markup von oben nach unten angeordnet. Wollen Sie, dass die Elemente von links nach rechts angeordnet werden, müssen Sie lediglich die Orientierung als horizontal festlegen.
41
Kapitel 2
Abbildung 2.7: Das StackPanel und die drei farbigen Buttons. Kann es sein, dass gerade die WM in Deutschland war?
Aber aufgepasst, das Ergebnis wird Sie ein wenig überraschen. Denn bei der standardmäßigen vertikalen Orientierung wird das Window von dem StackPanel-Element komplett ausgefüllt. Die Höhe der Buttons und die Titelleiste des Fensters addiert ergeben genau die Höhe des gesamten Fensters. Das StackPanel versucht, wenn nicht anders angegeben, das komplette Fenster auszufüllen, wobei die Buttons wiederum versuchen, das gesamte Panel auszufüllen. Dreht sich die Orientierung nun auf horizontal, wird die Höhe der Buttons von deren Height-Eigenschaft bestimmt. Die Breite der Buttons wird durch deren Inhalt terminiert. Dies bedeutet also, dass die Orientierung nicht nur die Anordnung, sondern auch das Aussehen der Elemente beeinflusst. Wenn Sie die Reihenfolge der horizontal angeordneten Elemente auch noch verändern möchten, dann bedienen Sie sich noch der FlowDirection.
Der .NET-Quellcode für ein StackPanel in Listing 2.11 sieht dieses Mal auch sehr übersichtlich aus. StackPanel stackPanelVertikal = new StackPanel(); Button buttonSchwarz = new Button(); buttonSchwarz.Background = Brushes.Black; buttonSchwarz.Height = 50; buttonSchwarz.Foreground = Brushes.White; buttonSchwarz.Content = "Schwarz"; Button buttonRot = new Button(); buttonRot.Background = Brushes.Red; buttonRot.Height = 50; buttonRot.Content = "Rot"; Button buttonGold = new Button(); buttonGold.Background = Brushes.Gold; buttonGold.Height = 50; buttonGold.Content = "Gold";
42
Windows Presentation Foundation stackPanelVertikal.Children.Add(buttonSchwarz); stackPanelVertikal.Children.Add(buttonRot); stackPanelVertikal.Children.Add(buttonGold); stackPanelVertikal.Orientation = Orientation.Horizontal; stackPanelVertikal.FlowDirection = FlowDirection.RightToLeft; this.Content = stackPanelVertikal; Listing 2.11: Ein StackPanel in seiner programmatischen Struktur im C#-Quellcode
Ihnen wird aufgefallen sein, dass die Farbgebung für die Button-Elemente nicht wie in Windows Forms durch die Struktur Color definiert wird. In der Windows Presentation Foundation sind Brushes für die farbliche Gestaltung zuständig, die in einem späteren Abschnitt (2.9.2) genauer besprochen werden. Zur Laufzeit wird die Oberfläche durch die veränderte Orientierung und Laufrichtung der Elemente so aussehen, wie es in Abbildung 2.8 nachzuvollziehen ist.
Abbildung 2.8: Ausgabe des StackPanels bei veränderter Orientierung und Flussrichtung
2.3.4 Das DockPanel-Steuerelement Das DockPanel Element hat große Ähnlichkeit zum gerade eben vorgestellten StackPanel, mit dem kleinen Unterschied, dass der Standard der Orientierung des DockPanels als horizontal definiert ist. Mit dem DockPanel kann sozusagen ein Anheften von untergeordneten Elementen an einer der Seiten (Left, Right, Bottom, Top) des DockPanels erzwungen werden. Dieses Anheften bedeutet, dass ein Element, dessen Docking auf eine Seite festgelegt wurde, immer versucht, mittig zu dieser Seite im übergeordneten Element positioniert zu bleiben. Dazu steht wieder eine Attached Dependency Property des DockPanels namens DockPanel.Dock zur Verfügung. Diese Eigenschaft muss auf eine der vier Seiten abgestimmt werden. Das Beispiel in Listing 2.12 soll deutlich machen, wie in einem DockPanel Kindelemente hinzugefügt und mit verschiedenen Dockings ausgestattet werden können.
43
Kapitel 2 Oben Links Rechts Unten Listing 2.12: Ein DockPanel, an dessen Seiten je ein Button angedockt ist
Das ergibt innerhalb eines Fensters die grafische Darstellung, die in Abbildung 2.9 zu sehen ist.
Abbildung 2.9: Ein einfaches DockPanel an dessen Seiten je ein Button gedockt ist
Bei dem Beispiel aus Listing 2.12 ähnelt die Anordnung der Elemente sehr einem gleichschenkligen Dreieck, dessen Basis von einer gedachten Gerade durch die drei unteren Buttons festgelegt ist. Wenn das Fenster in seiner Größe verändert wird, bleiben die Elemente passend zu ihrer Beschriftung mittig zu der Seite im DockPanel, an die sie angeheftet wurden. In diesem Beispiel sind vier Elemente an die vier unterschiedlichen Seiten des DockPanels geheftet worden. An eine Seite können auch mehrere Elemente angelegt werden. Auch die Reihenfolge des Hinzufügens der Elemente in ein DockPanel kann von entscheidender Rolle sein. Das soll das Beispiel in Listing 2.13 deutlich machen. Element 1 links gedockt Element 2 rechts gedockt Element 3 oben gedockt Element 4 unten gedockt Element 5 oben gedockt Listing 2.13: Ein DockPanel, in dem mehrere Elemente an eine Seite geheftet sind
44
Windows Presentation Foundation
Dieses DockPanel ordnet die beinhalteten Elemente sowohl nach der Reihenfolge, in der sie hinzugefügt wurden, als auch nach der Art des Dockings an.
Abbildung 2.10: Die Angabe des Dockings verändert das Layout der Elemente in einem DockPanel.
Die Verwendung des DockPanels kann auch programmatisch abgebildet werden, und wieder finden eine abhängige Eigenschaft und die Children-Kollektion des Panels ihren Einsatz. Das Listing 2.14 soll darüber Aufschluss geben. DockPanel dockPanelDocking = new DockPanel(); Button buttonElement1 = new Button(); buttonElement1.Content = "Element 1 links gedockt"; DockPanel.SetDock(buttonElement1, Dock.Left); dockPanelDocking.Children.Add(buttonElement1); Button buttonElement2 = new Button(); buttonElement2.Content = "Element 2 rechts gedockt"; DockPanel.SetDock(buttonElement2, Dock.Right); dockPanelDocking.Children.Add(buttonElement2); Button buttonElement3 = new Button(); buttonElement3.Content = "Element 3 oben gedockt"; DockPanel.SetDock(buttonElement3, Dock.Top); dockPanelDocking.Children.Add(buttonElement3); Button buttonElement4 = new Button(); buttonElement4.Content = "Element 4 unten gedockt"; DockPanel.SetDock(buttonElement4, Dock.Bottom); dockPanelDocking.Children.Add(buttonElement4); Button buttonElement5 = new Button(); buttonElement5.Content = "Element 5 oben gedockt";
45
Kapitel 2 DockPanel.SetDock(buttonElement5, Dock.Top); dockPanelDocking.Children.Add(buttonElement5); this.Content = dockPanelDocking; Listing 2.14: In .NET-Quellcode kommt es bei einem DockPanel für das Layout sowohl auf die Reihenfolge beim Hinzufügen der Elemente als auch auf das Docking der Elemente an.
In den vorangegangenen Containern zur Aufnahme von Elementen ist eine relative Positionierung durch eine Zelle im Grid, eine Orientierung im StackPanel und das Docking im DockPanel vorgeschrieben. Der Vorteil liegt darin, dass die Anordnung der Elemente bei einer Veränderung der Größe des Elternelements (Window, Page) oder eine Änderung der Auflösung konsistent bleiben kann. Eine direkte Kontrolle der Positionierung von Kindelementen über X- und Y-Koordinaten ist in diesen Containern nicht vorgesehen. Aber keine Angst, auch eine absolute Positionierung von Elementen in einem übergeordneten Container ist in der Windows Presentation Foundation nicht vergessen worden und kann mit dem Canvas-Element verwirklicht werden.
2.3.5 Das Canvas-Steuerelement Das Steuerelement Canvas (was zu Deutsch so viel wie Leinwand oder Gemälde bedeutet) erlaubt die Positionierung von Kindelementen mit absoluten Werten. Werden keine Angaben über die Positionierung gemacht, werden die hinzugefügten Elemente im Canvas einfach an der linken oberen Ecke des Canvas übereinandergestapelt. Übereinander besagt dabei nicht, dass das Standardverhalten des Canvas dem des StackPanels mit einer vertikalen Orientierung ähnelt. Übereinander bedeutet, dass die Elemente sich in Ebenen übereinanderlegen und sich gegenseitig verdecken. Sie sollten also den untergeordneten Elementen eine X- und/oder Y-Koordinate zuweisen. Dazu dienen wieder einmal die abhängigen Eigenschaften aus dem Canvas-Element. Mit Canvas.Left, Canvas.Right, Canvas.Top und Canvas.Bottom können Sie Elemente im Canvas positionieren. Auch wenn hier vier Koordinaten angegeben werden können, werden höchstens zwei davon, nämlich die ersten beiden angegebenen Koordinaten ausgewertet. Die absolute Positionierung von Elementen ist dennoch wieder relativ zur linken oberen Ecke des Canvas mit den Koordinaten 0,0. Wenn Sie ein Canvas in ihrer grafischen Benutzeroberfläche verwenden, dann werden Sie überrascht sein, dass ein Canvas-Element per Default keine Höhe und Breite besitzt, außer diese Werte werden durch das übergeordnete Control durch Resizing automatisch bestimmt.
46
Windows Presentation Foundation
> >
>
HINWEIS
Sowohl die Berechnungen für Left und Top als auch Right und Bottom beginnen bei den X- und Y-Werten 0,0. Das heißt, alle vier Ecken des Canvas liegen in 0,0. Daran ändert auch das Hinzufügen eines Kindelementes mit absoluten Koordinaten nichts, das Element wird dann einfach außerhalb des Canvas angezeigt. Das passiert im Übrigen immer, wenn Elemente über die Größe des Canvas hinauswachsen oder mit Koordinaten außerhalb des Canvas positioniert werden.
Ein erster Einblick soll in Listing 2.15 zwei Button-Elemente beinhalten, die in einem Canvas abgelegt werden, dessen Größe automatisch durch das übergeordnete Element, in diesem Fall ein Window, bestimmt ist. X1=30 Y1=30 Links Oben X2=30 Y2=130 Rechts Unten Listing 2.15: Ein erstes Canvas-Steuerelement, in dem weitere Elemente absolut positioniert werden
Und wie intuitiv wohl erwartet, präsentieren sich die beiden Buttons in einem Canvas (das seinerseits nicht sichtbar ist) jeweils in den beiden designierten Ecken (siehe Abbildung 2.11).
Abbildung 2.11: Ein Canvas als Container für zwei Button-Steuerelemente
Die Positionierung der Button-Elemente im Canvas aus Abbildung 2.11 geschieht aber nur auf diese Art und Weise, da Canvas durch das Fenster und dessen Größe in Breite und Höhe automatisch angepasst wird. Kindelemente eines Canvas-Elements werden
47
Kapitel 2
nicht automatisch angepasst, sie bekommen immer den Platz, den sie anfordern, selbst wenn sie über die Grenzen des Canvas hinauswachsen würden. Auch vertikales und horizontales Ausrichten über das Alignment haben darauf keinen Einfluss. Nun soll der Fakt, dass keine Kindelemente im Canvas automatisch in der Größe angepasst werden, noch einmal etwas näher betrachtet werden. Das Canvas aus dem Beispiel in Listing 2.15 wird einfach in einem weiteren Canvas untergeordnet. Was wird passieren? Die obere linke Ecke des überordneten Canvas-Elements und seines Kindelementes befinden sich bei den Koordinaten 0,0. Die anderen Ecken unterscheiden sich nun aber darin, dass das übergeordnete Canvas mit dem Window in seiner Größe angepasst wurde und somit die rechte untere Ecke nicht mehr in den Koordinaten 0,0 liegt. Das untergeordnete Canvas hingegen hat all seine Ecken in den Koordinaten 0,0, da durch das übergeordnete Canvas kein Resizing stattfindet. Somit verschiebt sich der Button mit den Koordinaten Canvas.Right="30" und Canvas.Bottom="30"aus den Grenzen des Fensters und ist nicht mehr sichtbar. Eine kleine Änderung noch an den Koordinaten des Buttons ins Negative würde den Button wieder auf das Fenster bringen. X1=30 Y1=30 Links Oben X2=10 Y2=10 Rechts Unten Listing 2.16: Ein Canvas in einem zweiten Canvas verschachtelt als Anzeige dafür, dass die Koordinaten aller Ecken des verschachtelten Canvas in 0,0 stehen
Der zweite Button liegt außerhalb der Grenzen des Fensters und wird somit nicht mit auf das Fenster gezeichnet. In der Einleitung haben Sie schon gesehen, dass durch die Wahl der Koordinaten Elemente im Canvas auch übereinanderliegen können. Um für diese Überlagerungen die Reihenfolge zu definieren, können Sie für die Kindelemente des Canvas, die sich überschneiden, den ZIndex aus der Klasse Panel angeben, wobei das Element mit dem höchsten Index auch am weitesten vorne angezeigt wird. Oberer Button Unterer Button Listing 2.17: Der ZIndex gibt an, in welcher Reihenfolge sich überlagernde Elemente im Canvas angeordnet sind.
48
Windows Presentation Foundation
Also gut aufgepasst beim Einsatz des Canvas-Elementes, denn viele Faktoren beeinflussen, wo und ob Kindelemente überhaupt zur Laufzeit sichtbar sind. Eingebettet in ein Window kann das Ganze auch wieder mit .NET-Quellcode auf dem Bildschirm präsentiert werden. Canvas canvasDefault = new Canvas(); Button buttonX1Y1 = new Button(); buttonX1Y1.Content = "X1=30 Y1=30 Links Oben"; Canvas.SetLeft(buttonX1Y1, 30); Canvas.SetTop(buttonX1Y1, 30); canvasDefault.Children.Add(buttonX1Y1); Button buttonX2Y2 = new Button(); buttonX2Y2.Content = "X2=10 Y2=10 Rechts Unten"; Canvas.SetRight(buttonX2Y2, 30); Canvas.SetBottom(buttonX2Y2, 30); canvasDefault.Children.Add(buttonX2Y2); Listing 2.18: Ein einfaches Canvas in .NET-Quellcode
Nun, da Sie die wichtigsten Möglichkeiten für die Positionierung von Elementen in Containern kennengelernt haben, wird es jetzt höchste Zeit, dass Sie einen Überblick über weitere Steuerelemente bekommen, damit auch das Positionieren noch mehr Spaß macht.
2.4 Steuerelemente – ein Überblick Windows Presentation Foundation bringt eine Vielzahl von bekannten, verbesserten und auch neuen Steuerelementen mit sich. Hierbei soll der Fokus zuerst einmal auf die wichtigsten Basisklassen gerichtet werden, um herauszuarbeiten, woher die einzelnen Steuerelemente in einer Ableitungshierarchie ihre Gemeinsamkeiten nehmen, aber auch wie sie sich in ihren Spezialisierungen unterscheiden. Wenn diese Grundlage einmal geschaffen ist, werden Sie einige ausgewählte Elemente aus WPF im Einsatz innerhalb von Beispielen näher betrachten. Leider würde eine detaillierte Darstellung aller angebotenen Elemente den Rahmen dieses Kapitels sprengen, darum wird eine kleine, aber feine Auswahl von Elementen genauer unter die Lupe genommen.
2.4.1 Elemente und deren Basisklassen Der wichtigste Namensraum in einer Erläuterung der Zusammenhänge und Abhängigkeiten von Elementen in WPF ist der Namensraum System.Windows.Controls. Die zentrale Basisklasse ist die Klasse UIElement, in der die grundlegendsten Eigenschaf-
49
Kapitel 2
ten und das Aussehen von Steuerelementen definiert sind. Dazu gehören die Deckkraft (Opacity) oder ob ein Element auch wirklich auf der Benutzeroberfläche angezeigt wird. Etwas spezieller in die Eigenschaften für die Präsentation steigt die von UIElement abgeleitete Klasse FrameworkElement ein. In ihr finden sich unter anderem Eigenschaften wie Höhe (Height), Breite (Width), Einzug (Margin), Name und Style wieder. Von dieser Klasse leiten sich Steuerelemente wie Image und TextBlock ab. Über eine weitere Klasse, die Klasse Panel, beziehen auch alle Panels wie das StackPanel einen Teil ihrer Eigenschaften aus FrameworkElement. Als Basisklasse für alle
Steuerelemente, die tatsächlich in Interaktion mit dem Benutzer treten, wie Button, ListBox oder TextBox, steht die Klasse Control zur Verfügung. Aus Control erben Steuerelemente eine Vielzahl Eigenschaften, zu denen Hintergrund (Background) und Schriftgröße (FontSize) zählen. Grundlegende Events, die in der Interaktion zwischen Benutzer und Steuerelement auftreten können, sind ebenfalls in der Klasse Control verankert. Dazu gehören Mausbewegungen über dem Control (z.B. MouseEnter) und die Eingabe mit der Tastatur (z.B. KeyDown). Der Rahmen des Buches erlaubt es nicht, die Komplexität der Klasse Control auch nur annähernd in Worte zu fassen, darum sei hier ein Verweis auf die Dokumentation der Klassenbibliothek, die mit dem SDK für das .NET Framework 3.0 ausgeliefert wird, erlaubt. Wenn Sie zum jetzigen Zeitpunkt in der besprochenen Hierarchie der WPF-Klassen die Content-Eigenschaft vermissen, dann hat Sie Ihr Gefühl in diesem Punkt nicht im Stich gelassen. Nicht jedes Steuerelement besitzt die Content-Eigenschaft. Nur Elemente, die als Oberklasse die Klasse ContentControl beerben, besitzen auch diese mächtige Eigenschaft. Content ist nicht nur mit seiner einfachen Übersetzung als Inhalt vollständig beschrieben, und nachdem einige Steuerelemente in diesem Abschnitt besprochen wurden, soll auf diese Eigenschaft etwas näher eingegangen werden.
2.4.2 Button, TextBox und Label Seit jeher darf auf einer Benutzeroberfläche dieses Trio aus Button, Label und TextBox nicht fehlen. Auf einer Loginseite gilt da schon fast das Motto: Einer für alle und alle für einen. Da diese Steuerelemente keiner großen Besprechung bedürfen, sollen sie einfach kurz an einem Beispiel gezeigt werden. Der Button und das Label unterscheiden sich zu den beiden Pendants in der Windows Forms-Programmierung vorerst nur dadurch, dass sie keine Texteigenschaft mehr definieren. Ihr Inhalt wird vielmehr durch die Content-Eigenschaft, geerbt von der Klasse ContentControl, bestimmt. Demgegenüber steht die TextBox, die direkt von FrameworkElement abgeleitet ist und deren Inhalt durch die Texteigenschaft definiert wird.
50
Windows Presentation Foundation Listing 2.19: Das Trio Label, Button und TextBox in einem Grid
Das Trio wurde in ein Grid, dessen Definition aus Gründen der Übersichtlichkeit in Listing 2.19 ausgelassen wurde, eingebunden. In Abbildung 2.12 ist zu sehen, wie sich das Zusammenspiel der drei Elementtypen zur Laufzeit gestaltet.
Abbildung 2.12: Eine Anmeldemaske in WPF mit den Elementen Button, Label und TextBox im Einsatz
An dieser Stelle muss nur noch ein kurzer Blick auf die PasswordBox geworfen werden. Die PasswordBox ist eine TextBox, deren Inhalt bei der Eingabe dem Benutzer nicht gezeigt wird. Was der Benutzer pro eingegebenes Zeichen dann wirklich zu sehen bekommt, hängt von dem Attribut PasswordChar ab. Als Standard werden einfach die runden Punkte (siehe Abbildung 2.12) angezeigt. Sie können aber auch andere Literale für die Maske der PasswordBox wählen.
2.4.3 Das ListBox-Steuerelement Das ListBox-Steuerelement soll in diesem Abschnitt stellvertretend für den Typ ItemsControls, speziell für Elemente vom Typ Selector, genauer betrachtet werden. ItemsControls beinhalten, wie der Name schon sagt, untergeordnete Items.
51
Kapitel 2
Zu den wichtigsten Steuerelementen vom Typ ItemsControl zählen die Menüs (Menu, MenuBase, ContextMenu), die Auswahlfelder (ComboBox; ListBox, ListView) sowie TreeView und StatusBar. Ihr Inhalt wird durch das Hinzufügen von Elementen in die Items-Kollektion bestimmt, und als Eintrag können unter anderem einfache Text-Objekte als auch UI-Elemente definiert werden. Zur Laufzeit können Sie dabei abfragen, welches Item gerade in der ListBox selektiert ist, oder dessen Index abrufen. Das ListBoxSteuerelement verhält sich sehr stark wie sein Gegenstück in Windows Forms, mit dem kleinen, aber feinen Unterschied, dass auch andere Objekte wie zum Beispiel ein Button als ListBoxItem verwendet und angezeigt werden können. Inhalt Listing 2.20: Eine ListBox mit verschachtelten Text- und UI-Elementen
Im XAML-Dokument aus Listing 2.20 ist sehr schön zu sehen, wie andere UI-Elemente als ListBoxItem einer ListBox verwendet werden können.
Selektierte Items können zur Laufzeit auch durch die Eigenschaft der ListBox namens SelectedItem referenziert werden. In Listing 2.20 wurde ein ContentControl definiert, das zur Laufzeit im Eventhandler des Button.Click-Events mit Inhalt ausgestattet wird.
52
Windows Presentation Foundation void ButtonItem_Click(object sender, RoutedEventArgs e) { Object temp = ListBoxMix.SelectedItem; ListBoxMix.Items.Remove(ListBoxMix.SelectedItem); ContentControlSelected.Content = temp; }
In der Methode ButtonItem_Click wird das selektierte ListBoxItem ermittelt und dem ContentControl zugewiesen.
!
!
!
ACHTUNG
Bitte beachten Sie dabei, dass vor der Zuweisung des ermittelten Items an das ContentControl ein Löschen des Items als Unterelement der ListBox vollzogen werden muss, denn ein Element kann immer nur Content von höchstens einem übergeordneten Element sein.
Das Ergebnis zur Laufzeit mit dem Button verschoben in das ContentControl ist in Abbildung 2.13 zu sehen.
Abbildung 2.13: Eine ListBox mit Text und UIElementen in der Kollektion Items
Durch die Ereignisbehandlung wird der Button, der noch beim Start der Applikation als Item in der ListBox seinen Dienst tat, in das ContentControl als Content übergeben.
2.4.4 Menüs in WPF Der altgewohnte und bewährte Ansatz, einem Benutzer eine Übersicht über die zur Verfügung stehende Funktionalität einer Anwendung und auch deren Navigation anzubieten, wird meist über Menüs realisiert. Jeder Benutzer kennt Menüs aus Windows-Applikationen. In den meisten bekannten Anwendungen befinden sich die Einträge der Menüleiste als Textelemente am oberen Rand der grafischen Benutzeroberfläche, und in vielen Anwendungen wird mit einem Rechtsklick auf Elementen ein sogenanntes Kontextmenü aufgeklappt. Wenn der Anwender einen der Menüeinträge anklickt, wird die hinterlegte Funktionalität abgearbeitet.
53
Kapitel 2
Menüleisten und Unterelemente In einer Menüleiste werden die Hauptelemente angezeigt. Diese besitzen meist Unterelemente, die weitere Elemente in der Hierarchie des Menüs definieren können. Das WPF-Element in der Menüleiste ist in der Klasse Menu implementiert, die sich von ItemControl ableitet. In einem Menü werden dann als Unterelemente Objekte aus der Klasse MenuItem abgelegt. Die Klasse MenuItem leitet sich von HeaderItemsControl ab und besitzt keine ContentEigenschaft, der angezeigte Text wird vielmehr über die Header-Eigenschaft gesetzt und wieder ausgelesen. Viele Benutzer von Anwendungen mit grafischer Oberfläche sind es gewohnt, nicht immer alle Menüpunkte mit der Maus zu bedienen. Eine Navigation durch das Menü wird oftmals durch die Eingabe einer Zeichenfolge bei gedrückter (Alt)-Taste ermöglicht. Um diese Benutzerinteraktion zu unterstützen, mussten VB 6.0- oder auch Windows Forms-Entwickler innerhalb Bezeichner der Menüelemente lediglich das kaufmännische Und (&) vor den Buchstaben des Bezeichners setzen, der mittels der (Alt)-Taste angesprochen werden sollte. In WPF hat sich dabei nur der Buchstabe geändert, und Entwickler als auch Designer müssen einen Unterstich vor den betreffenden Buchstaben einfügen. Zusätzlich zu einer Navigation durch die Menüs sind sogenannte Shortcuts in Menüs ein gern gesehener Gast. (Strg) + (C) als Shortcut für das Kopieren von markierten Elementen kann auch in WPF angeboten werden. Die Eigenschaft InputGestureText eines Menüelementes definiert die passende Tastenkombination, die in dem zugehörigen Menüeintrag mit angezeigt wird. Alleine durch diese Angabe wird aber noch keine Funktionalität zur Verfügung gestellt. Hilfestellung wird in diesem Fall zum Beispiel durch ApplicationCommand-Elemente in MenuItem.Command gegeben. Im XAMLAusschnitt in Listing 2.21 soll ein einfaches Menü mit dem wichtigsten Eigenschaften der MenuItems dargestellt werden. ApplicationCommands.Copy
54
Windows Presentation Foundation Listing 2.21: Eine erste Menüleiste mit untergeordneten Menüeinträgen
Zu den bereits beschriebenen Eigenschaften gesellt sich in Listing 2.21 (im menuItemDurchsichtig) noch die Eigenschaft IsCheckable, die bei der Auswahl eines MenuItems einen Haken vor die Beschriftung des Elementes setzt. Zu dieser Eigenschaft kommt noch das Checked- und UnChecked-Event. Die Eventhandler liegen in der Code-BehindDatei und werden im C#-Code, der in Listing 2.22 zu sehen ist, implementiert.
Abbildung 2.14: Die Menüleiste mit aufgeklapptem Bearbeiten-Menüeintrag
Programmatisch ist das Erstellen eines Menüs wieder sehr eingängig, in Listing 2.22 werden nur Teile des XAML Markups aus Listing 2.21 umgesetzt. Das komplette Beispiel finden Sie auf der Buch-CD. class MenuDemo : Window { private TextBlock _textBlockHilfe = null; private MenuItem _menuItemDurchsichtig = null; public MenuDemo() { StackPanel stackPanelMenu = new StackPanel(); this.Content = stackPanelMenu; Menu menueLeiste = new Menu(); stackPanelMenu.Children.Add(menueLeiste); menueLeiste.FontSize = 14.0; //Ausgelassene MenuItems MenuItem menuItemBearbeiten = new MenuItem();
55
Kapitel 2 menuItemBearbeiten.Header = "_Bearbeiten"; menueLeiste.Items.Add(menuItemBearbeiten); _menuItemDurchsichtig = new MenuItem(); _menuItemDurchsichtig.Header = "Durchsichtig"; _menuItemDurchsichtig.IsCheckable = true; menuItemBearbeiten.Items.Add(_menuItemDurchsichtig); MenuItem menuItemHilfe = new MenuItem(); menuItemHilfe.Header = "_Hilfe?"; menuItemHilfe.Click += new RoutedEventHandler(Hilfe_Click); menueLeiste.Items.Add(menuItemHilfe); _textBlockHilfe = new TextBlock(); _textBlockHilfe.FontSize = 14.0; _textBlockHilfe.Margin = new Thickness(20.0); stackPanelMenu.Children.Add(_textBlockHilfe); } void Hilfe_Click(object sender, RoutedEventArgs e) { _textBlockHilfe.Text = "Für eine ausführliche Hilfe ...."; } void Durchsichtig_Checked(object sender, RoutedEventArgs e) { _textBlockHilfe.Text = "Checked ? " + _menuItemDurchsichtig.IsChecked; } } Listing 2.22: Ein etwas verkürztes Menü mit den Methoden zur Ereignisbehandlung
Zusätzlich zu den Menüleisten erhalten Sie in der Windows Presentation Foundation auch die Möglichkeit, Menüs mit einem Rechtsklick der Maus aufklappen zu lassen.
Kontextmenüs Für Elemente in WPF, die sich von der Klasse Control ableiten, können Sie als Entwickler oder Designer ein Kontextmenü definieren, das bei einem Rechtsklick mit der Maus angezeigt wird. Diese Elemente besitzen eine abhängige Eigenschaft namens ContextMenu, die entweder als Unterelement des jeweiligen Elementes definiert oder in einer Ressource abgelegt werden kann. Die Vorgehensweise, um Elemente hinzuzufügen und deren Ereignisse zu behandeln, ist dieselbe wie bei einer Menüleiste. Im XAML-Ausschnitt in Listing 2.23 wird zu einem Button direkt ein Kontextmenü als Unterelement hinzugefügt: Rechtsklick zum Farbwechsel
56
Windows Presentation Foundation Listing 2.23: Ein Kontextmenü für einen Button als dessen untergeordnetes Element
Abbildung 2.15 zeigt, dass durch einen Rechtsklick mit der Maus auf den Button das Kontextmenü erscheint und die Farbe des Buttons durch die Ereignisbehandlung verändert wird.
Abbildung 2.15: Das Kontextmenü, das den Farbwechsel per Rechtsklick möglich macht
Auch programmatisch können Kontextmenüs sehr intuitiv an ein Element vom Typ Control geheftet werden, was in Listing 2.24 nachzuvollziehen ist. Button buttonKontext = new Button(); buttonKontext.Content = "Rechtsklick zum Farbwechsel"; buttonKontext.Margin = new Thickness(200, 100, 200, 100); buttonKontext.FontSize = 14.0; ContextMenu contextMenKontext = new ContextMenu(); MenuItem menuItemWeiss = new MenuItem(); menuItemWeiss.Header = "White"; MenuItem menuItemBlau = new MenuItem(); menuItemBlau.Header = "Blau"; contextMenKontext.Items.Add(menuItemWeiss); contextMenKontext.Items.Add(menuItemBlau); buttonKontext.ContextMenu = contextMenKontext; Listing 2.24: Kontextmenü für einen Button als C#-Quellcode
2.4.5 Das Toolbar-Steuerelement Der Benutzer einer grafischen Oberfläche ist ein Gewohnheitstier, und damit er seine gewohnte Benutzeroberfläche von Anwendung zu Anwendung gleich wieder erkennt und darin durch den Wiedererkennungswert sofort einsteigen kann, liegt meist direkt unter der Menüleiste eine Toolbar.
57
Kapitel 2
Die Toolbar ist wiederum eine Leiste in der Funktionalität der Anwendung, die über Mausklicks ausgelöst werden kann. Ganz vorne dabei in der Beliebtheit sind Aktivitäten wie das Neuanlegen von Dokumenten oder das Kopieren und Einfügen von Elementen. Werkzeugleisten können sich aus mehreren Leisten zusammensetzen, die in einem bestimmten Bereich der Anwendung abgelegt werden. Eine Toolbar kann entweder direkt als Content eines Elementes genutzt oder in einem Container an einer bestimmten Stelle abgelegt werden. Um mehrere Toolbars anzuordnen, stellt WPF mehrere Panels zur Verfügung. Allen voran ist das ToolBarTray (Tray, zu Deutsch Ablage) zu nennen, in dem mehrere Werkzeugleisten untergebracht werden können. Ein ToolBarTray kümmert sich sowohl um die Anordnung und Größe als auch um das Hinzufügen und Neuanordnen von Inhalten in einer oder mehreren Werkzeugleisten. In einem ToolBarTray werden die Werkzeugleisten in Zeilen, den sogenannten Bands, angeordnet. Innerhalb eines Bands werden Werkzeugleisten mittels des BandIndex platziert. In Listing 2.25 sehen Sie eine Toolbar in einem ToolBarTray mit den Steuerelementen für Ausschneiden, Kopieren und Einfügen. Listing 2.25: Eine Werkzeugleiste(Toolbar)
Wenn Sie das Beispiel zur Laufzeit betrachten, werden Sie Microsofts starkes Bestreben nach einfacher Usability (Bedienbarkeit) wieder erkennen. Werfen Sie dazu einen Blick auf die Abbildung 2.16.
58
Windows Presentation Foundation
Abbildung 2.16: Werkzeugleiste mit zugehörigen Schaltflächen
Im XAML Markup ist Ihnen vielleicht aufgefallen, dass die Buttons mit einem CommandAttribut ausgestattet wurden, das als Wert ein ApplicationCommand beinhaltet.
ApplicationCommands In der Programmierung von grafischen Benutzeroberflächen kommen Sie als Entwickler immer wieder an den Punkt, dass Standardfunktionalität wie das berühmte CopyUnd-Paste für den Benutzer angeboten werden soll. Damit hier das Rad nicht neu erfunden werden muss, stehen ApplicationCommands zur Verfügung. In Listing 2.25 wurden ApplicationCommands für das Ausschneiden, Kopieren und Wiedereinfügen definiert. Wird einer der zugehörigen Buttons gedrückt, ermittelt die Anwendung, ob die getroffene Auswahl (Text, UIElement usw.) das zugeordnete Kommando unterstützt (CommandBinding), und arbeitet gegebenenfalls die hinterlegte Logik ab. Unterstützt das ausgewählte Element das Kommando nicht, dann können Sie selbst die gewünschte Funktionalität dafür implementieren. Wenn der Benutzer den Inhalt eines Textfeldes markiert, ist für das Kopieren in der Textbox auch die passende Funktionalität verfügbar. Ist dagegen ein Button markiert, ist für ihn standardmäßig keine Implementierung für das Kopieren vorhanden. Zu den bereits vorgestellten Kommandos, das Kopieren und Einfügen betreffend, gehören weitere immer wiederkehrende Funktionalitäten wie Öffnen, Speichern, Wiederherstellen oder Rückgängigmachen.
2.4.6 Das MediaElement Um in der Windows Presentation Foundation Bewegung in die Benutzeroberfläche zu bringen, bedarf es nicht unbedingt der später noch vorgestellten Animationen, denn sowohl für das Wiedergeben von bewegten Bildern als auch für das Abspielen von Sound kann das MediaElement verwendet werden.
59
Kapitel 2
Abgeleitet von UIElement kann das MediaElement als Content weiterer Steuerelemente definiert werden und im Gegensatz zur altbekannten Windows Media Player-API ist es dem MediaElement vorbehalten, mittels XAML auch im Markup seine Verwendung zu finden. Der Einsatz des MediaElement ist sehr einfach, kann aber unter Umständen auch kleine Verzweiflungstaten bei einem Entwickler hervorrufen. Dazu aber gleich mehr. Werfen Sie doch zuerst einmal einen Blick auf das MediaElement im Markup von Listing 2.26. Listing 2.26: Ein einfaches MediaElement
Eigentlich gar nicht so tragisch, könnte man bei der Betrachtung von Listing 2.26 denken, das MediaElement mit seiner Eigenschaft Source scheint selbstbeschreibend zu sein. Dazu muss aber noch gesagt werden, dass bei einer Angabe eines relativen Pfades wie in diesem Beispiel die gewünschte Datei auch in der passenden Verzeichnisstruktur unter dem Anwendungsverzeichnis der Applikation eingehängt werden muss. Das kann durch ein PostBuildEvent, manuelles Kopieren oder das Hinzufügen eines benutzerdefinierten Eintrags in der Projektdatei geschehen. In Visual Studio 2005 kann dieser Eintrag im Eigenschaftsfenster der zu kopierenden Datei gesetzt werden, Always Listing 2.27: Eintrag in der Projektdatei, um die Mediendatei in das passende Verzeichnis zu kopieren
In der Gruppe benutzerdefinierter Einstellungen der Projektdatei wird damit eine Content-Datei angegeben. Eine Content-Datei ist eine lose Datei, die zu einer Assembly gehört, aber nicht in den Prozess des Kompilierens miteinbezogen wird, auch wenn die Assembly in ihren Metadaten in Assoziation mit dieser Datei steht. Ist die Datei einmal im richtigen Verzeichnis, muss das MediaElement nur noch dazu bewogen werden, auch die angegebene Datei abzuspielen. In diesem ersten Beispiel soll die Wiedergabe programmatisch mit einem Aufruf der Play-Methode des MediaElements gestartet werden, wobei dieser Aufruf hier noch ganz einfach im Konstruktor der Code-Behind-Datei stattfindet: mediaElementEinfach.Play();
Um dies zu ermöglichen, wurde die abhängige LoadedBehavior-Eigenschaft auf den MediaState Manual gesetzt. So, jetzt noch schnell die Assembly erstellt, kontrolliert, ob
60
Windows Presentation Foundation
im Anwendungsverzeichnis der Ordner media mit der Datei bee.wmv angelegt wurde, und dann mit spannungsgeladener Erwartung auf das erscheinende Fenster gestarrt.
> >
>
HINWEIS
Aber es kann sein, dass sich nichts tut, denn wie schon erwähnt kann der Versuch, die Datei abzuspielen, nach einiger Zeit der Fehlersuche (eine Exception wird nicht geworfen) zu etwas Frustration führen. Wenn Sie nämlich noch nicht das OCX für den Windows Media Player ab Version 10 installiert haben, werden Sie auf einen weißen Hintergrund blicken, und kein Video wird zu sehen sein. Das MediaElement braucht die Unterstützung eines Windows Media Players ab der Version 10, um Mediendateien wiederzugeben.
Soll der Start der Wiedergabe nicht programmatisch durch den Aufruf der PlayMethode veranlasst werden, so können das Setzen des LoadBehaviors und der Aufruf der Play-Methode ausgelassen werden.
Mit diesem kleinen Schnipsel kann schon ein Video ablaufen, vorausgesetzt WMP ab Version 10 und die passende Dateistruktur im Anwendungsverzeichnis sind vorhanden.
Modi für die Wiedergabe In der Windows Presentation Foundation kann das MediaElement auf zwei verschiedene Arten zurückgreifen, um Dateien wiederzugeben: Independent Mode: –
Der unabhängige Modus ist der Standardmodus und wird genutzt, wenn keine weiteren gegensätzlichen Angaben in der Clock-Eigenschaft des MediaElements gemacht werden.
– Im unabhängigen Modus kann die Wiedergabe des Mediums programmatisch beeinflusst werden. Die Angabe der Source als URI, das Starten und Stoppen der Wiedergabe und die Kontrolle über alle anderen Elemente unterliegen direkt dem Entwickler per Code. Für diese Kontrolle muss, wie schon angeführt, das LoadedBehavior auf Manual gesetzt werden. Clock Mode –
Im Uhr-Modus übernimmt eine MediaTimeline die Wiedergabe des Mediums, wobei sowohl die URI der Datei als auch das Starten und Stoppen der Wiedergabe durch die Timeline kontrolliert werden.
– Das Laden der Mediendatei wird durch das Setzen der Source- und Clock-Eigenschaft für das MediaTimeLine-Objekt durchgeführt.
61
Kapitel 2
–
Nur durch das explizite Setzen der Clock-Eigenschaft wird der Modus von Independent auf Clock umgestellt. Ist die Clock-Eigenschaft auf null gesetzt, so befindet sich das MediaElement automatisch im unabhängigen Modus.
MediaElement kontrollieren Um ein MediaElement zu kontrollieren, können Sie in das Geschehen mittels der Methoden Play, Pause und Stop als auch durch die Eigenschaften für die Lautstärke, die Position und die Abspielgeschwindigkeit eingreifen. Das einfache Beispiel soll dazu in Listing 2.28 mit Steuerelementen und zugehöriger Logik erweitert werden, die aus dem MediaElement ein vom Benutzer kontrollierbares Element machen. Lautstaerke Geschwindigkeit Position Listing 2.28: Steuerbares MediaElement
62
Windows Presentation Foundation
Für die Steuerung des MediaElements in Listing 2.28 werden Buttons für das Starten, Stoppen und Pausieren des Videos und deren zugehörige Ereignisbehandlungen zur Verfügung gestellt. void buttonPlay_Click(object sender, RoutedEventArgs e) { mediaElementKontrollierbar.Volume = (double)sliderLautstaerke.Value; mediaElementKontrollierbar.SpeedRatio = (double)sliderSpeed.Value; mediaElementKontrollierbar.Play(); } void buttonPause_Click(object sender, RoutedEventArgs e) { mediaElementKontrollierbar.Pause(); } void buttonStop_Click(object sender, RoutedEventArgs e) { mediaElementKontrollierbar.Stop(); } Listing 2.29: Die Ereignisbehandlung für die Steuerelemente zum Starten, Stoppen und Pausieren eines MediaElements
Das Slider-Steuerelement aus Listing 2.28 wurde in diesem Buch bisher noch nicht vorgestellt und soll daher einer kurzen Betrachtung unterzogen werden. Ein Slider ist gleichzusetzen mit einem Schieberegler, der einen maximalen, einen minimalen und einen aktuellen Wert definieren kann. Das Ereignis, das in unserem Beispiel eine tragende Rolle spielt, ist das ValueChanged Event des Slider-Controls. Die Parameter des Ereignishandlers sind anders als – vielleicht bisher bei UIElement-Klassen gewohnt – eine Referenz auf eine Instanz vom Typ Object und eine Referenz auf eine Instanz vom Typ RoutedPropertyChangedEventArgs. Die RoutedPropertyChangedEventArgs gehen einher mit dem Kontext der generischen Datentypen und belegen das Template zur Bestimmung eines Typs innerhalb der Klasse RoutedPropertyChangedEventArgs hier als den Typ Double. In den Argumenten für das Ereignis kann zusätzlich zu der Eigenschaft OldValue auch der NewValue des zugehörigen Schiebereglers abgerufen werden. So zu sehen bei der Veränderung des Schiebereglers für die Lautstärke. void sliderLautstaerke_ValueChanged(object sender, RoutedPropertyChangedEvent Args e) { mediaElementKontrollierbar.Volume = e.NewValue; } Listing 2.30: Lautstärkeregelung in einem MediaElement
63
Kapitel 2
Nach dem Laden der Mediendatei wird im MediaOpened-Ereignis des MediaElementObjektes die Abspieldauer des geladenen Mediums errechnet und die Anzahl der Millisekunden als Maximalwert für den Positionsschieberegler festgelegt. void mediaElementKontrollierbar_MediaOpened(object sender, RoutedEventArgs e) { SliderPosition.Maximum = mediaElementKontrollierbar.NaturalDuration.TimeSpan.TotalMilliseconds; } Listing 2.31: Ermitteln der Spieldauer eines Mediums
Schiebt der Benutzer den Regler auf die gewünschte Position, muss noch das passende TimeSpan-Objekt erstellt werden. Wobei in diesem Fall nur die Millisekunden betrachtet werden. Mit dem ermittelten Wert wird nun das MediaElement auf die zeitlich passende Stelle im Medium gesetzt. void sliderPosition_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { int gewaehltePostion =(int)e.NewValue; TimeSpan gewuenschtePosition = new TimeSpan(0, 0, 0, 0, gewaehltePostion); mediaElementKontrollierbar.Position = gewuenschtePosition; } Listing 2.32: Einstellen der zeitlichen Position im Medium
Auch die Geschwindigkeit der Wiedergabe kann beeinflusst werden: void sliderSpeed_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { mediaElementKontrollierbar.SpeedRatio = e.NewValue; } Listing 2.33: Festlegen der Abspielgeschwindigkeit
Das erzielte Ergebnis im laufenden Betrieb mit geladener Videodatei ist in Abbildung 2.17 zu bestaunen.
64
Windows Presentation Foundation
Abbildung 2.17: Das steuerbare MediaElement mit laufendem Video
2.4.7 Content – einfach nur Inhalt? In der traditionellen Programmierung von grafischen Benutzeroberflächen sind bestehende Steuerelemente hinsichtlich ihres grafischen Inhalts streng festgelegt. Bei einfachen Steuerelementen wie Buttons, Labels oder Textboxen ist der Inhalt auf Text oder eine Bitmap beschränkt. Selbst bei komplexeren Steuerelementen wie dem DataGrid ist die Erweiterbarkeit durch weitere Steuerelemente von vornherein streng festgelegt. Wenn Sie nicht über UserControls oder CustomControls selbst und vielleicht noch programmatisch Hand anlegen wollen, dann sind Ihre standardmäßigen Designs, den Inhalt eines Steuerelementes betreffend, in einem sehr straffen Rahmenwerk geregelt. Die Windows Presentation Foundation bietet Ihnen hingegen ein flexibles Modell an, was das äußere Erscheinungsbild von Steuerelementen anbelangt. Nahezu alles kann als Content von Elementen in Ihren Anwendungen herhalten. Text, Bitmaps, Zeichnungen, Animationen und sogar andere Controls, die ihrerseits wieder Content beinhalten können. Die einzige Limitierung, die für Content besteht, ist, dass ein Window nicht als Content eines anderen Fensters oder von anderen Steuerelementen dienen kann und dass ein Steuerelement nur als Content von höchstens einem direkt übergeordneten Element zugelassen ist. Diese Idee der Verschachtelung von Inhalten ebnet den Weg zu sehr umfangreichen und mächtigen Anwendungsszenarien, was gerade das visuelle Design von Applikationen betrifft. Alle Elemente, die von der Klasse ContentControl im Namensraum System.Windows.Controls abgeleitet sind, erben das Content Property vom Typ Object.
65
Kapitel 2
Um per XAML einem Element Content, hinzuzufügen reicht es meist, ein weiteres Element als Unterelement zu verschachteln. Der Content kann aber als Attribut auch über Datenbindung oder Ressourcen einem Element zugewiesen werden. In Listing 2.34 erfahren Sie, wie ein RadioButton als Content einen HyperLink besitzt, der seinerseits wiederum einen TextBlock in der Content-Eigenschaft als Unterelement in sich trägt. Bitte wählen Sie eine Versicherung Sichere Sache AG die Autoversicherer, weitere Infos http://www.sicheresache.de". Securitas Sach die Sachversicherer, weitere Infos http://www.sachsec.de". Listing 2.34: Content in seiner verschachtelten Ausprägung
Das Ergebnis lässt sich in Abbildung 2.18 sehen. Die Kombination aus RadioButton und HyperLink mit beinhaltetem TextBlock erscheint sogar sinnvoll. Ein Kunde kann vor einer Auswahl über die Optionsfelder noch auf die Informationsseite navigieren. Das StackPanel ist für die Abbildung 2.18 in einem Page-Element untergebracht worden, sodass sich das Ergebnis als XAML-Browseranwendung im Internet Explorer präsentiert. So einfach sich das XAML-Dokument aus Listing 2.34 auch gerade darstellen mag, programmatisch ist hier einiges dahinter. Es muss nämlich genau unterschieden werden, welche Teile Content sind und welche Elemente sich nicht von ContentControl ableiten, somit keine Content-Eigenschaft vererbt bekommen und deren Inhalt sich auf andere Art und Weise zusammensetzt. Der .NET-Quellcode in Listing 2.35 soll darüber Aufschluss geben.
66
Windows Presentation Foundation
Abbildung 2.18: Eine Page mit Elementen, die sich über ihren Content zusammenstellen
Label labelContent = new Label(); labelContent.Content = "Bitte wählen Sie eine Versicherung"; labelContent.FontSize = 14.0; stackPanelContent.Children.Add(labelContent); RadioButton radioButtonContent = new RadioButton(); radioButtonContent.Margin = new Thickness(10); Run run1 = new Run("Sichere Sache AG "); run1.FontWeight = FontWeights.Bold; Run run2 = new Run("Autoversicherer "); run2.FontStyle = FontStyles.Italic; Run run3 = new Run(" Weitere Infos "); Run run4 = new Run("http://www.sicheresache.de"); run4.FontWeight = FontWeights.Bold; TextBlock textBlockContent1 = new TextBlock(); textBlockContent1.FontSize = 14.0; textBlockContent1.TextWrapping = TextWrapping.Wrap; textBlockContent1.Inlines.Add(run1); textBlockContent1.Inlines.Add(run2); textBlockContent1.Inlines.Add(run3); textBlockContent1.Inlines.Add(run4); Hyperlink hyperLinkContent = new Hyperlink(); hyperLinkContent.Inlines.Add(textBlockContent1); hyperLinkContent.NavigateUri = new Uri("http://suche.versicherung.de"); radioButtonContent.Content = hyperLinkContent; Listing 2.35: Ein RadioButton mit verschachteltem Inhalt
67
Kapitel 2
Die Elemente TextBlock und Hyperlink (Listing 2.35) besitzen keine Content-Eigenschaft. Sie definieren ihren Inhalt durch Inline-Kollektionen. Die Klasse Inline leitet sich direkt von TextElement ab und dient als abstrakte Basisklasse für die Klasse Run, über die der Inhalt des TextBlocks festgeschrieben ist. In die Inline-Kollektion können des Weiteren Zeichenketten und UIElemente als Objekte hinzugefügt werden.
2.5 Lokalisierung von WPF-Komponenten Eine grafische Benutzeroberfläche muss in der heutigen Zeit nicht nur unterschiedlichen Benutzereinstellungen wie Bildschirmauflösung, Schriftgröße oder Farbgebung gerecht werden, vielmehr spielt es eine große Rolle, dass ein Benutzer die Oberfläche seinen regionalen und kulturellen Gegebenheiten angepasst wiederfindet. Viele Anwendungen, die erstellt werden, zielen auf Benutzer ab, die verstreut über den Erdball in unterschiedlichsten sprachlichen und kulturellen Regionen ihre Arbeit mit dieser Anwendung verrichten. Die Szenarien sind vielfältig, so soll ein Benutzer in den USA mit der deutschen Einstellung auf seinem Rechner auch eine deutsch eingestellte Oberfläche vorfinden, auch wenn seine amerikanischen Kollegen auf ihren Rechnern für dieselbe Anwendung sehr wohl die englische Einstellung bevorzugen und auch wieder finden wollen. In der Programmierung mit Windows Forms werden Sie bei der Lokalisierung Ihrer Benutzerschnittstellen sehr stark von Visual Studio 2005 unterstützt. Ein Add-In oder eine IDE-Integration für Lokalisierungsaufgaben gibt es für WPF-Oberflächen zum jetzigen Zeitpunkt leider noch nicht. Nichtsdestotrotz stellt Ihnen Microsoft mehrere Wege zur Verfügung, Ihre GUI für den internationalen Gebrauch salonfähig zu machen. Sie können dabei zum einen den Weg über Ressourcen gehen und die sprachlichen Unterschiede der Oberfläche in Dateien festhalten und je nach eingestellter Region eine der Dateien für die Lokalisierung Ihrer Applikation heranziehen. Die Ressourcen werden bei der Erstellung des Projektes in den sogenannten SatellitenAssemblies abgelegt. Für jede gewünschte Sprache wird eine Assembly als Dynamic Linked Library erstellt, die von der Applikation je nach eingestellter sprachlicher oder regionaler Gegebenheit zur Lokalisierung der Oberfläche herangezogen wird. Der andere Weg, den sie einschlagen können, ist der Einsatz des LocBaml-Tools, das die Satelliten-Assemblies bestehender Anwendungen auswerten und neue SatellitenAssemblies aus diesen Informationen mit anderen, sprachlich angepassten Inhalten erstellen kann. Um die Grundlage für eine Besprechung von Lokalisierung zu haben, soll ein einfaches Fenster dienen, das nur einen Button und einen TextBlock beinhalten wird. Die anfänglichen Werte für den Content im Button und den Text im TextBlock werden direkt im XAML-Dokument festgelegt.
68
Windows Presentation Foundation Der Standard Text. Klick Mich Listing 2.36: Das XAML-Dokument als Grundlage der Lokalisierung
2.5.1 Lokalisierung mit Ressourcen Um die Oberfläche aus dem Listing 2.36 an sprachliche und regionale Gegebenheiten anzupassen, gilt es vorerst, die zu verändernden Eigenschaften in einer Art Ressourcendatei festzuhalten. Dazu kann ein Ordner (zum Beispiel daten) in der Applikation angelegt werden, in dem die Ressourcendateien abgelegt sein sollen. In diesem Ordner befinden sich in unserem Beispiel vorerst zwei Dateien mit der Endung .restext. In der Beispielapplikation werden die Dateien \daten\keyvalue.de.restext und \daten\keyvalue. en-US.restext mit einer Art Schlüssel-Werte-Paar für die einzelnen zu lokalisierenden Eigenschaften der GUI ausgestattet. \daten\keyvalue.de.restext
\daten\keyvalue.en-US.restext
EinfacherText=Deutsche CultureInfo gewaehlt
EinfacherText=English CultureInfo chosen
EinfacherInhalt=Klick Mich!
EinfacherInhalt=Click Me!
Tabelle 2.1: Die beiden Ressourcendateien mit unterschiedlichen Werten für die Lokalisierung
Um die beiden Dateien auch für den Build-Prozess der Applikation bekannt zu machen und eine Erstellung der Satelliten-Assemblies in den unterschiedlichen Regionalangaben zu gewährleisten, muss die Projektdatei der Applikation angepasst werden. Listing 2.37: Bekanntmachen der Ressourcen für den Build-Prozess
Mit Visual Studio 2005 können Sie diese Anpassung in den Eigenschaften der beiden Ressourcendateien vornehmen. Setzen Sie dafür bei ausgewählter Datei im Eigenschaftsfenster den Wert EINGEBETTETE RESSOURCE für die Eigenschaft BUILDVORGANG. Wenn die Applikation mit dieser Projektdatei erstellt wird, werden die passenden Ordner und die Satelliten-Assemblies automatisch beim Erstellen der Applikation mit angelegt. Die Ordner erhalten ihren Namen analog zum Länderkürzel, das im jeweiligen Bezeichner der Ressourcendatei eingefügt wurde.
69
Kapitel 2
Abbildung 2.19: Die Ordnerstruktur mit den Satelliten-Assemblies
Die Applikation hat jetzt die Möglichkeit bekommen. auf die Informationen über Kultur und Sprache der Umgebung zu reagieren. Dazu muss aber jetzt noch die Brücke zwischen den Eigenschaften der UIElemente der grafischen Benutzeroberfläche und der jeweiligen Ressource geschlagen werden. Bisher wurden der Text des TextBlock-Elements und der Content in dem Button fest in XAML vorgegeben. Ziel der Übung ist es aber, diese Angaben abhängig von den sprachlichen und kulturellen Einstellungen zu machen. Das heißt für uns als Entwickler, dass die Eigenschaften mit Inhalten aus den Ressourcen verbunden werden müssen. Dabei hilft uns ein Objekt vom Typ ResourceManager, dessen Methoden Zugriff auf die Inhalte der Ressourcen gewähren. ResourceManager ressourceManagerLokalisierung = new ResourceManager("LokalizationResourcesDEMO.daten.stringtable", Assembly.GetExecutingAssembly()); textBlockLokalisierung.Text = ressourceManagerLokalisierung.GetString("EinfacherText"); buttonLokalisierung.Content = ressourceManagerLokalisierung.GetString("EinfacherInhalt"); Listing 2.38: Zugriff auf die Inhalte der Ressourcendateien mit den Methoden des ResourceManagers
Die GetString-Methode liefert je nach eingestellter Kultur zu dem übergebenen Schlüssel vom Typ String den zugehörigen Wert aus der angezogenen Ressource.
70
Windows Presentation Foundation
Wenn Sie den ResourceManager bei deutschen Einstellungen verwenden und die GetString-Methode zum Beispiel im Click-Ereignis des Button-Elements aufrufen, so wird die Satelliten-Assembly im Ordner de im Anwendungsverzeichnis durch den ResourceManager herangezogen. Wenn Sie auch die englischen Inhalte durch den ResourceManager anzeigen lassen wollen, stellen Sie einfach den aktuellen Thread auf die amerikanischen Kulturinformationen um. Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
Der ResourceManager bedient sich durch die Anpassung der CultureInfo im aktuellen Thread bei der Satelliten-Assembly im en-US Ordner. Dies hat zur Folge, dass die GetString-Methode jetzt die englischen Werte zu den übergebenen Schlüsseln liefert. In Abbildung 2.20 können Sie die einzelnen Ausgaben der grafischen Benutzeroberfläche nachvollziehen. Die Reihenfolge der eingefügten Screenshots stellt dar, dass zuerst die Applikation ohne Lokalisierung aufgerufen wurde. Wird der Button geklickt und in der zugehörigen Ereignisbehandlung die aktuelle CultureInfo noch von den deutschen Betriebssystemeinstellungen bestimmt, so ist auf dem Screenshot der Inhalt aus der de-Ressourcendatei zu sehen. Wird die CultureInfo für den aktuellen Thread auf en-US umgestellt, so resultiert dies in einer grafischen Benutzeroberfläche, die sich im englischen Kontext präsentiert.
Abbildung 2.20: Die Oberflächen mit den unterschiedlich lokalisierten Anzeigen
Bei diesem ohnehin schon sehr komplizierten Verfahren müssen leider alle grafischen Elemente, die an der Lokalisierung teilhaben sollen, eine Belegung ihrer Eigenschaften über die Methoden eines Ressourcen-Managers über sich ergehen lassen. In WPF gibt es darüber hinaus ein Verfahren, das es ermöglicht, aus einer grafischen Benutzeroberfläche die lokalisierungsfähigen Elemente zu extrahieren und als kom-
71
Kapitel 2
maseparierte Datei zur Verfügung zu stellen. Mit demselben Tool haben Sie dann die Möglichkeit, eine Veränderung dieser Datei wieder in eine Satelliten-Assembly zu übertragen und für die Anwendung zur Verfügung zu stellen.
2.5.2 Lokalisierung mit dem LocBaml-Tool Um die Lokalisierung mit dem LocBaml-Tool zu beschreiben, soll wieder die einfache Benutzeroberfläche aus dem Listing 2.36 als Grundlage dienen. In den erklärenden Schritten werde ich mich in den Pfadangaben auf Pfade meiner Umgebung beziehen, da dies zum besseren Verständnis unvermeidbar ist. Bitte passen Sie diese Pfade bei Bedarf den Gegebenheiten auf Ihrem System an. Nachdem ein neues Projekt mit der Benutzeroberfläche aus Listing 2.36 angelegt wurde, muss dem Build-Prozess mitgeteilt werden, dass eine Satelliten-Assembly generiert werden soll, welche die Informationen der zugehörigen Ländereinstellungen in sich tragen wird. Dazu muss in der Projektdatei die Kulturinformation für die Satelliten-Assembly eingefügt werden. Debug AnyCPU {F430E91A-5049-4BED-8206F9FF17948920} {60dc8134-eba5-43b8-bcc9bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B00C04F79EFBC} LocBamlLocalization LocBamlLocalization de
Listing 2.39: Der Eintrag in der Projektdatei (.csproj oder .vbproj)
Die PropertyGroup setzt die Einstellungen für den Debug-Modus. Sie können die Kulturinformationen aber auch in anderen PropertyGroup-Elementen unterbringen. Um die lokalisierbaren Inhalte für das LocBaml Tool auffindbar zu machen, müssen die Elemente in XAML mit Uid-Eigenschaften ausgestattet werden. Uids werden dazu benutzt, Änderungen in Elementen zu verfolgen und die Elemente für eine Übersetzung auszuzeichnen. Um die einfache Applikation mit Uids auszustatten, sollte auf der Kommandozeile das MSBuild-Tool mit den passenden Parametern aufgerufen werden. Dazu wird auf der Kommandozeile (Visual Studio 2005 Command Prompt) in das Verzeichnis gewechselt, in dem die Projektdatei der zu lokalisierenden Applikation liegt. msbuild -t:updateid LocBamlLocalization.csproj
72
Windows Presentation Foundation
Wird dieser Befehl erfolgreich abgearbeitet, bekommen Sie auf der Konsole eine Zusammenfassung der ausgeführten Aktionen, und die Elemente in der XAML-Datei des Projektes werden mit Uids ausgestattet.
Abbildung 2.21: Das MSBuild-Tool im Einsatz zur Generierung von Uids im XAML-Dokument eines Projektes
Der Standard Text. Klick Mich Listing 2.40: Die XAML-Datei mit eingefügten Uids
Nachdem die Uids in der Applikation verbaut sind, muss die Applikation noch neu erstellt werden, und dann wird es auch schon Zeit, das LocBaml-Tool auf die SatellitenAssembly loszulassen. Ab dem jetzigen Zeitpunkt sollten Sie sich anschnallen, denn es kann eine holprige Fahrt in Richtung fertiger Lokalisierung werden. Als Erstes müssen Sie das LocBamlTool selbst erstellen. Es ist in den Samples zu WPF (Microsoft SDKs\Windows\
73
Kapitel 2
v6.0\Samples\GlobalizationLocalization\LocBaml\CSharp) zu finden. Wechseln Sie dazu auf der Konsole in diesen Ordner und erstellen das Tool mit MSBuild. msbuild locbaml.csproj
Das LocBaml-Tool wird Sie im Folgenden dabei unterstützen, eine Satelliten-Assembly zu parsen und den Inhalt, der lokalisiert werden soll, zu extrahieren. Dazu stellt das Tool die Option parse zur Verfügung, die eine angegebene Satelliten-Assembly als Argument fordert. Zusätzlich muss in der out-Option noch eine kommaseparierte Datei oder eine Textdatei angegeben werden, welche die extrahierten Daten enthalten soll.
> >
>
HINWEIS
Die angegebene Ressourcendatei (Satelliten-Assembly) muss mit all ihren Abhängigkeiten im gleichen Verzeichnis wie die LocBaml.exe liegen, oder alle betroffenen Assemblies müssen im globalen Assembly-Cache registriert sein. In unserem Fall soll einfach die LocBaml.exe in das Anwendungsverzeichnis und zusätzlich die LocBamlLocalization.resources.dll auch in das Verzeichnis kopiert werden. In \bin\Debug\ liegen dann sowohl die LocBaml.exe, die Satelliten-Assembly als auch die Assembly der Applikation(LocalizationBAML.exe).
Der Befehl für das Extrahieren in eine .csv-Datei muss dann so lauten: LocBaml.exe /parse LocBamlLocalization.resources.dll /out:c:\LBR.csv
Das Resultat ist in Abbildung 2.20 als Excel-Datei zu sehen, und es ist ein Einfaches nachzuvollziehen, dass auch die beiden Werte (KlickMich und Der Standard Text), die lokalisiert werden sollen, in der Datei mit beinhaltet sind. Die einzelnen Einträge der .csv-Datei und deren Aufbau werden durch folgende Spalten definiert: Name der Baml-Datei: Binary Application Markup Language-Datei, die vom XAML-Compiler generiert und in eine Ressource eingebettet wird. Ressource Key: Schlüssel der Ressource, extrahiert aus der Uid. Lokalisierungskategorie: Kategorie von Eigenschaften oder Elementen, die lokalisiert werden sollen. Lesbar (Readable): Wertet ein Lokalisierungsattribut in XAML aus, das angibt, ob die Ressource lesbar ist. Veränderbar (Modifiable): Wertet ein Lokalisierungsattribut in XAML aus, das angibt, ob die Ressource veränderbar ist. Kommentare (Comments): Lokalisierungskommentare, die im XAML-Dokument eingefügt wurden. Wert: Der eigentlich wichtige Part für Lokalisierung. Der Wert sollte in die Zielsprache übersetzt werden.
74
Windows Presentation Foundation
Abbildung 2.22: Die .csv-Datei mit den extrahierten Inhalten der Satelliten-Assembly
Da nun die Werte der zu lokalisierenden Elemente verändert werden können, sollen die beiden für uns interessanten Werte übersetzt und in einer neuen .csv-Datei gespeichert werden. Die Übersetzung (siehe Abbildung 2.23) ist die Grundlage für eine neue und unterschiedlich lokalisierte Satelliten-Assembly. Diese Assembly gilt es jetzt noch, unter Zuhilfenahme des LocBaml-Tools zu generieren. Dazu muss dem Tool eine Ressourcendatei die übersetzte .csv-Datei und der Ort angeben werden, an dem die neue Satelliten-Assembly abgelegt werden soll. Die sprachlichen Änderungen wurden für dieses Beispiel in C:\LBRTest.csv abgelegt. Auf der Kommandozeile muss in dem Verzeichnis, in dem noch immer LocBaml.exe, die Satelliten-Assembly und auch die Assembly der Applikation(LocalizationBAML.exe) liegen, folgendes Kommando abgesetzt werden: LocBaml.exe /generate / trans:LBREnglisch.csv LocBamlLocalization.resources.dll / out:D:\LocBamlLocalization\LocBamlLocalization\bin\Debug\en-US /cul:en-US
75
Kapitel 2
Abbildung 2.23: Die Übersetzung in der .csv-Datei
Die Option generate sorgt dafür, dass die in der Option trans übersetzte Datei in einer Satelliten-Assembly resultiert. In der Option out wird das Ausgabeverzeichnis der generierten Satelliten-Assembly angegeben. Um die Lokalität (Locale) für die Assembly zu bestimmen, muss in der Option cul noch ein gültiges Länderkürzel angeführt werden. In diesem Fall sollte die Übersetzung ins Englische stattgefunden haben. Ist die Ressourcendatei im passenden Verzeichnis angelangt, gilt es jetzt, die getane Arbeit auch wirklich zu testen. Ist auf Ihrem System eine deutsche Sprachinstallation vorhanden und Sie starten die Anwendung, so sollten Sie die Oberfläche mit deutschen Texten sehen. Um die englische Oberfläche auf den Bildschirm zu zaubern, haben Sie nun zwei Möglichkeiten. Sie können einmal das Sprachpaket für die englischen Einstellungen auf Ihrem Betriebssystem installieren und dann die Sprache des Betriebssystems auf Englisch umstellen, oder Sie gehen den etwas einfacheren Weg und stellen die Kulturinformationen programmatisch beim Start der Applikation um. Dazu müssen Die in der App.xaml.cs-Datei der Anwendung die OnStartup-Methode überschreiben und für den aktuellen Thread die Kulturinformation wie in Listing 2.41 auf en-US einstellen.
76
Windows Presentation Foundation
Abbildung 2.24: Die einfache Oberfläche mit deutscher Lokalisierung
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); CultureInfo cultureInfoEnglisch = new CultureInfo("en-US"); Thread.CurrentThread.CurrentCulture = cultureInfoEnglisch; Thread.CurrentThread.CurrentUICulture = cultureInfoEnglisch; } Listing 2.41: Kulturinformationen werden für den aktuellen Thread auf en-US umgestellt.
Wenn Sie jetzt Ihre Applikation starten, werden Sie sehen, dass sich die Mühe auch gelohnt hat. Die passende Ressourcendatei wird von der Applikation angezogen, und die Ausgabe am Bildschirm erscheint jetzt in englischer Sprache.
Abbildung 2.25: Die lokalisierte Anwendung bei englischer Spracheinstellung
Sowohl der Zugriff auf Ressourcen über ein Objekt vom Typ ResourceManager als auch das Lokalisieren einer Applikation mit dem LocBaml-Tool haben es in sich. Aber beide Ansätze sind gerade in größeren Applikationen schon jetzt gern genommene Mittel, um grafische Benutzeroberflächen in unterschiedlichen sprachlichen und kulturellen Regionen passend anzuzeigen. Da Sie jetzt wissen, wie Sie Lokalisierung technisch umsetzen sollen, werde ich noch ein paar Denkanstöße hinsichtlich der GUI-Entwicklung im Hinblick auf Lokalisierung geben.
77
Kapitel 2
Um die Unterstützung der Lokalisierung API und LocBaml zu erhalten, schreiben Sie Ihre GUI in XAML. Vermeiden Sie die Verwendung von absoluter Positionierung und festgelegten Größen der Elemente. Elemente sollten ihre Größe mit SizeToContent oder mit automatischer Höhen- und Breitenangabe definieren. Bei der Vermeidung von absoluter Positionierung sollte der Einsatz des CanvasElementes wegfallen. Wenn Sie Einzüge zur Positionierung von Elementen verwenden, geben Sie dem Margin immer etwas mehr Platz, denn lokalisierte Elemente könnten den Platz vielleicht gut gebrauchen. In TextBlock-Elementen sollten Sie das TextWrapping auf Wrap einstellen, damit keine Texte aus lokalisierten Ressourcen abgeschnitten werden. Verändern Sie nicht die Uid-Eigenschaften Ihrer Elemente, nachdem Sie Applikationen einmal lokalisiert haben.
2.6 Vorlagen In Windows Presentation Foundation sind Vorlagen fast schon allgegenwärtig, und auch die Formen von Templates sind sehr mannigfaltig. Sie dienen dazu, das standardmäßige Aussehen und Verhalten von Elementen zu definieren und gegebenenfalls an einer zentralen Stelle abzulegen. Zusätzlich zu Aussehen und Verhalten können Sie mit Ressourcen, die in XAML abgelegt werden, Teile, die Sie immer wieder brauchen werden, zentral zur Verfügung stellen und immer wieder darauf zurückgreifen.
2.6.1 Ressourcen In der Definition von aufwendigen Benutzeroberflächen kommen Entwickler oder Designer immer wieder an den Punkt, an dem sie feststellen, dass Eigenschaften vieler Steuerelemente den gleichen Wert besitzen sollen. Dazu zählen sowohl die Schriftart und Schriftgröße als auch weitere Eigenschaften wie Farbe oder Inhalt, die konsistent über eine Reihe von Elementen hinweg definiert werden müssen. Natürlich können Sie jedem Element diese Werte als Attribute hart codiert zuweisen, aber was, wenn sich diese Werte über alle Steuerelemente hinweg konsistent ändern sollen? Suchen und Ersetzen ist in diesem Fall nur in sehr kleinen Projekten eine passende Unterstützung und auch dann noch mit einiger Arbeit verbunden. Um Elemente und Eigenschaften zu definieren, die Sie durch einen Platzhalter über das gesamte XAML-Dokument hinweg weiterverwenden können, führt WPF das Kon-
78
Windows Presentation Foundation
zept der Ressourcen ein. Jedes XAML-Element kommt mit einer Sammlung von Ressourcen, die für diesen Elementtyp sowohl Styling als auch weitere Elemente an einer zentralen Stelle definieren. Ressourcen, die innerhalb von untergeordneten Elementen abgelegt werden, nennt man lokale Ressourcen. Wollen Sie Ressourcen als globale Grundlage definieren, müssen Sie die Ressourcen im Root-Element bestimmen. Um einen ersten Einblick in die Verwendung von Ressourcen zu gewähren, soll das XAML-Dokument in Listing 2.42 innerhalb eines Root-Elements eine Farbe für eine SolidColorBrush festhalten und ein Image mit einem zugehörigen Pfad bestimmen. Listing 2.42: Zentrale Ressource, die im ganzen Kontext des Fensters zur Verfügung steht
In der Sektion für Ressourcen in einem Window-Element werden die Ressourcen definiert, die nicht im Standardnamensraum der Windows Presentation Foundation vorhanden sind. Im Window-Element ist dafür standardmäßig der Namensraum für die Verwendung von ressourcenspezifischen Attributen eingebunden. http://schemas.microsoft.com/winfx/2006/xaml
Das Präfix x steht dabei dem Key-Attribut voran, um Ressourcen über eine Zeichenkette wieder auffindbar zu machen. Elemente, die im Ressourcenabschnitt verwendet werden sollen, müssen einen eindeutigen Schlüssel im Key-Attribut ablegen. Um auf definierte Ressourcen zuzugreifen, bietet WPF eine statische und eine dynamische Alternative an. Die statische Alternative findet ihre Verwendung, wenn die
79
Kapitel 2
benutzte Ressource sich zur Laufzeit des Projektes nicht verändert. Ein Beispiel für die Verwendung einer statischen Ressource ist im obigen Beispiel für die Angabe der BorderBrush des Button-Elements zu sehen. BorderBrush="{StaticResource blaueBuerste}"
Die geschweiften Klammern deuten dabei darauf hin, dass es sich bei einer statischen Ressource um eine sogenannte XAML Extension handelt. XAML Extensions bieten die Möglichkeit, Konzepte wie Datenbindung oder Zuweisung von Ressourcen abzuarbeiten, was normalerweise nur mit .NET-Quellcode und nicht deklarativ möglich wäre. Wenn eine Ressource zur Laufzeit verändert werden kann, ohne dass dieser Veränderung ein automatisches und erneutes Zeichnen (RePaint) der Benutzeroberfläche folgt, so sollte sie dynamisch angewandt werden. Mehr zur Verwendung dynamischer Ressourcen im Abschnitt »Dynamische Zuweisung von Ressourcen«. Bei der Verwendung von Ressourcen ist weiter zu beachten, dass diese vor ihrer ersten Verwendung in einem Element abgelegt werden müssen. Nach getaner Arbeit sollte der Button verschachtelt im Grid sein und Window wie in Abbildung 2.26 aussehen.
Abbildung 2.26: Eine zentrale Ressource angewandt in einem Button
First come – first serve? In den einführenden Erläuterungen haben wir festgestellt, dass alle Windows Presentation Foundation-Steuerelemente einen eigenen Abschnitt für Ressourcen definieren können, die entweder direkt diesem Element oder untergeordneten Steuerelementen angehören. Dabei muss noch ein Blick auf das Verhalten von untergeordneten Elementen mit eigenen Ressourcen geworfen werden.
80
Windows Presentation Foundation
Intuitiv ist es eingängig, gesetzt den Fall, ein Element wie das Grid in Listing 2.42 definiert eigene Ressourcen, die für das komplette Grid mit einem Schlüsselwort zur Verfügung stehen. Zusätzlich dazu kann ein Element noch Ressourcen eines übergeordneten Elementes überschreiben. Definiert das Grid eine Ressource des übergeordneten Elementes mit gleichem Schlüssel, aber anderem Wert erneut, so wird für die in der Hierarchie untergeordneten Elemente die neu definierte Ressource herangezogen. In Listing 2.42 definiert das Window-Element für seine untergeordneten Elemente eine Farbe durch einen SolidColorBrush mit dem Schlüssel blaueBuerste. Legt das Grid einen eigenen Ressourcen-Abschnitt an und definiert zu genau dem Schlüssel blaueBuerste einen neuen Wert, dann wird die lokal definierte Ressource der übergeordneten Ressource vorgezogen. Listing 2.43: Überlagerung durch lokale Ressource
War die Umrandung des Buttons im vorangegangenen Listing 2.42 noch blau eingefärbt, so ist sie jetzt in Listing 2.43 durch die Überlagerung der Ressource im Grid zu Rot übergegangen. Programmatisch müssen Sie sich dazu keine Gedanken machen, da die Auswahl der anzuwendenden Ressourcen über die Resources-Kollektion des jeweiligen Elementes ablaufen muss (Listing 2.44). class ResourcesDemoWindow : Window { public ResourcesDemoWindow() { //Ressource des abgeleiteten Windows SolidColorBrush buersteFenster = new SolidColorBrush(Colors.Blue); this.Resources.Add("blaueBuerste", buersteFenster); //Ressource des Grids mit roter Bürste Grid ResourcesGrid = new Grid(); SolidColorBrush buersteGrid = new SolidColorBrush(Colors.Red); ResourcesGrid.Resources.Add("blaueBuerste", buersteGrid); string pfad = AppDomain.CurrentDomain.BaseDirectory; Image imageChips = new Image(); //Achtung! Bilderordner muss im Anwendungsverzeichnis liegen BitmapImage bitmapImageChips = new BitmapImage(new Uri(pfad + @"\Bilder\chips.jpg"));
81
Kapitel 2 imageChips.Source = bitmapImageChips; ResourcesGrid.Resources.Add("chips", imageChips); this.Content = ResourcesGrid; Button buttonBild = new Button(); ResourcesGrid.Children.Add(buttonBild); //Brush mit Ressource des Fensters und Content mit Resssource aus //dem Grid buttonBild.BorderBrush = (SolidColorBrush) this.Resources["blaueBuerste"]; buttonBild.Content = (Image)ResourcesGrid.Resources["chips"]; } } Listing 2.44: Ressourcen in Window und Grid
In Listing 2.44 wird die Zuweisung der Eigenschaften direkt an ein Element geheftet, indem das Element und seine Resources-Kollektion angegeben wird. So wird der Content des Buttons mit der Anweisung buttonBild.Content = (Image)ResourcesGrid.Resources["chips"];
automatisch an die Ressource aus dem Grid gebunden; selbst wenn das dem Grid übergeordnete Element ein Image als Ressource mit dem Key chips hätte, würde das keine Rolle für den Button spielen. Wollen Sie aber auf die Dynamik nicht verzichten. können Sie eine andere Methode aus der Oberklasse FrameworkElement verwenden, die es möglich macht, dass auch Ressourcen übergeordneter Elemente in die Suche nach einer bestimmten Ressource mit einbezogen werden. Dabei gilt es aber Vorsicht walten zu lassen, denn im obigen Beispiel werden die Zuweisungen der Eigenschaften über die Resources-Kollektion vor dem fertigen Aufbau der Vater-Kind-Beziehungen der Elemente vorgenommen. In XAML passiert das Ausarbeiten dieser Beziehung schon durch die Hierarchie der Elemente. In .NET-Code wird diese Hierarchie erst durch das Setzen der ContentEigenschaft oder das Hinzufügen von Kindelementen in Panels erstellt. Um über die Methode FindResource eine Ressource zuerst im aufrufenden Element und dann in übergeordneten Elementen zu suchen, muss die Baumstruktur der Elemente schon hergestellt sein! Im nun folgenden Codebeispiel leitet sich eine Klasse von Window ab, und auf einem untergeordneten Grid wird die FindResource-Methode aufgerufen: class ResourcesDemoWindow : Window { public ResourcesDemoWindow() { //Ressource des abgeleiteten Windows SolidColorBrush buersteFenster = new SolidColorBrush(Colors.Blue);
82
Windows Presentation Foundation this.Resources.Add("blaueBuerste", buersteFenster); //Ressource des Grids mit roter Bürste Grid ResourcesGrid = new Grid(); SolidColorBrush buersteGrid = new SolidColorBrush(Colors.Red); //Auskommentiert, um den Sucheffekt von FindResource zu zeigen //ResourcesGrid.Resources.Add("blaueBuerste", buersteGrid); string pfad = AppDomain.CurrentDomain.BaseDirectory; Image imageChips = new Image(); //Achtung! Bilderordner muss im Anwendungsverzeichnis liegen BitmapImage bitmapImageChips = new BitmapImage(new Uri(pfad + @"\Bilder\chips.jpg")); imageChips.Source = bitmapImageChips; ResourcesGrid.Resources.Add("chips", imageChips); //Vor der Suche mit FindResource Fertigstellen der Hierarchie this.Content = ResourcesGrid; Button buttonBild = new Button(); ResourcesGrid.Children.Add(buttonBild); buttonBild.BorderBrush = (SolidColorBrush)ResourcesGrid.FindResource("blaueBuerste"); buttonBild.Content = (Image)ResourcesGrid.Resources["chips"]; } } Listing 2.45: Programmatische Zuweisung von Ressourcen an Elemente
Bleibt die Zeile zum Hinzufügen des Schlüssel-Werte-Paares, wie in Listing 2.45 auskommentiert, findet die FindResource-Methode, aufgerufen auf der Instanz des Grids, keine passende Ressource in der Resources-Kollektion des Grid. Sie sucht aber dann im übergeordneten Window-Objekt weiter und wird fündig. Die Umrandung des Button wird blau eingefärbt. Bitte achten Sie auf die Reihenfolge der Zuordnung der Elemente untereinander, bevor die FindResource-Methode aufgerufen wird.
Dynamische Zuweisung von Ressourcen Wie schon im vorangegangenen Abschnitt erwähnt, werden die Ressourcen in statische und dynamische Ressourcen unterteilt. Falls eine Ressource zur Laufzeit verändert werden kann, so sollte sie als dynamisch verwendet werden. Das Verändern zur Laufzeit bezieht sich dabei auf eine Veränderung der Ressource, die kein automatisches RePaint der Benutzeroberfläche nach sich zieht. Wollen Sie etwa über das ClickEvent eines Buttons das Verändern der Ressource eines Elementes ändern, ist es sogar besser, die statische Variante zu wählen, um Systemressourcen zu sparen.
83
Kapitel 2
Frage & Antwort Jetzt stellt sich noch die Frage, wann der Wert, der in einer dynamischen XAML-Extension definiert wurde, verändert wird, ohne dass ein Event aus der Anwendung heraus gefeuert werden muss? Eine der Antworten liefert Ihnen die Einstellung Ihres Systems. Änderungen der Systemeinstellungen haben nicht zwangsläufig ihren Ursprung in einem Event der WPF-Anwendung. Dem Button aus den vorangegangenen Beispielen (z.B. Listing 2.43) wird nun die Farbgebung seiner Umrandung durch die Systemeinstellungen vorgegeben.
Beim Start der Applikation wird die Umrandung des Buttons auf den RessourceKey der SolidColorBrush gesetzt, die für die Farbe in der Titelleiste des aktiven Fensters zuständig ist (SystemColors.ActiveCaptionBrushKey). Wenn Sie nun zur Laufzeit das Farbschema der Anzeige ändern, wird sich auch die Farbe der Umrandung des Buttons Ihrer Auswahl dynamisch anpassen, obwohl kein explizites Neuzeichnen der grafischen Benutzeroberfläche durch zum Beispiel das Abarbeiten einer Ereignisbehandlungsmethode eingeleitet wurde.
Abbildung 2.27: Änderung der Darstellung zur dynamischen Anpassung von Ressourcen
84
Windows Presentation Foundation
2.6.2 Styles In den Ressourcen haben Sie bereits erfahren, wie Sie das Layout von Elementen an einer zentralen Stelle definieren können. Bis zum jetzigen Zeitpunkt muss aber jede Eigenschaft, die später angewandt werden soll, auch einzeln als Ressource festgelegt werden. Um mehrere Eigenschaften, die zusammen konsistent und an einer zentralen Stelle abgelegt werden sollen, später mit einer einzigen Zeile Markup an ein bestimmtes Element zuzuweisen, können Sie in WPF die sogenannten Styles verwenden. Mit diesen einführenden Worten im Hinterkopf wird wahrscheinlich ein Vergleich mit Cascading Style Sheets nicht ausbleiben. Und ja, Styles erfüllen die Aufgaben, die CSS-Dateien in der Webentwicklung übernehmen, darüber hinaus bekommen Sie mit Styles die Möglichkeit, die im Style festgelegten Eigenschaften in Abhängigkeit von Events und anderen Eigenschaften in XAML zu verändern. Analog zu den Ressourcen können auch Styles sowohl lokal für Elemente als auch global für die gesamte XAML-Hierarchie geregelt werden. Die wichtigsten Eigenschaften der Style-Klasse sind Setter und EventSetter. Ein Setter-Element legt in seinen Attributen namens Property und Value zu einer abhängigen Eigenschaft einen bestimmten Wert fest. Dies soll in Listing 2.46 an einem einfachen Beispiel erstmals deutlich gemacht werden: Email: Passwort: Verbinden Listing 2.46: Lokal definiertes Styling in einem StackPanel
Im Abschnitt StackPanel.Style wird über einen Setter die FontSize für die Steuerelemente des StackPanels gesetzt. Natürlich bin ich als neugieriger Entwickler auf die Idee gekommen, das Property im Setter-Element so zu bearbeiten, dass der Value vielleicht nur auf alle Button-Elemente im StackPanel angewandt wird:
85
Kapitel 2
Das Ergebnis hat sich aber zur Laufzeit nicht verändert. Das explizite Festlegen von Styles auf eine bestimmte Klasse von Elementen wird aber bald in diesem Abschnitt ein Thema sein. Um Styles global für alle untergeordneten Elemente anzubieten, können Sie in den Ressourcen des Root-Elementes mehrere Style-Abschnitte definieren und diesen auch einen eindeutigen Schlüssel zuweisen oder einfach die Art Element festlegen, für das der Style ausgewertet werden soll. Email: Passwort: Verbinden Listing 2.47: Im Wurzelelement wurde ein Style als globale Ressource definiert, der in allen untergeordneten Elementen verwendet werden kann.
Im XAML-Dokument in Listing 2.47 werden zwei Style-Abschnitte in den Ressourcen des Window-Elements deklariert und mit Werten für ausgesuchte Properties ausstaffiert. Um einen der Styles einem Control zuzuordnen, muss einfach der Schlüssel des Styles in eine XAML-Extension als statische oder dynamische Ressource angegeben werden. Für den Unterschied zwischen statischen und dynamischen Ressourcen sei auf den vorangegangenen Ressourcen-Abschnitt hingewiesen. Natürlich gilt hier, wie schon bei den Ressourcen näher erläutert, dass lokal definierte Styles die global abgelegten Styles überlagern und auf die lokalen Elemente angewandt werden. Properties, die schon in einem Style definiert wurden, können dennoch direkt in den Attributen der Elemente neu zugewiesen werden, auch wenn genau diesem Property schon durch einen Style ein Wert zugeordnet wurde. Schauen Sie sich dazu einfach den Button noch einmal etwas genauer an. Zusätzlich zu der Idee, dass ein Style auf jegliches Element angewandt werden kann, indem einfach
86
Windows Presentation Foundation
die zugehörige Ressource über XAML-Extension angeheftet wird, kann ein Style für eine bestimmte Klasse von Elementen vereinbart werden. Dazu muss im Style einfach noch zusätzlich der TargetType angegeben werden. Vielleicht ist Ihnen aufgefallen, dass im XAML-Dokument in Listing 2.47 nur die beiden Textfelder an den Style hoehen angebunden wurden. Das gleiche Ergebnis hätte auch erreicht werden können, ohne die StaticResource XAML-Extension in den beiden Textfeldern einzusetzen, indem die Klasse TextBox als TargetType definiert worden wäre. Listing 2.48: TextBox als Ziel des Stylings
Mit dem Attribut TargetType können die im Style untergeordneten Setter und deren Werte an eine bestimmte Klasse von Elementen gebunden werden, ohne im weiteren Verlauf eine Anbindung an eine Ressource nötig zu machen. Kombiniert mit dem TargetType liefern die beiden Textfelder in Abbildung 2.28 genau dieselbe Benutzeroberfläche wie das Styling der TextBox-Elemente in Listing 2.47.
Abbildung 2.28: Globale Styles und der TargetType, angewandt auf Elemente
Programmatisch wird dieses Beispiel schon etwas unüberschaubarer, und vor allem sollten Sie Ihr Augenmerk kurz auf den Zieldatentyp der zweiten Style-Instanz setzen. Intuitiv würden Sie wohl einfach dem Aufbau aus dem XAML-Dokument folgen und wie schon bei den Ressourcen in der richtigen Reihenfolge aggregieren. class GlobaleStylesDemo : Window { public GlobaleStylesDemo() {
87
Kapitel 2 //Ressource des abgeleiteten Windows Style styleGlobal = new Style(); this.Resources.Add("styleGlobal", styleGlobal); styleGlobal.Setters.Add(new Setter(Control.FontSizeProperty, 16.0)); styleGlobal.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Green))); styleGlobal.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.White))); styleGlobal.Setters.Add(new Setter(Control.MarginProperty, new Thickness(10, 10, 150, 0))); Style styleHoehen = new Style(); styleHoehen.TargetType = typeof(TextBox); styleHoehen.Setters.Add(new Setter(Control.HeightProperty, 30.0)); styleHoehen.Setters.Add(new Setter(Control.MarginProperty, new Thickness(10, 10, 50, 0))); this.Resources.Add(styleHoehen.TargetType, styleHoehen); StackPanel stackPanelVerbinden = new StackPanel(); this.Content = stackPanelVerbinden; Label labelEmail = new Label(); labelEmail.Content = "Email:"; stackPanelVerbinden.Children.Add(labelEmail); TextBox textBoxEmail = new TextBox(); stackPanelVerbinden.Children.Add(textBoxEmail); labelEmail.Style = (Style)labelEmail.FindResource("styleGlobal"); Label labelPasswort = new Label(); labelPasswort.Content = "Passwort:"; stackPanelVerbinden.Children.Add(labelPasswort); labelPasswort.Style = (Style)labelPasswort.FindResource("styleGlobal"); TextBox textBoxPasswort = new TextBox(); stackPanelVerbinden.Children.Add(textBoxPasswort); Button buttonSenden = new Button(); stackPanelVerbinden.Children.Add(buttonSenden); buttonSenden.Style = (Style)buttonSenden.FindResource("styleGlobal"); buttonSenden.Height = 30; buttonSenden.Width = 100; buttonSenden.Content = "Versenden"; } } Listing 2.49: Global verwaltetes Styling in C#-Quellcode
88
Windows Presentation Foundation
Leider ist das Hinzufügen des TargetTypes zu einem Style noch nicht der alleinige Ansatz, der verfolgt werden muss. Um den Style auch auf alle untergeordneten Textfelder ohne eine XAML-Extension, also sozusagen automatisch, anzuwenden, müssen den Ressourcen noch der TargetType und der Style als Schlüssel-Werte-Paar hinzugefügt werden. this.Resources.Add(LoginHoehen.TargetType, LoginHoehen);
Für etwas Verwirrung könnte zur Laufzeit eine Fehlermeldung die Height-Eigenschaft betreffend sorgen, wenn Sie die Höhenangabe nicht wirklich als Fließkommazahl angeben und sich auf eine implizite Typkonvertierung verlassen: LoginHoehen.Setters.Add(new Setter(Control.HeightProperty, 30.0));
> >
>
HINWEIS
Bitte geben Sie den Wert wirklich als Fließkommazahl an, denn aus irgendwelchen Gründen kann im aktuellen SDK eine Ganzzahl im Konstruktor der Setter-Klasse nicht implizit zu einer Fließkommazahl konvertiert werden. Vielleicht ist diese kleine Unebenheit schon in einer der nächsten SDK-Versionen Schnee von gestern.
2.6.3 Trigger Um auf Events der grafischen Benutzoberfläche zu reagieren, haben Sie weiterhin, wie in Abschnitt 2.2.2 schon kennengelernt, die Möglichkeit, in der Code-Behind-Datei eine Ereignisbehandlungsroutine zu implementieren. Zusätzlich zu diesem Ansatz können Sie in XAML die sogenannten Trigger verwenden. Trigger erlauben es Ihnen, in Abhängigkeit von bestimmten Ereignissen die Eigenschaften eines Elementes zu verändern, Animationen zu starten oder Daten an Elemente zu binden. Trigger sind im Gegensatz zu Eventhandlern einfacher zu erstellen und werden vor allem von Tools wie Microsoft Expression Blend eingesetzt, um den Designern so wenig wie möglich objektorientierten Code zuzumuten. Trigger verhalten sich konditional, sind also an eine Bedingung gehaftet, bei deren Eintreten festgelegte Aktionen ausgeführt werden. Somit stellen Trigger einen if-else-Ansatz im Markup dar.
89
Kapitel 2 Links Mitte Rechts Listing 2.50: Trigger, der das Styling durch die Maus über den Elementen verändert
Abhängig davon, ob die Maus sich über einem der drei Buttons befindet, werden mit dem Trigger im Style, dessen TargetType die Klasse Button ist, Setter auf einen Button angewandt. Fast wie bei if-else wertet der Trigger aus, ob ein Zustand eingetreten ist, und setzt dann die in den Settern beschriebenen Werte für die zugehörigen Eigenschaften. Das Property, auf das der Trigger angesetzt wird, ist in diesem Fall das IsMouseOver-Property des Buttons. Befindet sich die Maus über dem Button, wird dessen Farbgebung invertiert, eine neue Farbe für die Umrandung gesetzt, und der Button wird verbreitert. Beachten Sie dabei bitte, dass bei der Änderung der Breite im StackPanel die Elemente der Flussrichtung folgend verschoben werden.
Abbildung 2.29: Trigger im Einsatz
Wird das Property des Triggers auf True ausgewertet, so ändert sich das Erscheinungsbild des Buttons, über dem sich zu diesem Zeitpunkt die Maus befindet. Verlässt die Maus das zugeordnete Element wieder, so werden die Eigenschaften des Elements wieder auf die Werte zurückgestellt, die vor dem Eintreten des Ereignisses im Trigger für das Element definiert waren. Der Code-Block in Listing 2.51 soll zeigen, wie Sie das Ergebnis auch mittels Code erarbeiten können. Style buttonStyle = new Style(); buttonStyle.TargetType = typeof(Button); buttonStyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Blue))); buttonStyle.Setters.Add(new Setter(Control.ForegroundProperty,
90
Windows Presentation Foundation new SolidColorBrush(Colors.White))); buttonStyle.Setters.Add(new Setter(Control.WidthProperty, 50.0)); buttonStyle.Setters.Add(new Setter(Control.HeightProperty, 30.0)); Trigger triggerIsMouseOver = new Trigger(); triggerIsMouseOver.Property = Button.IsMouseOverProperty; triggerIsMouseOver.Value = Boolean.Parse("True"); triggerIsMouseOver.Setters.Add(new Setter(Button.BackgroundProperty, new SolidColorBrush(Colors.White))); triggerIsMouseOver.Setters.Add(new Setter(Button.ForegroundProperty, new SolidColorBrush(Colors.Blue))); triggerIsMouseOver.Setters.Add(new Setter(Button.WidthProperty, 100.0)); triggerIsMouseOver.Setters.Add(new Setter(Button.BorderBrushProperty, new SolidColorBrush(Colors.Blue))); buttonStyle.Triggers.Add(triggerIsMouseOver); this.Resources.Add(buttonStyle.TargetType, buttonStyle); Listing 2.51: Trigger in ihrer programmatischen Darstellung in C#
Auch der Quellcode in Listing 2.51 gestaltet sich wieder sehr eingängig. Die Stelle, an welcher der Trigger instanziert wird, soll aber noch kurz unsere Aufmerksamkeit erhalten. Trigger triggerIsMouseOver = new Trigger(); triggerIsMouseOver.Property = Button.IsMouseOverProperty; triggerIsMouseOver.Value = Boolean.Parse("True");
Das Property des Triggers muss auf eine abhängige Eigenschaft gesetzt werden und der Value auf einen booleschen Wert. Das Hinzufügen der Setter für die Definition des Styles, der beim Auslösen des Triggers auf die zugehörigen Elemente ausgewertet werden soll, geschieht diesmal über die Setter-Kollektion des Triggers.
2.7 Datenbindung in WPF Bei den anfänglichen Recherchen zu Datenbindung in WPF waren die ersten Worte, die ich dazu gelesen habe, DataBinding in WPF rocks, und diesem Statement möchte ich mich ein wenig anschließen, um im nun kommenden Abschnitt einen kleinen Einblick in die Datenbindung mit der Windows Presentation Foundation zu gewähren. In der Erstellung einer grafischen Benutzeroberfläche ist Datenbindung ein Mechanismus, um Steuerelemente und deren Inhalt mit Daten zu verbinden. Diese Daten sollen angezeigt und bei Bedarf in den Steuerelementen verändert werden können. Diese Veränderungen sollen gegebenenfalls auch wirklich in dem physischen Datenbestand übernommen werden, ohne dass dafür eine Ereignisbehandlung für die gerade vollzogene Änderung implementiert werden muss.
91
Kapitel 2
Die Vielzahl der Steuerelemente und die unterschiedlichen Quellen, aus denen die Daten dabei stammen können, spiegelt das Ausmaß der Aussichten wider, die Datenbindung mit WPF den Entwicklern und Designern gibt. Der Inhalt eines Elementes kann ganz einfach an die Auswahl oder den Wert, der in einem anderen Element festgelegt wird, gebunden sein. Zu dieser sehr einfachen Variante kommt die Bindung an schwergewichtige Objekte, wie eine Kollektion oder ein DataSet. Datenbindung stellt sozusagen einen bidirektionalen Pfad zwischen der Datenquelle und dem Ziel dar. Das Ziel muss dabei eine abhängige Eigenschaft des Elementes sein, das datengebunden werden soll. Alle abhängigen Eigenschaften der Elemente vom Typ UIElement, die nicht als read-only deklariert wurden, unterstützen das Konzept der Datenbindung. Die Quelle einer Datenbindung in der Windows Presentation Foundation können zusätzlich zu einer Eigenschaft eines anderen Elementes sowohl ein CLR-Objekt als auch Daten im XML-Format sein. Eine Datenbindung kann somit ADO.NET-Objekte, wie das DataSet, Kollektionen, Knoten aus einem XML-Dokument oder auch XML Web Services als Bezugspunkt festlegen. Die Datenbindung kann entweder nur von der Quelle in Richtung des Ziels und umgekehrt oder auch in beide Richtungen zustande kommen. In welche Richtung die Daten schließlich fließen, wird von der Eigenschaft Mode der Datenbindung definiert. Für den Datenfluss gibt es verschiedene Modi: TwoWay: Ein Update der Quelle oder des Ziels findet statt. Ändert sich etwas an der Datenquelle, wird das Ziel geupdatet und umgekehrt. OneWay: Ein Update der Zieleigenschaft wird angestrebt, wenn eine Änderung der Datenquelle auftritt. OneTime: Beim Start der Applikation wird das Ziel genau einmal mit der Quelle verbunden, und weitere Änderungen werden ignoriert. OneWayToSource: Änderungen, die im Ziel der Datenbindung auftreten, werden an die Datenquelle übermittelt.
2.7.1 Datenbindung zwischen zwei grafischen Elementen Als einfachste Art der Datenbindung stellt sich die Möglichkeit dar, den Wert eines Elementes an den Wert eines anderen Elementes zu binden. Lassen Sie dazu zuerst einmal das simple Beispiel aus Listing 2.52 auf sich wirken.
92
Windows Presentation Foundation Listing 2.52: Eine einfache Datenbindung zwischen zwei UIElement-Objekten
In Listing 2.52 ist die TextBox über eine XAML-Extension deklarativ an den Inhalt des ausgewählten Elements der ComboBox gebunden. Die XAML-Erweiterung namens Binding hat die Aufgabe, den Wert einer Eigenschaft über einen datengebundenen Wert zu setzen. Dazu wird als eine Art Vermittler eine Zeichenkette als Anweisungsobjekt definiert, das zur Laufzeit im Kontext der Datenzugehörigkeit ausgewertet und auf das zugehörige Attribut angewandt wird. In diesem einfachen, aber äußerst wertvollen Beispiel finden die Attribute ElementName und Path der XAML-Erweiterung Binding ihren Einsatz. In der Eigenschaft ElementName kann ein anderes Steuerelement angegeben werden, für das eine Bindung an dessen Eigenschaften oder Inhalt definiert werden soll. Die Path-Eigenschaft gibt in diesem Beispiel eine Eigenschaft des Elements an, das in ElementName näher spezifiziert wurde. Hier können einzelne Eigenschaften und auch deren aggregierte Eigenschaften angesprochen werden. Sogar Indexer von Objekten können in eckigen Klammern für die Datenbindung eingesetzt werden. Die Frage, die sich in dieser Datenbindung noch stellt, ist, welcher Modus implizit auf die Datenquelle und das Ziel angewandt wird. Dazu soll der Screenshot des laufenden Betriebs in Abbildung 2.30 Aufschluss geben.
Abbildung 2.30: Eine einfache Datenbindung zwischen zwei Elementen im TwoWay Modus
93
Kapitel 2
In beiden Controls der Abbildung zu einfacher Datenbindung ist als Inhalt Ladies und Gentleman zu lesen. Denken Sie noch einmal an das XAML-Dokument aus Listing 2.52 zurück! Ein Inhalt Ladies und Gentleman ist in keinem der Elemente definiert, und die ComboBox lässt keine Eingabe per Tastatur zu. Das kann also nur bedeuten, dass bei einer Auswahl eines Items in der ComboBox und einer darauffolgenden Eingabe in der TextBox der eingegebene Wert an die Quelle, also die ComboBox, übermittelt und dort persistiert wird. Der Modus der Datenbindung zwischen den beiden Elementen ist, wie unschwer zu erkennen, der TwoWay-Mode. In diesem Fall ändern sich die Rollen der Datenbindung bei Bedarf. Die Quelle kann zum Ziel werden, wenn dabei das vormalige Ziel zur Quelle wird. Wie war das noch einmal mit dem Berg und dem Propheten? Aber woher erfahren die Elemente untereinander, dass eine Änderung stattgefunden hat und der Berg jetzt doch zum Propheten kommen muss? Für das Benachrichtigen der in Relation stehenden Elemente ist ein Trigger zuständig. Die UpdateSourceTrigger-Eigenschaft des Bindings ermittelt, durch welche Konstellation das gegenseitige Update ausgelöst wird, je nachdem, in welchem Modus die Datenbindung gerade läuft. Die zulässigen Werte des UpdateSourceTriggers sind: Explicit: Veränderungen der gebundenen Datenquelle werden nur bei einem programmatischen Aufruf der UpdateSource-Methode weitergegeben. LostFocus: Die Quelle der Datenbindung erfährt ein Update, wenn das Ziel der Datenbindung den Fokus verliert. PropertyChanged: Die Quelle der Datenbindung erfährt ein Update, sobald im Ziel der Datenbindung eine gebundene Eigenschaft verändert wird. Default: Der Standardwert des UpdateSourceTriggers wird von den jeweiligen Elementen vorgegeben, die gerade das Ziel der Datenbindung sind. Meist ist das der Wert PropertyChanged. Für Textfelder ist der Wert für UpdateSourceTrigger allerdings LostFocus. Eine kleine Änderung in der TextBox in Bezug auf die UpdateSourceTrigger-Eigenschaft des Bindings, und schon wird bei jeder Änderung im Textfeld auch der Eintrag in der ComboBox verändert: Listing 2.53: Update der Datenquelle bei jeder Änderung der gebundenen Eigenschaft.
94
Windows Presentation Foundation
Die Datenbindung zwischen den Elementen kann alleine durch Quellcode ebenso gut herbeigeführt werden. Eine Datenbindung mit einem ausdrücklichen OneWay-Modus könnte in C# wie in Listing 2.54 implementiert werden. StackPanel stackPanelDatenbindung = new StackPanel(); this.Content = stackPanelDatenbindung; Label labelDatenbindung1 = new Label(); labelDatenbindung1.Content = "Auswahl, an die gebunden wird"; ComboBox comboBoxQuelle = new ComboBox(); comboBoxQuelle.Text = "Datenbindung"; comboBoxQuelle.Items.Add("Herr"); comboBoxQuelle.Items.Add("Frau"); comboBoxQuelle.Items.Add("Sehr geehrte Damen und …"); comboBoxQuelle.Margin = new Thickness(10.0, 10.0, 10.0, 30.0); Label labelDatenbindung2 = new Label(); labelDatenbindung2.Content = "Ergebnis der Bindung"; TextBox textBoxZiel = new TextBox(); textBoxZiel.Margin = new Thickness(10.0, 10.0, 10.0, 30.0); Binding bindingEinfacheBindung = new Binding(); bindingEinfacheBindung.Mode = BindingMode.OneWay; bindingEinfacheBindung.Source = comboBoxQuelle; bindingEinfacheBindung.Path = new PropertyPath(ComboBox.SelectedValueProperty); textBoxZiel.SetBinding(TextBox.TextProperty, bindingEinfacheBindung); Listing 2.54: Eine einfache programmatische Datenbindung zwischen zwei Elementen
Der wichtigste programmatische Teil in Listing 2.54 ist dabei die Verknüpfung zwischen Quelle und Ziel der Datenbindung über ein Binding-Objekt. In der Pfadangabe für die Bindung muss festgelegt werden, welche abhängige Eigenschaft eines Quellelements gebunden werden soll. Im Textfeld muss dann über die SetBinding-Methode die Eigenschaft der TextBox angegeben werden, die als Ziel einer Bindung dienen soll und explizit mit übergeben werden muss.
Datenbindung an Kollektionen Nicht immer sind die Pfeiler der Brücke zwischen Datenquelle und deren Ziel einfache Elemente vom Typ UIElement. Zwischen UIElementen ist die Quelle der Datenbindung ein einzelnes Objekt, dessen Eigenschaften in der Binding-XAML-Erweiterung ausgewertet wird. Zusätzlich zu einzelnen Objekten ist es wünschenswert, Steuerelemente abgeleitet von ItemsControl wie die ListBox, die mehrere Unterelemente aufnehmen können, an mehrere Objekte und deren Eigenschaften in einer Auflistung anbinden zu können.
95
Kapitel 2
In WPF ist es ein Leichtes, eine Kollektion von Objekten als Datenquelle für eine Datenbindung zu definieren. Eine Kollektion kann dabei sowohl eine generische Liste oder eine benutzerdefinierte Kollektion sein, die das IEnumerable-Interface implementiert. In Zusammenarbeit mit Kollektionen treten oftmals ObjectDataProvider auf, die es ermöglichen, Methoden als Lieferanten der Kollektion anzugeben oder CLR-Typen als Quelle zu instanzieren. Listing 2.55: Datenbindung an eine Kollektion, die aus einer Rückgabe einer Methode resultiert
Im XAML-Dokument aus Listing 2.55 ist zu sehen, wie einfach es ist, eine Methode anzugeben, die eine Kollektion als Rückgabe an das Ziel einer Datenbindung heftet. Der ObjectDataProvider wird hier dazu benutzt, um einen Typ anzuführen, in dem eine Methode für die Lieferung der Daten definiert ist. Die Klasse liegt im Namensraum CollectionDataBindingDemo, der mit dem Schlüssel lokal als CLR-Namensraum in der Eigenschaft ObjectType des ObjectDataProviders referenziert wird: xmlns:lokal="clr-namespace:CollectionDataBindingDemo"
Die Methode selbst wird über die Eigenschaft MethodName des ObjectDataProviders angegeben und muss in dem Typ aus dem Attribut ObjectType zur Verfügung stehen. Die ComboBox setzt dann die Quelle der Datenbindung für die ItemsSource auf die Ressource, in welcher der ObjectDataProvider abgelegt wurde. Die Klasse, welche die Daten über Methoden mit dem Rückgabewert als generische Liste beinhaltet, ist in Listing 2.56 zu sehen. public class KollektionVonMethode { public List _listAutoren = new List(); public List HoleDaten() { _listAutoren.Add("Rouven Haban"); _listAutoren.Add("Jürgen Kotz");
96
Windows Presentation Foundation _listAutoren.Add("Simon Steckermeier "); return _listAutoren; } public List HoleDaten(int anzahl) { if (anzahl > 2) { _listAutoren.Add("Rouven Haban"); _listAutoren.Add("Jürgen Kotz"); _listAutoren.Add("Simon Steckermeier "); for (int i = 0; i < anzahl - 3; i++) { _listAutoren.Add("Ghost Writer"); } } return _listAutoren; } } Listing 2.56: Die Klasse, welche die Kollektion zur Datenbindung liefert
In Listing 2.56 werden zwei überladene Methoden als Quelle für die Datenbindung angeboten. Wenn die Daten in Abhängigkeit von bestimmten Übergabewerten ermittelt werden sollen, können als Eigenschaften des ObjectDataProviders auch Parameter übergeben werden. 5 Listing 2.57: Methodenparameter im ObjectDataProvider für den Aufruf einer überladenen Methode.
Um in einem ObjectDataProvider Parameter an eine Methode übergeben zu können, müssen die Parameter streng typisiert mit einem Wert in der MethodParameters-Kollektion angegeben werden, und natürlich muss eine überladene Methode namens HoleDaten in der Klasse KollektionVonMethode vorhanden sein, die einen Int32-Wert als Parameter akzeptiert. Jetzt muss noch betrachtet werden, was passiert, wenn die Kollektion zur Laufzeit verändert wird, denn ob sich die Einträge in der ComboBox dabei auch automatisch ändern, steht noch in den Sternen, oder um es empirisch zu sagen, es steckt in der Implementierung des INotifyCollectionChanged-Interface. Dieses Interface schreibt das CollectionChanged-Ereignis für implementierende Klassen vor. Dieser Event sollte gefeuert werden, wenn die zugehörige Kollektion einer Änderung durch Hinzufügen oder
97
Kapitel 2
Löschen eines Eintrags unterliegt. Kurz in die Dokumentation gespickt, und schon wird klar, dass die gemeine generische Liste dieses Interface nicht implementiert und somit eine Änderung der Liste als Quelle der Datenbindung keinerlei Auswirkungen auf deren Ziel hat. Eine schon fertige Implementierung dieses Interface bietet dagegen die ObservableCollection an. Wenn Sie eine eigene Kollektion erstellen wollen, die als Quelle einer Datenbindungen auch Änderungen an das Ziel übermitteln soll, dann können Sie die Klasse als Bauplan für die benutzerdefinierte Auflistung einfach von ObservableCollection ableiten. Eine sehr einfache Implementierung, die eine Liste vom Typ String beinhalten soll, sehen Sie gleich in einem Codebeispiel (Listing 2.58). Das Hinzufügen der einzelnen Elemente als Änderung der Kollektion, die an das Ziel der Datenquelle weitergereicht werden soll, passiert in diesem Beispiel einfach durch einen Timer in dessen Tick-Ereignisbehandlung. public class GetimteKollektion : ObservableCollection { private DispatcherTimer _timer; private int _aktuellerIndex = 0; private String ItemElement; public GetimteKollektion() { _timer = new DispatcherTimer(DispatcherPriority.Background, Application.Current.Dispatcher); _timer.Interval = TimeSpan.FromMilliseconds(1000); _timer.Tick += new EventHandler(Timer_Tick); _timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { ItemElement = "Neues Item " + _aktuellerIndex.ToString(); this.Add(ItemElement); _aktuellerIndex++; } } Listing 2.58: Eine Kollektion, abgeleitet von ObservableCollection
Nach jeder Sekunde tritt eine Veränderung auf, die automatisch an das Ziel der Datenbindung weitergereicht wird. Bisher ist in Listing 2.58 aber das Ziel nicht näher spezifiziert worden, das soll jetzt aber in XAML nachgeholt werden. Zu dem Beispiel von gerade eben (Listing 2.55), in dem das Binden einer Kollektion als Rückgabe einer Methode gezeigt wird, kann ein weiterer ObjectDataProvider als Ressource hinzugefügt werden:
98
Windows Presentation Foundation
Dieses Mal wird nur der Objekttyp angegeben, der zur Laufzeit für die Datenbindung instanziert werden muss, er entspricht hier der von ObservableCollection abgeleiteten Klasse namens GetimteKollektion. Zur Laufzeit können Sie beobachten, wie die anfangs noch leere ListBox befüllt wird und jede Sekunde ein weiteres Element in der Liste auftaucht:
Abbildung 2.31: In der ListBox werden die Daten der ObservableCollection gebunden.
Durch die Implementierung von INotifyCollectionChanged werden nun Änderungen einer Kollektion, die durch das Hinzufügen oder Löschen eines Eintrags entstehen, direkt an das Ziel der Datenbindung durchgesetzt. Frage & Antwort Was aber, wenn eine Kollektion aus Objekten besteht, deren Eigenschaften als Quelle der Datenbindung innerhalb der Kollektion dienen, und die Eigenschaften irgendwelchen Änderungen unterliegen? Werden diese Änderungen auch an das Ziel der Datenquelle weitergegeben? Vorerst noch nicht, dazu müssen noch ein paar Zeilen Code geschrieben werden. Um genau zu sein, muss die Klasse der Objekte, die in der Kollektion gesammelt werden, selbst auf die Veränderung reagieren, indem sie ein bestimmtes Ereignis auslöst. Dazu muss das Interface INotifyPropertyChanged, dessen einziges Member der Event PropertyChanged vom Typ PropertyChangedEventHandler ist, implementiert werden. Bei einer Änderung in einer der Eigenschaften muss im zugehörigen Property der Event gefeuert werden. Die Signatur des Delegaten PropertyChangedEventHandler fordert als Übergabe an die referenzierte Methode einen String. Sie können dabei den Bezeichner der Property direkt angeben, um genau diese Veränderung in genau der einen Eigenschaft an das Ziel der Datenquelle zu propagieren. Unterliegen gleich mehrere Eigenschaften diesem anstehenden Änderungsvorgang, so kann der Event mit String.Empty als Übergabe ausgelöst werden, und die komplette Änderung wird an das Ziel der Datenbindung weitergeleitet.
99
Kapitel 2
Eine Klasse für Objekte, die auf Änderungen reagieren könnte, ist in Listing 2.59 implementiert. public class Buch : INotifyPropertyChanged { private DateTime _ErscheinungsTermin = System.DateTime.Now; private string _Autor; public string Autor { get { return _Autor; } set { if (_Autor != null) { _ErscheinungsTermin = System.DateTime.Now; OnPropertyChanged(String.Empty); } _Autor = value; } } public DateTime ErscheinungsTermin { get { return _ErscheinungsTermin; } set { if (_ErscheinungsTermin != null) { OnPropertyChanged("ErscheinungsTermin"); } _ErscheinungsTermin = value; } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string info) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(info)); } } Listing 2.59: Implementierung des INotifyPropertyChanged-Interface
Die Klasse Buch implementiert das INotifyPropertyChanged-Interface, indem sie den passenden Event definiert und eine einfache Methode zum Auslösen des Events zur Verfügung stellt. Das Ereignis wird in den beiden Properties ErscheinungsTermin und Autor ausgelöst.
100
Windows Presentation Foundation
Wenn der Erscheinungstermin sich nach seiner erstmaligen Initialisierung ändert, so wird diese Änderung gegebenenfalls an das Ziel der Datenquelle weitergeleitet, indem die Methode OnPropertyChanged aufgerufen wird. Ändert sich der Erscheinungstermin, wird nur der Termin an das Ziel der Datenbindung durchgereicht. Wird aber ein anderer Autor für das Buch besetzt, so verschiebt sich in der Logik der Klasse auch der Erscheinungstermin, und die Änderungen auf den beiden Eigenschaften werden an das Ziel der Datenquelle propagiert, indem die OnPropertyChanged-Methode mit String.Empty aufgerufen wird. Jetzt muss nur noch eine ObservableCollection gebaut werden, die diese Objekte aufnehmen kann. Analog zum vorhergegangenen Beispiel einer ObservableCollection, die nur Zeichenketten aufgenommen hat, sollen nun Bücher in der Kollektion abgelegt werden. public class GetimteKollektionBuecher : ObservableCollection { private DispatcherTimer _timer; public GetimteKollektionBuecher() { _timer = new DispatcherTimer(DispatcherPriority.Background, Application.Current.Dispatcher); _timer.Interval = TimeSpan.FromMilliseconds(3000); _timer.Tick += new EventHandler(Timer_Tick); this.Add(new Buch()); this[0].Autor = "Juergen Kotz"; this.Add(new Buch()); this[1].Autor = "Simon Steckermeier"; this.Add(new Buch()); this[2].Autor = "Rouven Haban"; _timer.Start(); } } Listing 2.60: Eine ObservableCollection, deren Elemente die Änderung an Eigenschaften durch die Implementierung von INotifyPropertyChanged weiterleiten
In dieser Kollektion GetimteKollektionBuecher werden drei Bücher mit zugeordneten Autoren in der Kollektion abgelegt. Der Timer ändert alle drei Sekunden den Autor des ersten Objektes und den Erscheinungstermin. Intuitiv und von dem Beispiel der ObservableCollection vom Typ String beeinflusst, könnte der findige Leser nun denken, dass er die Kollektion mit Büchern auch einfach in einem ObjectDataProvider für eine Datenbindung zur Verfügung stellen könnte.
101
Kapitel 2
> >
>
HINWEIS
Aber nur ein neuer Schlüssel und der neue Typ bringen nicht das gewünschte Ergebnis zur Laufzeit. Die Einträge in einer gebundenen ListBox sind nur die Zeichenketten der vollqualifizierten Klassennamen der Klasse Buch.
Abbildung 2.32: Datenbindung an eine Kollektion von Buch-Objekten ohne passendes DataTemplate
Wie aber können einzelne Eigenschaften angezeigt und deren Änderungen nachvollzogen werden? Der einfachste Weg wäre es, die ToString-Methode der Klasse Buch zu überschreiben. Das wäre aber fast schon Flucht vor dem Feind, und da es in diesem Zusammenhang eine elegantere Möglichkeit gibt, soll dies auch vorgestellt werden. Um sich dem eben genannten Kontrahenten zu stellen, werden mit WPF die sogenannten DataTemplates verwendet.
DataTemplates In der Windows Presentation Foundation wird die grafische Repräsentation von Daten, die über einfache Zeichenketten hinausgehen, mit den sogenannten DataTemplates festgelegt. DataTemplates sind ein Konzept zur Erstellung von Vorlagen, die festlegen, wie Daten, die später in einer Datenbindung als Quelle dienen, grafisch dargestellt werden sollen. Eine sehr einfache Datenbindung an Eigenschaften von Objekten in einer Kollektion als Szenario, das ja der Auslöser für die Überlegungen hinsichtlich der DataTemplates war, kann direkt in der ListBox durchgeführt werden (Listing 2.61).
102
Windows Presentation Foundation Listing 2.61: Ein DataTemplate als Vorlage für die Einträge in einer ListBox
Die Vorlage der einzelnen Einträge im Listenfeld wird nun durch ein DataTemplate bestimmt. In der Vorlage für die grafische Aufbereitung der Daten werden Elemente und deren Eigenschaften so definiert, dass sie den Ansprüchen der Anzahl der gelieferten Eigenschaften und deren Werten entsprechen. Der wichtigste Teil dabei ist jedoch, in der XAML-Erweiterung Binding zu finden, die als Angabe einer der Eigenschaften der datengebundenen Objekte dient. Über die ItemsSource-Eigenschaft der ListBox wird die statische Ressource als Datenquelle gebunden, die eine Auflistungen von Buch-Objekten mit sich bringt. In der XAMLErweiterung Binding können die Elemente in dem DataTemplate über die Path-Angabe an eine Eigenschaft der Buchklasse gebunden werden und somit nur den jeweiligen Wert des gebundenen Objektes in der Kollektion anzeigen. Die Vorlagen für die Datenbindung können auch in einem Ressourcenabschnitt abgelegt und somit von weiteren datengebundenen Elementen als Template wieder verwendet werden. Zur Laufzeit wird dann dieses umfassende Beispiel die Autoren mit dem Abgabezeitpunkt in einem Listenfeld so anzeigen, dass durch das Canvas und die Datenbindung die einzelnen Einträge im Listenfeld je einen TextBlock für den Erscheinungstermin und einen TextBlock für den Autor haben.
Abbildung 2.33: Die Datenbindung mit der Reaktion auf die Änderung von Eigenschaften der Elemente in der ObservableCollection-Kollektion
103
Kapitel 2
In diesem Beispiel wurde noch einmal ein kleines Potpourri aus den vorangegangenen Teilen der Datenbindung zusammengestellt. Wenn Sie das komplette Beispiel ausprogrammiert haben oder einfach auf der Buch-CD die zugehörige Lösung starten, werden Sie sehen, dass sich die Werte des Erscheinungsdatums und der Autoren ändern. Dies wird durch den Timer, der in der ObservableCollection vom Typ Buch hinterlegt ist, veranlasst. Ändert sich alle drei Sekunden nur der Erscheinungstermin in einem Objekt vom Typ Buch in der Kollektion, so wird die Benutzeroberfläche auch nur die Anzeige des Datums ändern. Ändert sich aber ein Autor, entweder durch den Timer nach fünf Sekunden oder durch die Eingabe des Benutzers (der TwoWay-Modus ist für die Bindung des Textfeldes eingestellt), so ändert sich im Ziel der Datenbindung sowohl der Autor als auch der Erscheinungstermin, da bei einer Änderung des Autors im SetZweig der Property Autor die OnPropertyChanged-Methode mit String.Empty als Parameter aufgerufen wird.
Master-Detail-Beziehung in der Datenbindung abbilden Datenbindung in WPF zielt nicht nur darauf ab, Daten aus Kollektionen oder einzelne Datentabellen aus einer anderen Datenquelle wie zum Beispiel aus einem typisierten DataSet anzuzeigen. Oftmals stehen Daten in Tabellen untereinander über Fremdschlüssel in Beziehung. Analog dazu halten Klassen über Aggregation Referenztypen als Eigenschaften. Stellen Sie sich dazu einfach den möglichen Aufbau einer Firma vor. Zu einer Firma könnten mehrere Referate gehören, und diesen Referaten wiederum sind Büros zugeordnet. In einer Datenbank hätte die Tabelle Firma eine ID, die Referate würden über eine Fremdschlüsselbeziehung die Zugehörigkeit zu einer bestimmten Firma ablegen, und in der Tabelle Büro würden Sie Büros wiederum mit einer Fremdschlüsselbeziehung in einer Relation zu einem bestimmten Referat definieren. Als Klasse könnte die Relation über Kollektionen abgelegt werden, sodass eine Firma eine Kollektion vom Typ Referat und das Referat eine Kollektion vom Typ Büro beinhalten könnte. In dem Beispiel aus Listing 2.62 wird genau diese Idee in einem Objektmodell umgesetzt, und zwei Firmen mit zugehörigen Referaten und untergeordneten Büros werden instanziert. namespace MasterDetailDemoXAML { public class Buero { string _name; public Buero(string name) { _name = name; }
104
Windows Presentation Foundation public string Name { get { return _name; } } } public class BueroListe : ObservableCollection { public BueroListe() : base() { } } public class Referat { string _name; BueroListe _bueros; public Referat(string name) { _name = name; _bueros = new BueroListe(); } public string Name { get { return _name; } } public BueroListe Bueros { get { return _bueros; } } } public class ReferatListe : ObservableCollection { public ReferatListe() : base() { } } public class Firma { string _name; ReferatListe _referate; public Firma(string name) { _name = name; _referate = new ReferatListe(); } public string Name { get { return _name; } } public ReferatListe Referate { get { return _referate; } } }
105
Kapitel 2 public class FirmenListe : ObservableCollection { public FirmenListe() : base() { Firma dieFirma; Referat einReferat; dieFirma = new Firma("Shemans"); Add(dieFirma); einReferat = new Referat("Monitore"); dieFirma.Referate.Add(einReferat); einReferat.Bueros.Add(new Buero("München")); einReferat.Bueros.Add(new Buero("Stuttgart")); einReferat.Bueros.Add(new Buero("Hamburg")); einReferat.Bueros.Add(new Buero("Karlsruhe")); einReferat.Bueros.Add(new Buero("Mainz")); einReferat = new Referat("PC Systeme"); dieFirma.Referate.Add(einReferat); einReferat.Bueros.Add(new Buero("Deggendorf")); einReferat.Bueros.Add(new Buero("Kaiserslautern")); einReferat.Bueros.Add(new Buero("Wolfsburg")); dieFirma = new Firma("Daell"); Add(dieFirma); einReferat = new Referat("Notebooks"); dieFirma.Referate.Add(einReferat); einReferat.Bueros.Add(new Buero("Dublin")); einReferat.Bueros.Add(new Buero("Florida")); einReferat.Bueros.Add(new Buero("Peking")); einReferat.Bueros.Add(new Buero("Berlin")); einReferat = new Referat("Handheld"); dieFirma.Referate.Add(einReferat); einReferat.Bueros.Add(new Buero("Mailand")); einReferat.Bueros.Add(new Buero("Rio")); einReferat.Bueros.Add(new Buero("Tokyo")); einReferat.Bueros.Add(new Buero("Gotham")); einReferat.Bueros.Add(new Buero("Essen")); einReferat.Bueros.Add(new Buero("Saarbrücken")); } } } Listing 2.62: Eine Beziehung zwischen Klassen über Aggregation als Grundlage für eine MasterDetail-Relation
Um das ohnehin sehr umfangreiche Codebeispiel in Listing 2.62 nicht weiter aufzublähen, wurde auf die Implementierung des INotifyPropertyChanged-Interface verzichtet,
106
Windows Presentation Foundation
da es hier nicht auf die Änderungen der Inhalte der einzelnen Büros oder Referate ankommt. Die Klasse Firmenliste wird als ObservableCollection implementiert und kann nun als Typ in einer Datenbindung dienen. Ziel dabei ist es, dass sowohl die beiden Firmen angezeigt werden als auch die zu der jeweils ausgewählten Firma zugehörigen Referate und deren Büros. Die Überschrift dieses Kapitels lautet Master-Detail-Beziehung, wobei in diesem Fall eine Firma der Master über seine Referate ist und die Referate den Details-Part in der Beziehung einnehmen. Referate sind der Master für die Büros, die Büros sind die Details der Referate. In XAML gilt es jetzt noch, über geschicktes Binden der Daten diese Relationen auf datengebundene Steuerelemente und DataTemplates umzusetzen. Lassen Sie dazu das XAML-Dokument in Listing 2.63 auf sich wirken, das direkt im Anschluss hinsichtlich der Master-Detail-Beziehung näher erläutert wird. Datenbindung Master-Detail Firmenübersicht Listing 2.63: Die Master-Detail-Beziehung in ihrer XAML-Ausprägung
Die Datenbindung der ersten Liste sollte noch aus den vorangegangenen Beispielen zur Datenbindung bekannt sein. Spannend wird es, wenn Sie einen Blick auf die Datenbindung der beiden folgenden Listenfelder werfen:
Die Liste bezieht sich dabei auf die ItemsSource mit dem Pfad auf Referate, die in der Klasse FirmenListe als Eigenschaft definiert sind. Die Angabe, dass eine Referenz der Klasse Firmenliste verwendet werden soll, ist in der Definition des Datenkontexts im StackPanel festgelegt:
Das Listenfeld für die Büros wird mit einem Verweis auf die Büros an die Datenquelle gebunden:
Alle Klassen besitzen die Eigenschaft Name vom Typ String, und somit kann in allen drei Fällen dieselbe Datenvorlage verwendet werden. Wenn Sie sich nun zur Laufzeit diese Master-Detail-Beziehung einmal zu Gemüte führen, werden Sie feststellen, dass in den drei Listenfeldern die instanzierten Firmen mit den zugehörigen Referaten und Büros angezeigt werden. Bei der Auswahl einer anderen Firma werden die Referate und Büros passend zu der Auswahl der Firma verändert. Wir die Auswahl des Referates zusätzlich umgesetzt, so werden die passenden Büros angezeigt. Die Magie, die Sie nun zur Laufzeit in den Händen haben, wird erst durch das IsSynchronizedWithCurrentItem-Attribut wirklich zum Leben erweckt. Dieses Attribut
sorgt in der Datenbindung dafür, dass eine Synchronisation unter den abhängigen Objekten und Eigenschaften durchgeführt wird.
108
Windows Presentation Foundation
Abbildung 2.34: Die Master-Detail-Beziehung zwischen Firmen, Referaten und Büros
2.8 Dokumente in Windows Presentation Foundation Jeder Anwender kennt und nutzt Dokumente in Applikationen wie Word, Excel oder PowerPoint, um nur drei der gängigsten zu nennen. Die Aufgabe von Dokumenten ist es, Information anzuzeigen und zu verwerten. In diesem Zusammenhang lassen sich Dokumente in zwei grundsätzliche Kategorien unterteilen: Flussdokumente –
Optimiert für Anzeige und Lesbarkeit
Fixdokumente – WYSIWYG als Grundgedanke für präzise und gleichbleibende Anzeige Fixdokumente (Fixed Documents) zielen auf Anwendungen und Dokumente ab, die unabhängig von der Auflösung oder von Druckereinstellungen immer mit derselben Darstellung angezeigt oder ausgegeben werden sollen. Die Anordnung betreffend hält ein fixiertes Dokument immer explizit dieselbe Position der im Dokument beinhalteten Elemente. Anders als die Fixdokumente sind Flussdokumente darauf ausgelegt, Lesbarkeit zu gewährleisten, indem sie sich den Einstellungen des Benutzers anpassen. Zu diesen Einstellungen gehören sowohl die Auflösung des Bildschirms als auch die Anpassung der Schriftart und der Schriftgröße.
109
Kapitel 2
2.8.1 Flussdokumente (Flow Documents) Wenn ein vorgefertigtes Layout, das noch dazu nicht veränderbar sein soll, unerwünscht ist und eher die Einstellungen des Benutzers als Grundlage der Anzeige von Dokumenten in Betracht gezogen werden müssen, so sollten die sogenannten Flow Documents eingesetzt werden. Flussdokumente sind darauf ausgelegt, die Ansicht und die Lesbarkeit von Dokumenten optimal den Gegebenheiten anzupassen. Diese Anpassung bezieht sich unter anderem auf die Auflösung des Bildschirms, die Größe des Applikationsfensters, die Schriftart und andere Einstellungen, die der Benutzer vorgeben kann. Um diesen Sachverhalt greifbar zu machen, malen Sie sich doch ein konkretes Beispiel aus. Sie wollen einen Newsletter für Ihre Kunden oder Mitarbeiter anbieten, der die neuesten Infos über Ihre angebotenen Produkte und einige Fotos beinhalten soll. Ziel dabei soll es sein, dass der Empfänger des Newsletters den Inhalt auf seiner Hardware je nach seinen Einstellungen vorgegeben lesen kann, ohne dass Sie als Versender irgendwelche Einschränkungen machen. Der Fluss des Dokumentes soll sich an einen Desktop-PC mit 1600 x 1024 Pixel als Auflösung genauso anpassen wie an einen PDA mit 320 x 240 als Auflösung. Zusätzlich zu dieser Anpassung, in der Fachliteratur auch mit dem Anglizismus Reflow bezeichnet, haben Flussdokumente mitgelieferte Such- und Ansichtsfunktionalitäten sowie die Möglichkeit, die Größe und das Erscheinungsbild der Schrift zu verändern. Ein erstes Flussdokument in XAML soll mit einer eingängigen Folge von Abschnitten Aufschluss über das Verhalten eines Flussdokumentes liefern. Flussdokument In einem Flussdokument werden die einzelnen Elemente je nach Größe des anzeigenden Elementes und den Einstellungen des Benutzers angezeigt Ein neuer Abschnitt Die Auflistung wird auch im Fluss mit einbezogen Fetter Text Unterstrichen Kursiv
110
Windows Presentation Foundation Fluss Dokumente Machen Spass Blöcke und Zeilenumbrüche, Paragraph Eine Sektion stellt einen Block dar Und noch ein Block Ein Umbruch ,der gleich sichtbar wird Ein Absatz mit einer Einrückung Listing 2.64: Ein erstes Flussdokument
In einem Flussdokument wurden in Listing 2.64 verschiedene dokumentenspezifische Elemente definiert, die Sie vielleicht aus Ihrem alltäglichen Umgang mit Dokumenten kennen. Das erzielte Ergebnis ist in Abbildung 2.35 zu sehen.
Abbildung 2.35: Ein Flussdokument mit zwei angezeigten Spalten
111
Kapitel 2
Wenn nun der Benutzer an der Breite des Fensters keinen Gefallen mehr findet und diese verkleinert, so passt sich das Flussdokument an und verschiebt den Inhalt, der im Screenshot aus Abbildung 2.35 noch rechts angeordnet ist, nach links unten.
Abbildung 2.36: Das Flussdokument hat sich der Form des Fensters angepasst.
Es bleibt noch zu klären, warum ein einfaches Dokument automatisch in einem zugehörigen Container angezeigt wird, der es ermöglicht, im Dokument zu suchen, das Dokument zu zoomen und das Dokument in Spalten anzuzeigen. Ein Flussdokument wird automatisch in einem FlowDocumentPageViewer angezeigt, der einen Bereich für den Inhalt und eine Werkzeugleiste für den Anzeigemodus, Navigations- und ZoomSteuerelemente mit sich bringt. In diesem Beispiel lässt sich wunderbar nachvollziehen, wie ein Flussdokument auf Anpassungen verschiedenster Art reagiert. Dabei darf aber nicht außer Acht gelassen werden, dass bisher nur Text angezeigt wird. Ein wichtiger Gesichtspunkt in der Arbeit mit Flussdokumenten ist der wirkliche Fluss von Text um Elemente herum. Tabellen, Grafiken und Bilder sind gerne genommener Inhalt in Dokumenten und sollen so positioniert werden können, dass eine Anpassung der Gegebenheiten, wie Schrift- oder Fenstergröße, sich nicht auf diese Elemente auswirken.
112
Windows Presentation Foundation
In WPF können Sie zur Positionierung solcher Objekte die Elemente Figure und Floater verwenden. Im Aufbau eines Flussdokumentes ist eine Figure ein Teil des Inhalts, der über zugehörige Eigenschaften (horizontales und vertikales Verankern und Abstand) im Dokument platziert werden kann. Im Unterschied dazu können FloaterElemente nur parallel zur Flussrichtung des Dokumentes ihre Position einnehmen und sind auf eine Spalte des Dokumentes beschränkt, während Figure-Elemente mehrere Spalten des Dokumentes überspannen können. Ein Bild innerhalb einer Figure kann im Fluss so wie in Listing 2.65 angeordnet werden. Abbildung: Sogar Kinder wollen das Buch! Lange Schlangen vor den einschlägigen Buchhändlern ... Listing 2.65: Ein Bild wird in einem Flussdokument als Figure angeordnet.
Das Fenster zeigt bei einer Anpassung der Breite das Bild, innerhalb der Figure im Flussdokument, umflossen von Text an. Die Positionierung wurde relativ zum Flussdokument auf der linken Seite definiert (siehe Abbildung 2.37).
2.8.2 Fixierte Dokumente (Fixed Documents) Im Gegensatz zu den Flussdokumenten sollen fixierte Dokumente dazu dienen, Inhalt statisch und unveränderbar anzuzeigen. Im Zusammenhang mit Fixed Documents sollen weitere Elemente in einem kurzen Überblick vorgestellt werden. An einem zusammengehörigen Beispiel werden dabei die XML Paper Specification, kurz XPSDokumente, der DocumentViewer und die Verwendung von Kommentaren, vorgestellt. Jeder dieser Punkte würde eine tiefer gehende Betrachtung verdienen, das würde aber den Rahmen dieses Abschnitts sprengen.
113
Kapitel 2
Abbildung 2.37: Der Fluss um das Bild herum wird durch die Änderung der Fenstergröße deutlich.
XPS-Dokumente XPS beschreibt durch die vorgegebene Spezifikation elektronisches Papier, das von Hardware, Software und vom Benutzer gelesen werden kann. In WPF können Sie mit dem XpsDocument dieses elektronische Papier in Ihren Anwendungen dazu nutzen, Dokumente zu verpacken und bei Bedarf auch wieder als Paket von Dokumenten (FixedDocumentSequence) zur Verfügung zu stellen. Das Ziel, das mit der Spezifikation verfolgt wird, ist die immer gleiche Darstellung von Dokumenten unabhängig davon, wo und wie diese Dokumente betrachtet werden. Somit eignen sich XPS-Dokumente bestens, um als FixedDocument in einem DocumentViewer betrachtet zu werden.
Dokumente betrachten mit dem DocumentViewer Das DocumentViewer-Element ist die Grundlage für die Anzeige von fixen Dokumenten und bietet zusammen mit den angezeigten Dokumenten die WYSIWYG-Unterstützung. Als Element bietet er eine beinhaltete Werkzeugleiste an, mit der das betrachtete Dokument gezoomt, gedruckt, angeordnet und ein neues Dokument angelegt werden kann. Leider muss die Funktionalität für das Drucken und die Neuanlage eines Dokumentes vom Entwickler noch zusätzlich implementiert werden. Nachdem ein Dokument in den DocumentViewer geladen wurde, ist es vorerst nicht möglich, dieses Dokument zu verändern. Auf Papier gedruckte Dokumente bieten diese Möglichkeit auch nicht
114
Windows Presentation Foundation
direkt, aber gegen Anmerkungen per Kugelschreiber, einen Text-Marker oder ein aufgeklebtes Zettelchen hat selbst das dünnste Papier nichts einzuwenden. In WPF haben Sie in einem DocumentViewer die Mittel, auch dem elektronischen Papier Kommentare und Hervorhebungen durch die sogenannten System.Windows.Annotations mitzugeben.
Kommentare und Markierungen in einem Fixed Document Das gute alte Papier ist, wie Sie alle wissen, sehr geduldig, und das soll es in der elektronischen Form auch bleiben. Dazu hat Microsoft die netten Markierungen und Kommentare, wie Sie sie vielleicht aus Ihrer Textverarbeitung kennen, auch in die Windows Presentation Foundation integriert.
XPS Dokument mit Kommentaren im DocumentViewer – ein Beispiel Im Beispiel in Listing 2.66 wird in XAML ein Layout für einen DocumentViewer mit zugehörigem Kontextmenü vorgegeben. Per Code-Behind-Datei wird eine XPS-Datei als FixedDocument geladen und angezeigt. In diesem Dokument können Kommentare über ein Kontextmenü hinzugefügt werden: Listing 2.66: Ein DocumentViewer mit der Infrastruktur für Kommentare
Die einzelnen Elemente für Kommentare und Markierungen im Text werden im Namensraum Annotations definiert, der dazu im XAML-Dokument eingebunden werden muss: xmlns:ann= "clr-namespace:System.Windows.Annotations;assembly=PresentationFramework"
Im Kontextmenü werden für die einzelnen Elemente Aktionen definiert, die automatisch abgearbeitet werden: Command="ann:AnnotationService.CreateTextStickyNoteCommand" Header="Text Kommentar"
115
Kapitel 2
Wird das zugehörige Menüelement geklickt, so kann ein neuer Kommentar hinzugefügt werden. Aber so einfach, wie es hier im XAML aussieht, verhält sich der Mechanismus in seiner internen Implementierung nicht. Das XPS-Dokument muss geladen und nach fixen Dokumenten durchsucht werden. Um die Kommentare zu erlauben, muss für den DocumentViewer zusätzlich noch der Kommentardienst aktiviert werden. Zusätzlich muss festgelegt werden, wo und in welchem Format die Kommentare und Markierungen geladen werden, und schließlich und letztendlich muss dafür gesorgt sein, dass neue Anmerkungen oder Kommentare auch wieder gespeichert werden können. Ein Überblick über den gesamten Quellcode würde in gedruckter Form zu unübersichtlich sein, darum als kleinen Teaser in Abbildung 2.38 das fertige Ergebnis und ein Hinweis auf das komplette Beispiel auf der Buch-CD (Ordner DocumentViewerAnnotationsXAML). Beim Start der Applikation wird der Benutzer aufgefordert, eine XPSDatei anzugeben, um diese Datei nach fixierten Dokumenten zu durchsuchen und die zugehörigen Kommentare zu laden.
Abbildung 2.38: Ein XPS-Dokument mit Hervorhebungen und Kommentaren
116
Windows Presentation Foundation
Nach dem ersten Eindruck und der Suche nach dem Beispiel auf Buch-CD werden Sie nun berechtigte Bedenken hinsichtlich des XPS-Dokumentes hegen. Frage & Antwort Denn woher kommt denn eigentlich eine Datei mit der Endung .xps, die in sich ein oder mehrere FixedDocument-Elemente trägt? Das Erstellen von XPS-Dokumenten ist in der Tat nicht so einfach und wurde in diesem konkreten Fall mit der zugehörigen XPS-API der Windows Presentation Foundation erledigt. Da aber für Office 2007 ein kostenloser Download eines Exporttools angeboten wird, können Sie auch ohne das vorherige Wälzen der Dokumentation zur XPS-Klassenbibliothek sehr schnell und einfach fixe Dokumente erstellen. Da Office 2007 leider nicht mit dem Add-In zur Erstellung von XPS-Dokumenten ausgeliefert wird, müssen Sie nach einer Prüfung der Echtheit Ihrer Office-Version das Add-In installieren. Das Exporttool ist zum Zeitpunkt der Erstellung dieses Buches auf den Webseiten von Microsoft zu finden und in den für Office 2007 verfügbaren Sprachen erhältlich: http:// www.microsoft.com/downloads/details.aspx?FamilyID=4d951911-3e7e-4ae6-b059a2e79ed87041&DisplayLang=de. Sollten Sie schon stolzer Besitzer von Office 2007 sein und haben Sie auch das Add-In installiert, scheuen Sie nicht davor, eine bestehende Datei zu exportieren. In Microsoft Word 2007 müssen Sie dazu einfach die zu exportierende Datei öffnen und als XPS-Dokument speichern.
Abbildung 2.39: In Office 2007 können Dokumente als XPS-Dokument abgespeichert werden, wenn das Add-In dafür installiert ist.
117
Kapitel 2
*
*
*
TIPP
In den kommenden Tagen werden weitere Softwarehändler die Infrastruktur anbieten, um Ihre Dokumente in XPS zu exportieren. Und um den Beweis anzutreten, dass die exportierte Datei auch von Ihrem DocumentViewer angezeigt werden kann, laden Sie die Datei einfach mit der Beispielanwendung. Zu guter Letzt werden Sie XPS auch in Windows Vista als vorinstallierten Ausgabedrucker wieder finden, Ihre Dokumente können Sie dann einfach über den Drucken-Dialog erstellen.
Abbildung 2.40: XPS ist als Drucker in Windows Vista automatisch installiert.
2.9 Grafiken mit WPF Die grafischen Benutzeroberflächen und deren Elemente, die bislang in diesem Buch besprochen wurden, sind von ihrer Gestaltung her vorgegeben und außer durch Höhe, Breite oder Farbe kaum beeinflussbar. Mit den bisher kennengelernten Möglichkeiten, eine Zeichnung wie Gottes Fingerzeig in der Sixtinischen Kapelle nachzustellen, ist kaum vorstellbar. Aber die Windows Presentation Foundation gibt uns weitere Klassen an die Hand, die uns mit etwas Übung zu wahren Künstlern machen können, und Grafiken, wie das Sample (Abbildung 2.41) aus der Beispielsammlung von Microsoft Expression Blend, können mit dem Design-Tool für XAML-Oberflächen entstehen.
118
Windows Presentation Foundation
> >
>
HINWEIS
Microsoft Expression Blend ist ein Tool, mit dem Videos, Vektorgrafiken, Animationen, Bilder, Oberflächen und 3D-Elemente in der Manier eines Grafikprogramms erstellt werden können.
Abbildung 2.41: Sample-Grafik mit Animation aus den Vorlagen in Microsoft Expression Blend
Auch wenn Sie als grafisch unbescholtener Leser jetzt in Ehrfurcht zusammenzucken, kann ich Ihnen die Angst ein wenig nehmen, denn in diesem Buch wird nur an der Oberfläche der grafischen Elemente gekratzt. Und nur mal so nebenbei, mit Microsoft Expression Blend steht Ihnen bald ein Design-Tool zur Verfügung, mit dessen Hilfe XAML Markup erzeugt werden kann. Im folgenden Abschnitt soll es nun zuerst einmal um die grundsätzlichen geometrischen Elemente gehen.
2.9.1 Geometrische Grundfiguren Bei einem ersten Kontakt mit den Klassen der Windows Presentation Foundation, die für das Zeichnen von Figuren und Linien zur Verfügung stehen, taucht immer wieder das englische Wort Geometry auf. Zu Deutsch einfach Geometrie, aber was ist Geometrie eigentlich, und wozu wird die Klasse Geometry verwendet?
119
Kapitel 2
Das Wort Geometrie hat seinen Ursprung im Griechischen, bedeutet so viel wie Erdmaß oder Landmessung und definiert Teilgebiete der Mathematik. Inzidenzgeometrie, Differentialgeometrie, algebraische Geometrie, konvexe Geometrie oder algorithmische Geometrie, um nur einige zu nennen. Das Teilgebiet, auf das die Klassen der WPF abzielen, ist die euklidische Geometrie, die uns schon in der Grundschule mit Punkten, Geraden, Rechtecken und Ellipsen vermittelt werden sollte. In diesem Abschnitt soll das Thema noch einmal für Sie aufgefrischt und mit XAML ein wenig aufgepeppt werden. Geometrische Objekte und deren zweidimensionale Darstellung werden in Klassen beschrieben: System.Windows.Media.CombinedGeometry Kombination von zwei geometrischen Objekten. System.Windows.Media.EllipseGeometry Kreis oder Ellipse. System.Windows.Media.GeometryGroup Komposition von geometrischen Objekten. System.Windows.Media.LineGeometry Legt eine Linie durch Start- und Endpunkt fest. System.Windows.Media.PathGeometry Komplexes geometrisches Objekt, das durch Geraden, Kurven, Ellipsen Rechtecke beschrieben wird. System.Windows.Media.RectangleGeometry Zweidimensionales Rechteck. System.Windows.Media.StreamGeometry Geometrische Figur, festegelegt durch einen StreamGeometryContext. Zu den geometrischen Klassen gesellen sich die sogenannten Shapes (dt. Gestalten), die in analoger Manier Rechtecke, Ellipsen, Linien und Pfade beschreiben. Einen wichtigen Unterschied gibt es aber zwischen den beiden Klassen, denn Shapes sind Elemente aus einer Klasse, abgeleitet von UIElement, wohingegen die Klasse Geometry vom Typ Animatable ist. Durch die Ableitung von UIElement können Objekte vom Typ Shape sich sozusagen selbst auf der Oberfläche darstellen. Im Gegensatz dazu brauchen die Objekte vom Typ Geometry immer eine andere Klasse, die sie zeichnet. In der Path-Klasse, abgeleitet von Shape, wird diese Abhängigkeit der geometrischen Objekte zum ersten Mal sehr deutlich, da ein Pfad seinen Inhalt über ein Geometry-Element bestimmt. Im ersten grafischen XAML-Abschnitt soll eine Ellipse als Shape und als geometrisches Objekt innerhalb eines Path-Elementes gezeigt werden.
120
Windows Presentation Foundation Listing 2.67: Ellipsen als einfacher Shape und als Geometry-Element
Die erste Ellipse in Listing 2.67 ist ein Shape-Objekt, das einfach innerhalb des Canvas erstellt werden kann. Die Positionierung kann bei diesem Element aber leider nur über die abhängigen Eigenschaften Top, Bottom, Left und Right festgelegt werden. Die Stroke-Eigenschaft ist vom Typ Brush und bestimmt, wie die Umrandung der Ellipse gezeichnet werden soll. Im Path-Objekt, das auch ein Shape ist, wird in der Data-Eigenschaft das geometrische Objekt zugewiesen, das die Gestalt des Shape-Elementes spezifiziert. Das Element vom Typ EllipseGeometry muss zwar mit einigem Aufwand erstellt werden, ist dann aber flexibler in der Verwendung innerhalb des Canvas-Containers. So kann die Position einfach über die Center-Eigenschaft in X- und Y-Richtung angegeben werden. In einem Fenster mit blauem Hintergrund sind die beiden Ellipsen in Abbildung 2.42 zu betrachten.
Abbildung 2.42: Zwei weiße Ellipsen auf blauem Grund
Um Rechtecke aus den beiden Ellipsen zu machen, genügt in dem einfachen Shape die Umbenennung des Typs Ellipse in den Typ Rectangle. Im Pfad muss das Rechteck als RectangleGeometry allerdings etwas anders beschrieben werden.
121
Kapitel 2
Listing 2.68: Aus Ellipsen werden Rechtecke.
Die Rect-Eigenschaft hat ihren Ursprung in der Struktur Rect, deren überladener Konstruktor vier Parameter vom Typ Double akzeptiert. Mit den ersten beiden Werten wird der Ursprung des Rechtecks als Punkt in X- und Y-Richtung spezifiziert, und mit den letzten beiden Werten werden Breite und Höhe ausgehend vom Ursprung zugewiesen.
Abbildung 2.43: Zwei weiße Rechtecke auf dunklem Grund
Um zusätzlich zu den vorgefertigten Ellipsen und Rechtecken eigene Striche durch die Landschaft ziehen zu können, haben Sie die Möglichkeit, mit den Klassen Line und LineGeometry unter der Angabe von Start- und Endpunkt eine Linie zu zeichnen. Eine Linie als einfaches Shape-Objekt muss den Startwert über X1 und Y1 und analog den Endwert über X2 und Y2 als seine Eigenschaften festlegen. Innerhalb eines Pfades im LineGeometry-Element müssen Start- und Endpunkt als einzelne Punkte angegeben werden. Listing 2.69: Je eine Linie in einem einfachen Shape- und in einem LineGeometry-Element
Die beiden Linien sind in Abbildung 2.44 leicht über deren X- und Y-Werte grafisch nachzuvollziehen, dabei gilt es aber wie bei allen bisher benutzten grafischen Elementen zu beachten, dass der Ursprung der X- und Y-Koordinaten in der linken oberen Ecke des Fensters, direkt unter Titelleiste zu finden, ist.
122
Windows Presentation Foundation
Abbildung 2.44: Zwei Linien auf dunklem Grund
Innerhalb eines Pfades kann auch ein Objekt vom Typ PathGeometry als Komposition von Linien, Kurven oder Bögen seine Verwendung finden. Die Grundlage bietet dafür eine Kollektion von PathFigure-Objekten, die ihrerseits als Serie verbundener zweidimensionaler Segmente im Pfad auftreten. Jedes PathSegment-Objekt als einzelner Abschnitt einer kompletten PathFigure kann folgenden Inhalt definieren: ArcSegment: Erstellt einen Bogen zwischen zwei Punkten. BezierSegment: Erstellt eine kubische Bézierkurve (benannt nach Pierre Bézier) zwischen zwei Punkten. LineSegment: Stellt eine Linie zwischen zwei Punkten dar. PolyBezierSegment: Beinhaltet eine Folge von Bézierkurven. PolyLineSegment: Beinhaltet eine Folge von Linien. PolyQuadraticBezierSegment: Beinhaltet eine Folge von quadratischen Bézierkurven QuadraticBezierSegment: Erstellt eine quadratische Bézierkurve. Die einzelnen Segmente in einer PathFigure werden zu einem einzigen Shape zusammengestellt, wobei der Endpunkt des vorangehenden Segments automatisch der Startpunkt des darauffolgenden Segmentes ist. Der Startpunkt aller Segmente wird in der StartPoint-Eigenschaft der PathFigure festgesetzt. Versuchen Sie dazu, das XAML-Beispiel in Listing 2.70 nachzuvollziehen!
123
Kapitel 2 Sprechblase Listing 2.70: Eine PathFigure mit ihren Segmenten
Der Inhalt des Canvas-Steuerelementes kann nachvollzogen werden, wenn Sie sich ein kariertes Blatt Papier nehmen und in der Reihenfolge die einzelnen Punkte vom Startpunkt der PathFigure an aufzeichnen. Durch das Setzen der IsClosed-Eigenschaft wird der Startwert auch zum Endwert der PathFigure. Dazwischen müssen Sie nur noch die einzelnen Objekte und deren Eigenschaften skizzieren, was bei den beiden Linien nicht weiter schwierig ist. Genauer betrachten müssen wir noch den Bogen, der in seiner Size-Eigenschaft die Ausdehnung in X- und Y-Richtung definiert, die sich aber nur dann wirklich entfalten kann, wenn auch die IsLargeArc auf den Wert True gesetzt ist. In welcher Richtung der Bogen gezeichnet wird, muss in der Eigenschaft SweepDirection, die standardmäßig auf Counterclockwise steht, gesetzt werden. Wenn Sie Ihre Zeichnung auf Papier fertig gestellt haben, besteht mit Abbildung 2.45 die Möglichkeit zu kontrollieren, ob Ihre Zeichnung dem wirklichen Ergebnis nahe kommt.
Abbildung 2.45: Durch eine PathFigure wird eine Sprechblase beschrieben.
124
Windows Presentation Foundation
In dieser sehr einfachen Sprechblase blitzt schon auf, welche gewaltigen Möglichkeiten Ihnen mit den Klassen aus WPF zur Verfügung stehen. Um jetzt auch noch Farbe ins Spiel zu bringen, sollen Brushes, die Pinsel der Windows Presentation Foundation, etwas näher betrachtet werden.
2.9.2 Brushes Jedes Mal, wenn Sie bisher einem Element eine Farbe, zum Beispiel für den Hintergrund, zugewiesen haben, wurde in XAML lediglich eine Farbangabe per Zeichenkette im Markup gemacht. Dahinter verbirgt sich aber jedes Mal ein Objekt vom Typ Brush (dt. Bürste, Pinsel), das vom XAML-Compiler auch als solches identifiziert und verarbeitet wird. Die abstrakte Klasse Brush und ihre abgeleiteten Unterklassen legen fest, wie ein Bereich ausgemalt wird. SolidColorBrush: Implizit wird bei einer einfachen Farbangabe in XAML immer ein Objekt vom Typ SolidColorBrush als Pinsel verwendet, wobei genau eine Farbe für ein bestimmtes Element festgelegt wird. GradientBrush: Ermöglicht es, Farbübergänge durch den Einsatz von GradientStops zu erstellen. TileBrush: Ermöglicht es Ihnen, Elemente gekachelt zu befüllen. Um die Sprechblase aus dem vorangegangenen Beispiel mit einem Farbverlauf auszustatten, können Sie ein GradientBrush mit zugehörigen Übergängen in GradientStops verwenden. Listing 2.71: Ein Farbübergang von Weiß nach Blau mit einem LinearGradientBrush
Der Start- und der Endpunkt des Farbübergangs beziehen sich prozentual auf das Objekt, das mit einem Farbübergang befüllt werden soll. Standardmäßig wird der Farbübergang von der linken oberen Ecke (0,0) zur rechten unteren Ecke des zu befüllenden Bereichs durchgeführt. Die einzelnen GradientStop-Elemente definieren im Farbübergang die einzelnen Farben und die Punkte, an denen der Übergang tatsächlich stattfinden soll. Der Start des Übergangs wird in einem relativen Abstand zum Startpunkt des
125
Kapitel 2
Farbverlaufs in der Offset-Eigenschaft des GradientStop-Elements festgelegt. Zusammen mit der Sprechblase von vorhin könnte der Farbverlauf wie in Abbildung 2.46 aussehen.
Abbildung 2.46: Sprechblase mit Farbverlauf
Wie Sie in Abbildung 2.46 sehen, erscheint die Sprechblase anscheinend ohne zugehöriges Fenster direkt auf dem Hintergrund von Windows Vista. Diese Illusion wird erschaffen, indem einige Eigenschaften des Window-Elementes gesetzt wurden. Dies gehört allerdings nicht in den Kontext der Brushes, und somit soll hier nur auf das Beispiel auf der Buch-CD im Ordner PathFigureXAML verwiesen werden.
2.10 Animationen Alles bewegt sich – auch wenn die Forscher noch kein Perpetuum mobile erschaffen konnten, so haben doch Bewegungen und Animationen in unsere täglichen Applikationen Einzug gehalten. Eine Animation ist auch in der Philosophie von Microsoft nichts als Illusion, die durch schnelle Anzeige von sich unterscheidenden Bildern das menschliche Gehirn austricksen und ihm eine zusammenhängende Szene vorspielen sollen. Die einzelnen Bilder werden in der Terminologie der Animationserschaffung meist als Frames bezeichnet. Wer hat noch nicht gefesselt auf den Bildschirm gestarrt, wenn in einer Flash-Animation oder einem animierten GIF Lichter zucken, Effekte eingeblendet werden und zeitlich abgestimmte Bewegungen den Animationsreigen vervollständigen? Für den Entwickler von Benutzeroberflächen mag das vielleicht noch nicht die lang ersehnte Idee der GUI-Programmierung sein, aber das kann auch daran liegen, dass alleine das Ändern einer Eigenschaft eines Steuerelementes von einem Start- zu einem
126
Windows Presentation Foundation
Endwert über einen vorgegebenen zeitlichen Rahmen hinweg mit der Klassenbibliothek zu Windows Forms nicht ohne zusätzliche Unterstützung durch ein Timer-Objekt zu erreichen ist. Einfache Änderungen von Farbe, Größe, Fades oder Bewegung von Objekten sind für Flash-Entwickler seit der ersten Version ein Leichtes und werden sogar von zugehörigen IDEs unterstützt. Für .NET-Entwickler werden solche Aufgaben schnell zu zeitaufwendigen und komplizierten Herausforderungen, die bisher nicht im Verhältnis zum erzielten Ergebnis standen. Mit .NET 3.0 und der Windows Presentation Foundation sollen diese Einschränkungen aber der Vergangenheit angehören. In WPF werden Designer und Entwickler von den sogenannten Storyboards unterstützt, um in Animationen verschiedenste Fades, Farbänderungen, Positionswechsel und andere Transformationen durchzuführen. Wie schon erwähnt spielt bei Animationen das Timing eine zentrale Rolle, um die Koordination der einzelnen Veränderungen übergangslos und aneinander angepasst ablaufen zu lassen. Zum Glück liefert WPF ein ausgeklügeltes System, in dem der Entwickler der Animation kaum noch selbst programmatisch Hand anlegen muss.
2.10.1 Timelines Animationen haben ihre Grundlage in der Vererbungshierarchie in der Klasse Timeline. Mit einer Instanz der Klasse Timeline, die eigentlich nur einen Zeitabschnitt festlegt, haben Sie das unmittelbare Werkzeug in der Hand, die Dauer, den Start- und Endzeitpunkt einer Animation festzulegen. Über die Eigenschaften einer Timeline kann darüber hinaus noch festgelegt werden, wie oft die Animation sich wiederholen und mit welcher Geschwindigkeit die Animation ablaufen soll. In der Windows Presentation Foundation sind Timelines in die folgenden Animationen unterteilt: AnimationTimeline: Eine AnimationTimeline ist dafür zuständig, einer zugeordneten Eigenschaft neue Werte entlang eines zeitlichen Ablaufs zuzuordnen. ParallelTimelines: Parallele Timelines haben die Aufgabe, mehrere Timelines zu gruppieren. Storyboards: Ein Storyboard ist als eine spezialisierte ParallelTimeline mit der hauptsächlichen Aufgabe, die gruppierten Timelines zu verwalten und Informationen über deren Eigenschaften und Zustände zu exponieren.
127
Kapitel 2
2.10.2 Storyboards In WPF werden Animationen meist über die sogenannten Storyboards gruppiert und können entweder für jedes Objekt vom Typ FrameworkElement speziell definiert oder als Teil eines Styles zentral für eine grafische Benutzeroberfläche abgelegt werden. Storyboards ermöglichen eine Kombination von einzelnen Timelines, um unterschiedliche Objekte und deren Eigenschaften in der kompletten Animation mit einzubeziehen. Um ein Objekt an einer Animation teilnehmen zu lassen, muss die TargetName-Eigenschaft des Storyboards auf das passende Objekt gesetzt und die zu animierende Eigenschaft mit dem TargetProperty-Member der Klasse Storyboard angeführt werden. Timeline als Basisklasse hat eine breite Ableitungshierarchie, um dem Entwickler ein weit gefächertes Spektrum an Animationen, die auf WPF-Elemente angewendet werden können, zu bieten. Daraus wird in einer ersten Betrachtung die zeitlich beeinflusste Veränderung einer Steuerelement-Eigenschaft mittels einer DoubleAnimation herangezogen. Der animierte Button Listing 2.72: Eine DoubleAnimation in XAML, die einen Button mit seiner Schriftart vergrößert
In diesem ersten, sehr einfachen Beispiel aus Listing 2.72 wird die grundsätzliche Struktur einer Animation klar. Innerhalb eines Storyboards wird eine einzelne Animation für einen Button definiert. Diese Animation zielt auf die Schriftgröße als Fließkomma -Wert ab, der von der Größe 14 auf die Größe 30 verändert werden soll. In der DoubleAnimation wird mit einer abhängigen Eigenschaft der Klasse Storyboard der Button namens buttonStoryboard als Ziel der Veränderung ausgewiesen. Die Eigenschaft in dem Button, auf welche die Animation angewendet werden soll, ist die Schriftgröße und wird in der abhängigen Eigenschaft TargetProperty des Storyboards festgelegt.
128
Windows Presentation Foundation
In den Attributen From und To können Start- und Endwert des Double-Wertes angeführt sein, die in diesem konkreten Fall innerhalb der Duration (Stunden:Minuten: Sekunden) auf die Schriftgröße des Buttons umgesetzt werden. Einzelne Momentaufnahmen sind in Abbildung 2.47 festgehalten. Während die Animation durchlaufen wird, ändern sich die Schriftgröße und der Button scheinbar fließend.
Abbildung 2.47: Die Momentaufnahmen einer DoubleAnimation, welche die Schriftgröße eines Buttons verändert
Der Übergang der Schriftgröße 14 zur Schriftgröße 30 wird per Standard in 24 Frames pro Sekunde abgearbeitet, und unser Auge kommt da kaum mit und nimmt die Animation als zusammenhängend wahr.
2.11 Animationen für Entwickler Ein Button, der größer wird, ist nur der Tropfen auf den heißen Stein, der aber gerade in der Entwicklung vor der Zeit von Windows Presentation Foundation und dessen Objektmodell für Animationen schnell zum Stein des Anstoßes werden konnte. In XAML abgebildet können mehrere Animationen auf ein und demselben Objekt auch gleichzeitig ablaufen und somit zum Beispiel zusätzlich zur Größe auch die Transparenz oder die Farbgebung eines Objektes verändern. Natürlich können auch wieder die Entwickler programmatisch an Animationen herangehen. Im nun folgenden Teil einer Klasse wird die Methode RechteckeZeichnen einmal komplett aufgeführt, und daraufhin werden einzelne Teile herausgegriffen und besprochen. Ziel der Methode ist es, mehrere Rechtecke zu erstellen und den Recht-
129
Kapitel 2
ecken Animationen in Bezug auf Größe, Richtungsausdehnung und Transparenz mitzugeben. Das Ergebnis erscheint zur Laufzeit etwas psychedelisch, aber nun erst einmal zum Code (Listing 2.73). private void RechteckeZeichnen() { double zentrumX = this._canvas.ActualWidth / 2.0; double zentrumY = this._canvas.ActualHeight / 2.0; Color[] farben = new Color[] { Colors.White, Colors.Red, Colors.Green, Colors.Yellow }; for (int i = 0; i < 36; ++i) { Rectangle rectangleBasis = new Rectangle(); byte alpha = (byte)_zufall.Next(96, 192); int farbIndex = _zufall.Next(4); rectangleBasis.Stroke = new SolidColorBrush(Color.FromArgb(alpha, farben[farbIndex].R, farben[farbIndex].G, farben[farbIndex].B)); rectangleBasis.StrokeThickness = _zufall.Next(1, 4); rectangleBasis.Width = 0.0; rectangleBasis.Height = 0.0; double offsetX = 32 - _zufall.Next(32); double offsetY = 32 - _zufall.Next(32); this._canvas.Children.Add(rectangleBasis); rectangleBasis.SetValue(Canvas.LeftProperty, zentrumX + offsetX); rectangleBasis.SetValue(Canvas.TopProperty, zentrumY + offsetY); double dauer = 6.5 + 10.0 * _zufall.NextDouble(); double verzoegerung = 16.0 * _zufall.NextDouble(); DoubleAnimation doubleAnimationGroesse = new DoubleAnimation( 0.0, 300.0, new Duration(TimeSpan.FromSeconds(dauer))); doubleAnimationGroesse.RepeatBehavior = RepeatBehavior.Forever; doubleAnimationGroesse.BeginTime = TimeSpan.FromSeconds(verzoegerung); rectangleBasis.BeginAnimation(Rectangle.WidthProperty, doubleAnimationGroesse); rectangleBasis.BeginAnimation(Rectangle.HeightProperty, doubleAnimationGroesse); TranslateTransform verschiebung = new TranslateTransform(); DoubleAnimation doubleAnimationRichtung = new DoubleAnimation(0.0, -150.0, new Duration(TimeSpan.FromSeconds(dauer))); doubleAnimationRichtung.RepeatBehavior = RepeatBehavior.Forever; doubleAnimationRichtung.BeginTime = TimeSpan.FromSeconds(verzoegerung);
130
Windows Presentation Foundation verschiebung.BeginAnimation(TranslateTransform.XProperty, doubleAnimationRichtung); verschiebung.BeginAnimation(TranslateTransform.YProperty, doubleAnimationRichtung); rectangleBasis.RenderTransform = verschiebung; DoubleAnimation doubleAnimationTransparenz = new DoubleAnimation(dauer 1.0, 0.0, new Duration(TimeSpan.FromSeconds(dauer))); doubleAnimationTransparenz.BeginTime = TimeSpan.FromSeconds(verzoegerung + 2.0); doubleAnimationTransparenz.RepeatBehavior = RepeatBehavior.Forever; rectangleBasis.BeginAnimation(Rectangle.OpacityProperty, doubleAnimationTransparenz); } } Listing 2.73: Die komplette Methode ZeichneRechtecke
Nun wollen wir uns einmal einzelne Abschnitte zur näheren Betrachtung aus dem Code herausnehmen. Beim Start der Methode wird in einer for-Schleife dafür gesorgt, dass 36 Rechtecke instanziert und deren Umrandung farblich per Zufall aus einer vorgegebenen Farbpalette gestaltet werden. Die Breite der Umrandung wird ebenfalls zufällig ausgewählt. Rectangle rectangleBasis = new Rectangle(); byte alpha = (byte)_zufall.Next(96, 192); int farbIndex = _zufall.Next(4); rectangleBasis.Stroke = new SolidColorBrush(Color.FromArgb(alpha, farben[farbIndex].R, farben[farbIndex].G, farben[farbIndex].B)); rectangleBasis.StrokeThickness = _zufall.Next(1, 4);
Die Rechtecke unterliegen im weiteren Verlauf einer Animation, die deren Größe beeinflusst, und dafür werden die Höhe und die Breite der Rechtecke anfangs auf den Wert 0 gesetzt. Damit in dieser Animation der Eindruck entsteht, dass die Rechtecke nicht denselben Ursprung haben, wird die Mitte des Canvas-Steuerelementes in X-und Y-Richtung berechnet, und die Rechtecke werden mit einem zufälligen Offset um den Mittelpunkt des Canvas platziert. rectangleBasis.SetValue(Canvas.LeftProperty, zentrumX + offsetX); rectangleBasis.SetValue(Canvas.TopProperty, zentrumY + offsetY);
Die Animation der Größe wird in einem Objekt vom Typ DoubleAnimation festgelegt und konfiguriert, wobei die Dauer wieder aus einem bestimmten Zeitrahmen per Zufall herausgepickt wird. Im Konstruktor werden neben der Dauer auch der Startund der Endzustand des Double-Wertes festgelegt, der später auf eine der abhängigen Eigenschaften des Rechtecks zugewiesen wird.
131
Kapitel 2
Im RepeatBehavior wird festgelegt, dass die Animation sich wiederholen soll, solange die Applikation nicht beendet wird. Die Animation startet mit einer angegebenen Verzögerung, die in der Eigenschaft BeginTime verankert liegt. Um die Animation zu starten, muss für das zugehörige Rechteck die BeginAnimationMethode aufgerufen werden, der eine abhängige Eigenschaft und die Animation, die ablaufen soll, übergeben werden muss. Die Animation wird in diesem Fall auf die abhängigen Eigenschaften für Höhe und Breite des Rechtecks angesetzt. Dabei wird das Rechteck also in X- und Y-Richtung gleichermaßen vergrößert. Die Animation zieht die Rechtecke sozusagen nach rechts unten auf. DoubleAnimation doubleAnimationGroesse = new DoubleAnimation( 0.0, 300.0, new Duration(TimeSpan.FromSeconds(dauer))); doubleAnimationGroesse.RepeatBehavior = RepeatBehavior.Forever; doubleAnimationGroesse.BeginTime = TimeSpan.FromSeconds(verzoegerung); rectangleBasis.BeginAnimation(Rectangle.WidthProperty, doubleAnimationGroesse); rectangleBasis.BeginAnimation(Rectangle.HeightProperty, doubleAnimationGroesse);
Um die Illusion zu erzeugen, dass die Rechtecke bis auf den Offset konzentrisch vergrößert werden, wird eine sogenannten Translation (Verschiebung) der Rechtecke nach rechts oben als weitere Animation auf die Rechtecke angesetzt. Denn alleine durch die Veränderung der X- und Y-Werte des Rechtecks würden sich die Rechtecke aus ihrem Ursprung heraus nur in eine Richtung vergrößern. Um den Tunneleffekt zu ermöglichen, können Sie für die Rechtecke ein TranslateTransform Objekt instanzieren, das dann die Grundlage einer Verschiebungsanimation mit seinen Eigenschaften XProperty und YProperty sein kann. Eine neue DoubleAnimation wird erstellt, die daraufhin für die beiden eben genannten Eigenschaften des TranslateTransform-Objektes ihren Dienst tun wird. TranslateTransform verschiebung = new TranslateTransform();
Wie bei der vorangegangenen Animation für die Größe der Rechtecke werden auch hier die Anzahl der Wiederholungen und eine Startverzögerung für die Animation bestimmt. Beachten Sie, dass der Endwert in der Animation genau die Hälfte des Endwertes für die Animation der Größe beträgt, um in derselben Zeitspanne abzulaufen. Das Rechteck vergrößert sich quasi zwei Einheiten nach links unten, während es durch die Verschiebung als Ganzes eine Einheit nach rechts oben versetzt wird. Daraus entsteht der Effekt der konzentrischen Vergrößerung. verschiebung.BeginAnimation(TranslateTransform.XProperty, doubleAnimationRichtung); verschiebung.BeginAnimation(TranslateTransform.YProperty, doubleAnimationRichtung);
132
Windows Presentation Foundation
Wo aber ist in dieser Animation denn das Rechteck geblieben? Das Rechteck taucht in der Animation für die Verschiebung noch gar nicht auf, lediglich die abhängigen Eigenschaften des TranslateTransform-Objektes werden in der BeginAnimationMethode desselben Objektes in die Animation miteinbezogen. Um eine Verschiebung für ein Element durchzusetzen, muss die RenderTransform-Eigenschaft des betreffenden Elementes auf das erstellte TranslateTransform-Objekt gesetzt werden. Dabei werden die Informationen über die Verschiebung des Objektes angegeben, welche die Position des Objektes beim Zeichnen beeinflussen: rectangleBasis.RenderTransform = verschiebung;
Um den Eindruck zu erwecken, dass die Rechtecke mit der Zeit verschwinden, wird noch eine Animation für die Transparenz erstellt und auf die Rechtecke losgelassen, die Dauer und der Start wurden in Hinsicht auf alle anderen Animationen ein klein wenig modifiziert: DoubleAnimation doubleAnimationTransparenz = new DoubleAnimation(dauer 1.0, 0.0, new Duration(TimeSpan.FromSeconds(dauer))); doubleAnimationTransparenz.BeginTime = TimeSpan.FromSeconds(verzoegerung + 2.0); doubleAnimationTransparenz.RepeatBehavior = RepeatBehavior.Forever; rectangleBasis.BeginAnimation(Rectangle.OpacityProperty, doubleAnimationTransparenz);
Die Kombination der verschiedenen Animationen sollte zur Laufzeit Rechtecke erstellen, die sozusagen aus dem Fenster hinausfliegen. Die Farbe und der Offset werden dabei zufällig gewählt, und die Rechtecke scheinen nach einiger Zeit wieder zu verschwinden.
Abbildung 2.48: Die Animation auf den Rechtecken in Kombination
133
Kapitel 2
Wenn Sie eine wirkliche Tunnelerfahrung mit Ihrem Computer teilen wollen, passen Sie einfach den Offset an, und verändern Sie den Hintergrund und die Farbgebung der Rechtecke auf eine Schwarz-Weiß-Kombination. Aber bitte nicht zu lange hinsehen!
2.11.1 Tools in XAML Wie in der Einleitung zu Windows Presentation Foundation beschrieben, soll mit WPF und der neu eingeführten Markup-Sprache XAML eine einfache Trennung zwischen Logik und Benutzeroberfläche ermöglicht werden. Hierbei lag ein wichtiger Bestandteil auch darin, die Entwickler durch grafische Designer zu entlasten. Dies bringt es schon als Anforderung mit sich, dass ein Designer sich nicht erst mühsam in Visual Studio 2005 und das Code-Behind-Modell einarbeiten muss. Es sollten den Designern schon eher Tools zur Verfügung gestellt werden, mit denen Sie vielleicht sogar intuitiv oder wie aus anderen grafischen Entwicklungstools gewohnt eine WPF-Oberfläche im wahrsten Sinne des Wortes zusammenklicken können. Nur in seltenen Fällen sollten Sie wirklich programmatisch in den XAML-Code eingreifen müssen. Die eine Sprechblasen-Zeichnung als Grafik, die in diesem Buch als Grundlage für das tiefe Verständnis von XAML und der grafischen Klassen mühsam per XAML kodiert wurde, kann mit diesen Designern mit Leichtigkeit erstellt werden. Zu den jetzt schon bekannten Designern zählen allen voran Microsofts Expression Blend, ZAM3D, und viele weitere werden folgen. Viele Hersteller von Grafikprogrammen haben mit Hochdruck daran gearbeitet, dass die erstellten Grafiken auch in XAML exportiert werden können. Ein besonders beeindruckendes Exporttool wird für den Adobe Illustrator zur Verfügung gestellt. Michael Swenson bietet das Plug-Inn auf seiner Internetseite zum Download an (http://www.mikeswanson.com/XAMLExport/). Microsoft selbst legt großen Wert darauf, bald ein eigenes Tool auf den Markt zu bringen, das die Erstellung grafischer Benutzeroberflächen mit all ihren WPF-Features ohne den Einsatz von Visual Studio 2005 ermöglicht. Microsoft Expression Blend liegt zurzeit in der Beta 1-Version vor und stellt den Benutzern moderne und interaktive Benutzeroberflächen und Anwendungen in Aussicht. Mit diesem Tool stehen Ihnen die Türen zu allen multimedialen Möglichkeiten von Windows offen. Videos, Vektorgrafiken, Animationen, Bilder und 3D-Elemente mit eingebauter Interaktivität können ohne programmatischen Aufwand erstellt werden.
Microsoft Expression Blend Expression Blend (vorher unter den Namen Sparcle und Interactive Expression Designer bekannt) wurde von Grund auf dafür entwickelt, die Interaktion zwischen Designern und Entwicklern auf ein Maximum zu erhöhen. Designer können ohne Wissen
134
Windows Presentation Foundation
über die Klassenbibliothek von WPF und ohne objektorientierte Kenntnisse überzeugende Prototypen und Benutzeroberflächen gestalten. Ein wichtiger Gesichtspunkt liegt dabei darin, dass die fertige Oberfläche ohne jegliche Konvertierung direkt in .NET-Anwendungen mit einfließen kann.
Abbildung 2.49: Die Arbeitsumgebung in Expression Blend
In Abbildung 2.49 soll ein Ausblick auf die Arbeitsumgebung von Microsoft Expression Blend gewährt werden. Im linken Teil der Abbildung ist die Leiste für grafische Elemente in WPF gut zu sehen. In den rechts angeordneten Leisten werden die einzelnen Ebenen, deren Farbverläufe und Timelines verwaltet. Alles in allem sieht die Oberfläche den bekannten Grafikprogrammen sehr ähnlich und sollte von Designern sehr intuitiv anwendbar sein. Leider ist eine ausführliche Besprechung eines XAML-Tools nicht vorgesehen, aber ich hoffe, ich konnte Ihnen, ob Designer oder Entwickler, den Mund mit diesem ersten Ausblick etwas wässerig machen.
135
Kapitel 2
2.12 Ausblick In diesem Kapitel wurden alle wesentlichen Themenschwerpunkte der Windows Presentation Foundation angeschnitten. Im Rahmen dieses Buches bleibt es auch nur ein kleiner Ausschnitt dessen, was Microsoft Ihnen als Handwerkszeug wieder einmal zur Verfügung stellt. In der Arbeit mit WPF war ich sehr davon beeindruckt, welch mächtige Klassenbibliothek alleine für die Programmierung von Oberflächen entworfen wurde. Zu diesem ersten Eindruck kommt die Faszination von XAML, das beim Entwurf der Oberflächen ganz ohne den wirklichen Programmierer auskommt. Schon heute setzen Entwicklungsabteilungen auf WPF, um von genau diesen Erfahrungen zu profitieren. Wenn ich die Möglichkeiten betrachte, die XAML in Zusammenarbeit mit grafischen Tools anbietet, und wenn ich nur auf die Oberflächen zurückblicke, die ich im Nu mit solchen Designern erstellen konnte, dann weiß ich, dass die heutige Gestaltung von grafischen Benutzeroberflächen nicht an dieser Technologie vorbeikommen wird.
136
3
Windows Communication Foundation
Nachdem im ersten Kapitel die Windows Presentation Foundation besprochen wurde, kommen wir jetzt zum zweiten neuen Hauptbestandteil des .NET Frameworks 3.0, der Windows Communication Foundation (WCF). Bei der soeben besprochenen Windows Presentation Foundation konnte man mit bunten Grafiken und Animationen sehr einfach für viele Effekte und Spannung sorgen, bei Kommunikation, um die es hier jetzt geht, wird es leider etwas schwieriger. Zwar ist Kommunikation ein wesentlicher Bestandteil von Softwareprodukten, jedoch erwartet man, dass die Kommunikation zwischen Programmen auf einem oder auch auf verschiedenen Rechnern problemlos funktioniert. Und wie das mit der neuen Windows Communication Foundation zu realisieren ist, will ich Ihnen in diesem Kapitel zeigen und Ihnen einen ersten Eindruck dieser spannenden Technologie vermitteln.
3.1 Einführung in WCF Vielleicht haben Sie in der Vergangenheit schon mal etwas über Indigo gelesen, dies war nämlich der Codename für diese neue Technologie, die nun als Teil des .NET Frameworks 3.0 erschienen ist. Entgegen der weit verbreiteten Meinung, dass WCF nur für Windows Vista verfügbar ist, wird es auch unter den
Kapitel 3
Vorgängerversionen genutzt werden können. Die Windows Communication Foundation wird dabei für folgende Betriebssysteme verfügbar sein: Windows XP Service Pack 2 Windows Server 2003 Service Pack 1 Windows Vista Windows Longhorn Server (noch im Betastatus) Nachdem wir in der Vergangenheit in regelmäßigen Abständen neue Kommunikationstechnologien von Microsoft vorgesetzt bekamen, taucht natürlich als Erstes die Frage auf, ob sich die WCF in die mittlerweile schon lange Liste von unterschiedlichen Techniken einreiht. Diese Frage will ich mit einem klaren Nein beantworten. WCF ist mehr eine standardisierte Kommunikationsform, in der viele, in der Vergangenheit eingeführte, Technologien zusammenfinden. In der Vergangenheit gab es in unterschiedlichen Szenarien immer ein Für und ein Wider, manchmal auch ein Ausschlusskriterium, über den Einsatz einer bestimmten Kommunikationstechnologie. Egal ob DCOM (Distributed COM), MSMQ (Microsoft Message Queues), Enterprise Services oder auch Web Services, jede der eingesetzten Technologien besitzt in unterschiedlichen Einsatzszenarien Vor- und natürlich auch Nachteile gegenüber einer anderen Methodik. Die Windows Communication Foundation schickt sich nun an, ein standardisiertes Programmiermodell zu werden, das in allen Situationen die erste Wahl für den Softwareentwickler (oder Architekten) ist. Dabei werden alle Vorteile der Vorgängertechnologien vereint und diese vereinheitlicht, egal ob es sich um Kommunikation auf einem lokalen Rechner, auf unterschiedlichen Rechnern innerhalb des Intranets oder auch des Internets handelt. Außerdem wird dabei auch eine interoperable Kommunikationsmöglichkeit mit anderen Plattformen gewährleistet. In der Vergangenheit musste sich der Entwickler nicht nur um die Implementierung der benötigten Logik kümmern, sondern auch um die Art und Weise, wie Daten ausgetauscht werden, ob Daten verschlüsselt übertragen werden, über welche Protokolle die Softwarekomponenten miteinander kommunizieren, und er musste viele weitere infrastrukturellen Probleme lösen. Viel schöner wäre es doch, wenn sich der Entwickler ausschließlich um die Geschäftslogik seiner Anwendung kümmern müsste und sämtliche infrastrukturellen Einstellungen frei konfigurieren könnte. Und somit sind wir beim Thema: Herzlich Willkommen bei der Windows Communication Foundation!
138
Windows Communication Foundation
Die WCF stellt sich sozusagen als eine Vorlage einer Software Factory für die Kommunikation von Softwareteilen dar, indem sie eine Laufzeitumgebung für Dienste bereitstellt, in der die Geschäftslogik abgearbeitet wird. Die gesamte Kommunikation wird dabei von der Foundation durchgeführt, die der Entwickler nach Bedarf konfigurieren kann. Als eine Art Nachfolger der Webdienste wird die Kommunikation in verteilten Applikationen dabei auf einer serviceorientierten Architektur durchgeführt. Bevor wir unser erstes WCF-Projekt in Angriff nehmen, will ich noch ein paar Grundlagen auffrischen.
3.1.1 Serviceorientierte Architekturen Nachdem SOA (serviceorientierte Architektur) eines der großen Schlagwörter in der heutigen Zeit darstellt, sollten wir uns zuerst ein paar Grundgedanken über serviceorientierte Architekturen machen. Ein Service stellt dabei eine Sammlung von Operationen (Funktionen) dar, die von einem Client aufgerufen werden können. Über fest definierte Schnittstellen kann der Client die Funktionalität eines Service in Anspruch nehmen. Der Service wird dabei durch Metadaten beschrieben, die Schnittstellen für den Client bereitstellen. Diese Schema- und Kontraktinformationen müssen dabei interoperabel vorliegen, um sie für jeden beliebigen Client auch lesbar zu machen. Dabei wird beschrieben, welche Operationen (Funktionen) und Nachrichten ein Dienst anbietet und welche Daten dabei übergeben werden. Über welches Protokoll dabei die Kommunikation erfolgt, wird in sogenannten Policies beschrieben. Die Kommunikation zwischen Client und Service erfolgt dabei immer durch das Austauschen von Nachrichten (Messages) über Endpoints (Endpunkte) die vom Service bereitgestellt werden wie in Abbildung 3.1 dargestellt.
Abbildung 3.1: Kommunikation zwischen Client und Server
139
Kapitel 3
Ein Endpunkt besteht dabei aus: einer Adresse, einem Binding und einem Contract. Dabei definiert die Adresse, wo der Endpunkt liegt, das Binding, wie der Endpunkt aufgerufen wird, und der Contract, was der Endpunkt an Operationen bietet. Bei einer Nachricht werden dabei, auch wenn wir mit .NET in einer objektorientierten Welt leben, keine Objekte ausgetauscht, sondern immer nur Datenstrukturen, die im Contract beschrieben sind. Es werden also nur Daten ausgetauscht und nicht Objekte mit Methoden und Daten. Ein Service zeichnet sich natürlich auch durch eine sehr lose Koppelung zum Client aus. Am Client dürfen keine Implementierungsdetails vom Service bekannt sein, sodass der gesamte Service neu aufgesetzt werden könnte, solange seine Schnittstellen nicht verändert wurden, ohne dass der Client (Konsument) dies überhaupt merken würde.
3.1.2 WCF im Überblick Die Implementierung der Windows Communication Foundation innerhalb des .NET Frameworks 3.0 liegt in der Bibliothek System.ServiceModel.dll.
> >
>
HINWEIS
Sie müssen in jedem WCF-Projekt einen Verweis auf die System.ServiceModel-Bibliothek hinzufügen.
Die Windows Communication Foundation besteht aus einem Servicemodell sowie dem zugehörigen Klassenframework, beides befindet sich in der entsprechenden Bibliothek. Dabei besteht das Servicemodell aus den Klassen des Namespace System.ServiceModel und einer interoperablen Konfigurationssprache. In der heutigen Zeit eignet sich als Konfigurationssprache nichts besser als XML. Somit wird die Konfiguration einer WCF-Applikation mittels einer XML-Datei durchgeführt, und dadurch liegen sämtliche Schema- und Kontraktinformationen, wie für einen Service üblich, in einer implementierungsunabhängigen Form vor.
> >
>
HINWEIS
Sehr oft wird dieser Teil eines Servicemodells auch DSL (Domain Specific Language) genannt.
140
Windows Communication Foundation
Sehr ähnlich wie bei einer WSDL (Web Service Description Language) beschreibt die WCF-Konfiguration sämtliche benötigten Informationen, sodass ein beliebiger Client diesen Service nutzen kann. Informationen
WSDL
WCF
Wo liegt der Service?
Service
Adresse
Welche Bindungen?
Binding
Binding
Welche Operationen bietet der Service?
PortType
Contract
Tabelle 3.1: Gegenüberstellung der Sektionsnamen bei einem Web Service und einer WCF-Applikation
3.1.3 Message Exchange Patterns Als Letztes sollten wir uns noch Gedanken machen, welche Kommunikationsformen es bei verteilten Anwendungen gibt, also in welche Richtungen Nachrichten fließen können. WCF unterstützt folgende Message Exchange Patterns: Simplex Ein Client sendet eine Nachricht an einen Service und erwartet keine Antwort. One-Way-Verhalten Duplex Nachrichtenaustausch in beide Richtungen möglich. Beide Softwareteile können sowohl die Rolle des Clients als auch des Service wahrnehmen. Verhalten zum Beispiel wie bei einer Chatanwendung. Request/Reply Nach dem Senden einer Nachricht erwartet der Client eine Antwort vom Service. Verhalten wie bei einem WebService. So, das sollte aber nun der grauen Theorie genügen, und wir gehen über, das ABC noch einmal anders zu lernen.
141
Kapitel 3
3.2 Das ABC eines Endpoints Wie in der Einführung bereits beschrieben sind die benötigten Informationen eines Endpoints in die drei Kategorien Adresse, Binding und Contract – dem sogenannten ABC eines Endpoints – aufzugliedern. Betrachten wir uns nun, wie sich dieses ABC genau zusammensetzt, indem wir ein kleines Beispiel machen. Aber wir wollen nicht mit dem A beginnen, sondern wir beginnen zuerst mit der Schnittstellenbeschreibung, wie es sich für einen ContractFirst-Programmierer auch gehört.
> >
>
HINWEIS
In der Regel werden Sie bei der Erstellung eines Endpoints nicht ABC, sondern CBA sagen, aber dann wäre dieses Wortspiel nur halb so schön.
Im folgenden Beispiel wollen wir einen WCF-Service schreiben, der über eine Operation die Zeichenkette »Hello Windows Communication Foundation« zurückgibt. Sollte Ihnen das zu trivial erscheinen, können Sie natürlich auch komplexere Operationen verwenden, aber ich denke, im ersten Schritt geht es nur um die Kommunikation und nicht um die Komplexität des Service. Dazu öffnen Sie bitte unter Visual Studio 2005 ein neues Projekt vom Typ KONSOLENANWENDUNG und benennen es HelloWCF_Service.
3.2.1 C – Contract Also kümmern wir uns zuerst um einen sauberen Contract, den Vertrag, den wir mit dem Entwickler des Clients abschließen. Diesen Vertrag, im Grunde geben wir hier genau an, welche Schnittstellen unsere Softwarekomponente bietet und wie diese aufgerufen werden, dürfen wir nach Fertigstellung unseres Service keinesfalls mehr ändern. Ansonsten würden die Clients nicht mehr kompatibel mit unserem Dienst arbeiten können.
*
*
*
TIPP
Nicht umsonst schwören viele Architekten und Entwickler darauf, zuerst eine saubere Schnittstellendefinition zu machen, bevor mit der Implementierung einer Softwarekomponente begonnen wird. ContractFirst ist dazu das Zauberwort.
142
Windows Communication Foundation
Am saubersten ist es, wenn wir den Vertrag in einem Interface definieren. Fügen Sie dazu dem Projekt ein neues Element vom Typ Interface hinzu und benennen die Datei IHelloService.cs. Fügen Sie dann diesem Interface eine Methode DoIt hinzu, die einen String zurückgibt, wie in Listing 3.1. public interface IHelloService { string DoIt(); } Listing 3.1: Definition eines Interface IHelloService
Um dieses Interface nun als Contract eines Service zu definieren, müssen Sie lediglich noch Attribute für das Interface und die zu veröffentlichenden Operationen setzen. Versehen Sie dazu die Interfacedefinition mit dem Attribut ServiceContract. Diese Attributklasse befindet sich im Namespace System.ServiceModel. Also vergessen Sie bitte nicht, zuvor einen Verweis auf die Bibliothek System.ServiceModel zu setzen und diesen Namespace auch zu importieren. Die Operatoren versehen Sie anschließend noch mit dem OperationContract-Attribut. Die gesamte Codedatei sollte, nachdem Sie die beiden Attribute gesetzt haben, aussehen wie in Listing 3.2. using using using using
System; System.Collections.Generic; System.Text; System.ServiceModel;
namespace HelloWCF_Service { [ServiceContract] public interface IHelloService { [OperationContract] string DoIt(); } } Listing 3.2: Interface als ServiceContract
Somit haben Sie Ihren ersten Contract einer WCF-Applikation fertig gestellt.
143
Kapitel 3
Benannte Parameter Beide Attribute besitzen benannte Parameter, die angegeben werden können und in der Tabelle 3.2 für ServiceContract und Tabelle 3.3 für OperationContract erläutert sind. Parametername Datentyp
Bedeutung
Callback Contract
Type
Der Typ des CallbackContract bei einem Duplex Exchange Pattern
Configuration Name
String
Der Name des Service in einer applikationsspezifischen Konfigurationsdatei
Name
String
Name des PortType-Elementes in der WSDL
Namespace
String
Namespace des PortType-Elementes in der WSDL, dieser sollte immer eindeutig angegeben werden, ansonsten ist der Standardwert http://tempuri.org/.
ProtectionLevel
System.Net.Security. ProtectionLevel
SessionMode
SessionMode
Der Standardwert für die Mindestanforderung des ProtectionLevels für alle Operationen dieses Contracts
Gibt an, ob Sessions erlaubt, nicht erlaubt oder benötigt werden
Tabelle 3.2: Benannte Parameter für das Attribut ServiceContract
Parametername Datentyp
Bedeutung
Action
String
Gibt den eindeutigen Namen der Operation bei einem Request an (wird benötigt z.B. bei überladenen Methoden).
AsyncPattern
Boolean
Wird für asynchrone Aufrufe benötigt.
IsInitiating
Boolean
Gibt an, ob die Operation eine Session auf dem Server starten kann.
IsOneWay
Boolean
Gibt an, ob die Operation eine Antwort gibt (bei OneWay Exchange Pattern false).
IsTerminating
Boolean
Gibt an, ob die Operation eine Session, nach dem Senden der Antwort, beenden kann.
Name
String
Name der Operation.
ProtectionLevel
System.Net.Security. ProtectionLevel
Kann den Wert des ProtectionLevel für den Service für eine bestimmte Operation erhöhen.
ReplyAction
String
Gibt den eindeutigen Namen der Operation bei einem Response an.
Tabelle 3.3: Benannte Parameter für das Attribut OperationContract
144
Windows Communication Foundation
Implementierung des Interface Um unser Beispiel auch zum Laufen zu bringen, fügen wir unserer Applikation noch eine weitere Klasse HelloService hinzu, die dieses Interface implementiert. Statten Sie die DoIt-Funktion auch mit Logik aus wie in Listing 3.3 geschehen. public class HelloService:IHelloService { #region IHelloService Member public string DoIt() { return "Hello Windows Communication Foundation"; } #endregion } Listing 3.3: Klasse HelloService, die den ServiceContract aus IHelloService implementiert.
*
*
*
TIPP
Wie Sie sehen, benötigen Sie bei der Definition der Klasse keine Attribute mehr. Sie können theoretisch auch auf die Definition des Interface verzichten und die Attribute direkt an die Klasse binden. Der hier gezeigte Weg ist jedoch wesentlich sauberer und wartbarer, und deswegen sollten Sie bei der Definition des Contracts immer ein Interface einsetzen.
Und somit haben wir den ersten Teil, die Definition des Contracts, erfolgreich hinter uns gebracht.
3.2.2 B – Binding Im zweiten Schritt wollen wir nun das Binding unserer WCF-Applikation definieren. Bindings definieren dabei, wie Endpoints miteinander kommunizieren. Gerade bei Bindings gibt es sehr viele unterschiedliche Einstellungs- und Konfigurationsmöglichkeiten, auf die wir in Kapitel 3.5, noch genauer eingehen wollen. Sie können sich ein Binding am besten als einen Stapel vorstellen, der sich aus einem Transportprotokoll, einem Encoder und verschiedenen Protokollen (Transaktionen, Security, Reliable Messages etc.) zusammensetzt. Die wichtigste Einstellung, die beim Binding vorgenommen wird, ist die Angabe des Transportprotokolls (HTTP oder TCP), das zur Nachrichtenübermittlung verwendet wird, und wie die Nachricht encodiert wird (binär oder Text). Weitere Einstellungen bezüglich Sicherheit und Transaktionen sind ebenso möglich, auf diese soll aber im ersten Beispiel noch verzichtet werden.
145
Kapitel 3
Ein Binding besteht in der Regel aus mehreren Binding-Elementen, in denen Angaben über das Protokoll, die Nachrichtencodierung, Transaktionsverhalten und weitere Sicherheitseinstellungen definiert sind. Ein Binding verlangt dabei immer eine Angabe des Transportprotokolls und der Nachrichtencodierung, alle weiteren Elemente sind optional. Die standardmäßig unterstützten Transportprotokolle sind: HTTP TCP Named Pipes MSMQ (Microsoft Message Queues) Die Nachricht wird immer als SOAP-Nachricht übertragen, wobei für die Encodierung der SOAP-Nachricht eines der drei folgenden Formate möglich ist: Text Binär MTOM (Message Transmission Optimization Mechanism) An dieser Stelle soll erst einmal ein grundlegender Überblick über die verwendbaren Bindings, die bei WCF out of the box kommen, geschaffen werden. WCF stellt dabei schon einige vordefinierten Bindings bereit, diese sind: BasicHttpBinding WsHttpBinding WsDualHttpBinding WsFederationHttpBinding NetTcpBinding NetNamedPipeBinding NetMsmqBinding NetPeerTcpBinding MsmqIntegrationBinding In der Tabelle 3.4 sehen Sie eine kurze Beschreibung der Bindings sowie deren standardmäßige Nachrichtencodierung.
146
Windows Communication Foundation
Binding
Beschreibung
Standardencoding
BasicHttpBinding
Zur Kommunikation mit WS-Basic Profilekonformen WebServices.
Text
WsHttpBinding
Zur sicheren Kommunikation mit interoperablen Diensten. Unterstützt kein Duplex Message Exchange Pattern
Text
WsDualHttpBinding
Zur sicheren Kommunikation mit interoperablen Diensten. Unterstützt Duplex Message Exchange Pattern.
Text
WsFederationHttpBinding
Zur sicheren Kommunikation mit WS-Federation-konformen Diensten, um eine optimale Authentifizierung und Autorisierung von Usern zu gewährleisten.
Text
NetTcpBinding
Zur sicheren und optimierten Kommunikation zwischen unterschiedlichen Rechnern. Keine Interoperabilität zu anderen Technologien.
Binär
NetNamedPipeBinding
Zur sicheren und optimierten Kommunikation zwischen Softwareteilen auf ein und demselben Rechner. Keine Interoperabilität zu anderen Technologien.
Binär
NetMsmqBinding
Zur sicheren und optimierten Kommunikation über Nachrichtenwarteschlangen zwischen unterschiedlichen Rechnern. Keine Interoperabilität zu anderen Technologien.
–
NetPeerTcpBinding
Zur sicheren Kommunikation zwischen mehreren unterschiedlichen Rechnern.
–
MsmqIntegrationBinding
Zur Kommunikation zwischen einer WCF- und einer MSMQ-Applikation auf unterschiedlichen Rechnern.
–
Tabelle 3.4: Vordefinierte Bindings und deren Bedeutung
> >
>
HINWEIS
Alle Bindings, die Text unterstützen, unterstützen auch MTOM für die Übermittlung großer Nachrichtendaten.
Sollten die hier aufgeführten Bindings nicht Ihren Anforderungen entsprechen, können Sie natürlich Ihr eigenes Binding definieren, dies würde man als Custom Binding bezeichnen. Eine weitere Möglichkeit besteht auch darin, ein Standardbinding zu verwenden und durch Konfiguration Ihren Bedürfnissen anzupassen. In unserem Beispiel wollen wir die Konfiguration des Bindings in eine applikationsspezifische XML-Konfigurationsdatei schreiben. Da uns das Microsoft Windows SDK
147
Kapitel 3
dafür einen eigenen Editor zur Verfügung stellt, indem wir sowohl das Binding wie auch die Adresse und den Contract unseres Service angeben, werden wir die Erstellung der Konfigurationsdatei in Kapitel 3.2.4, vornehmen.
> >
>
HINWEIS
In diesem Abschnitt wurde immer von der Konfiguration des Bindings gesprochen. Sie können natürlich das Binding auch per Programmcode definieren. Sie müssen sich aber darüber im Klaren sein, dass jede Änderung eine Neukompilierung und Auslieferung des Codes nach sich zieht. Ein Systemadministrator könnte nicht durch Anpassen von Konfigurationsdateien Bindingeinstellungen ändern, da sie durch Programmcode immer überschrieben werden würden.
3.2.3 A – Adresse Die Adresse ist sehr einfach zu definieren. Sie wird in einer Konfigurationsdatei oder programmatisch festgesetzt und beinhaltet die Angabe des Protokolls, den Maschinennamen, den dazugehörigen Port sowie den Pfad zur Anwendung. Über diese Adresse kann der Endpunkt auf derselben oder auch auf einer anderen Maschine gefunden und natürlich auch aufgerufen werden. Zur Konfiguration der Adresse in unserem kleinen Beispielprojekt werden wir den Service Configuration Editor verwenden, der im nächsten Abschnitt beschrieben ist. Nachdem jetzt die Grundbegriffe des Endpoints ABC erläutert sind, können wir Abbildung 3.1 ein bisschen verfeinern, um die Kommunikation zwischen Endpunkten wie in Abbildung 3.2 darzustellen.
Abbildung 3.2: Kommunikation zwischen Client und Server über das Endpoint ABC
3.2.4 Service Configuration Editor Der Service Configuration Editor ist ein Tool zur Erstellung sämtlicher benötigter Contract-, Binding- und Adresselemente einer XML-Konfigurationsdatei. Dieses Tool ist Bestandteil des .NET 3.0 Windows SDK und kann nach der Installation des SDK unter START – ALLE PROGRAMME – MICROSOFT WINDOWS SDK – TOOLS – SERVICE CONFIGURATION EDITOR aufgerufen werden.
148
Windows Communication Foundation
Bevor wir dies tun, fügen wir aber noch unserem Beispielprojekt eine applikationsspezifische Konfigurationsdatei hinzu, wie Sie in Abbildung 3.3 sehen.
Abbildung 3.3: Hinzufügen einer applikationsspezifischen Konfigurationsdatei
Danach speichern und kompilieren wir unser Projekt. Jetzt können wir den Service Configuration Editor starten. Über das Menü OPEN – CONFIG FILE… wählen wir die gerade neu angelegte app.config aus.
> >
>
HINWEIS
Stören Sie sich nicht daran, dass manche Dialoge in diesem Editor schon (teilweise) ins Deutsche übersetzt sind und andere noch gar nicht.
Im folgenden Dialog, den Sie in Abbildung 3.4 sehen, wählen Sie bitte CREATE A NEW SERVICE aus, um einen neuen Service anzulegen. Als Nächstes werden Sie aufgefordert, den Typ des Service anzugeben, den Ihr Dienst bereitstellt. Das ist im Normalfall der Name der Klasse (inkl. Namensraum), welche die gewünschten Operationen beinhaltet, in unserem Fall also HelloWCF_Service. HelloService. Mit der Schaltfläche BROWSE können Sie dabei nach der Auswahl der zuvor erzeugten Assembly die Klasse auch über den Dialog auswählen.
149
Kapitel 3
Abbildung 3.4: Anlegen eines neuen Service
Abbildung 3.5: Auswahl des Service-Typs
150
Windows Communication Foundation
Im nächsten Schritt wird der Contract abgefragt, den wir ursprünglich über das Interface IHelloService beschrieben haben. Dadurch, dass wir bei der Angabe des Typs bereits unsere Klasse HelloService ausgewählt haben und diese das Interface IHelloService implementiert, schlägt uns der Editor bereits auch den richtigen ServiceContract vor, wie Sie in Abbildung 3.6 sehen.
Abbildung 3.6: Auswahl des ServiceContracts
Somit sind alle Angaben bezüglich des C (Contract) gemacht. Als Nächstes folgt die Konfiguration des Bindings. Im nächsten Dialog, siehe Abbildung 3.7, können wir die Auswahl des Transportprotokolls vornehmen. Am besten können wir hier den Standardwert HTTP übernehmen. Da wir im ersten Beispiel keine weiteren Sicherheits- und Transaktionseinstellungen wünschen, bleiben wir auch im nächsten Dialog (Abbildung 3.8) bei der Standardauswahl BASIC WEB SERVICES INTEROPERABILITY. Dieser Dialog erscheint nur bei der vorherigen Auswahl HTTP als Transportprotokoll. Bei allen anderen Varianten wären wir sofort zur Angabe der Adresse unseres Service gelangt.
151
Kapitel 3
Abbildung 3.7: Auswahl des Transportprotokolls
Abbildung 3.8: Auswahl der WebService-Interoperabilität
152
Windows Communication Foundation
Jetzt werden wir, auch für die Auswahl http, nach der Adresse unseres Dienstes gefragt. Geben Sie dazu einfach in das vorgesehene Dialogfeld folgende Adresse an: http://localhost:2121/HelloService Somit geben wir als Protokoll HTTP an, die Anwendung liegt auf dem lokalen Rechner durch die Angabe von localhost. In einer produktiven Umgebung sollte hier natürlich die IP-Adresse des Servers stehen. Dieser folgt eine frei wählbare Angabe der Portnummer und ein Pfad zur Anwendung, der wiederum frei wählbar ist.
*
*
*
TIPP
Die Portnummer ist zwar frei wählbar, sollte aber immer größer als 1024 sein, da darunter die vom System belegten Ports liegen. Der angegebene Port darf auch auf dem System von keiner anderen Applikation bereits verwendet werden.
Nachdem wir alle Angaben gemacht haben, zeigt uns der Service Configuration Editor noch eine Zusammenfassung unserer Angaben an. Dies sollte bei Ihnen dann auch so aussehen wie in Abbildung 3.9.
Abbildung 3.9: Zusammenfassung der Eingaben
153
Kapitel 3
Mit FINISH können wir jetzt den Assistenten zur Erstellung des Endpoints beenden und sehen im folgenden Dialog (siehe Abbildung 3.10) das Ergebnis unserer Eingaben.
Abbildung 3.10: Auflistung der Endpoints
An dieser Stelle können wir natürlich noch weitere Dinge verfeinern und einstellen, aber für das erste Beispiel sollte dies genügen. Wenn Sie wollen, können Sie dem Endpoint noch einen sprechenden Namen geben und sich seine Eigenschaften noch einmal anschauen, wenn Sie in der Baumansicht den Knoten ENDPOINTS öffnen und den einzigen darunter liegenden Knoten (EMPTY NAME) anklicken. Ich habe hier dem Endpoint den Namen HelloServiceHttpEndpoint gegeben, wie Sie in Abbildung 3.11 sehen. Wenn Sie dann im Menü den Befehl FILE – SAVE aufrufen, wird Ihre applikationsspezifische Konfigurationsdatei im Projekt angepasst.
154
Windows Communication Foundation
Abbildung 3.11: Konfiguration eines Endpoints
> >
>
HINWEIS
Wenn Sie die Microsoft Visual Studio 2005 CTP Extensions for .NET Framework 3.0 installiert haben, dann können Sie den Service Configuration Editor auch direkt aus der Entwicklungsumgebung heraus aufrufen. Legen Sie dazu eine applikationsspezifische Konfigurationsdatei an, und dann können Sie im Projektmappen-Explorer im Kontextmenü dieser Datei den Eintrag EDIT WCF CONFIGURATION… aufrufen.
Die app.config sollte danach wie in Listing 3.4 aussehen.
155
Kapitel 3 Listing 3.4: Konfigurationsdatei nach Durchlaufen des Konfigurationseditors
Dabei sehen Sie, dass es innerhalb von einen neuen Tag gibt, innerhalb dessen alle Services definiert werden. Das -Tag listet alle Services auf, die einzeln innerhalb eines -Tags definiert werden. Die Angaben, die innerhalb eines Service vorgenommen wurden, sind der Name des Service sowie alle ABC-Angaben für den Endpoint, die in einem eigenen Tag zusammengefasst wurden.
*
*
*
TIPP
Wenn Sie dieselbe Operation auch über das TCP-Protokoll aufrufbar machen wollen, dann brauchen Sie nur einen weiteren Endpoint innerhalb des Service zu definieren und ihm eine andere Adresse, z.B. in der Form net.tcp://localhost:2122/HelloService, und ein anderes Standardbinding, z.B. netTcpBinding, zu geben. Ein alternativer Name ist natürlich auch sinnvoll.
Damit sind wir fürs Erste mit der Definition unseres Service fertig. Was uns in unserem kleinen Beispiel noch fehlt, sind das Hosting des Service sowie das Erstellen eines Clients, der unseren Service aufruft. Dies wird in den nächsten beiden Abschnitten dargestellt.
3.2.5 Hosting der Beispielanwendung Gleich vorneweg, für das Hosting von Communication Services gibt es viele Möglichkeiten, auf die ich ganz explizit in Kapitel 3.4, eingehen möchte. An dieser Stelle wollen wir unser kleines Beispiel erst einmal zum Laufen bringen und hosten den Dienst in der ursprünglichen Konsolenanwendung. Dazu wählen wir in unserem Beispielprojekt die Datei Program.cs aus und erweitern die Main-Methode, wie Sie in Listing 3.5 sehen. using using using using
System; System.Collections.Generic; System.Text; System.ServiceModel;
namespace HelloWCF_Service { class Program {
156
Windows Communication Foundation static void Main(string[] args) { using (ServiceHost sh = new ServiceHost(typeof(HelloService))) { sh.Open(); Console.WriteLine("Service bereit..."); Console.ReadLine(); } } } } Listing 3.5: Main-Methode des Service
Innerhalb der Main-Methode instanzieren wir ein Objekt vom Typ ServiceHost. Ein Objekt vom Typ ServiceHost ist dabei ein Hilfsobjekt, das es ermöglicht, einen bestimmten Service zu hosten. Es hostet also unseren HelloService, dessen Typ wir im Konstruktor des ServiceHost angeben. Die using-Anweisung definiert dazu einen Block, in dem der ServiceHost gültig ist. Durch das Drücken der (¢_)-Taste wird dieser Block verlassen und der ServiceHost sh aus dem Speicher entfernt.
*
*
*
TIPP
Wenn Sie die Sache ohne einen using-Block programmieren wollen, müssen Sie am Ende die Methode Close des ServiceHost-Objektes aufrufen.
Die Methode Open des Objektes ServiceHost öffnet den Port, und der Dienst steht somit für die Clients zur Verfügung. Durch das Drücken der Taste (F5) wird die Applikation gestartet und durch die (¢_)-Taste wieder beendet.
Abbildung 3.12: Gestarteter Service
Wenn Sie auf Ihrem System bereits Vista installiert haben, müssen Sie dieses Programm explizit als Administrator starten, da Sie ansonsten keine Rechte besitzen, um eine Kommunikation zu starten. Das erfordert das neue Sicherheitssystem von Windows Vista. Abbildung 3.13 zeigt die Fehlermeldung, die ansonsten bei der Ausführung in Visual Studio 2005 auftritt.
157
Kapitel 3
Abbildung 3.13: Fehlermeldung unter Windows Vista wegen fehlender Ausführungsberechtigung
Jetzt müssen wir nur noch einen Client definieren, der unseren Service nutzt.
3.2.6 Erstellen des Clients mit dem Tool svcutil Als Client wollen wir der Einfachheit halber wiederum eine Konsolenanwendung anlegen. Fügen Sie dazu unserer Projektmappe eine weitere Konsolenanwendung mit dem Namen HelloWCF_Client hinzu. Frage & Antwort Nun, welche Informationen benötigen wir am Client, um den Service aufzurufen? Zum einen benötigen wir eine Konfigurationsdatei, um uns korrekt mit dem Service zu verbinden, und eine Proxyklasse, über welche die Operationen aufrufbar sind.
Um diese beiden Dateien automatisch zu erstellen, gibt es das Befehlszeilentool svcutil.exe, das über die Visual Studio 2005 Eingabeaufforderung aufrufbar ist. Leider können wir mit den bisherigen Konfigurationseinstellungen des Service dieses Tool noch nicht einsetzen, denn wir haben noch kein zusätzliches Serviceverhalten definiert, das es uns erlaubt, Metadaten zu erzeugen.
> >
>
HINWEIS
Behaviors können für Services definiert werden und ermöglichen ein bestimmtes Verhalten, das eine Windows Communication Foundation Applikation annehmen kann.
158
Windows Communication Foundation
Service Behavior Nun wollen wir zuerst ein Behavior unserem Service hinzufügen, das es uns erlaubt, Metadaten mit dem Tool svcutil zu erzeugen. Dazu gehen wir zum Service Configuration Editor zurück. Falls Sie ihn schon geschlossen haben, dann starten Sie das Tool bitte noch einmal und öffnen dann die vorhin angelegte app.config. Öffnen Sie bitte den Knoten ADVANCED und wählen den darunter liegenden Knoten SERVICE BEHAVIORS in der Baumansicht aus. Legen Sie dann mittels NEW SERVICE BEHAVIOR CONFIGURATION ein neues Serviceverhalten an (siehe Abbildung 3.14).
Abbildung 3.14: Anlegen eines neuen ServiceBehaviors
Dem neuen ServiceBehavior habe ich den Namen ServiceBehaviorMeta gegeben. Mit der Schaltfläche ADD fügen Sie bitte ein neues Behavior-Element vom Typ serviceMetadata hinzu, wie Sie es in Abbildung 3.15 sehen. Über den neuen Knoten SERVICEMETADATA können schließlich URLs angegeben werden, über welche die Metadaten per HTTP oder HTTPS abgerufen werden können.
159
Kapitel 3
Abbildung 3.15: Hinzufügen eines serviceMetadata-Behaviorelements
In unserem Beispiel reicht es, die Metadaten für http zu aktivieren, indem Sie die Eigenschaft HttpGetUrl auf true setzen. Als URL gebe ich, wie in Abbildung 3.16 zu sehen, die Adresse http://localhost:2121/HelloService/meta ein.
Abbildung 3.16: Angabe der URL zum Abruf der Metadaten
160
Windows Communication Foundation
Um unseren Service jetzt noch mit diesem neuen Behavior auszustatten, wählen Sie bitte innerhalb des obersten Knotens SERVICES unseren Service HELLOWCF_ SERVICE.HELLOSERVICE aus. Wählen Sie dann auf der rechten Seite für den Eintrag BEHAVIORCONFIGURATION den gerade angelegten Knoten ServiceBehaviorMetadata wie in Abbildung 3.17 aus.
Abbildung 3.17: Definition des Behaviors für den Service
Wenn Sie diese Anpassungen mit FILE – SAVE wieder speichern, wird die entsprechende Applikationskonfigurationsdatei wieder angepasst. Den neuen Anteil der app.config sehen Sie in Listing 3.6.
161
Kapitel 3 Listing 3.6: Konfigurationsdatei nach Hinzufügen des ServiceBehaviors
Im Vergleich zu Listing 3.4 ist ein behaviors-Abschnitt hinzugefügt worden. Innerhalb dieses Abschnitts gibt es eine weitere Auflistung serviceBehaviors, in dem letztendlich unser neu hinzugefügtes ServiceBehaviorMeta-Behavior angegeben ist. Als Element sehen wir unsere Extension serviceMetadata mit den beiden Attributen httpGetEnabled und httpGetUrl. Außerdem wurde im service-Tag ein zusätzliches Attribut behaviorConfiguration hinzugefügt, das auf das neu definierte ServiceBehaviorMetadata verweist. So, nun können wir die gewünschten Metadaten aus dem Service extrahieren. Der Service muss dazu neu gestartet werden, damit die Änderungen der app.config auch eingelesen werden.
svcutil Mit dem Befehlszeilentool svcutil können Sie nun die gewünschten Dateien automatisch erzeugen. Dazu geben Sie in der Visual Studio 2005-Eingabeaufforderung bitte folgende Parameter an: URL zum Download der Metadaten out: Name der Codedatei für den Proxy config: Name der zu erstellenden Konfigurationsdatei In unserem Fall sieht also die Befehlszeile, wie Sie auch in Abbildung 3.18 sehen, wie folgt aus. svcutil http://localhost:2121/HelloService/meta /out:Client.cs / config:app.config
162
Windows Communication Foundation
Standardmäßig erzeugt das svcutil-Tool C#-Code. Wollen Sie VB-Code erzeugen, müssten Sie noch explizit den language-Parameter angeben. Die Befehlszeile zum Erzeugen der Proxyklasse als VB-Code würde dann wie folgt aussehen: svcutil http://localhost:2121/HelloService/meta /out:Client.vb / config:app.config /language:vb
> >
>
HINWEIS
Bitte beachten Sie, dass die Serveranwendung gestartet sein muss, bevor die Metadaten downgeloadet werden.
Abbildung 3.18: Ausgabe nach Aufruf des Tools
Dieses Tool hat jetzt zwei Dateien Client.cs und app.config angelegt, die Sie bitte dem neuen HelloWCF_Client-Projekt hinzufügen. Vergessen Sie auch nicht, im Clientprojekt einen Verweis auf die System.ServiceModel-Bibliothek zu setzen.
> >
>
HINWEIS
Für dieses Tool gibt es noch eine ganze Reihe weiterer Parameter, die Sie selber gerne erforschen können. Wenn Sie in der Eingabeaufforderung einfach svcutil eintippen ,wird Ihnen die gesamte Hilfe angezeigt.
Sehr häufig wird dieses Tool auch eingesetzt, um aus einem XML-Schema (einer xsdDatei) Programmcode in Form von Datenstrukturen zu generieren.
Clientproxy Innerhalb der Client.cs werden unterschiedliche Interfaces und Klassen definiert. IHelloService Implementiert dieselben Schnittstellen wie das Interface, das dem Contract zugrunde liegt, sowie zusätzliche Attribute.
163
Kapitel 3 [System.CodeDom.Compiler.GeneratedCodeAttribute ("System.ServiceModel", "3.0.0.0")] [System.ServiceModel.ServiceContractAttribute (ConfigurationName="IHelloService")] public interface IHelloService { [System.ServiceModel.OperationContractAttribute (Action="http://tempuri.org/IHelloService/DoIt", ReplyAction= "http://tempuri.org/IHelloService/DoItResponse")] string DoIt(); }
IHelloServiceChannel Implementiert das Interface IHelloService sowie das Interface IClientChannel aus dem Namespace System.ServiceModel. Ein ClientChannel-Objekt ist dabei das Gegenstück des ServiceHost auf dem Client. [System.CodeDom.Compiler.GeneratedCodeAttribute ("System.ServiceModel", "3.0.0.0")] public interface IHelloServiceChannel : IHelloService, System.ServiceModel.IClientChannel { }
HelloServiceClient Erbt von der generischen Basisklasse ClientBase und implementiert das Interface IHelloService. Enthält außerdem noch einige überladene Konstruktoren, welche die übergebenen Informationen an den Konstruktor der generischen Basisklasse weiterreichen. [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute ("System.ServiceModel", "3.0.0.0")] public partial class HelloServiceClient : System.ServiceModel.ClientBase, IHelloService { public HelloServiceClient() { } public HelloServiceClient(string endpointConfigurationName) : base(endpointConfigurationName) { }
164
Windows Communication Foundation public HelloServiceClient(string endpointConfigurationName, string remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public HelloServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public HelloServiceClient( System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public string DoIt() { return base.Channel.DoIt(); } }
Sie müssen anschließend nur noch im Hauptprogramm (Program.cs) ein Objekt vom Typ HelloServiceClient instanzieren und die entsprechende DoIt-Methode des Objektes aufrufen, um den Dienst zu konsumieren.
Clientkonfiguration Den Inhalt der Konfigurationsdatei app.config sehen Sie in Listing 3.7.
165
Kapitel 3 Listing 3.7: Konfiguration des Clients
Wie Sie in Listing 3.7 sehen, wurden die Eigenschaften des HelloServiceHttpEndpoint in der bindings-Auflistung als httpBinding konfiguriert. In der Clientkonfiguration wurde dann der Endpoint mit der zugehörigen Adresse, dem verwendeten Binding, der Bindingkonfiguration HelloServiceHttpEndpoint sowie dem zugrunde liegenden Contract IHelloService definiert.
Anpassungen im Clientprojekt Somit sind alle Informationen zum Starten der Applikation definiert. Wir müssen nur noch in der Mainroutine die Operation des Typs aufrufen. Nachdem die Operation aufgerufen wurde, können wir mit der Methode Close die Verbindung wieder trennen. Den Code in der Mainroutine sehen Sie in Listing 3.8. static void Main(string[] args) { HelloServiceClient hsc = new HelloServiceClient(); Console.WriteLine(hsc.DoIt()); hsc.Close(); Console.ReadLine(); } Listing 3.8: Mainroutine des Clients
Damit aus dieser Projektmappe heraus beide Applikationen starten, müssen Sie noch über das Kontextmenü STARTPROJEKTE FESTLEGEN… der Projektmappe beide Applikationen als Startprojekte festlegen, wie Sie in Abbildung 3.19 sehen.
166
Windows Communication Foundation
Abbildung 3.19: Startkonfiguration des Beispielprojektes
Die Projektausgabe unserer ersten WCF-Applikation sehen Sie in Abbildung 3.20.
Abbildung 3.20: Ausgabe der beiden Konsolenanwendungen
> >
>
HINWEIS
Das Ganze hätte natürlich auch funktioniert, wenn beide Softwarekomponenten auf unterschiedlichen Rechnern gelaufen wären. Dazu hätten Sie lediglich in der Clientkonfiguration die Adresse anpassen müssen (anstatt localhost die IP-Adresse des Servers).
Somit haben Sie gerade Ihre erste WCF-Applikation komplett fertig gestellt. Ich hoffe, dass das Grundprinzip somit verstanden wurde, bevor wir in den folgenden Abschnitten ein bisschen in die Tiefe gehen.
167
Kapitel 3
3.3 Datenserialisierung In unserem Einführungsbeispiel wurden an die Operation DoIt keine Parameter übergeben, und der Rückgabewert war vom Typ String. Nicht besonders spannend, muss ich zugeben. Deswegen werden wir in diesem Abschnitt betrachten, wie wir komplexere Datenstrukturen zwischen den Softwarekomponenten austauschen können und welche Möglichkeiten der Datenserialisierung wir mit WCF besitzen.
!
!
!
ACHTUNG
Eines gleich nochmals vorneweg: Bei WCF werden KEINE Objekte ausgetauscht, sondern es werden lediglich Datenstrukturen, oder auch Datentransferobjekte, in Nachrichten ausgetauscht. Diese Datenstrukturen besitzen keinerlei Funktionalität in Form von Methoden, sondern lediglich reine Daten.
3.3.1 Datenserialisierer WCF arbeitet mit drei unterschiedlichen XML-Datenserialisierern: DataContractSerializer XmlSerializer NetDataContractSerializer Der DataContractSerializer ist dabei der unter WCF eingesetzte Standardserialisierer, der mit dem .NET 3.0 Framework neu eingeführt wurde. Um ihn zu nutzen, muss man einen Verweis auf die Bibliothek System.Runtime.Serialization.dll setzen. Wenn Sie mit dem DataContractSerializer arbeiten, muss jedes Member einer Klasse, das serialisiert werden soll, explizit mit einem Attribut DataMember versehen werden. Dabei macht es keinen Unterschied, ob es sich um ein öffentliches oder internes Member handelt. Die zu serialisierende Klasse selbst muss mit dem Attribut DataContract versehen werden. Diese Klasse ist somit auch Teil des Contracts, wie es der Name des Attributs auch bereits aussagt, eines Endpoints. Denn deren Strukturen müssen der Clientanwendung auch bekannt sein. Der DataContractSerializer kann dabei auch Typen serialisieren, die das Interface IDictionary, wie z.B. eine Hashtable, implementieren. Im Gegensatz zum XmlSerializer, der bereits in den .NET-Vorgängerversionen zur Verfügung stand, bietet der DataContractSerializer nicht so viele Einstellungsmöglichkeiten, wie Objekte in SOAP serialisiert werden können, was jedoch zu einer besseren Performance gegenüber dem XmlSerializer führt, da der Serialisierungsprozess optimiert werden kann. Der XmlSerializer befindet sich im Namespace System.Xml.Serialization.
168
Windows Communication Foundation
Mittels des XmlSerializer ist eine bei weitem bessere Kontrolle über das Mapping (Name, Position, Reihenfolge etc.) der Datenstrukturen zu XML-Datentypen möglich. Er kann jedoch als Einschränkung nur öffentliche Datenmember serialisieren. Sollten Sie mit dem standardmäßigen Ergebnis der Serialisierung Probleme haben oder nicht zufrieden sein, sollten Sie sich für den XmlSerializer entscheiden. In den allermeisten Fällen jedoch wird der DataContractSerializer seine Arbeit zufriedenstellend erledigen. Der NetDataContractSerializer arbeitet identisch wie der DataContractSerializer, er serialisiert zusätzlich alle .NET-Typinformationen in XML. Nun wollen wir in unserer kleinen Beispielanwendung noch eine Klasse Person implementieren, die in einer weiteren (noch zu definierenden) Methode als Datenstruktur für Personendaten, als Rückgabewert eingesetzt wird. Fügen Sie dazu der Serveranwendung eine Klasse Person mit den Properties Vorname, Famname und Geburtstag hinzu. Versehen Sie danach die Klasse Person mit dem Attribut DataContract und die privaten Variablen mit dem Attribut DataMember. Das DataContract-Attribut versehe ich dabei auch noch mit dem benannten Parameter Namespace für eine eindeutige Zuordnung eines Namensraums.
> >
>
HINWEIS
Vergessen Sie nicht den Verweis auf die Bibliothek System.Runtime.Serialization und den Import des gleichnamigen Namespace. Ansonsten erkennt IntelliSense die beiden Attribute nicht.
Listing 3.9 zeigt die neue Codedatei Person.cs. using using using using
System; System.Collections.Generic; System.Text; System.Runtime.Serialization;
namespace HelloWCF_Service { [DataContract(Namespace="http://primetime-software.de/NET30")] public class Person { [DataMember] private string mVorname; [DataMember] private string mFamname; [DataMember] private DateTime mGeburtstag; public string Vorname
169
Kapitel 3 { get { return mVorname; } set { mVorname = value; } } public string Famname { get { return mFamname; } set { mFamname = value; } } public DateTime Geburtstag { get { return mGeburtstag; } set { mGeburtstag = value; } } } } Listing 3.9: Code der Datenstruktur Person
!
!
!
ACHTUNG
Sollten Sie ein Member nicht mit dem DataMember-Attribut versehen haben, wird es nicht serialisiert!
Benannte Parameter Beide Attribute besitzen benannte Parameter, die optional angegeben werden können und in der Tabelle 3.5 für DataContract und Tabelle 3.6 für DataMember erläutert sind. Parametername
Datentyp
Bedeutung
Name
String
Name des Datenkontrakts
Namespace
String
Namespace des Datenkontrakts, ist auch der Name im XML-Schema, ansonsten wäre es wieder http://tempuri.org/.
Tabelle 3.5: Benannte Parameter für das Attribut DataContract
Parametername
Datentyp
Bedeutung
EmitDefaultValue
Boolean
Gibt an, ob eine Property serialisiert werden soll, falls die Property einen Initialwert für den Datentyp (z.B. 0 bei Zahlentypen) besitzt. Soll dieses Verhalten nicht gewünscht werden, muss man diesen Wert auf false setzen, was aber nur in Ausnahmefällen passieren sollte.
Tabelle 3.6: Benannte Parameter für das Attribut DataMember
170
Windows Communication Foundation
Parametername
Datentyp
Bedeutung
IsRequired
Boolean
Gibt an, dass dieser Wert gesetzt sein muss.
Name
Boolean
Name des Datenmembers im XML-Schema
Order
Boolean
Gibt die Reihenfolge der Serialisierung an. In Ausnahmefällen muss vielleicht ein Datenmember vor einem anderen serialisiert bzw. deserialisiert werden.
Tabelle 3.6: Benannte Parameter für das Attribut DataMember (Fortsetzung)
Extrahieren eines XML-Schemas Nachdem Sie Ihre Datenstrukturen mit den entsprechenden Attributen versehen haben, gibt es auch die Möglichkeit, sich mit dem svcutil-Befehlszeilentool eine xsdSchemadatei automatisch erzeugen zu lassen. Dies geschieht durch den Parameter datacontractonly gefolgt vom Namen der Assembly, in der die Datenstrukturen definiert sind. Für unser Beispiel benötigen Sie kein xsd-Schema, ich möchte hier nur demonstrieren, wie Sie prinzipiell Schemas aus Ihren Klassen extrahieren können. Öffnen Sie dazu wiederum die Visual Studio 2005-Eingabeaufforderung, und tippen Sie folgende Befehlszeile: svcutil /datacontractonly HelloWCF_Service.exe
Bitte geben Sie für die Exe-Datei den kompletten Pfad an. Das Tool erzeugt dann, neben zwei weiteren allgemeinen Schemadateien, die gewünschte XML-Schemadatei. Der Name der Schemadatei ist in unserem Falle primetime-software.de.NET30.xsd. Dabei wird der Namespace des DataContract in den Dateinamen umgesetzt, wobei das Zeichen / in einen Punkt umgesetzt wurde, da es sich ansonsten um einen ungültigen Dateinamen handeln würde. Derselbe Name wird später auch als Namespace im Client verwendet werden (ohne /). Das erzeugte Schema für unsere Datenstruktur sehen Sie in Listing 3.10. Listing 3.10: XML-Schema der Datenstruktur im Namespace primetime-software.de/NET30
Anpassungen am Interface und an der Serviceklasse Nun wollen wir auch noch unser Interface IHelloService um eine Operation erweitern, die eine Datenstruktur vom Typ Person zurückgibt. Erweitern Sie dazu das Interface wie in Listing 3.11 dargestellt. [ServiceContract] public interface IHelloService { [OperationContract] string DoIt(); [OperationContract] Person GetPerson(string vorname); } Listing 3.11: Erweitertes Interface IHelloService
Ich habe anfangs von drei möglichen Serialisierern gesprochen, wir haben aber bislang noch nicht gesehen, wo wir einen speziellen Serialisierer auswählen. Nun, da wir offensichtlich keine Auswahl getroffen haben, wird der Standardserialisierer DataContractSerializer verwendet. Wollen Sie jedoch den XmlSerializer verwenden, dann müssen Sie für das Interface noch zusätzlich das Attribut XmlSerializerFormat angeben. Wenn Sie mehrere Operationen besitzen, bei denen Daten serialisiert werden müssen, Sie jedoch nur spezielle Operationen mit dem XmlSerializer serialisieren wollen, dann können Sie das Attribut auch auf Methodenebene setzen. Listing 3.12 zeigt das identische Interface IHelloService jedoch mit dem Attribut, das die WCF-Laufzeitumgebung anweist, dass die Serialisierung der XmlSerializer vorzunehmen ist. [ServiceContract] [XmlSerializerFormat] public interface IHelloService { [OperationContract] string DoIt(); [OperationContract]
172
Windows Communication Foundation //[XmlSerializerFormat] Person GetPerson(string vorname); } Listing 3.12: Erweitertes Interface IHelloService mit dem XmlSerializer
Für den weiteren Verlauf des Beispiels habe ich das XmlSerializerFormat-Attribut wieder entfernt und verwende somit den DataContractSerializer. Die Klasse HelloService habe ich schließlich noch mit der Methode GetPerson erweitert, so wie in Listing 3.13 dargestellt. public Person GetPerson(string vorname) { Person p = new Person(); switch (vorname.ToLower()) { case "jürgen": p.Famname = "Kotz"; p.Vorname = "Jürgen"; p.Geburtstag = new DateTime(1967,12,24); break; case "rouven": p.Famname = "Haban"; p.Vorname = "Rouven"; p.Geburtstag = new DateTime(1976,8,24); break; case "simon": p.Famname = "Steckermeier"; p.Vorname = "Simon"; p.Geburtstag = new DateTime(1980,1,29); break; default: p.Famname = "Nowhere"; p.Vorname = "Man"; p.Geburtstag = new DateTime(1965, 5, 1); break; } return p; } Listing 3.13: Implementierte Methode GetPerson
In Abhängigkeit eines übergebenen Vornamens wird dabei ein Personenobjekt instanziert und mit den entsprechenden Werten versorgt.
173
Kapitel 3
Anpassungen am Client Um diese Methode nun auch am Client aufrufen zu können, müssen wir die Proxyklasse mit dem Tool svcutil noch einmal neu erstellen. Gehen Sie dabei bitte genauso vor, wie bereits im Abschnitt »svcutil« auf Seite 162 beschrieben. An der applikationsspezifischen Konfigurationsdatei hat sich nichts geändert. Listing 3.14 zeigt die neue Client.cs-Datei. [assembly: System.Runtime.Serialization.ContractNamespaceAttribute ("http://primetime-software.de/NET30", ClrNamespace="primetimesoftware.de.NET30")] namespace primetimesoftware.de.NET30 { using System.Runtime.Serialization; [System.CodeDom.Compiler.GeneratedCodeAttribute ("System.Runtime.Serialization", "3.0.0.0")] [System.Runtime.Serialization.DataContractAttribute()] public partial class Person : object, System.Runtime.Serialization.IExtensibleDataObject { private System.Runtime.Serialization.ExtensionDataObject extensionDataField; private string mFamnameField; private System.DateTime mGeburtstagField; private string mVornameField; public System.Runtime.Serialization.ExtensionDataObject ExtensionData { get { return this.extensionDataField; } set { this.extensionDataField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public string mFamname { get
174
Windows Communication Foundation { return this.mFamnameField; } set { this.mFamnameField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public System.DateTime mGeburtstag { get { return this.mGeburtstagField; } set { this.mGeburtstagField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public string mVorname { get { return this.mVornameField; } set { this.mVornameField = value; } } } [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] [System.ServiceModel.ServiceContractAttribute (ConfigurationName="IHelloService")] public interface IHelloService { [System.ServiceModel.OperationContractAttribute (Action="http://tempuri.org/IHelloService/DoIt", ReplyAction="http://tempuri.org/IHelloService/DoItResponse")] string DoIt(); [System.ServiceModel.OperationContractAttribute
175
Kapitel 3 (Action="http://tempuri.org/IHelloService/GetPerson", ReplyAction="http://tempuri.org/IHelloService/GetPersonResponse")] primetimesoftware.de.NET30.Person GetPerson(string vorname); } [System.CodeDom.Compiler.GeneratedCodeAttribute ("System.ServiceModel", "3.0.0.0")] public interface IHelloServiceChannel : IHelloService, System.ServiceModel.IClientChannel { } [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute ("System.ServiceModel", "3.0.0.0")] public partial class HelloServiceClient : System.ServiceModel.ClientBase, IHelloService { public HelloServiceClient() { } public HelloServiceClient(string endpointConfigurationName) : base(endpointConfigurationName) { } public HelloServiceClient(string endpointConfigurationName, string remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public HelloServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public HelloServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public string DoIt() { return base.Channel.DoIt();
176
Windows Communication Foundation } public primetimesoftware.de.NET30.Person GetPerson(string vorname) { return base.Channel.GetPerson(vorname); } } Listing 3.14: Aktualisierte Client.cs
Sie sehen, dass sich diese Datei im Vergleich zur ersten Version, die Sie ab der Seite 164 sehen, doch deutlich vergrößert hat. Zu Beginn der Codedatei sehen wir ein Attribut, das für die gesamte Assembly gilt. Hier wird der Namespace für unseren DataContract definiert. Das bedeutet, der Namespace der Klasse Person ist am Client primetime-software.de.NET30. Im Folgenden sehen Sie dann die Definition der Klasse Person im Namensraum primetime-software.de.NET30. Beachten Sie dabei, dass Person abgeleitet ist vom Typ object und das Interface IExtensibleDataObject aus dem Namespace System.Runtime.Serialization implementiert. Dazu ein bisschen mehr im folgenden Abschnitt 3.3.2.
> >
>
HINWEIS
Sie benötigen jetzt auch am Client einen Verweis auf die Bibliothek System.Runtime.Serialization.dll.
Innerhalb der Klasse sehen wir die Properties der Datenstruktur Person. Da wir das DataMember-Attribut bei den privaten Feldern angegeben haben, denen wir ein m als Präfix vorangestellt haben, besitzen die Properties auch dieses m als Präfix. Hätten wir das DataMember-Attribut ursprünglich bei den Properties gesetzt, dann hätten die Properties im Proxy auch kein Präfix. Diese Properties besitzen auch alle das DataMemberAttribute. Ein bisschen merkwürdig scheint jedoch auch die zusätzliche Property ExtensionData vom Typ ExtensionDataObject. Diese Property kommt aus dem Interface IExtensibleDataObject und besitzt auch kein Attribut. Aber wie gesagt, mehr dazu gleich. Ansonsten ist sowohl das Interface IHelloService sowie die Klasse HelloServiceClient nur um die neue Methode GetPerson erweitert worden. Um die neue Funktionalität noch zu testen, müssen wir nur noch die Main-Methode in der Clientanwendung erweitern und die neue Methode aufrufen. Die benötigten Anpassungen hierzu sehen Sie in Listing 3.15.
177
Kapitel 3 static void Main(string[] args) { HelloServiceClient hsc = new HelloServiceClient(); Console.WriteLine(hsc.DoIt()); primetimesoftware.de.NET30.Person p = hsc.GetPerson("Jürgen"); Console.WriteLine("{0} {1} ist am {2} geboren.", p.mFamname, p.mVorname, p.mGeburtstag.ToShortDateString()); hsc.Close(); Console.ReadLine(); } Listing 3.15: Neue Main-Methode am Client
Hierzu rufen wir die Methode GetPerson der Proxyklasse HelloServiceClient auf und übergeben als Parameter den Vornamen Jürgen. Der Rückgabewert vom Typ Person im angegebenen Namespace primetimesoftware.de.NET30 nimmt das Responseobjekt wieder auf, und die Eigenschaften werden dann in der Konsole ausgegeben, wie Sie in Abbildung 3.21 sehen.
Abbildung 3.21: Ausgabe der Datenstruktur Person
3.3.2 Versionierung Nun ja, leider passiert es allzu oft, dass Datenstrukturen geändert werden müssen und somit unterschiedliche Versionen der Datenstrukturen existieren, da vielleicht die Clients noch nicht an das neue Modell angepasst wurden. WCF bietet jedoch die Möglichkeit, dass auch alte Clients mit neueren Servern, oder auch umgekehrt, kommunizieren können. Diese Technik innerhalb von WCF nennt sich Round Tripping und muss in den entsprechenden Typen, die den DataContract darstellen, implementiert sein. Unter Round Tripping versteht man, dass Daten zwischen einem Client und einem Server mit unterschiedlichen Versionen von Datenstrukturen ausgetauscht werden können, ohne dass irgendwelche Daten dabei verloren gehen.
178
Windows Communication Foundation
Um Round Tripping zu aktivieren, muss der entsprechende Typ das IExtensibleDataObject-Interface implementieren. Das svcutil-Tool, das uns die Proxyklasse für den Client erzeugte, hat automatisch dieses Interface beim Typ Person hinzugefügt, wie Sie in Listing 3.14 sehen können. Und dieses Interface implementiert genau die eine zusätzliche Property ExtensionData vom Typ ExtensionDataObject, das wir im selben Listing wieder finden. Wenn wir nun unsere Klasse Person um ein weiteres Datenmember erweitern, wird die Serialisierung der Person vom Server zum Client trotzdem funktionieren, da am Proxy dieses Interface implementiert ist. Zu Ihrer Überraschung funktioniert es aber auch, wenn Sie in der Proxyklasse das Interface entfernen. Der DataContractSerializer ignoriert einfach die zusätzlichen Daten, ohne einen Laufzeitfehler zu verursachen. Jetzt taucht natürlich sofort die Frage auf, für was ist denn dieses Interface sinnvoll? Wenn Sie das Interface nicht implementiert haben, gehen die Daten von neuen Properties verloren, während sie durch die Implementierung des Interface in dem Property ExtensionData zwischengespeichert werden. Wird das Objekt jetzt wieder zum Server zurück übertragen, dann wird auch auf dem Server die ursprüngliche Property wieder hergestellt. Ohne das Interface wäre diese Information verloren. Um dies zu testen, müssen wir in zwei Schritten vorgehen. Zum Ersten implementieren wir auf dem Server, im Interface IHelloService und in der Klasse HelloService, eine neue Methode GetDetails, die als Parameter ein Objekt vom Typ Person erwartet. Sie gibt dabei einen beliebigen String zurück, der sich aus den Daten der Person zusammensetzt (siehe Listing 3.16 und Listing 3.17). [ServiceContract] public interface IHelloService { [OperationContract] string DoIt(); [OperationContract] Person GetPerson(string vorname); [OperationContract] string GetDetails(Person p); } Listing 3.16: Interface IHelloService mit zusätzlicher Methode GetDetails
179
Kapitel 3 public string GetDetails(Person p) { return p.Vorname + " " + p.Famname; } Listing 3.17: Ausschnitt aus der Klasse HelloService (neue Methode GetDetails)
Dann erstellen wir den Service neu und generieren wiederum mit dem svcutil-Tool die neue Proxyklasse client.cs, mit der wir im Clientprojekt die bisherige Version ersetzen. Im Proxy wurde jetzt ebenso die neue Methode GetDetails implementiert, die wir noch in der Main-Routine (Listing 3.18) aufrufen wollen. class Program { static void Main(string[] args) { HelloServiceClient hsc = new HelloServiceClient(); Console.WriteLine(hsc.DoIt()); primetimesoftware.de.NET30.Person p = hsc.GetPerson("Jürgen"); Console.WriteLine("{0} {1} ist am {2} geboren.", p.mFamname, p.mVorname, p.mGeburtstag.ToShortDateString()); //Objekt wird wieder zum Server zurück übergeben Console.WriteLine(hsc.GetDetails(p)); hsc.Close(); Console.ReadLine(); } } Listing 3.18: Main-Routine des Clients
In Abbildung 3.22 sehen Sie, dass die Funktionalität auch erfolgreich aufgerufen wird, was jedoch nicht sonderlich überraschend war.
Abbildung 3.22: Projektausgabe mit dem zusätzlichen Aufruf von GetDetails
Im nächsten Schritt führen wir jetzt nur Erweiterungen am Server durch. Zum einen fügen wir der Struktur Person eine weitere Property Wohnort hinzu und versehen die dazugehörige Feldvariable auch mit dem DataMember-Attribut.
180
Windows Communication Foundation
In der Methode GetPerson in der Klasse HelloService setzen wir jeweils den Wohnort, und in der Methode GetDetails geben wir dann zusätzlich den Wohnort mit aus, wie Sie es auch in Listing 3.19 sehen. public class HelloService:IHelloService { #region IHelloService Member public string DoIt() { return "Hello Windows Communication Foundation"; } public Person GetPerson(string vorname) { Person p = new Person(); switch (vorname.ToLower()) { case "jürgen": p.Famname = "Kotz"; p.Vorname = "Jürgen"; p.Geburtstag = new DateTime(1967,12,24); //Mit Angabe des Wohnorts p.Wohnort = "Metten"; break; case "rouven": p.Famname = "Haban"; p.Vorname = "Rouven"; p.Geburtstag = new DateTime(1976,8,24); p.Wohnort = "München"; break; case "simon": p.Famname = "Steckermeier"; p.Vorname = "Simon"; p.Geburtstag = new DateTime(1980,1,29); p.Wohnort = "Deggendorf"; break; default: p.Famname = "Nowhere"; p.Vorname = "Man"; p.Geburtstag = new DateTime(1965, 5, 1); p.Wohnort = "Liverpool"; break; }
181
Kapitel 3 return p; } public string GetDetails(Person p) { //Jetzt mit Wohnort return p.Vorname + " " + p.Famname + " aus " + p.Wohnort; } #endregion } Listing 3.19: Anpassungen der Implementierung der Klasse HelloService
Wenn Sie das Projekt jetzt noch einmal starten, dann werden Sie sehen, dass der Wohnort beim Roundtrip vom Server zum Client nicht verloren ging, da der Client das Interface IExtensibleDataObject implementiert. Abbildung 3.23 zeigt, wie der Wohnort mit ausgegeben wird.
Abbildung 3.23: Ausgabe mit Wohnort
Interessant ist es nun auch, einen Haltepunkt an der Stelle im Client zu setzen, an dem das Objekt Person wieder an den Server zurückgeschickt wird. Abbildung 3.24 zeigt dabei den internen Aufbau des Personenobjektes während des Debuggens im LOKALFENSTER. Hier können Sie sehr gut sehen, wie die zusätzliche Property Wohnort im ExtensionDataField serialisiert wurde, obwohl es am Client nicht bekannt ist. Dies ist auch die Voraussetzung dafür, dass der Wert bei einem Roundtrip nicht verloren geht. Der Vollständigkeit halber sollten wir das Interface IExtensibleDataObject auch in der Datenstruktur Person implementieren, da es ja durchaus möglich wäre, dass der Client eine aktuellere Datenbeschreibung besitzt als der Server. Und das Round Tripping sollte schon in beide Richtungen funktionieren.
182
Windows Communication Foundation
Abbildung 3.24: Anzeige der Eigenschaft ExtensionData des Personenobjektes während der Laufzeit
Dazu implementieren wir das Interface in der Klasse Person wie in Listing 3.20. [DataContract(Namespace="http://primetime-software.de/NET30")] public class Person:IExtensibleDataObject { [DataMember] private string mVorname; [DataMember] private string mFamname; [DataMember] private DateTime mGeburtstag; [DataMember] private string mWohnort; public string Vorname { get { return mVorname; } set { mVorname = value; } }
183
Kapitel 3 public string Famname { get { return mFamname; } set { mFamname = value; } } public DateTime Geburtstag { get { return mGeburtstag; } set { mGeburtstag = value; } } public string Wohnort { get { return mWohnort; } set { mWohnort = value; } } #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } } #endregion } Listing 3.20: Vollständiges Listing der Klasse Person
In Listing 3.20 sehen Sie dabei in der Region IExtensibleDataObject Member die neu hinzugefügte Property. Beachten Sie dabei unbedingt, dass weder die Feldvariable noch die Property mit dem DataMember-Attribut versehen ist.
Das Attribut KnownType Im Normalfall liegen die DataContracts beim Austausch von Datenstrukturen an beiden beteiligten Endpunkten komplett vor, sodass sowohl die Serialisierung als auch die Deserialisierung dieser Datenstrukturen problemlos funktioniert.
184
Windows Communication Foundation
Bei der Deserialisierung wird dabei nach einem passenden Typ gesucht, der kompatibel mit dem Datenkontrakt der Nachricht ist. Die dazu in Frage kommenden Typen sind die sogenannten Known Types des Deserialisierers. Wie ich aber gerade geschrieben habe, liegen sie nur im Normalfall komplett vor, denn es gibt Situationen, in denen die Informationen auf einer Seite nicht komplett vorhanden sind. Dies hat zur Folge, dass die Serialisierung bzw. Deserialisierung der Daten nicht funktioniert. Bei folgenden Szenarien liegen die DataContracts bei beiden Endpunkten in der Regel nicht komplett vor: 1. Server schickt eine abgeleitete Klasse von der als Übergabeparameter erwarteten Basisklasse. Die tatsächlich übergebene Klasse ist für den Empfänger nicht bekannt. 2. Bei der Definition der Übergabeparameter wird ein Interface verwendet. Der Empfänger kann nicht alle Klassen kennen, die tatsächlich übergeben werden, und erkennt diesen Parameter somit nicht als Teil des Datenvertrags. 3. Der Übergabeparameter ist definiert als Object. 4. Das übergebene Objekt enthält als Datenmember ein Objekt, für das eines der drei gerade genannten Punkte zutrifft. Um diese Sonderfälle zu beheben, wurde das KnownType-Attribut eingeführt. Mit diesem Attribut können Sie den DataContract von verschiedenen Typen bekannt geben, die obigen Regeln entsprechen. Dem KnownType-Attribut können Sie im Konstruktor einen Typ mitgeben, dessen DataContract anschließend auch dem anderen Endpoint zur Verfügung steht. Somit steht dieser Typ in der Liste der bekannten Typen des Deserialisierers. Das KnownType-Attribut kann mehrfach, aber nur auf Ebene des DataContract, also für die gesamte Klasse und nicht für einzelne Member, vergeben werden. Für einfache Datentypen (string, int, date etc.) benötigen Sie keine Angabe als KnownType. Machen wir aber vielleicht dazu ein kleines Beispiel, indem wir eine zusätzliche Klasse PersonMitTelefon anlegen, die sich von Person ableitet. Der Einfachheit halber habe ich die Klasse in derselben Codedatei wie die Klasse Person implementiert. Listing 3.21 zeigt die neue Klasse. [DataContract(Namespace = "http://primetime-software.de/NET30")] public class PersonMitTelefon : Person { [DataMember] private string mTelNummer;
185
Kapitel 3 public string TelefonNummer { get { return mTelNummer; } set { mTelNummer = value; } } } Listing 3.21: Neue Klasse PersonmitTelefon
In der Methode GetPerson gebe ich dann in der Klasse HelloService nicht ein Objekt vom Typ Person, sondern vom Typ PersonMitTelefon zurück, indem ich im defaultZweig folgende Codeanpassung durchführe: default: //kein objekt vom Typ Person, //sondern vom Typ PersonmitTelefon p = new PersonmitTelefon(); p.Famname = "Nowhere"; p.Vorname = "Man"; p.Geburtstag = new DateTime(1965, 5, 1); p.Wohnort = "Liverpool"; break;
So, nun erweitere ich den Client noch um einen weiteren Aufruf der Methode GetPerson und übergebe dabei einen nicht bekannten Vornamen. Da dies zu einer Rückgabe eines Objektes vom Typ PersonMitTelefon und dies zum jetzigen Zeitpunkt noch zu einem Problem führt, habe ich auch noch zusätzlich eine Ausnahmebehandlung in die Main-Routine eingebaut. Den aktuellen Code sehen Sie in Listing 3.22. static void Main(string[] args) { HelloServiceClient hsc = null; try { hsc = new HelloServiceClient(); Console.WriteLine(hsc.DoIt()); primetimesoftware.de.NET30.Person p = hsc.GetPerson("Jürgen"); Console.WriteLine("{0} {1} ist am {2} geboren.", p.mFamname, p.mVorname, p.mGeburtstag.ToShortDateString()); //Objekt wird wieder zum Server zurück übergeben Console.WriteLine(hsc.GetDetails(p)); //Aufruf mit einem unbekannten Vornamen //es wird ein Objekt vom Typ PersonMitTelefon zurückgegeben p = hsc.GetPerson("xxx"); Console.WriteLine("{0} {1} ist am {2} geboren.", p.mFamname, p.mVorname, p.mGeburtstag.ToShortDateString()); } catch (Exception ex) {
186
Windows Communication Foundation Console.WriteLine(ex.Message); } finally { hsc.Close(); Console.ReadLine(); } } Listing 3.22: Main-Methode mit dem zusätzlichen Aufruf von GetPerson mit einem unbekannten Vornamen
Vergessen Sie bitte nicht, die Metadaten für den Proxy mit svcutil neu zu generieren. Wenn Sie das Projekt jetzt starten, sehen Sie, wie Abbildung 3.25 zeigt, dass die Ausführung des Programms am Client zu einem Fehler führt. Zugegeben, die Fehlermeldung an dieser Stelle ist nicht unbedingt hilfreich. Das Problem tritt an der Stelle auf, an dem versucht wird, das Objekt am Client zu serialisieren.
Abbildung 3.25: Ausnahme wurde ausgelöst beim Aufruf von GetPerson
Wenn Sie jetzt aber das KnownType-Attribut für die Klasse Person wie in Listing 3.23 verwenden und daraufhin die Proxyklasse wieder neu erstellen, sehen Sie in Abbildung 3.26, dass die Programmausführung wieder fehlerfrei funktioniert. Der Deserialisierer kann jetzt mit dem entsprechenden Objekt umgehen, denn er hat es in seiner Liste von bekannten Typen stehen. [DataContract(Namespace="http://primetime-software.de/NET30")] [KnownType(typeof(PersonMitTelefon))] public class Person:IExtensibleDataObject { … } Listing 3.23: KnownType-Attribut
187
Kapitel 3
Abbildung 3.26: PersonMitTelefon wurde erfolgreich deserialisiert.
Best Practice bei der Gestaltung von DataContracts Um bei der Versionierung von Datenstrukturen auf relativ wenige Probleme zu stoßen, sollten Sie folgende Grundregeln beachten: Implementieren Sie immer das Interface IExtensibleDataObject. Ändern Sie in Folgeversionen keinesfalls: – den Namen oder Namespace eines DataContract – den Namen oder Namespace eines DataMember –
die IsRequired-Eigenschaft eines DataMember
– den Datentyp eines DataMember –
die Order-Eigenschaft eines DataMember
Entfernen Sie keine bereits definierten DataMember. Wenn Sie neue DataMember zu Ihrem DataContract hinzufügen: –
Setzen Sie nie den benannten Parameter IsRequired des DataMember auf true.
–
Geben Sie beim benannten Parameter Order des DataMember einen Wert an, welcher der Version der Datenstruktur entspricht. Die Originaldatenmember besitzen keinen Order-Parameter, die der nächsten Version den Wert 2, der übernächsten Version den Wert 3 usw. Der benannte Parameter Order muss innerhalb eines DataContract nicht eindeutig sein.
Außerdem sollten Sie sich überlegen, ob Sie nicht prinzipiell alle Eingabeparameter einer Methode als einen Datentyp definieren. Das würde bedeuten, dass jede Methode genau einen Request-Parameter besitzt. Das schaut vielleicht auf den ersten Blick etwas umständlich aus, besitzt jedoch unglaubliche Vorteile, die ich kurz erläutern möchte. Schauen wir uns die Signatur der bislang verwendeten Operation GetPerson an. Person GetPerson(string vorname);
188
Windows Communication Foundation
Ich denke, wir können uns sehr gut vorstellen, dass irgendwann der Vorname einer Person nicht mehr eindeutig ist. Um dieses Problem zu umgehen, übergeben wir einfach den Vornamen und den Familiennamen, was zur folgenden nicht kompatiblen Signatur Person GetPerson(string vorname, string famname);
führt. Auch durch den Einsatz des IExtensibleDataObject-Interface werden wir einen inkompatiblen Server und Client nicht mehr zum Laufen bringen. Wenn wir aber von vornherein unsere Schnittstelle mit folgender Signatur Person GetPerson(GetPersonRequest requestData);
geplant hätten und das Objekt GetPersonRequest im ersten Schritt lediglich aus einer einzigen Property Vorname bestanden hätte, dann könnten wir für die benötigte Umstellung problemlos unser Objekt um eine zweite Property Famname erweitern und hätten keine Kompatibilitätsprobleme zwischen einem aktuellen Server und einem veralteten Client.
*
*
*
TIPP
Um das konsequent durchzuziehen, sollten Sie sich darüber im Klaren sein, dass Sie auch einen Request-Parameter definieren müssen, auch wenn Sie ursprünglich planen, eine parameterlose Methode zu implementieren. Sie können natürlich auch jeden Rückgabewert einer Methode in ein eigenes Responseobjekt packen, damit wären Sie dann wirklich absolut auf der sicheren Seite.
Um Ihnen einen Eindruck zu vermitteln, wie das aussehen könnte, habe ich in Listing 3.24 das Interface IHelloService_BestPractice, auf der Basis des ursprünglichen Interface IHelloService, und sämtliche benötigten Request- und Responseobjekte definiert. using using using using using
System; System.Collections.Generic; System.Text; System.ServiceModel; System.Runtime.Serialization;
namespace HelloWCF_Service { [ServiceContract] public interface IHelloService_BestPractice { [OperationContract] DoItResponse DoIt(DoItRequest request);
189
Kapitel 3 [OperationContract] GetPersonResponse GetPerson(GetPersonRequest request); [OperationContract] GetDetailsResponse GetDetails(GetPersonRequest request); } [DataContract(Namespace="http://primetime-software.de/NET30")] public class DoItRequest:IExtensibleDataObject { #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } } #endregion } [DataContract(Namespace="http://primetime-software.de/NET30")] public class DoItResponse:IExtensibleDataObject { #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } } #endregion private string mResult; public string Result { get { return mResult; } set { mResult = value; } } } [DataContract(Namespace = "http://primetime-software.de/NET30")] public class GetPersonRequest : IExtensibleDataObject { #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } }
190
Windows Communication Foundation #endregion private string mVorname; public string Vorname { get { return mVorname; } set { mVorname = value; } } } [DataContract(Namespace = "http://primetime-software.de/NET30")] public class GetPersonResponse : IExtensibleDataObject { #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } } #endregion private Person mResult; public Person Result { get { return mResult; } set { mResult = value; } } } [DataContract(Namespace = "http://primetime-software.de/NET30")] public class GetDetailsRequest : IExtensibleDataObject { #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } } #endregion private Person mPerson; public Person Person { get { return mPerson; } set { mPerson = value; } } } [DataContract(Namespace = "http://primetime-software.de/NET30")] public class GetDetailsResponse : IExtensibleDataObject
191
Kapitel 3 { #region IExtensibleDataObject Member private ExtensionDataObject mExtensionData; public ExtensionDataObject ExtensionData { get { return mExtensionData; } set { mExtensionData = value; } } #endregion private string mResult; public string Result { get { return mResult; } set { mResult = value; } } } } Listing 3.24: Sauber modelliertes Interface mit Request- und Responseobjekten
Zugegebenermaßen ist das ein nicht zu verachtender Zusatzaufwand, jedoch sollten Sie sich bei großen Projekten darüber bewusst sein, dass dies eine Investition in die Zukunft darstellt. Bei Änderungen an den Datenstrukturen stehen Sie nämlich jetzt auf der sicheren Seite, wenn Sie sich an die Regeln am Beginn dieses Abschnitts gehalten haben. Denn dann können Sie auch problemlos versionieren und neuere Services bereitstellen, ohne dass sämtliche Clients, die Sie vielleicht nicht mal alle kennen, an die neue Version angepasst werden müssen.
3.3.3 Fehlerbehandlung Auch die Fehlerbehandlung kann man durch die Datenserialisierung wesentlich verbessern. Nehmen wir einfach an, dass wir in der Methode GetPerson eine Ausnahme werfen wollen, falls kein Vorname eines Autors dieses Buchs übergeben wird. Wir geben also keine PersonMitTelefon zurück, sondern lösen eine Ausnahme aus. Dazu ändern wir am Server, in der Methode GetPerson der Klasse HelloService, den default-Zweig wie folgt: default: throw new Exception("Ungültiger Vorname " + vorname);
192
Windows Communication Foundation
Wenn Sie jetzt das Projekt wieder starten und »Ungültiger Vorname« am Client als Fehlermeldung erwarten, werden Sie leider enttäuscht sein. In Abbildung 3.27 sehen Sie die aussagelose Fehlermeldung, die am Client erscheint und uns nicht wirklich auf das Problem hinweist.
Abbildung 3.27: Aussagelose Fehlermeldung am Client
Mittels des generischen FaultException-Objekts aus dem Namespace System.ServiceModel haben wir ein Exceptionobjekt zur Verfügung, mit dem wir unsere Ausnahmemeldungen zum Client serialisieren können. Wir können diesem generischen Objekt einen Typ übergeben, der eine Fehlermeldung aufnehmen kann, die dann am Client ausgegeben wird. Dazu definieren wir uns zuerst einen Typ GetPersonFault mit einer einzigen Property Message. Markieren Sie bitte die Klasse mit dem DataContract-Attribut und die Property Message mit dem DataMember-Attribut, sodass die Serialisierung auch klappt. Außerdem fügen Sie der Klasse noch einen Konstruktor hinzu, an den die Fehlernachricht übergeben werden kann. Die fertige neue Klasse sehen Sie in Listing 3.25. [DataContract(Namespace = "http://primetime-software.de/NET30")] public class GetPersonFault { private string mMessage; [DataMember] public string Message { get { return mMessage; } set { mMessage = value; } } public GetPersonFault(string message) {
193
Kapitel 3 this.Message = message; } } Listing 3.25: Klasse GetPersonFault
An der Stelle, an der wir die Ausnahme auslösen, müssen wir auch wieder eine kleine Codeanpassung durchführen. Ändern Sie dazu am Server, in der Methode GetPerson der Klasse HelloService, wiederum den default-Zweig wie folgt: default: //throw new Exception("Ungültiger Vorname " + vorname); throw new System.ServiceModel.FaultException (new GetPersonFault("Ungültiger Vorname " + vorname));
Sie lösen jetzt an dieser Stelle eine generische FaultException des Typs GetPersonFault aus und übergeben dabei dem Konstruktor der Klasse FaultException eine neue Instanz der Klasse GetPersonFault. An diese neue Instanz wird ebenso dem Konstruktor der String »Ungültiger Vorname« und der tatsächliche Wert des Parameters vorname übergeben. Damit der Datenvertrag der Klasse GetPersonFault auch am Client bekannt ist, muss nun nur noch der Operation GetPerson im Interface ein FaultContract-Attribut hinzugefügt werden. Ändern Sie dazu im Interface IHelloService die Definition der Methode GetPerson wie folgt: [OperationContract] [FaultContract(typeof(GetPersonFault))] Person GetPerson(string vorname);
Sie sehen das zusätzliche Attribut FaultContract. Diesem Attribut wird dabei im Konstruktor der Typ der Klasse übergeben, in der die tatsächliche Fehlermeldung steht. Somit haben wir am Server alle notwendigen Vorkehrungen getroffen und erstellen, Sie sollten jetzt schon darin geübt sein, mit dem svcutil-Tool ein weiteres Mal die Proxyklasse für den Client. Zu guter Letzt müssen wir jetzt noch die Fehlerbehandlung am Client anpassen. Fügen Sie dazu einen weiteren Catch-Zweig hinzu wie in Listing 3.26 dargestellt. catch (FaultException ex) { Console.WriteLine(ex.Detail.Message); } Listing 3.26: Catch-Zweig für die FaultException
194
Windows Communication Foundation
Über die Eigenschaft Detail des FaultException-Objekts haben wir Zugriff auf die Klasse GetPersonFault und können auf alle öffentlichen Member (in diesem Fall haben wir nur die Eigenschaft Message) zugreifen. Wenn Sie jetzt das Projekt wiederum starten und einen nicht bekannten Vornamen übergeben, sehen Sie, wie in Abbildung 3.28 dargestellt, die gewünschte Ausgabe der Fehlermeldung.
Abbildung 3.28: Treffende Fehlermeldung am Client
3.4 Hosting von Communication Services Bislang haben wir die Funktionalität, die unser Service bereitgestellt hat, einfach in einem Projekt vom Typ Konsolenanwendung implementiert. Ich denke, es ist allen Lesern klar, dass dies nur zur vereinfachten Illustration gedacht war und in produktiven Projekten keinesfalls so gemacht werden sollte. In diesem Kapitel will ich Ihnen nun die unterschiedlichen Hostingmöglichkeiten von WCF-Applikationen zeigen. Dazu holen wir alle Dateien, die zum Service gehören, aus unserem Konsolenprojekt heraus und implementieren sie in einer Klassenbibliothek mit dem Namen WCFService. Dabei handelt es sich um folgende Dateien: GetPersonFault.cs HelloService.cs IHelloService.cs Person.cs Vergessen Sie bitte nicht, auch einen Verweis auf die beiden Bibliotheken System.ServiceModel und System.Runtime.Serialization zu setzen, bevor Sie die Bibliothek erstellen. Ihr Projektmappen-Explorer sollte aussehen wie in Abbildung 3.29 dargestellt.
195
Kapitel 3
Diese Bibliothek implementiert unseren Service und wird in allen nun folgenden aufgezeigten Hosts als Grundlage verwendet. Unter Host versteht man dabei den Prozess, der diese Bibliothek zur Laufzeit lädt, denn eine DLL ist alleine nicht lauffähig. Sie benötigt immer einen Host.
Abbildung 3.29: Projektmappen-Explorer der Klassenbibliothek
3.4.1 Selfhosting Unser bislang entwickeltes Beispiel wurde selbst gehostet, entspricht also dem Selfhosting, um das es in diesem Abschnitt geht. Dabei ist Selfhosting die mit Abstand einfachste Möglichkeit, einen Service zu hosten, denn es wird keine Infrastruktur benötigt, die zusätzlich konfiguriert und installiert werden muss. Zur Demonstration von WCF eignet sich Selfhosting durchaus sehr gut, doch sollte man innerhalb von Produktionsumgebungen auf andere Hosts, die bereits erweiterte Hosting-Features und administrative Tools von sich aus mitbringen, zurückgreifen. Als Projekttypen für Selfhosting eignen sich in der Regel Konsolenapplikationen und Windows Forms/WPF-Applikationen.
*
*
*
TIPP
Bei der Entwicklung eines Service bietet sich Selfhosting wunderbar an, denn man kann direkt mit dem Entwickeln beginnen und das Programm und den Service auch sehr einfach testen und debuggen. Die Umstellung auf einen anderen Host wird, wie Sie noch sehen werden, nicht allzu viel Aufwand bedeuten.
196
Windows Communication Foundation
Als Unterschied zur bisher gezeigten Lösung will ich den Service nicht mehr innerhalb der Hostanwendung implementieren, sondern Ihnen zeigen, wie Sie auf den Service über die gerade erstellte Klassenbibliothek zugreifen können. Dabei verwende ich wieder eine Konsolenapplikation, der ich den Namen WCFSelfHost gebe. Innerhalb des Projekts setze ich dann einen Verweis auf die beiden Bibliotheken System.ServiceModel.dll sowie auf unsere gerade erstellte Bibliothek WCFService.dll. Danach füge ich als applikationsspezifische Konfigurationsdatei die app.config zum Projekt hinzu, die wir bislang auch in der Serveranwendung verwendet haben. Der Programmcode in der Mainroutine ist eigentlich auch identisch mit dem Code im ursprünglichen Projekt. Beachten Sie nur, dass die zu hostende Klasse HelloService jetzt aus einem anderen Namensraum kommt, deswegen auch der Import des Namensraum HelloWCF_Service. Listing 3.27 zeigt den Code der Datei Program.cs. using using using using using
System; System.Collections.Generic; System.Text; System.ServiceModel; HelloWCF_Service;
namespace WCFSelfHost { class Program { static void Main(string[] args) { using (ServiceHost sh = new ServiceHost(typeof(HelloService))) { sh.Open(); Console.WriteLine("Service bereit..."); Console.ReadLine(); } } } } Listing 3.27: Programmcode der Selfhost-Applikation
Nach der Instanzierung des ServiceHost und dem Aufruf der Open-Methode steht der Server für Clients zur Verfügung. Die Konfiguration der Endpunkte wird dabei automatisch aus der applikationsspezifischen Konfigurationsdatei gelesen. Und schon ist der Selfhost lauffähig, und der Client, Sie können dazu wiederum das bisherige Clientprojekt verwenden, kann auf den Service zugreifen.
197
Kapitel 3
3.4.2 IIS-Hosting Ähnlich wie .NET WebServices oder .NET Remoting-Anwendungen können auch WCF-Applikationen von den Internet-Informationsdiensten gehostet werden. Somit übernimmt der Standard-Webserver das Laden der entsprechenden Bibliotheken und stellt sie den Clients zur Verfügung.
> >
>
HINWEIS
WCF-Applikationen können dabei von den IIS ab der Version 5.1 gehostet werden, wobei bei den beiden Versionen 5.1 und 6.0 als Transportprotokoll nur http zur Verfügung steht. Erst ab der Version IIS 7.0 unter Windows Vista stehen alle Transportprotokolle zur Verfügung, mehr dazu in Kapitel 3.4.4.
Hosting durch die Internet Information Services bietet folgende Vorteile: Administrative Tools Bekannte Deploymentszenarien (genauso wie Web Services) Prozess wird bei einem Request automatisch aktiviert, er muss nicht von Hand gestartet werden Erhöhte Skalierbarkeit, da mehrere Applikationen in einem gemeinsamen Workerprozess laufen können Möglichkeit der dynamischen Kompilierung wie in ASP.NET 2.0
Einrichten einer WCF-Applikation unter IIS Damit das Hosting einer WCF-Applikation korrekt funktioniert, muss sich die WCF bei den IIS registrieren. Das läuft normalerweise im Installationsprozess des .NET Frameworks 3.0 ab. Sollten jedoch die IIS erst nachträglich installiert worden sein, ist noch ein Schritt für die korrekte Registrierung nötig. Und dieser Schritt schaut in Abhängigkeit von der Version der IIS unterschiedlich aus. Wenn Sie den IIS 5.1 oder 6.0 installiert haben, können Sie die Registrierung mit dem Befehlszeilentool ServiceModelReg durchführen. Geben Sie einfach in der Visual Studio 2005-Eingabeaufforderung folgende Befehlszeile ein: ServiceModelReg.exe /i /x
Für den IIS 7.0 unter Windows Vista müssen Sie unter SYSTEMSTEUERUNG – SOFTWARE – WINDOWS-KOMPONENTEN HINZUFÜGEN/ENTFERNEN die Windows Communication Foundation Activation Components zusätzlich installieren. Somit sind die Voraussetzungen zum Einrichten einer WCF-Applikation unter IIS erfüllt.
198
Windows Communication Foundation
Im Folgenden sind die Schritte für die Installation auf dem IIS 5.1 beschrieben. Die Installation auf den anderen Versionen verläuft sehr ähnlich, lediglich die hier gezeigten Abbildungen können etwas abweichen. Starten Sie eine neue Instanz von Visual Studio 2005, und legen Sie eine neue Website vom Typ WCF Service, wie in Abbildung 3.30 dargestellt, an.
Abbildung 3.30: Anlegen eines neuen WCF Service im IIS
Fügen Sie dann als Erstes einen Verweis auf Ihre Klassenbibliothek WCFService.dll hinzu. Die Verweise auf die Bibliotheken System.ServiceModel bzw. System.Runtime. Serialization sind bereits bei diesem Projekttyp gesetzt. Im App-Code-Verzeichnis des Projektes ist eine Beispieldatei service.cs enthalten, die wir nicht benötigen. Deswegen sollten wir diese Datei auch entfernen. Als Nächstes müssen wir dringend die Datei service.svc bearbeiten.
> >
>
HINWEIS
Die Dateiendung svc steht für spezial content file (wundern Sie sich nicht, dass die Abkürzung irgendwie nicht passt). Sie entspricht in etwa einer asmx-Datei bei Webdiensten und enthält nur WCF-spezifische Direktiven.
199
Kapitel 3
Löschen Sie den bisherigen Inhalt der Datei, und fügen Sie folgende Direktive in die Datei ein:
Diese Direktive startet bei einem Aufruf über den IIS einen ServiceHost über die WCFInfrastruktur und hostet die Klasse HelloService aus dem Namespace HelloWCF_ Service. Sie entspricht somit der Anweisung ServiceHost sh = new ServiceHost(typeof(HelloService))
die wir beim Selfhosting verwendet haben. Am besten benennen wir die Datei auch noch um. Verwenden Sie dazu den Namen HelloService.svc. Im nächsten Schritt passen wir die web.config an. Sie sehen hier bereits die Konfiguration für den Service, der standardmäßig angelegt wurde. Auch diesen gesamten Abschnitt benötigen wir nicht mehr. Am besten ersetzen wir den Block system.serviceModel mit dem gleichnamigen Block aus unserer app.config in der bisherigen Serverapplikation. Allerdings müssen wir darin ein paar kleine Anpassungen durchführen. Dadurch, dass der WCF-Service über die IIS gehostet wird, benötigen wir keine Adressangaben mehr. Entfernen Sie deswegen das Attribut httpGetUrl des Elementes serviceMetadata. Das Attribut httpGetEnabled belassen Sie unbedingt mit dem Wert true. Die Metadaten können dann mit dem zusätzlichen Requestparameter ?wsdl abgerufen werden. Für den Endpunkt braucht auch keine Adresse mehr angegeben zu werden. Sie können also das gesamte Attribut address des Elements endpoint entfernen. Zusätzlich muss noch der ASPNet-Kompatibilitätsmodus, dazu gleich noch mehr, aktiviert werden, um die Sache im Internet Explorer zu starten. Dies machen Sie mit folgender Anweisung innerhalb des System.ServiceModelElements:
Die web.config sollte im Anschluss so aussehen wie in Listing 3.28. >
>
HINWEIS
Für ein bisschen Verwirrung können an dieser Stelle die Bezeichnungen führen. Sehr oft liest man im Deutschen auch anstatt Windows-Dienst den Begriff Windows Service. Bitte verwechseln Sie ihn nicht mit unserem WCF Service. Windows Services wurden früher als NT Services bezeichnet. Im weiteren Verlauf bleibe ich bei der Bezeichnung Windows-Dienst.
Ein Windows-Dienst bietet keine Oberfläche und wird über den Service Control Manager gesteuert, der den Dienst starten, anhalten und stoppen kann.
Implementierung in einem Windows-Dienst Um unseren HelloService jetzt innerhalb eines Windows-Diensts zu starten, legen wir ein neues Projekt vom Typ WINDOWS-DIENST an und geben ihm den Namen WCFDienstHost. Dann fügen wir einen Verweis auf die Bibliothek System.ServiceModel.dll und auf unsere eigene Klassenbibliothek WCFService.dll hinzu. Als Nächstes bearbeiten wir die Datei Service1.cs. Zur besseren Illustration benennen wir die Klasse Service1 auf WCFNTDienstHost um. Führen Sie die Umbenennung bitte mit den Refactoring-Möglichkeiten durch, sodass alle Vorkommnisse des Klassennamens angepasst werden, auch die in der Main-Routine in der Datei Program.cs. In der Dienstklasse definieren wir im allgemeinen Deklarationsteil ein Objekt vom Typ ServiceHost. In der Methode OnStart schließlich instanzieren wir den ServiceHost und rufen dessen Methode Open auf, während in der OnStop-Methode die Methode Close des ServiceHost-Objektes aufgerufen wird. In der OnStart-Methode können Sie außerdem noch einen Delegaten für den FaultedEvent des ServiceHost definieren. In diesem Beispiel wird der Windows-Dienst bei einem Fehler im WCF-Service gestoppt. Sie können natürlich auch anders auf diesen Fehler reagieren. Listing 3.31 zeigt die Implementierung in der Datei Service1.cs. using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data;
205
Kapitel 3 using using using using using
System.Diagnostics; System.ServiceProcess; System.Text; System.ServiceModel; HelloWCF_Service;
namespace WCFDienstHost { public partial class WCFNTDienstHost : ServiceBase { private ServiceHost sh; public WCFNTDienstHost() { InitializeComponent(); } protected override void OnStart(string[] args) { sh = new ServiceHost(typeof(HelloWCF_Service.HelloService)); sh.Open(); sh.Faulted += new EventHandler(sh_Faulted); } void sh_Faulted(object sender, EventArgs e) { this.Stop(); } protected override void OnStop() { sh.Close(); } } } Listing 3.31: Die Windows-Dienstklasse WCFNTDienstHost
Zur Konfiguration des WCF-Service verwenden wir einfach wieder die app.config, die wir in den bisherigen Beispielen bereits eingesetzt haben. Vielleicht ändern Sie die Portnummer in der Adressangabe, damit wir auch wirklich sichergehen können, dass wir mit dem Windows-Dienst kommunizieren. Außerdem wollen wir später auch keine Konflikte mit anderen Beispielprojekten haben, weil der Windows-Dienst im Hintergrund den Port belegt. Um dem Service noch einen lesbaren Namen zu geben, ändern Sie bitte die Methode InitializeComponent in der Datei Service1.Designer.cs, wie in Listing 3.32 dargestellt.
206
Windows Communication Foundation private void InitializeComponent() { components = new System.ComponentModel.Container(); this.ServiceName = "WCFNTDienstHost"; } Listing 3.32: InitializeComponent für die Namensvergabe des Service
Installation des Windows-Dienstes Um den Windows-Dienst auch noch einfach auf einem System installieren zu können, fügen Sie einfach zu Ihrem Projekt ein neues Element vom Typ INSTALLERKLASSE hinzu. Passen Sie dabei den Code an wie in Listing 3.33 dargestellt. using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Configuration.Install; System.ServiceProcess;
namespace WCFDienstHost { [RunInstaller(true)] public partial class Installer1 : Installer { private ServiceInstaller serviceInstaller; private ServiceProcessInstaller processInstaller; public Installer1() { InitializeComponent(); processInstaller = new ServiceProcessInstaller(); serviceInstaller = new ServiceInstaller(); processInstaller.Account = ServiceAccount.LocalSystem; serviceInstaller.StartType = ServiceStartMode.Automatic; serviceInstaller.ServiceName = "WCFNTDienstHost"; Installers.Add(serviceInstaller); Installers.Add(processInstaller); } } } Listing 3.33: Code der Installerklasse
Mit diesem Code installieren wir den Dienst mit dem Account Lokales System und mit einem automatischen Start. Als Namen vergeben wir für den Dienst WCFNTDienstHost.
207
Kapitel 3
Wenn Sie den Dienst jetzt übersetzen – Vorsicht, Sie können ihn nicht starten –, wird automatisch die Exe-Datei kompiliert. Natürlich nur für den Fall, dass wir keine Kompilierfehler im Code haben. Anschließend können wir mit dem Befehlszeilentool installutil den Dienst aus der Visual Studio 2005-Eingabeaufforderung heraus am System installieren. Geben Sie dazu bitte folgende Befehlszeile an, und schauen Sie, dass Sie sich im richtigen Verzeichnis befinden, also im Verzeichnis, in dem auch die Dienst-Exe liegt. Installutil WCFNTDienstHost.exe
Wenn Sie jetzt sofort den Client dagegen testen wollen, wird das nicht funktionieren. Der Dienst muss nämlich noch explizit gestartet werden. Die Startart Automatisch führt erst beim nächsten Systemstart zu einem automatischen Start des Windows-Dienstes. Starten Sie dafür in der Dienstverwaltung unseren WCFNTDienstHost und danach den Client. Vergewissern Sie sich, dass Sie in der app.config die Portnummer wie im Service angepasst haben. Abbildung 3.34 zeigt die erfolgreiche Ausführung des Clients.
Abbildung 3.34: Erfolgreicher Aufruf an den Windows-Dienst
Unterschiede zu IIS- und WAS-Hosting Ein Windows-Dienst zeigt vor allem in puncto Lebenszeit einen Unterschied zu den beiden anderen Hostingmöglichkeiten IIS- und WAS-Hosting. Ein Windows-Dienst startet den WCF-Service automatisch beim Starten des Dienstes, was sehr oft beim
208
Windows Communication Foundation
Starten des Rechners sein kann, während bei den alternativen Hosts der WCF-Service erst beim ersten Aufruf aktiviert wird. Nachdem der WCF-Service innerhalb des Windows-Dienstes gestartet wurde, lebt er auch so lange, bis der Rechner heruntergefahren oder der Dienst explizit gestoppt wird. Auch hier kann bei den anderen beiden Hosts zur Ressourcenschonung der WCF-Service heruntergefahren und bei Bedarf wieder neu aktiviert werden. Wie gesehen müssen Sie bei einem Windows-Dienst auch explizit Programmcode zur Aktivierung des WCF-Service schreiben.
3.4.4 WAS-Hosting WAS ist die Abkürzung für Windows Activation Service und ist erst unter Windows Vista verfügbar, warum ich mich in diesem Abschnitt auch etwas kürzer fasse. Generell kann WAS natürlich WCF-Applikationen hosten. Dabei wird beim ersten Aufruf an einen Service dieser auch automatisch aktiviert. WAS übernimmt dabei sowohl die Aktivierung als auch die Lebenszeitverwaltung des WCF-Dienstes in einer sehr ähnlichen Weise, wie das auch der IIS tut. Im Gegensatz zum IIS überwindet er aber die Restriktionen, sich an das HTTP-Transportprotokoll zu binden. WAS stellt das gesamte IIS-Prozessmodell mit administrativer Oberfläche, Prozessrecycling und Serviceaktivierung zur Verfügung. Um WAS zu nutzen, müssen Sie die WCF Activation Components unter Windows Vista installieren.
*
*
*
TIPP
Bitte beachten Sie, dass die Aktivierung von Services über TCP explizit konfiguriert werden muss. Dafür steht ein Befehlszeilentool appcmd.exe zur Verfügung.
Die Implementierung erfolgt sehr ähnlich wie die Implementierung eines Service, der vom IIS gehostet wird. Schlüsselstelle ist dabei wieder die svc-Datei.
3.5 Erweitertes Binding Bislang haben wir uns einen Überblick verschaffen können, wie WCF prinzipiell funktioniert, wie Daten serialisiert werden und wie die erstellten WCF Services auch gehostet werden können. In diesem Abschnitt will ich noch einmal auf das Binding zurückkommen und die damit verbundenen Möglichkeiten und Vorteile erläutern. Einen Großteil der bisherigen Beispiele hätten wir auch mit herkömmlichen Webdiensten realisieren können. Auch wenn zum Beispiel die Datenserialisierung mittels DataContract etwas komfortabler wirkt, eine Implementierung mit Webdiensten ist genauso möglich, um komplexe Datenstrukturen auszutauschen.
209
Kapitel 3
Der größte Vorteil von WCF gegenüber allen bisherigen Kommunikationstechnologien liegt im Binding. Bislang, in der Zeit vor WCF, hat man sich programmatisch immer für ein Programmiermodell entscheiden müssen, und wenn man zu einem späteren Zeitpunkt seine Applikation auf ein anderes Programmiermodell umstellen wollte, so war der Migrationsaufwand in der Regel sehr hoch. Unter WCF gibt es jetzt nur noch ein einziges Programmiermodell, und wenn man die Art und Weise, wie Softwarekomponenten miteinander kommunizieren, umstellen will, muss man nur noch Änderungen am Binding der Endpunkte vornehmen. Aufwändige Änderungen am Programmcode müssen somit nicht mehr durchgeführt werden, im Idealfall muss man nur Einträge in der applikationsspezifischen Konfigurationsdatei anpassen. Der Entwickler implementiert unter WCF somit seine Geschäftslogik, und die Infrastruktur zur Kommunikation wird durch WCF bereitgestellt. WCF holt sich dabei die Informationen, wie die Kommunikation erfolgen soll, aus den bereitgestellten Bindinginformationen. Durch Änderungen des Bindings kann sich somit das Verhalten der Applikation grundlegend ändern, und jetzt wollen wir uns hier noch einen Überblick verschaffen, was wir mit Bindings überhaupt alles anstellen können.
3.5.1 Programmatisches Binding Bei allen unseren bisherigen Beispielen wurden sämtliche Konfigurationseinstellungen aus der app.config gelesen. Das ist auch die Art und Weise, wie Sie in der Praxis vorgehen sollten, denn bei benötigten Änderungen können Systemadministratoren diese direkt durchführen, ohne dass ein Entwickler Programmänderungen implementieren und neu übersetzte Softwarekomponenten verteilen muss. Der Vollständigkeit halber möchte ich hier aber noch kurz zeigen, wie Sie diese Informationen auch per Programmcode angeben können.
!
!
!
ACHTUNG
Sobald Sie per Programmcode Konfigurationseinstellungen vornehmen, werden die Einträge, die Sie in applikationsspezifischen Konfigurationsdateien bereitstellen, grundsätzlich überschrieben oder erweitert. Bitte kommentieren Sie deswegen im folgenden Beispiel die bisherigen Angaben in der app.config ein, damit nicht zwei Endpoints definiert werden, die einen gemeinsamen Port verwenden. Das führt nämlich zu einem Laufzeitfehler.
Ich möchte dabei auf das Beispiel aus Kapitel 3.2 aufsetzen und die Definition des Endpoints nicht mehr über das Konfigurationsfile, sondern per Programmcode zur Verfügung stellen.
210
Windows Communication Foundation
Serveranpassungen Um den Endpunkt am Server im Programmcode zu definieren, müssen wir in der Codedatei Program.cs folgende Anweisung zur Definition des Endpunktes innerhalb des using-Blocks hinzufügen: sh.AddServiceEndpoint(typeof(IHelloService), new BasicHttpBinding(), "http://localhost:2121/HelloService");
Hierdurch fügen wir dem ServiceHost einen neuen Endpunkt hinzu. Als Parameter der Methode AddServiceEndpoint wird dabei der Contract, das zu verwendende Binding sowie die Adresse des Endpoints angegeben, also das Endpoint-ABC. Den gesamten Programmcode der Main-Routine sehen Sie in Listing 3.34. static void Main(string[] args) { using (ServiceHost sh = new ServiceHost(typeof(HelloService))) { //programmatische Definition des Endpoints sh.AddServiceEndpoint(typeof(IHelloService), new BasicHttpBinding(), "http://localhost:2121/HelloService"); sh.Open(); Console.WriteLine("Service bereit..."); Console.ReadLine(); } } Listing 3.34: Programmatische Definition des Endpunkts
> >
>
HINWEIS
Anmerken möchte ich an dieser Stelle noch, dass keine Programmänderung am WCF-Service durchgeführt wurde, sondern nur im Servicehost.
Clientanpassungen Nun will ich Ihnen auch noch zeigen, wie wir den Client umstellen können, sodass wir ohne Konfigurationsdatei auskommen. Um die Definition des Endpunktes am Client vorzunehmen, benötigen wir ein ChannelFactory-Objekt. Dieses Objekt befindet sich im Namespace System.ServiceModel. Ein ChannelFactory-Objekt ist dabei eine Klasse, welche die Verbindung vom Client zu einem Service-Endpoint erstellt und verwaltet. ChannelFactory ist im Framework auch als generische Klasse definiert, die den Typ des Contract darstellen kann.
211
Kapitel 3
Um eine ChannelFactory zu erstellen, benötigen Sie folgende Programmzeile: ChannelFactory factory = new ChannelFactory (new BasicHttpBinding(), "http://localhost:2121/HelloService");
Es wird hier eine neue Instanz vom Typ ChannelFactory erstellt. Als generischer Typ wird das Interface unseres gewünschten Contract IHelloService angegeben. Dem Konstruktor werden an dieser Stelle das gewünschte Binding sowie die Adresse des Endpunktes mitgeteilt (und wieder sehen wir das ABC). Im nächsten Schritt benötigen wir noch eine Instanz für unsere Proxyklasse, über die wir den Service aufrufen. Dazu stellt uns das gerade erzeugte ChannelFactory-Objekt eine Methode CreateChannel zur Verfügung, die uns aufgrund der generischen Typangabe ein Objekt vom Typ IHelloService zurückgibt. Die dafür benötigte Programmzeile sieht wie folgt aus: IHelloService hsc = factory.CreateChannel();
Den gesamten Programmcode der Main-Routine sehen Sie in Listing 3.35. Die ursprünglichen Codeteile habe ich dabei als Kommentar im Listing gelassen. static void Main(string[] args) { //erstellen der ChannelFactory ChannelFactory factory = new ChannelFactory (new BasicHttpBinding(), "http://localhost:2121/HelloService"); //Erzeugen des Kanals IHelloService hsc = factory.CreateChannel(); //HelloServiceClient hsc = new HelloServiceClient(); Console.WriteLine(hsc.DoIt()); factory.Close(); //hsc.Close(); Console.ReadLine(); } Listing 3.35: Mainroutine am Client
Nach diesen beiden Anpassungen sollte dieses Projekt ohne Konfigurationsdateien, oder mit auskommentierter Konfigurationsdatei, wieder lauffähig sein.
3.5.2 Vordefinierte Bindings und Interoperabilität Als Nächstes wollen wir uns betrachten, welche vordefinierten Bindings uns WCF zur Verfügung stellt, und uns an dieser Stelle auch Gedanken machen, wann wir welches Binding verwenden sollten.
212
Windows Communication Foundation
BasicHttpBinding Das ist das Binding, das wir bislang in unseren Beispielen verwendet haben. Dieses Binding dient zur Kommunikation mit WS-Basic Profile-konformen Webdiensten und unterstützt Security nur auf Ebene des Transportprotokolls (HTTPS). BasicHttpBinding unterstützt keine Sessions und auch keine Transaktionen. Um die Interoperabilität des Nachrichtenaustausches zwischen Services auf unterschiedlichen Plattformen zu gewährleisten, hat die Web Services Interoperability Organization (WS-I) Profile veröffentlicht, die eingehalten werden müssen, um Interoperabilität zu gewährleisten. Das Basisprofil BP 1.1. (Basic Profile 1.1) besteht aus Richtlinien, die bei der Erstellung eines interoperablen Web Services eingehalten werden müssen. Darunter fällt die Verwendung von Web Servicestandards wie SOAP 1.1, WSDL 1.1, UDDI 2.0 und weiteren. Wenn Sie einen interoperablen Service zu diesem Basic Profile benötigen, ist BasicHttpBinding am besten dafür geeignet.
WsHttpBinding Eine Erweiterung des BP 1.1-Profils stellt die Spezifikation WS-* dar. BP 1.1. unterstützt, wie es auch der Name bereits ausdrückt, Basisinteroperabilität. WS-* ist eine Erweiterung dieser Spezifikation, die vor allem in puncto Sicherheit und Zuverlässigkeit einige Sachen mehr bereitstellt und fordert. WS-* steht dabei für Advanced Web Services und beinhaltet eine Reihe weiterer Standards zur sicheren und interoperablen Kommunikation. Diese erweiterten Standards werden von dem internationalen OASIS-Konsortium (Organization for the Advancement of Structured Information Standards) definiert. WsHttpBinding unterstützt dabei Interoperabilität zu WS-* und somit auch Sicherheit auf der Basis von WS-Security, einem Bestandteil der WS-*-Spezifikation, und auf der Ebene des Transportprotokolls. Im Gegensatz zu BasicHttpBinding unterstützt es auch zusätzlich Sessions und Transaktionen. Wenn Sie einen interoperablen WS-* Service benötigen, dann sind Sie bei WsHttpBinding richtig. WsHttpBinding unterstüzzt das Request/Response Exchange Pattern.
WsDualHttpBinding WsDualHttpBinding ist eine Erweiterung des WsHttpBindings und unterstützt zusätzlich eine Duplexkommunikation. Es ist somit das geeignete Binding für WS-*-interoperable Duplexkommunikation. Es unterstützt jedoch keine Sicherheit auf Basis des Transportprotokolls.
213
Kapitel 3
WsFederationHttpBinding Ws-Federation ermöglicht den Zugang zu unterschiedlichen Sicherheitsdomänen durch die Übertragung von Identitäten, ohne dass sich ein Dienst bei jeder Sicherheitsdomäne neu anmelden muss. WsFederationHttpBinding setzt auf dem WsHttpBinding auf und erweitert diesen um Ws-Federation und Duplexkommunikation. Sämtliche gerade aufgeführten HttpBindings encodieren Ihre Nachrichten mittels SOAP als Textnachrichten und verwenden das HTTP-Protokoll (bzw. HTTPS). Durch die Unterstützung von MTOM (Messaging Transmission Optimization Mechanism) können auch große Datenmengen als Nachrichten versendet werden.
Erzeugen von Metadaten für Interoperabilität Wie gerade beschrieben, sind die bislang hier aufgeführten Bindings entweder mit BP 1.1 oder auch WS-* interoperabel. In den bislang gezeigten Beispielen erzeugte uns das Tool svcutil eine Proxyklasse sowie die Konfigurationsdatei. Das hilft uns jedoch relativ wenig, wenn wir interoperable Anwendungen schreiben wollen. Dazu benötigen wir eine Beschreibung unseres WCF-Service auf der Basis der WSDL (Web Service Description Language). Wie Sie es sich vielleicht denken können, kann das svcutil-Tool auch Metadaten in Form von WSDL aus dem Service extrahieren, und das möchte ich Ihnen jetzt zeigen. Starten Sie dazu wiederum die Visual Studio 2005-Eingabeaufforderung, um die WSDL aus dem laufenden Server zu extrahieren, und geben folgenden Befehl ein: svcutil http://localhost:2121/HelloService/meta /target:metadata
Durch den Parameter target können Sie angeben, ob Sie Code (code), das ist der Standardwert, wenn Sie den Parameter nicht angeben, oder Metadaten (metadata) erzeugen wollen.
!
!
!
ACHTUNG
Beachten Sie bitte, dass wir in dem gerade erstellten Beispiel aus Kapitel kein Behavior definiert haben, das uns Metadaten erzeugt. Verwenden Sie, wie ich es auch aufgrund der Übersichtlichkeit getan habe, das Beispiel aus Kapitel 3.2.
Das Tool erzeugt dabei folgende drei Dateien: schemas.microsoft.com.2003.10.Serialization.xsd tempuri.org.xsd tempuri.org.wsdl
214
Windows Communication Foundation
Der Dateiname tempuri.org.* kommt dabei aus dem beim ServiceContract angegebenen Namespace. Dadurch, dass wir keinen Namespace angegeben haben, wird automatisch der Standardnamespace tempuri.org verwendet. Innerhalb von schemas.microsoft.com.2003.10.Serialization.xsd sind Schemas von Standardtypen beschrieben, die keiner genaueren Betrachtung bedürfen. Die Datei tempuri.org.xsd enthält Schemadefinitionen für den Aufruf der Methode DoIt und die Response dieser Methode. In Listing 3.36 sehen Sie den Inhalt dieser Datei. Listing 3.36: Inhalt der Schemadatei tempuri.org.xsd.
Sie sehen hierbei die Definition eines leeren komplexen Typs DoIt sowie die Definition eines komplexen Typs DoItResponse, der als einziges Element einen String enthält. Zur Erinnerung: Die Methode DoIt unseres WCF-Service ist eine parameterlose Methode mit einem Rückgabewert vom Typ String. Vielleicht erinnern Sie sich auch noch an den Abschnitt »Best Practice bei der Gestaltung von DataContracts« ab der Seite 188, in dem ich einen ganz ähnlichen Vorgang zur Definition von DataContracts beschrieben habe. Und nun betrachten wir uns noch die WSDL, die in Listing 3.37 dargestellt ist.
216
Windows Communication Foundation Listing 3.37: Extrahierte WSDL unseres Beispielprojektes
Am Anfang der WSDL werden verschiedene Namespaces importiert. Danach folgt eine Beschreibung der verwendeten Typen. Dabei sind diese aufgeteilt in die Standardtypen (die in der Datei schemas.microsoft.com.2003.10.Serialization.xsd beschrieben sind), sowie in unsere Typen, die in tempuri.org.xsd enthalten sind. Nach den Typen werden die Messages definiert, DoIt für den Request und DoItResponse für die Response unserer Operation DoIt. Die zu verwendenden Parameter sind dabei DoIt und DoItResponse aus dem Namespace tns, der auf tempuri.org verweist (xmlns:tns=http://tempuri.org/). Nach der Definition der portTypes, die unseren Contract beschreiben, folgt die Beschreibung der Binding-Informationen. Im abschließenden Service-Element folgt schließlich, noch zum Abschluss der WSDL, die Adresse des Service. Somit finden wir auch wieder unser Endpoint-ABC in einer interoperablen WSDL umgesetzt.
*
*
*
TIPP
Erstellen Sie sich ruhig einmal die Schemabeschreibung und WSDL des Beispiels, in dem wir bereits einen DataContract definiert haben. Das Ganze ist dann schon etwas umfangreicher als dieses kleine Beispiel.
Die nun folgenden Bindingvarianten bieten keine Interoperabilität zu anderen Technologien. Sie können ausschließlich nur im Microsoft-/.NET-Umfeld eingesetzt werden und übertragen die Nachrichten in einem binären Format.
NetTcpBinding NetTcpBinding unterstützt Sicherheit auf Ebene des Transportprotokolls sowie auf der Basis von WS-Security. Es unterstützt auch Sessions, Transaktionen und Duplexkommunikation. Für dieses Binding sollten Sie sich entscheiden, wenn Sie keine Interoperabilität und keine MessageQueues benötigen und innerhalb eines Netzwerks auf der Basis des TCP-Protokolls kommunizieren wollen.
217
Kapitel 3
NetNamedPipeBinding Das NetNamedPipeBinding ist dem NetTcpBinding gleichzustellen, ist jedoch eingeschränkt auf die Kommunikation von Softwarekomponenten auf einem lokalen Rechner. Dieses Binding unterstützt Security nur auf der Basis des Transportprotokolls.
NetPeerTcpBinding NetPeerTcpBinding unterstützt Peer-to-Peer-Kommunikation zwischen zwei Rechnern. Es unterstützt Sicherheit auf Ebene des Transportprotokolls sowie auf der Basis von WS-Security, aber keine Sessions und auch keine Transaktionen. Jedoch ist eine Duplexkommunikation möglich. Wenn Sie also Peer-to-Peer-Kommunikation verwenden wollen, sollten Sie sich für NetPeerTcpBinding entscheiden.
NetMsmqBinding NetMsmqBinding unterstützt Sicherheit auf Ebene des Transportprotokolls sowie auf der Basis von WS-Security. Es unterstützt auch Sessions, Transaktionen, aber keine Duplexkommunikation. Die Kommunikation zwischen Softwarekomponenten basiert dabei auf MSMQ (Microsoft Message Queues) und sollte somit das zu wählende Binding sein, wenn Sie MSMQ verwenden.
MsmqIntegrationBinding Dieses Binding sollten Sie verwenden, wenn Sie bestehende MSMQ-Applikationen integrieren müssen, also für die Kommunikation zwischen WCF und reinen MSMQApplikationen. Dieses Binding unterstützt Security nur auf der Basis des Transportprotokolls.
3.5.3 Behaviors Nachdem das Endpoint-ABC doch relativ ausführlich erläutert wurde, fehlt aber noch dringend eine genauere Betrachtung der, im Verlauf dieses Kapitels, bereits mehrfach verwendeten Behaviors. Mittels Behaviors kann man das Verhalten eines Endpoints, eines Service oder auch einer Operation steuern. Es gibt somit ein EndpointBehavior, ein ServiceBehavior sowie ein OperationBehavior. Die Einstellungen für das EndpointBehavior sowie das ServiceBehavior können über den Service Configuration Editor definiert und in der Konfigurationsdatei gespeichert werden.
218
Windows Communication Foundation
Die Einstellungen für das OperationBehavior sowie das ServiceBehavior können über das entsprechende OperationBehaviorAttribut beziehungsweise ServiceBehaviorAttribut im Programmcode angegeben werden. Für das ServiceBehavior stehen also beide Möglichkeiten zur Verfügung. Im Folgenden finden Sie eine Auflistung der möglich einzustellenden Behaviors: EndpointBehavior CallbackDebug Definiert das Debugging von WCF-Callbacks. CallbackTimeouts Spezifiziert Timeouts bei Clientrückrufen. ClientCredentials Ermöglicht es dem User, Client- und Servercredentials für clientseitige Kommunikation zu konfigurieren. ClientVia Spezifizierung der URI für den Transportkanal. DataContractSerializer Ermöglicht Angaben zum DataContractSerializer. SynchronousReceive Definiert, ob Kanäle synchron oder asynchron auf Requests horchen. TransactedBatching Einstellungen für die Optimierung der Requestoperationen. ServiceBehavior DataContractSerializer Ermöglicht Angaben zum DataContractSerializer. ServiceAuthorization Stellt Einstellungen für die Serverautorisierung bereit. ServiceCredentials Konfiguriert Servicecredentials. ServiceDebug Debugeinstellungen für den Server.
219
Kapitel 3
ServiceMetadata Einstellungen für das Generieren von Metadaten auf dem Server. ServiceSecurityAudit Einstellungen für Protokollierungen von Securityereignissen ins Ereignisprotokoll ServiceThrottling Einstellungen, um die Performance zu tunen. ServiceTimeouts Spezifiziert Timeouts auf dem Server. OperationBehavior AutoDisposeParameters Einstellung, ob Parameterobjekte automatisch freigegeben werden. Impersonation Spezifiziert das Impersonationsverhalten einer Operation. ReleaseInstanceMode Einstellung, wann das Serviceobjekt wieder freigegeben wird. TransactionAutoComplete Gibt an, ob eine Transaktion bei erfolgreichem Durchlaufen der Operation automatisch abgeschlossen werden kann. TransactionScopeRequired Gibt an, ob eine Operation einen TransactionScope benötigt.
3.5.4 Security Im vorherigen Abschnitt wurde bei allen Bindings, bis auf BasicHttpBinding, beschrieben, dass diese WS-Security unterstützen. Wie diese Unterstützung jedoch tatsächlich aussieht, soll jetzt beschrieben werden. WCF-Security ist dabei in drei Bereiche aufgeteilt: 1. Transfer Security Transfer Security kann unter WCF auf Basis des Transportprotokolls (Transport Security Mode) bzw. auf der Basis von Nachrichten (Message Security Mode) imple-
220
Windows Communication Foundation
mentiert werden. Alternativ ist eine Kombination der beiden Modi möglich (TransportWithMessageCredential). Transfer Security implementiert dabei die folgenden drei Funktionen: Überprüfung der Integrität einer Nachricht Sicherstellen der Vertraulichkeit der Daten durch Kryptografie Authentifizierung von Identitäten 2. Autorisierung Unter Autorisierung versteht man, dass unterschiedliche Identitäten oder Rollen unterschiedliche Rechte beim Zugriff auf Ressourcen besitzen können. 3. Auditing Unter Auditing versteht man die Dokumentierung von sicherheitsrelevanten Ereignissen in ein Ereignisprotokoll.
Transfer Security Die Implementierung der Transfer Security auf Basis des Transportprotokolls (wie z.B. HTTPS) ist ein weit verbreitetes und auf unterschiedlichen Plattformen verfügbares Konzept. Es hat jedoch den großen Nachteil, dass es nur zur Kommunikation zwischen zwei Punkten geeignet ist. In der heutigen Servicewelt kommunizieren jedoch sehr oft Dienste wiederum mit anderen Diensten, sodass Nachrichten über mehrere Punkte (Aktoren) laufen. Message Security Mode ist eine rechenintensivere Möglichkeit, Security auf der Basis von WS-Security zu implementieren. Dadurch, dass diese Art von Sicherheit direkt in der SOAP-Nachricht eingebettet ist, ist sie auch völlig unabhängig vom Transportprotokoll und bietet Sicherheit nicht nur zwischen zwei Punkten, sondern über alle beteiligten Aktoren hinweg. Der Nachteil dieser Lösung besteht darin, dass aufgrund des benötigten Auswertens von XML innerhalb des SOAP Envelopes dieser Mode wesentlich langsamer als der Transport Security Mode ist. TransportWithMessageCredential, als Mischung der beiden Security Modi, authentifiziert den Client mittels dem Message Security Mode, während die Authentifizierung des Servers, die Datenintegrität und Datenvertraulichkeit mittels dem Transport Security Mode durchgeführt werden. Auf diese Art und Weise wird eine Geschwindigkeit im Bereich des Transport Security Modes erzielt, jedoch wird die Sicherheit auch nicht über alle durchlaufenen Punkte sichergestellt.
221
Kapitel 3
Auswahl des Security Mode Um einen Security Mode auszuwählen, muss man sich zuerst für ein Binding entscheiden. Sollten Sie sich für ein Binding entscheiden, dass Security nur auf der Ebene von Transport oder auf der Basis von WS-Security implementiert (z.B. WsDualHttpBinding oder NetNamedPipeBinding), gibt es sowieso keine Alternative. Für die Bindings, die beide Arten von Sicherheit unterstützen, haben Sie jetzt die Wahl zwischen den drei oben aufgeführten Modi. Starten Sie dazu den Service Configuration Editor und öffnen eine bereits bestehende app.config einer unserer bestehenden Serverapplikationen. Wählen Sie dann den Knoten BINDING aus und fügen mit NEW BINDING CONFIGURATION ein neues Binding hinzu. Im folgenden Dialog, siehe Abbildung 3.35, wählen Sie dann bitte WsHttpBinding aus, denn dieses Binding unterstützt alle Modi.
Abbildung 3.35: Auswahl des Bindings
Im nächsten Fenster vergebe ich als Namen für das Binding MyBinding und wechsle dann auf die Registerkarte SECURITY. Wie Sie in Abbildung 3.36 sehen, kann hier der gewählte Sicherheitsmodus mittels der Eigenschaft Mode ausgewählt werden. Die verfügbaren Modi sind in Abbildung 3.37 dargestellt.
222
Windows Communication Foundation
Zu erwähnen ist auch noch die Eigenschaft MessageClientCredentialType auf dieser Registerkarte mit den möglichen Werten, die in Abbildung 3.38 dargestellt sind.
Abbildung 3.36: Sicherheitseinstellungen für das ausgewählte Binding
Abbildung 3.37: Verfügbare Sicherheitsmodi
Abbildung 3.38: Verfügbare Einstellungen für den MessageClientCredentialType
Damit die Einstellungen dieses Bindings auch verwendet werden, muss es auch noch unserem Endpoint zugewiesen werden. Wählen Sie dazu in der Baumansicht unter SERVICES – HELLOWCF_SERVICE.HELLOSERVICE – ENDPOINTS den Endpunkt HelloServiceHttpEndpoint aus.
223
Kapitel 3
Ändern Sie dann das Binding auf WsHttpBinding und wählen dann unter BindingConfiguration das gerade angelegte MyBinding aus, so wie es in Abbildung 3.39 dargestellt ist.
Abbildung 3.39: Zuweisen des Bindings zum Endpoint
Wenn Sie danach im Menu mit FILE – SAVE die Änderungen speichern, wird unsere app.config aktualisiert. Dabei wird ein neuer Abschnitt bindings eingefügt, in dem unser Binding namens MyBinding definiert wird. Da wir für alle Eigenschaften des Bindings die Standardwerte ausgewählt haben, wird auch nur der Name des Bindings als Attribut aufgeführt. Bei der Definition des Endpoints wird schließlich auf diese BindingConfiguration verwiesen. Listing 3.38 zeigt die neue Konfigurationsdatei.
224
Windows Communication Foundation /behaviors> Listing 3.38: Aktualisierte app.config
Damit der Client weiterhin funktioniert, muss nun noch die Konfiguration des Clients angepasst werden. Dazu können wir mit dem svcutil–Tool die Konfigurationsdatei neu erzeugen lassen. Nach der Aktualisierung der Clientkonfiguration sollte die Applikation wieder einwandfrei funktionieren. Schauen Sie sich ruhig auch die neu erstellte app.config im Clientprojekt an. Sie werden unter anderem einen neuen Abschnitt finden, der den Securitymodus des Bindings definiert, wie in Listing 3.39 dargestellt. Listing 3.39: Ausschnitt der Security-Einstellungen des Bindings in der app.config des Clientprojektes
Autorisierung Autorisierung wird in WCF durch die Integration von PrincipalPermission-Attributen und eines sogenannten Identity Models implementiert. Das Identitätsmodell, das von WCF unterstützt wird, basiert auf einem auf Behauptungen (Claims) basierenden Authentifizierungsmodell. Die Claims werden dabei aus übergebenen Tokens extrahiert. Diese
225
Kapitel 3
Claims werden dann mit Security Policies abgeglichen, die dann eine Entscheidung darüber treffen können, ob der Zugriff autorisiert wird oder eben nicht. Die Policy kann mittels Behaviors konfiguriert bzw. auch per Programmcode definiert werden.
> >
>
HINWEIS
Da dies ein sehr umfangreiches Thema ist, das sehr viel Grundwissen über Sicherheitskonzepte voraussetzt, möchte ich den sich für diese Thematik interessierenden Leser auf die ausführliche Dokumentation im Windows SDK verweisen.
Auditing Auditing unterstützt Systemadministratoren dabei, Attacken, die bereits durchgeführt wurden oder gerade stattfinden, zu erkennen. Mit den von WCF bereitgestellten Auditing-Features können Sie Einträge in das Windows-Ereignisprotokoll schreiben. Um Auditing für Ihre Applikation zu aktivieren, starten Sie bitte noch einmal den Service Configuration Editor und öffnen die Konfigurationsdatei des Serverprojektes. Wählen Sie in der Baumstruktur ADVANCED – SERVICE BEHAVIORS – SERVICEBEHAVIOR META aus.
Abbildung 3.40: Hinzufügen eines serviceSecurityAudit-Behavior-Elements
226
Windows Communication Foundation
Fügen Sie dann mit der Schaltfläche ADD ein neues Behavior-Element vom Typ SERVICESECURITYAUDIT dem Behavior hinzu, so wie in Abbildung 3.40 dargestellt. Stellen Sie dann die Eigenschaften für das Element serviceSecurityAudit wie in Abbildung 3.41 ein.
Abbildung 3.41: Eigenschaften des serciceSecurityAudit-Behavior-Elements
Die Bedeutung der verschiedenen Eigenschaften finden Sie in Tabelle 3.8. Eigenschaft
Bedeutung
AuditLogLocation
Gibt an, in welches Ereignisprotokoll die Events protokolliert werden. Zur Auswahl stehen Default, Application und Security. Beachten Sie, dass für das Schreiben in das Sicherheitsprotokoll bestimmte Rechte für die Applikation vorhanden sein müssen. Unter Windows XP wird das Schreiben in das Sicherheitsprotokoll nicht unterstützt.
MessageAuthenticationAuditLevel
Gibt an, welches Ergebnis der Authentifizierung auf Nachrichtenebene protokolliert wird. Die möglichen Einstellungen sind None, Success, Failure und SuccessOrFailure. In diesem Fall lasse ich die Authentifizierung sowohl im erfolgreichen wie auch für den Fall eines Fehlers protokollieren.
Tabelle 3.8: serviceSecurityAudit-Eigenschaften
227
Kapitel 3
Eigenschaft
Bedeutung
ServiceAuthorizationAuditLevel
Gibt an, welches Ergebnis der Autorisierung auf Serviceebene protokolliert wird. Die möglichen Einstellungen sind None, Success, Failure und SuccessOrFailure. In diesem Fall lasse ich die Autorisierung sowohl im erfolgreichen als auch für den Fall eines Fehlers protokollieren.
SuppressAuditFailure
Gibt an, ob ein Fehler, der aufgrund des Schreibens in das Ereignisprotokoll auftritt, unterdrückt werden soll. Vorsicht: Wenn Sie hier false auswählen, wird der Request nicht durchgeführt, wenn der Eintrag in das Ereignisprotokoll schief geht.
Tabelle 3.8: serviceSecurityAudit-Eigenschaften (Fortsetzung)
Das Behavior ServiceBehaviorMeta, das wir gerade erweitert haben, ist bereits unserem Service zugeordnet, deswegen müssen wir keine erneute Zuweisung auf Serviceebene durchführen und können mit FILE – SAVE die Konfigurationsdatei wieder aktualisieren. Listing 3.40 zeigt das aktualisierte serviceBehavior-Element in der app.config. Listing 3.40: Aktualisiertes serviceBehavior-Element in der app.config der Serverapplikation
Wenn wir nach diesen Änderungen unser Projekt wieder starten, werden bereits fleißig Ereigniseinträge in das Ereignisprotokoll geschrieben, wie uns Abbildung 3.42 zeigt.
3.5.5 Session Unter Sessions versteht man allgemein, dass alle Nachrichten, die zwischen zwei Endpunkten ausgetauscht werden, zusammengehören und in einem selben Kontext behandelt werden. WCF-Sessions müssen von der aufrufenden Applikation initialisiert und auch wieder terminiert werden. Auf dem Server werden die Nachrichten einer Session dabei in der Reihenfolge abgearbeitet, in der sie eintreffen. Im Gegensatz zum ASP.NET Sessionobjekt gibt es keinen Datenspeicher bei einer WCF-Session. Die Objekte werden einfach, in Abhängigkeit des Instanzierungsverhaltens (das gleich beschrieben wird), auf dem Server gehalten.
228
Windows Communication Foundation
Abbildung 3.42: Audit-Einträge im Ereignisprotokoll
Bei der Definition eines ServiceContract kann man dem Attribut als benannten Parameter einen SessionMode angeben (siehe Tabelle 3.2). Die möglichen Werte hierfür sind: Allowed Dieser Wert besagt, dass Sessions von diesem Contract unterstützt werden, wenn das zugrunde liegende Binding Sessions unterstützt. Dieser Wert ist auch der Standardwert, falls keine Angaben gemacht werden. NotAllowed Dieser Wert besagt, dass dieser Contract kein Binding unterstützt, das Sessions initialisiert. Required Dieser Wert besagt, dass der Contract zwingend Sessions erfordert. Unterstützt das Binding keine Sessions, dann wird ein Laufzeitfehler ausgelöst. Auf der Ebene des OperationContract kann man dem Attribut als benannte Parameter angeben (siehe Tabelle 3.3), ob diese Operation eine Session starten (IsInitiating) oder eine Session beenden (IsTerminating) kann.
229
Kapitel 3
Instanzierungsverhalten In einem engen Zusammenhang mit Sessions steht das Instanzierungsverhalten von Services. Mittels des ServiceBehavior-Attributs kann man einen Wert für den InstancingContextMode angeben. Der Codeausschnitt in Listing 3.41 zeigt dabei die Definition eines ServiceContract für ein Interface. Der ServiceContract muss dabei Sessions unterstützen und hat den InstancingContextMode PerSession. [ServiceContract(SessionMode=SessionMode.Required)] [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)] public interface IHelloService { … } Listing 3.41: Attribute zur Steuerung einer Session für einen ServiceContract
Tabelle 3.9 zeigt die Bedeutung der möglichen InstanceContextModi. InstanceContextMode
Bedeutung
PerCall
Ein neuer InstanceContext, und somit auch ein ServiceObject, werden bei jedem Clientrequest angelegt.
PerSession
Ein neuer InstanceContext, und somit auch ein ServiceObject, werden bei jeder neuen Session eines Clients angelegt. Die Lebenszeit des Contexts ist dabei abhängig von der Session.
Single
Ein InstanceContext, und somit auch nur ein ServiceObject, bedienen sämtliche Clientanfragen.
Tabelle 3.9: Bedeutung der möglichen InstanceContextModes
Concurrency Das Verhalten bei einem gleichzeitigen Zugriff auf Serverobjekte ist ebenso sehr eng mit Sessions verbunden. Über den ConcurrencyMode kann dabei festgelegt werden, wie viele Threads gleichzeitig innerhalb eines InstanceContext beziehungsweise im Ganzen erlaubt sind. Der ConcurrencyMode kann dabei als weiterer benannter Parameter des ServiceBehavior-Attributs angegeben werden. Tabelle 3.10 zeigt die Bedeutung der möglichen ConcurrencyModi.
230
Windows Communication Foundation
ConcurrencyMode
Bedeutung
Single
Auf einen InstanceContext kann maximal ein Thread gleichzeitig zugreifen. Die restlichen Threads werden blockiert.
Multiple
Auf einen InstanceContext können parallel mehrere Threads zugreifen. Der Service muss natürlich threadsicher implementiert sein.
Reentrant
Ein InstanceContext bearbeitet gleichzeitig einen Thread, akzeptiert aber wiederkehrende Aufrufe von WCF-Clients.
Tabelle 3.10: Bedeutung der möglichen ConcurrencyModes
3.5.6 Transaktionen Unter einer Transaktion versteht man eine Reihe von zusammengehörigen Operationen, die als eine atomare, unzertrennbare Einheit ausgeführt werden müssen. Dabei zählt das Prinzip alles oder nichts, entweder werden alle Operationen ausgeführt oder keine einzige. Das bedeutet für den Fall, dass auch nur eine einzige Operation nicht erfolgreich durchgeführt werden kann, dass der Zustand vor Beginn der Transaktion wieder hergestellt werden muss. Transaktionen müssen dem sogenannten ACID-Prinzip (Atomar, Consistent, Isolated, Durable) entsprechen. WCF unterstützt dabei das Protokoll WS-AT (Atomic Transaction) für verteilte Anwendungen zwischen interoperablen Services sowie OLE Transactions für verteilte Anwendungen, die keine Interoperabilität benötigen. Innerhalb von WCF können Einstellungen zu Transaktionen an folgenden Stellen durchgeführt werden: ServiceBehaviorAttribute Hier können, über benannte Parameter, TimeOuts und IsolationLevels gesetzt werden. OperationBehaviorAttribute Hier kann angegeben werden, ob eine Transaktion automatisch abgeschlossen werden kann und ob ein TransactionScope benötigt wird. ServiceContractAttribute Hier kann angegeben werden, ob eine Transaktion benötigt, erlaubt oder verboten ist. OperationContractAttribute Hier kann angegeben werden, ob eine Transaktion benötigt, erlaubt oder verboten ist.
231
Kapitel 3
3.5.7 Tracing und Logging Bei so vielen Built-In-Features dürfen natürlich Tracing und Logging nicht fehlen, und auch in diesem Punkt werden wir von WCF nicht enttäuscht. Gerade Tracing- und Loggingmechanismen erleichtern die Arbeit ungemein, wenn es zu Problemen mit der Applikation kommt. WCF-Tracing baut dabei auf dem Namespace System.Diagnostics auf. Um einen Tracer zu erstellen, benötigen Sie als einen Listener, den wir gleich mit dem Service Configuration Editor erstellen werden. Ein Listener hört danach auf eine Tracesource. WCF bietet Ihnen dazu unterschiedliche Sources an: System.ServiceModel Trace Source Der am häufigsten verwendete WCF Tracesource. Der TraceSource zeichnet dabei den gesamten Kommunikationsstack auf. System.ServiceModel.MessagingLogging Trace Source Der Tracesource zeichnet alle Messages auf. Um den Listener und die TraceSources zu erstellen, verwenden wir, wie bereits gewohnt, den Service Configuration Editor. Öffnen Sie dabei wieder die Servicekonfigurationsdatei mit diesem Tool. Wählen Sie dann in der Baumstruktur den Eintrag DIAGNOSTICS aus. Als Erstes müssen wir dazu MessageLogging und Tracing prinzipiell aktivieren. Aktivieren Sie die beiden Diagnosemöglichkeiten durch einen Klick auf den Link ENABLE MESSAGELOGGING bzw. ENABLE TRACING. Damit wir beim Tracing auch etwas sehen, ändern wir den TraceLevel, wie in Abbildung 3.43 dargestellt, auf den Wert Verbose. Die Bedeutung der TraceLevel-Eigenschaften wird in Tabelle 3.11 dargestellt. Wert
Bedeutung
Off
Keine Tracingausgabe
Critical
Ausgabe von kritischen Fehlern
Error
Ausgabe von Fehlern
Warning
Ausgabe von Warnungen und Fehlern
Information
Ausgabe von Informationen, Warnungen und Fehlern
Verbose
Ausgabe sämtlicher Debugging- und Tracingnachrichten
Tabelle 3.11: Bedeutung des TraceLevels
232
Windows Communication Foundation
Abbildung 3.43: Eigenschaften des Listeners
Wenn wir jetzt den Knoten LISTENERS bzw. SOURCES öffnen, dann werden wir feststellen, dass bereits zwei Listener und zwei Sources, mit den gerade gemachten Einstellungen, angelegt wurden. Abbildung 3.44 zeigt die angelegten Listeners und Sources.
Abbildung 3.44: Angelegte Listener und Sources
233
Kapitel 3
Mit FILE – SAVE speichern wir die gemachten Änderungen wieder in unser Konfigurationsfile, das in Listing 3.42 abgebildet ist.
234
Windows Communication Foundation Listing 3.42: Servicekonfigurationsdatei mit Tracingeinstellungen
Sämtliche Tracingeinstellungen finden wir im Element system.diagnostics wieder. Darin finden wir die Definition unserer beiden Sources und darunter der beiden Listener. Innerhalb von system.serviceModel finden wir dann noch Eigenschaften für das MessageLogging innerhalb eines geschachtelten diagnostics-Elements. Wenn Sie das Projekt anschließend starten, dann werden automatisch zwei *.svclogDateien in das Verzeichnis, in dem auch die Sourcen liegen, erstellt. app_tracelog.svclog Tracingdatei für das normale Tracing. app_messages.svclog Tracingdatei für das messageLogging.
235
Kapitel 3
Die erzeugten Dateien besitzen die Endung *.svclog. Beides sind sehr umfangreiche XML-basierte Dateien, die mit einem normalen Editor kaum lesbar sind, da der Inhalt zu unübersichtlich dargestellt wird. Deswegen gibt es im Windows SDK einen eigenen Viewer, mit dem man nach Einträgen filtern und suchen kann. Dieses Tool heißt Service Trace Viewer und eignet sich sehr gut, um die Ursachen eines Fehlers zu erkennen. Abbildung 3.45 soll dabei die Mächtigkeit dieses Tools zeigen und auch die Summe an Informationen, die protokolliert wurden. In dieser Abbildung wurde die app_ tracelog.svc-Datei geladen, die wir gerade angelegt haben.
Abbildung 3.45: Service Trace Viewer
3.6 Kompatibilität Bei der Einführung einer neuen Technologie ist es natürlich auch immer wichtig, inwieweit Vorgängertechnologien integriert wurden bzw. wie aufwendig sich die Migration dieser veralteten Technologien auf die neue Technologie darstellt. Dazu möchte ich zum Abschluss des WCF-Kapitels noch einen kurzen Überblick geben.
236
Windows Communication Foundation
3.6.1 Integration von COM+ und COM Mittels WCF kann man COM+-Applikationen wunderbar integrieren und deren Logik erweitern. Ein häufig verwendetes Integrationsszenario liegt vor, wenn die Funktionalität von COM+ oder Enterprise Services über einen Web Service bekannt gegeben wird. Die zu veröffentlichenden Schnittstellen werden dabei von dem Web Service automatisch gemappt. Allerdings müssen die Interfaces einigen Regeln entsprechen. Folgende Interfaces können nicht mittels Webdiensten veröffentlicht werden: Interfaces, die Objektreferenzen als Parameter übergeben Interfaces, die nicht .NET-kompatible Typen übergeben Interfaces von Applikationen. die Applikationspooling unterstützen Komponenteninterfaces, die als privat markiert sind COM+-Infrastrukturschnittstellen Interfaces aus der Systemapplikation Interfaces aus den Enterprise Services, die nicht im Global Assembly Cache vorliegen Die Authentifizierung- und Autorisierungsmechanismen von COM+ bleiben weiterhin erhalten, auch wenn die Funktionalität über einen Web Service bekannt gegeben wird. Um einen Web Service für ein Interface hinzuzufügen, wird das Tool COM+ Service Model Configuration (ComSvcConfig.exe) zur Verfügung gestellt. Mittels einem WCF Service Moniker können auch bestehende COM-Komponenten in WCF integriert werden.
3.6.2 Integration von .NET Remoting .NET Remoting und WCF laufen wunderbar parallel, auch im selben Prozess, und stören sich nicht gegenseitig. Microsoft betont auch, dass .NET Remoting in der Zukunft weiter supported wird, jedoch nicht weiter entwickelt wird. Eine Migration einer bestehenden .NET Remoting-Applikation zu einer WCF-Applikation ist jedoch mit größerem Aufwand verbunden, und einige Konzepte lassen sich auch nicht ganz so einfach in der neuen serviceorientierten WCF-Welt abbilden. Der Aufwand einer Migration lohnt sich trotzdem, denn WCF wartet einfach mit vielen neuen Features auf, und über kurz oder lang wird aus meiner Sicht die .NET
237
Kapitel 3
Remoting-Technologie von der Bildfläche verschwinden. Und dann will man doch nicht noch weitere eigene Software-Releases mit einer sozusagen veralteten Technologie implementieren.
3.6.3 Migration von Web Services und WSE 3.0 Web Services Durch die Migration von Web Services zu WCF-Applikationen stehen bei weitem mehr Features für Ihren Webdienst zur Verfügung, als er unter ASP.NET hatte. Ich denke nicht, dass ich an dieser Stelle nochmals auf die Vorteile von WCF gegenüber Web Services einzugehen brauche. Eine Migration von Web-Service-Applikationen zu WCF ist sehr einfach durchzuführen, da beide serviceorientierte Technologien darstellen. WCF bietet auch gegenüber WSE 3.0 (Web Service Enhancements 3.0) Web Services mehr Features und Möglichkeiten. Vor allem ist nach einer erfolgreichen Migration ein deutlicher Performancegewinn gegenüber WSE 3.0-basierten Web Services zu verzeichnen. Auch hier ist eine Migration sehr einfach durchführbar, da WSE 3.0 auch bereits auf den WS-Spezifikationen aufbaut. Viele WSE 3.0 Features können in WCF sehr ähnlich konfiguriert werden. Nichtsdestotrotz liegt bei WSE 3.0 ein anderes Programmiermodell zugrunde. Beide Technologien sind natürlich völlig interoperabel zu WCF und können noch viel problemloser integriert werden. Sie können zum Beispiel mit dem svcutil-Tool auch einen WCF-Client aufgrund der Web Service-WSDL generieren lassen.
3.7 Zusammenfassung Die Zukunft wird beweisen, wie sich die Windows Communication Foundation durchsetzen wird. Jedoch muss ich zugeben, dass ich bereits zum jetzigen Zeitpunkt von dieser Technologie begeistert und überzeugt bin. Und wenn ich daran denke, welche tollen Tools wohl alleine noch in diesem Jahr zu erwarten sind, welche die Entwicklung noch komfortabler gestalten, dann würde ich am liebsten sofort alle laufenden Projekte auf WCF migrieren. Ich denke, Microsoft hat das Ziel eines einheitlichen Programmiermodells zur Kommunikation von verteilten Applikationen mit Bravour gemeistert und den Großteil der bestehenden Kommunikationstechnologien in dieses neue Framework integriert. Schade, und das ist der einzige Wermutstropfen für mich, dass .NET Remoting dabei wohl auf der Strecke bleibt.
238
Windows Communication Foundation
Die konsequente Serviceorientierung und die damit verbundene Interoperabilität mit anderen Plattformen, die mit WCF bereitsteht, machen das moderne Schlagwort SOA richtig greifbar. Bemerkenswert ist auch, wie viele Features mit WCF bereits out of the box kommen, für die man sich in der Vergangenheit teilweise die Finger wund tippen musste. Und das meiste funktioniert mit reiner Konfiguration. Das intuitive Endpoint-ABC bietet dabei sehr viele Möglichkeiten, das Verhalten der WCF-Applikation ohne großen Aufwand zu verändern. Einfach das Binding ändern und fertig. Und was keinesfalls vergessen werden darf, ist die Tatsache, dass WCF ein erweiterbares Framework ist. Sie können eigene Bindings und eigene Behaviors definieren. Den Möglichkeiten sind also kaum Grenzen gesetzt. Auch wenn manche (wenige) Sachen noch ungewohnt und vielleicht auch noch in den Kinderschuhen stecken, macht es Spaß, mit dieser neuen Technologie zu arbeiten. Der Ausblick auf die nächste Framework-Version und die neue Entwicklungsumgebung Orcas, die zum Ende des Jahres erscheinen sollen, steigern den Appetit dabei noch deutlich.
239
4
Windows Workflow Foundation
Die Windows Workflow Foundation ist einer der drei Hauptbestandteile des .NET Frameworks 3.0. Dabei darf man sich diese Erweiterung nicht als ein Produkt für Endanwender vorstellen, sondern als eine Art Werkzeugkasten bzw. Framework für Entwickler. Ausgestattet mit diesem Rüstzeug hat der Programmierer die Möglichkeit, regelmäßig wiederkehrende arbeitsplatz- und sogar unternehmensübergreifende Prozesse in teilautomatisierte Abläufe zu transformieren, um in Richtung Standardisierung und Transparenz einen Mehrwert für das Unternehmen zu gewinnen. Natürlich konnte man diese Teilautomatisierung auch schon mithilfe der Verwendung von Klassen aus dem .NET Framework 2.0 realisieren, jedoch hat man jetzt ein einheitliches Baukastensystem zur Hand, dessen Bausteine durch selbst geschriebene erweitert werden können. Somit muss im Gedanken der Wiederverwendbarkeit das Rad nicht jedes Mal neu erfunden werden.
4.1 Der etwas andere Denkansatz Wenn man das Ganze jetzt noch aus einem etwas entfernteren Blickwinkel betrachtet, so erkennt man doch, dass Microsoft auch in dieser technologischen Richtung nach mehr Abstraktion, einer größeren, höheren und weiter entwickelteren Gangart der Programmierung strebt. Denn hat man bisher versucht, die Welt und ihre teilnehmenden
Kapitel 4
Personen in einem Programm als Objekte und ihre Tätigkeiten in Methoden zu kapseln, so geht man bei der Windows Workflow Foundation einen Schritt weiter. Wichtig ist hier nicht nur die Identifizierung der Objekte, sondern vielmehr deren wiederkehrende Interaktion, die sich in einem gewissen Zeitfenster abspielen. Hat man diese entdeckt, so wäre es sinnvoll, einen eigenen Baustein zu konstruieren, den man in nachfolgenden Workflow-Projekten mit Leichtigkeit wieder einbinden oder bei Beachtung eines gewissen abstrahierteren Programmierstils auch anderen zur Verfügung stellen kann.
4.2 Workflow Foundation-Grundlagen Um Ihnen ein Grundverständnis für den Aufbau und die Verwendung von Workflows in der Windows Workflow Foundation zu vermitteln, werden in diesem Kapitel die Basics besprochen. Sie werden lernen, aus welchen Einheiten sich ein Workflow zusammensetzt und wie diese miteinander in Verbindung stehen. Außerdem erfahren Sie, welche Vorlagen man bei der Verwendung des .NET Frameworks 3.0 in Verbindung mit Microsoft Visual Studio 2005 zur Verfügung hat und in welchen Szenarien diese am besten verwendet werden können.
4.2.1 Projektvorlagen im Visual Studio 2005 Möchte man das allererste Mal einen Workflow mithilfe des Visual Studios 2005 in die Tat umsetzen, hat man die Wahl zwischen zwei Workflow-Typen, den sequenziellen und den Statuscomputer-Workflows. Exkurs Damit im Visual Studio 2005 die Projektvorlagen wie in Abbildung 4.1 erscheinen, müssen die Visual Studio 2005 Extensions for .NET Framework 3.0 installiert sein, die wiederum eine Installation des .NET Frameworks 3.0 voraussetzen. Beachten Sie jedoch, dass die Express-Editionen von Visual Studio 2005 nicht mit den Extensions erweitert werden können.
In welchem Fall man jetzt welche Art einsetzen soll, hängt ganz von der jeweiligen Situation ab. Mit Sicherheit lassen sich wohl alle automatisierbaren Vorgänge sowohl sequenziell als auch statusbasiert umsetzen. Die Frage, welche Vorlage jedoch bei welchem Einsatzgebiet den Vorzug erhalten soll, kann mit einfachen Überlegungen bezüglich der Kontrolle der Ausführung beantwortet werden. Ist man der Meinung, der Ablauf ist in seiner Abfolge der einzelnen Aktivitäten vorgegeben und kann vom Benutzer nur wenig beeinflusst werden, so empfiehlt sich der sequenzielle Ansatz.
242
Windows Workflow Foundation
Abbildung 4.1: Workflow-Projektvorlagen im Visual Studio 2005
Abbildung 4.2: Struktur eines sequenziellen Workflows
Kristallisieren sich jedoch mehrere Zustände heraus, wobei zwischen den Zustandswechseln keine festgelegte Reihenfolge besteht und es dem beteiligten Anwendern überlassen wird, welche Aktion als Nächstes erfolgen soll, so ist es wohl ratsam, den anderen Typen zu bevorzugen. Mit einer kurzen Vorstellung mehrerer Anwendungsszenarien zu den beiden Workflow-Typen möchte ich Ihnen aufzeigen, dass bei der Realisierung derartiger Projekte überhaupt keine Grenzen gesteckt sind.
243
Kapitel 4
Abbildung 4.3: Struktur eines statusbasierten Workflows
Beispiele sequenzieller Workflows Wie der Name schon andeutet, laufen diese nach einem sequenziellen Muster ab. Die Kontrolle über den Ablauf hat in diesem Fall der vom Entwickler implementierte Workflow größtenteils selbst. Die Anwendungsfälle reichen von der Automatisierung einfacher, lästiger persönlicher Aufräumarbeiten auf einem einzelnen Computer, wie z.B. das Entmüllen oder Leeren einzelner Ordner, oder von einem simplen Backup, bei dem ausgewählte Bereiche der Festplatte gepackt und auf eine externe Festplatte gesichert werden, bis zu komplizierten Abläufen, bei denen z.B. in einer Client-ServerArchitektur mehrere Personen zu unterschiedlichen Zeiten räumlich voneinander getrennt mit unterschiedlichen mobilen Geräten miteinander interagieren. Auch etwas weiter entfernte Szenarien sind denkbar. Denken Sie an die Zukunft des intelligenten Wohnens, wo Fenster, Lüftung und Heizung miteinander kommunizieren und sich einheitlich auf vorprogrammierte Informationskonstellationen abstimmen. Dieser Fall bringt uns jedoch zur nächsten Form, den statusbasierten Workflows.
Beispiele statusbasierter Workflows Denn hier spielen die Daten und Informationen eine besondere Rolle, die von außen, d.h. von Ereignissen der mich umgebenden Umwelt, kommen und folglich nicht beeinflussbar sind. Nehmen Sie das Beispiel von vorhin aus der Sicht des vernetzten Wohnens. Hier kann man im Vorhinein kein Ablaufmuster definieren. Die Aktivität der Heizung hängt ab von der zu haltenden Raumtemperatur, die Inbetriebnahme der Lüftung von der Rauchentwicklung, mit unterschiedlichen gekoppelten Abhängigkeiten je nach Art der Fensteröffnung. Doch innerhalb einer gewissen Zeitspanne befinden sich alle Akteure in einem gleichbleibenden Tätigkeitszustand, der nach einer gewissen Zeit aufgrund der sich ändernden Einflussfaktoren in einen anderen Zustand wechselt. Sie erkennen, worauf ich hinauswill? Es handelt sich hier um Abläufe, die in einer Statemachine beschrieben werden können, wo es eine bestimmte Anzahl von Zuständen gibt, zwischen denen beliebig gewechselt werden kann.
244
Windows Workflow Foundation
Ich möchte Ihnen jedoch noch einen weiteren Anwendungsfall schildern, bei dem es um die Realisierung einer Menüführung durch den Benutzer geht. Wie im vorherigen Beispiel kann man auch hier nicht voraussagen, welcher Menüpunkt wohl als nächster gewählt wird, und bestimmte Untermenüs können auch öfter als nur einmal abgerufen werden. Falls Sie jetzt nur Bilder von Tastatureingaben mit anschließender Bildschirmausgabe im Kopf haben, möchte ich diese Grenze ein bisschen erweitern. Denken Sie an die Abrufe der Mobilbox. Auch das lässt sich im heutigen Zeitalter durch die Erfindung der Spracherkennung verwirklichen. Oder an die Fernbedienung, die wohl in Zukunft nicht immer in Richtung Fernseher, sondern schon bald auf den MultimediaPC im Wohnzimmer gerichtet wird, der intern einen Workflow startet und auf Knopfdruck in einen anderen Zustand wechselt. Ich gebe zu, in dem letzten Absatz bin ich in ein paar Gedanken unserer Realität etwas voraus. Doch wie weit? Oder wie nah sind uns solche Gegebenheiten? Fest steht, dass unsere Wirklichkeit immer komplexer wird und wir versuchen müssen, die Vernetzung der vielfältigsten Dinge und Abhängigkeiten unter diesen so einfach wie möglich zu halten, um nicht den Überblick zu verlieren und Herr der Sache zu bleiben. Und ich denke, dass uns Microsoft mit dieser neuen Weise der Programmierung ein mächtiges Werkzeug in die Hand gibt, die auf der einen Seite eine etwas andere Denkweise bei der Entwicklung erfordert, dessen Anwendung und technische Beherrschung wir jedoch mit Blick auf die uns bevorstehenden Möglichkeiten als Ziel haben sollten.
4.2.2 Aktivitäten als Grundbausteine Ein Workflow setzt sich aus mehreren Aktivitäten zusammen, wobei eine Aktivität in .NET dabei nichts anderes ist als ein Objekt, das von dem Basistyp System.Workflow.ComponentModel.Activity abgeleitet wird. Wie bei der Oberflächenprogrammierung in Windows Forms auch, gibt es hier eine Toolbox, gespickt mit den gängigsten Objekten, die man bei der Modellierung per Drag & Drop auf die Oberfläche des Workflow Designers ziehen kann. Um den Ablauf zwischen diesen einzelnen Bausteinen zu koordinieren, gibt es einen speziellen Satz vorgefertigter Aktivitäten, die in ihrer Funktionalität den Kontrollstrukturen einer Programmiersprache entsprechen. Beispiele hierfür wären die IfElseActivity, die WhileActivity, die ReplicatorActivity oder die ConditionedActivityGroup, die in ihrer Bezeichnung schon ihre Aufgabe erkennen lassen.
245
Kapitel 4
4.2.3 Hosting-Möglichkeiten Damit wir nun einen von uns zusammengestellten Ablauf starten können, benötigen wir noch eine Applikation, die eine Instanz der Klasse WorkflowRuntime hosted, die sich wiederum um die Ausführung der einzelnen Workflow-Instanzen kümmert.
Abbildung 4.4: Hosting-Architektur
Als Host-Applikation kann man übrigens neben einer Konsolenanwendung, die bei den Projekttypen im Visual Studio 2005 standardmäßig als Vorlage eingestellt ist, die WorkflowRuntime auch in eine Windows Forms-, ASP.NET-, Web Service- oder Windows-Dienst-Anwendung integrieren.
4.3 Hello World-Workflow So wie in vielen anderen Programmierbüchern auch, möchte ich Ihnen zunächst ein sehr einfaches Beispiel in der Art eines Hello World-Programms zeigen, das die notwendigen Schritte beschreibt, die für die erfolgreiche Implementierung eines Workflows notwendig sind.
4.3.1 Beispiel eines sequenziellen Ablaufs Dazu öffnen wir Visual Studio 2005 und wählen KONSOLENANWENDUNG FÜR SEQUENZIELLE WORKLFOWS als Vorlage. Ohne irgendetwas programmieren zu müssen, sehen wir innerhalb der Main-Methode in Program.cs (Listing 4.1) eine fertige Implementierung einer WorkflowRuntime.
246
Windows Workflow Foundation using using using using
System; System.Workflow.Runtime; System.Workflow.Runtime.Hosting; System.Threading;
namespace SequentialWorkflowKonsoleHelloWorld { class Program { static AutoResetEvent _handle; static void Main(string[] args) { using (WorkflowRuntime wfRuntime = new WorkflowRuntime()) { wfRuntime.WorkflowCompleted += new EventHandler (wfRuntime_WorkflowCompleted); wfRuntime.WorkflowTerminated += new EventHandler (wfRuntime_WorkflowTerminated); _handle = new AutoResetEvent(false); WorkflowInstance instance = wfRuntime.CreateWorkflow(typeof( SequentialWorkflowKonsoleHelloWorld.WorkflowHelloWorld)); instance.Start(); _handle.WaitOne(); Console.ReadLine(); } } static void wfRuntime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine(e.Exception.Message); } static void wfRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { Console.WriteLine("Workflow beendet."); _handle.Set(); } } } Listing 4.1: Hosting der WorkflowRuntime in einer Konsolenanwendung
247
Kapitel 4
Zentraler Dreh- und Angelpunkt dieses Programmausschnitts stellt hier die Klasse WorkflowRuntime dar, die sofort nach dem Start der Anwendung instanziert wird. Diese ist die oberste Überwachungsstation, die sich darum kümmert, wann welcher Workflow welche Aktivitäten ausführen muss. Wird die Dispose()-Methode dieser Instanz aufgerufen, z.B. weil das Ende des Using-Blocks erreicht wird, so werden auch alle gerade laufenden Workflow-Instanzen beendet. Deshalb finden wir nach dem Aufruf der Start-Methode der instance-Variablen einen Mechanismus, der den Hauptthread anhält. Denn nach dem Aufruf der Methode WaitOne des AutoResetEvents _handle wartet der Hauptthread so lange, bis ein anderer Thread (nämlich der Thread der ersten beendeten Workflow-Instanz) die Blockade mittels Set wieder aufhebt. Dies erreicht man wiederum, indem man dem Ereignis WorkflowCompleted einen Handler hinzufügt, in dessen Ausführung die Threadfreigabe mittels Set implementiert ist. Nun öffnen wir die Workflow-Klasse, die ich in WorkflowHelloWorld.cs umbenannt habe, dabei erscheint der Designer, auf dessen Mitte wir per Drag & Drop zuerst eine WhileActivity von der Toolbox auf die Oberfläche ziehen und diese mittels Doppelklick auf eine CodeActivity befüllen. Zuletzt fügen wir noch eine zweite CodeActivity als Startaktivität ein.
Abbildung 4.5: Workflow Designer
248
Windows Workflow Foundation
Denn mit einem einfachen Hello World auf der Konsole geben wir uns nicht zufrieden. Wir fragen den User innerhalb der ersten CodeActivity caWieOft, wie oft er denn gerne diesen wunderschönen Satz auf der Konsole ausgegeben haben möchte. Damit jetzt noch diese hässlichen rot umkreisten Ausrufezeichen vom Designer verschwinden, springen wir mit einem Doppelklick auf caWieOft automatisch in die Codeansicht unseres Workflows, wo wir festlegen, was bei der Ausführung von caWieOft_ExecuteCode geschieht. private void caWieOft_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Wie oft wollen Sie denn 'Hello World' sehen?"); try { WieOft = Convert.ToInt32(Console.ReadLine()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } Listing 4.2: Speicherung des Konsoleninputs
Damit die Eingabe vom Anwender entgegengenommen werden kann und auch den anderen Klassen innerhalb des Workflows zur Verfügung steht, deklarieren wir in der Workflow-Klasse selbst eine Integer-Property mit der Bezeichnung WieOft. Zudem noch eine weitere Variable mit dem Namen Zaehler, die bei jeder Hello World-Ausgabe um 1 erhöht wird. using using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules; System.Windows.Forms;
namespace SequentialWorkflowKonsoleHelloWorld { public sealed partial class WorkflowHelloWorld: SequentialWorkflowActivity { public WorkflowHelloWorld()
249
Kapitel 4 { InitializeComponent(); WieOft = 1; Zaehler = 0; } private int mWieOft; public int WieOft { get { return mWieOft; } set { mWieOft = value; } } private int mZaehler; public int Zaehler { get { return mZaehler; } set { mZaehler = value; } } //weiterer Code, der der Übersichtlichkeit wegen ausgeblendet wird } } Listing 4.3: Deklaration von Properties in der Workflow-Klasse
Denn somit können wir der WhileActivity eine deklarative Regelbedingung im Eigenschaftsfenster zuweisen, der wir wiederum eine Bedingung hinzufügen, dass die While-Bedingung true ist und somit ausgeführt wird, solange die Eigenschaft WieOft größer als Zaehler ist (Abbildung 4.6).
> >
>
HINWEIS
Nachdem Sie die Regel im Designer mithilfe IntelliSense erstellt haben, finden Sie im Projektmappen-Explorer eine XML-Datei mit der Bezeichnung WorkflowHelloWorld.rules, in der Sie ihre Regel auch außerhalb des Designers nachträglich noch ändern können.
Damit der Zählerstand auch aktuell gehalten wird, erhöhen wir diesen in jedem Aufruf von caHelloWorld_ExecuteCode um 1. private void caHelloWorld_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Hello World"); Zaehler += 1; } Listing 4.4: Ausgabe von Hello World auf der Konsole
Jetzt können wir das Projekt durch Drücken der Taste (F5) starten!
250
Windows Workflow Foundation
Abbildung 4.6: Eigenschaftsfenster einer While-Aktivität
Abbildung 4.7: Ausgabe des sequenziellen Workflows auf der Konsole
4.3.2 Beispiel eines Statuscomputer-Workflows Da ein Hello World-Beispiel für das Vorstellen eines zustandsbasierten Workflows nicht gerade am besten geeignet ist, schlage ich vor, dass wir die Architektur einer benutzergeführten Konsolenapplikation realisieren. Dazu erstellen wir ein neues Projekt mit der KONSOLENANWENDUNG FÜR STATUSCOMals Vorlage. Wie in dem vorherigen sequenziellen Beispiel auch haben wir hier als Host-Applikation eine Konsolenanwendung, die sich in Form der Program.cs-Klasse repräsentiert (vgl. Listing 4.1). Um den Benutzer bei unserem Beispiel mit dem Namen begrüßen zu können, werden wir beim Aufruf unserer Workflow-Instanz einen Parameter übergeben. Dazu implementiert die Klasse WorkflowRuntime eine überladene Methode von CreateWorkflow, der wir zusätzlich ein Dictionary mit Parametern in Form von Schlüssel-Wert-Paaren übergeben können. PUTER-WORKFLOWS
251
Kapitel 4 //… Console.WriteLine("Wie heißt du?"); Dictionary parameter = new Dictionary(); parameter.Add("BenutzerName", Console.ReadLine()); WorkflowInstance instance = wfRuntime.CreateWorkflow (typeof(StatuscomputerWFKonsole.StatusInformationsplattform),parameter); instance.Start(); //… Listing 4.5: Auszug aus Program.cs
Die Parameterübergabe funktioniert jedoch nur, falls wir vorher in der WorkflowKlasse eine öffentliche Eigenschaft deklariert haben, deren Bezeichnung mit dem Namen unseres Parameters übereinstimmt. using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules;
namespace StatuscomputerWFKonsole { public sealed partial class StatusInformationsplattform: StateMachineWorkflowActivity { private string mbenutzername; public string BenutzerName { get { return mbenutzername; } set { mbenutzername = value; } } //… } } Listing 4.6: Auszug aus StatusInformationsplattform.cs
252
Windows Workflow Foundation
> >
>
HINWEIS
Wir können einer Workflow-Instanz nicht nur einfache Datentypen, sondern auch komplexe, selbst geschriebene Datentypen übergeben.
Wenn wir in die Codeansicht von StatusInformationsplattform.cs wechseln, sehen wir, dass diese Klasse nicht mehr wie vorhin von SequentialWorkflowActivity, sondern von StateMachineWorkflowActivity abgeleitet ist. Auch der Designer zeigt uns jetzt ein anderes Bild. Denn Visual Studio 2005 stattet diesen nun mit einer Oberfläche aus, auf der wir die einzelnen Zustände per Drag & Drop pixelgenau positionieren können. Außerdem entdecken wir in unserem Workflow schon einen ersten Zustand vom Typ StateActivity, indem sich die Instanz automatisch nach ihrem Start befindet.
> >
>
HINWEIS
Der Start- und Endzustand eines Workflows ist in den Eigenschaften der Workflow-Klasse selbst als InitialStateName bzw. CompletedStateName hinterlegt.
Als Beispielanwendung habe ich mir eine Informationsplattform über die gängigsten Programmiertechnologien überlegt. Um diese zu realisieren, fügen wir zuerst einmal alle für den Ablauf benötigten Zustände hinzu, indem wir mit einem rechten Mausklick auf den Designer den Punkt STATE HINZUFÜGEN auswählen und jeweils einen Bezeichner für den gerade eingefügten Zustand festlegen.
!
!
!
ACHTUNG
Belassen Sie bitte die nachfolgenden Namen, da sich die Verständlichkeit der folgenden Absätze ansonsten dramatisch verschlechtern wird.
Hinzuzufügende Zustände: saDotNet saWF saWCF saWPF saJava saLinux saEnde Nun können wir beginnen, die Logik für den Initialzustand zu implementieren. Dazu ziehen wir ein Objekt vom Typ StateIntitializationActivity von unserer Toolbox auf den Zustand saInitialState.
253
Kapitel 4
> >
>
HINWEIS
Sobald Sie mit der Workflow Foundation besser vertraut sind, können Sie den Zustandswechsel auch von der Host-Applikation aus mittels einer HandleExternalEventActivity herbeiführen, ein Beispiel dazu finden Sie in Kapitel 4.5. Der Einfachheit halber wählen wir aber die StateInitializationActivity als Beweggrund.
Mit einem Doppelklick auf diese öffnet sich uns eine detaillierte Ansicht über das Innenleben dieser Aktivität wie in Abbildung 4.8.
Abbildung 4.8: Detailansicht von saInitialState
Hier können wir nun festlegen, welche Schritte vollzogen werden, unmittelbar nachdem dieser Zustand beim Start des Workflows den Fokus erhält. In diese ziehen wir eine CodeActivity hinein, der wir Programmcode wie in Listing 4.7 durch einen Doppelklick zuweisen: private void caHauptmenue_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("\nHallo " + this.mbenutzername + "!"); Console.WriteLine("Zu welchem Thema möchtest du gerne mehr erfahren?\n"); Console.WriteLine("Drücke bitte die\n[1] für DotNet\n[2] " +
254
Windows Workflow Foundation "für Java\n[3] für Linux\n"); try { this.AktuelleAntwort = Convert.ToInt32(Console.ReadLine()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } Listing 4.7: Der nächste Zustand wird in der Variablen AktuelleAntwort festgehalten.
Wir fragen also den Anwender, zu welchem Zustand bzw. Themengebiet er weiter vorrücken will, und speichern seine Antwort in einer öffentlichen Eigenschaft mit der Bezeichnung AktuelleAntwort vom Typ Integer ab, die wir vorher in der WorkflowKlasse definiert haben. Um die Antwort bzw. den nächsten Zustand der WorkflowRuntime mitzuteilen, ziehen wir noch eine IfElseActivity unterhalb der CodeActivity auf den Designer. Denn dadurch, dass wir die Antwort in einer öffentlichen Property der Workflow-Klasse abgespeichert haben, können wir innerhalb von ieaAntwort in Abhängigkeit des Wertes einen bestimmten Zweig ansteuern. Fokussieren Sie den ersten Zweig mit der Maus, wählen Sie im Eigenschaftsfenster als Condition die DEKLARATIVE REGELBEDINGUNG wie in Abbildung 4.9, und klicken Sie auf den viereckigen Button, der am Ende der Zeile erscheint.
Abbildung 4.9: Eigenschaftsfenster einer IfElseBranchActivity
255
Kapitel 4
Anschließend öffnet sich ein Regeleditor, in dem Sie sich eine Regel zusammenstellen können, wie Sie in Abbildung 4.10 sehen.
Abbildung 4.10: Regeleditor im Visual Studio 2005
In diesem habe ich definiert, dass der Benutzer auf jeden Fall in dem Themengebiet DotNet landet, auch wenn er eine Zahl eingibt, die außerhalb des gültigen Bereichs liegt. Anschließend ziehen wir eine SetStateActivity in diese Bedingung hinein und geben als TargetStateName DotNet an. Das Gleiche wiederholen wir für den zweiten Zweig der Bedingung, mit dem Unterschied, dass wir jetzt für die AktuelleAntwort den Wert 2 erwarten und anschließend den Zustand auf Java setzen. Jetzt fehlt uns allerdings noch ein dritter Weg, den wir mit einem rechten Mausklick auf die IfElseActivity ieaAntwort über den Menüpunkt VERZWEIGUNG HINZUFÜGEN erzeugen und wobei wir anschließend die entsprechende Logik für den darauffolgenden Zustand Linux mit dem zugehörigen Antwortwert 3 konfigurieren. Zu diesem Zeitpunkt müssten Sie das Bild in Abbildung 4.10 bzw. Abbildung 4.11 in Ihrem Designer sehen.
*
*
*
TIPP
Haben Sie auch den roten Punkt in Abbildung 4.11 entdeckt? Im Kontextmenü einer Aktivität können Sie einen Haltepunkt setzen und anschließend die Stelle debuggen.
256
Windows Workflow Foundation
Abbildung 4.11: Großansicht der stateInitializationActivity des ersten Workflow-Zustands
Blicken Sie nun wieder auf die Übersicht des Workflows, werden Sie sehen, dass die Übergangswechsel mit Pfeilen grafisch dargestellt werden. Starten Sie den Workflow durch Drücken der Taste (F5), werden Sie beobachten, dass dieser nach der ersten Frage in einem bestimmten Zustand verweilt und nicht beendet wird. Denn auch das Beenden müssen wir mit einer SetStateActivity bewusst herbeiführen. Doch noch ist es nicht so weit. Lassen Sie uns noch ein kleines Untermenü für den Oberpunkt DotNet implementieren! Wenn der Benutzer als Zahl einen Wert kleiner als 1 oder größer als 3 eingibt, soll er die Wahl zwischen den übrig gebliebenen Themengebieten WF, WPF, WCF und CardSpace haben. Um das zu realisieren, fügen wir wiederum eine stateInitializationActivity in den Zustand DotNet ein und statten diese mit den gleichen Aktivitäten wie in Abbildung 4.11 aus, nur dass wir diesmal eine SetStateActivity zusätzlich für die Rückwärtsnavigation benötigen. Denn hat sich der Anwender erst einmal für das Thema DotNet entschieden, wollen wir ihm die Möglichkeit geben, sich nachträglich auch noch mit anderen Themen auseinanderzusetzen.
257
Kapitel 4
Das fertige Ergebnis sehen Sie in Abbildung 4.12.
Abbildung 4.12: Der Zustand DotNet in der Detailansicht
caDotNetMenue_ExecuteCode habe ich dabei wie in Listing 4.8 ausprogrammiert. private void caDotNetMenue_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Wir befinden uns im Zustand " + this.CurrentStateName + "\n"); Console.WriteLine("Drücke bitte die\n[1] für WPF\n[2] für WCF\n" + "[3] für WF\n[4] für zurück\n"); try { this.AktuelleAntwort = Convert.ToInt32(Console.ReadLine()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } Listing 4.8: Der Konsoleninput vom Anwender bestimmt den nächsten Schritt im Workflow.
258
Windows Workflow Foundation
Die einzelnen Zweige von ieaDotNet werden dem Konsolenoutput entsprechend mit einer neuen Regel belegt, wobei ich das Thema WF zugleich als Else-Zweig benutze, um evtl. falsche Benutzereingaben abzufangen (siehe Abbildung 4.13).
Abbildung 4.13: Regel für den Wechsel in den Zustand WF
Die Bedingungen der übrigen Zweige habe ich in Tabelle 4.1 aufgeführt. Erstrebter Zustand
Bedingung
saWF
this.AktuelleAntwort == 3 || this.AktuelleAntwort < 1 || this.AktuelleAntwort > 4
saWPF
this.AktuelleAntwort == 1
saWCF
this.AktuelleAntwort == 2
saInitialState
this.AktuelleAntwort == 4
Tabelle 4.1: Bedingungen für die Zustände WPF, WCF und Zurück
Schauen wir uns jetzt noch einmal die Übersicht an, so erkennen wir die möglichen Zustandswechsel anhand der neu hinzugefügten Pfeile. Eine Kleinigkeit fehlt uns jedoch noch, da bei der aktuellen Workflow-Implementierung noch immer kein Ende der Instanz in Sicht ist. Doch das lösen wir ganz einfach, indem wir eine stateInitiali-
259
Kapitel 4
zationActivity in einen beliebigen noch nicht konfigurierten Zustand wie z.B. WPF ziehen und mit einer CodeActivity und SetStateActivity ausstatten (Abbildung 4.14).
Abbildung 4.14: Initialisierung des WPF-Zustands
In caUntermenueWPF weise ich auf das Ende des Workflows hin und beende in der sich anschließenden SetStateActivity ssaEndeWPF den Workflow, indem ich als TargetState den Zustand saEnde zuweise. private void caUntermenue_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("\nWir befinden uns im Zustand " + this.CurrentStateName); Console.WriteLine("Hier könnte wieder ein Untermenü implementiert sein..."); Console.WriteLine("Bitte noch einmal drücken, um den Workflow zu " "beenden...\n"); Console.ReadLine(); } Listing 4.9: Benutzer wird auf das anschließende Ende des Workflows hingewiesen
260
Windows Workflow Foundation
Da wir den Programmcode in Listing 4.9 mit der Eigenschaft CurrentStateName relativ allgemein gehalten haben, können wir diese Baugruppe als Muster nehmen und in die übrigen noch nicht konfigurierten Zustände kopieren. Dazu wechseln Sie in die Designeransicht des gesamten Workflows und kopieren diese eben erzeugte stateInitializationActivity siWPF in die anderen freien Zustände, indem Sie diese markieren, per Copy & Paste in die anderen Zustände kopieren und evtl. die Bezeichnungen der kopierten Aktivitäten anpassen.
Abbildung 4.15: Zustandsbasierter Workflow in der Designeransicht
Jetzt sind wir so weit, dass wir den Workflow testen können. Zur besseren Verfolgung empfehle ich Ihnen, vorher noch ein paar Haltepunkte zu setzen, um die durchgeführten Aktionen besser nachvollziehen zu können. Los geht’s!
261
Kapitel 4
Abbildung 4.16: Konsolenoutput unserer State Machine
4.4 Hosting-Dienste Die WorkflowRuntime bietet uns einige Dienste, mit denen wir uns ein paar Löcher in die Workflow Blackbox bohren können. Sie stellen eine Art Schnittstelle zwischen Workflow und Host-Applikation dar und erweitern das Konfigurationsspektrum in Bezug auf Interaktion, Transparenz, Performanz und Beständigkeit. Auf die wichtigsten werde ich in den folgenden Kapiteln eingehen und mittels Beispielen erklären, wie diese integriert werden können und welchen Zweck sie im Leben eines Workflows haben.
4.4.1 Persistence Service In der Toolbox des Visual Studios 2005 finden wir in der WF-Rubrik eine Aktivität mit der Bezeichnung Delay, welche die Ausführung des Ablaufs für eine bestimmte Dauer blockiert. Die eingestellte Zeitspanne kann dabei mehrere Minuten bis einige Stunden umfassen. Neben dieser Art gibt es auch noch weitere, ich nenne sie mal Blockierer, die auf ein bestimmtes Ereignis von der Hostanwendung warten und deshalb die Ausführung während dieser Zeit stoppen. Der Persistence Service versucht nun, diesen Umstand zu optimieren, und beschreibt einen Mechanismus, der innerhalb der WorkflowRuntime dafür sorgt, dass der Zustand einer Workflow-Instanz auf einem persistenten Medium gespeichert wird, falls sich diese in einem Ruhezustand befindet oder es explizit vom Entwickler gefordert wird. Out of the box wird dabei die Speicherung in einer Microsoft SQL Server-
262
Windows Workflow Foundation
Datenbank unterstützt, wobei auch die Persistierung in einer XML-Datei oder einem anderen Datencontainer durch Erweiterung des Service implementiert werden kann.
SqlWorkflowPersistenceService Die Skripte zum Anlegen der für den SqlWorkflowPersistenceService benötigten SQL Server-Datenbank finden Sie im .NET Framework 3.0 in dem Ordner C:\ WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\DE .
Abbildung 4.17: SQL-Skripte für die Nutzung des SqlPersistencyService
> >
>
HINWEIS
Installieren Sie zuerst die Schema-Datei, bevor Sie das Logik-Skript ausführen.
Nach der Installation der Datenbank finden Sie in ihr zwei Tabellen mit der Bezeichnung InstanceState und CompletedScope. Wenn der in der WorkflowRuntime integrierte Scheduler die Anweisung gibt, die Instanz zu speichern, und vorher ein SqlWorkflowPersistenceService hinzugefügt wurde, wird der aktuelle WorkflowZustand nun automatisch in dieser Datenbank persistiert und nach seiner Beendigung wieder aus der Datenbank gelöscht.
> >
>
HINWEIS
Falls Sie sich fragen, warum Sie nach erneutem Testen bei einem Workflow-Stillstand keinen Datensatz in der CompletedScope-Tabelle finden: In dieser werden Daten nur gespeichert, falls eine Aktivität vorhanden ist, welche die Schnittstelle ICompensatableActivity implementiert.
Betrachten wir ein Beispiel, wie wir einen SqlWorkflowPersistenceService der WorkflowRuntime hinzufügen: using System; using System.Collections.Generic; using System.Text;
263
Kapitel 4 using System.Threading; using System.Workflow.Runtime; using System.Workflow.Runtime.Hosting; namespace SequentialWorkflowPersistence { class Program { static void Main(string[] args) { using(WorkflowRuntime wfRuntime = new WorkflowRuntime()) { wfRuntime.WorkflowIdled += new EventHandler (wfRuntime_WorkflowIdled); wfRuntime.WorkflowPersisted += new EventHandler (wfRuntime_WorkflowPersisted); wfRuntime.WorkflowLoaded += new EventHandler (wfRuntime_WorkflowLoaded); wfRuntime.WorkflowCompleted += new EventHandler (wfRuntime_WorkflowCompleted); string connstr = @"Data Source=localhost\SQLEXPRESS;" + @"Integrated Security=SSPI;" + @"Initial Catalog=WorkflowPersistence;"; SqlWorkflowPersistenceService persistence = new SqlWorkflowPersistenceService(connstr,true, new TimeSpan(0,1,0,0),new TimeSpan(0,0,0,2)); wfRuntime.AddService(persistence); WorkflowInstance instance = wfRuntime.CreateWorkflow (typeof(SequentialWorkflowPersistence.WorkflowPersistence)); instance.Start(); Console.ReadLine(); } } static void wfRuntime_WorkflowIdled(object sender, WorkflowEventArgs e) { Console.WriteLine(e.WorkflowInstance.InstanceId.ToString() + "(" + System.DateTime.Now.Subtract( new TimeSpan(1, 0, 0)).ToLongTimeString() + "):\nStillstand ..., wo bleibt meine Persistierung???\n"); } static void wfRuntime_WorkflowPersisted(object sender, WorkflowEventArgs e)
264
Windows Workflow Foundation { Console.WriteLine(e.WorkflowInstance.InstanceId.ToString() + "(" + System.DateTime.Now.Subtract( new TimeSpan(1, 0, 0)).ToLongTimeString() + "):\nDas ging ja schnell, schon persistiert.\n"); } static void wfRuntime_WorkflowLoaded(object sender, WorkflowEventArgs e) { Console.WriteLine(e.WorkflowInstance.InstanceId.ToString() + "(" + System.DateTime.Now.Subtract( new TimeSpan(1, 0, 0)).ToLongTimeString() + "):\nJetzt komm schon, weiter geht´s!\n"); } static void wfRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { Console.WriteLine(e.WorkflowInstance.InstanceId.ToString() + "(" + System.DateTime.Now.Subtract( new TimeSpan(1,0,0)).ToLongTimeString() + "):\nFertig!"); } } } Listing 4.10: Hinzufügen eines SqlWorklfowPersistenceService zur WorkflowRuntime
Damit wir erfahren, welches Ereignis wann eintritt, habe ich einigen Ereignissen der WorkflowRuntime (WorkflowIdled, WorkflowPersisted, WorkflowLoaded, WorklflowCompleted) jeweils Delegates zugewiesen, die den aktuellen Ausführungszeitpunkt und das momentane Geschehen auf der Konsole ausgeben. Zudem habe ich der WorkflowRuntime mit der Methode AddService eine Instanz des SqlWorkflowPersistenceService als Parameter übergeben, dessen Verhalten schon im Konstruktor konfiguriert wurde. SqlWorkflowPersistenceService persistence = new SqlWorkflowPersistenceService (connstr,true,new TimeSpan(0,1,0,0),new TimeSpan(0,0,0,2)); Listing 4.11: Konfiguration des SqlWorkflowPersistenceService mittels Konstruktor
Die Bedeutung der Parameter darf dabei nicht vernachlässigt werden, deshalb finden Sie deren Bedeutung in Tabelle 4.2: 1. Parameter: connstr
Eine Variable, die den ConnectionString beinhaltet
2. Parameter: true
Gibt an, ob der Workflow sofort persistiert werden soll, falls er im Zustand idle ist
Tabelle 4.2: Konstruktorparameter der SqlWorkflowPersistenceServices
265
Kapitel 4
3. Parameter: new TimeSpan(0,0,1,0)
Die Zeitspanne, wie lange der Workflow nach einem Start von seiner aktuellen WorkflowRuntime geschützt bzw. blockiert werden soll; nur wichtig, falls mehrere Hostapplikationen auf dieselben Workflow-Instanzen zugreifen
4. Parameter: new TimeSpan(0,0,0,2)
Die Angabe, nach wie viel verstrichener Zeit die WorkflowRuntime die Datenbank abfragen soll, ob irgendeine Workflow-Instanz wieder gestartet werden soll
Tabelle 4.2: Konstruktorparameter der SqlWorkflowPersistenceServices (Fortsetzung)
Den Workflow selbst habe ich recht einfach gehalten, indem ich zwei Delay-Aktivitäten hinzugefügt habe und vor jeder eine Code-Aktivität, die den Zustand des Workflows auf der Konsole ausgibt (Abbildung 4.18).
Abbildung 4.18: Designeransicht des sequenziellen Workflows
Nachdem man das Projekt gestartet hat, kann man zwischendurch in der Tabelle InstanceState beobachten, wie sich die Datensätze zwischen den beiden Persistierungen verändern. Abbildung 4.19 zeigt den Stand nach dem ersten Insert.
266
Windows Workflow Foundation
Abbildung 4.19: Persistierte Workflow-Instanz nach der ersten Persistierung
Die Workflow-Instanz wurde also gegen 15:18:56 Uhr persistiert und müsste laut Spalte nextTimer gegen 15:19:10 wieder zum Leben erweckt werden. In Abbildung 4.20 sehen wir aber den Zeitpunkt 15:19:11. Diese Verzögerung kann man damit erklären, dass die WorkflowRuntime, wie im Konstruktor übergeben, nur alle zwei Sekunden die Datenbank pollt, ob irgendwelche Instanzen gestartet werden sollen.
Abbildung 4.20: Konsolenausgabe nach dem Ende des Workflows
Kurz nach dem erneuten Start müsste nun die zweite Persistierung folgen, da wir nach einer kurzen Ausgabe auf der Konsole erneut eine DelayActivity positioniert haben. Und tatsächlich wird die Instanz unmittelbar nach der Konsolenausgabe gegen
267
Kapitel 4
15:19:11 Uhr ohne Verzögerung in unserer eingestellten SQL Express-Datenbank WorkflowPersistence eingefroren und pünktlich zwei Minuten danach fortgesetzt, wie in der zweiten DelayActivity eingestellt.
Abbildung 4.21: Persistierte Workflow-Instanz nach der zweiten Persistierung
Aus der vorletzten Konsolenausgabe in Abbildung 4.20 kann man ableiten, wie schnell der PersistenceService reagiert. Denn bevor der in der WorkflowRuntime integrierte Scheduler melden kann, dass sich keine auszuführende Aktivität in der auszuführenden Aktivitäten-Queue mehr befindet, wird die Instanz persistiert. Die Ausgabe, dass der Workflow wieder geladen wurde, greift aber nicht mehr, weil er vorher beendet wird und somit keine Events an die Host-Applikation mehr feuern kann.
4.4.2 Tracking Service Falls Sie sich bisher gefragt haben, wie wir denn eigentlich herausfinden können, an welcher Stelle sich gerade eine bestimmte Workflow-Instanz befindet, so erhalten Sie in diesem Kapitel die Antwort dazu. Gerade in längerfristigen Workflow-Konstellationen, in denen an mehreren Stellen Benutzeraktionen erforderlich sind, die sich manchmal über Stunden oder sogar Tage bzw. Wochen hinziehen, möchte man wissen, an welcher Stelle sich ein gestarteter Workflow momentan befindet. Natürlich hat das Team der Workflow Foundation auch an solche Informationen gedacht und stellt hier out of the box, wie bei dem Persistence Service auch, einen SQL Tracking Service zur Verfügung, der die gewünschten Daten in einer Microsoft SQL Server Datenbank ablegt.
SQL Tracking Service Wie im vorherigen Kapitel schon beschrieben finden Sie die fertigen SQL-Installationsskripte bei installiertem .NET Framework 3.0 im Ordner C:\WINDOWS\Microsoft. NET\Framework\ v3.0\Windows Workflow Foundation\SQL\DE (vgl. Abbildung 4.17). Für die Nutzung erstellen wir uns eine Datenbank – ich nenne sie WorkflowTracking –
268
Windows Workflow Foundation
und installieren die beiden Tracking-Skripte in der Reihenfolge wie bei dem PersistenceService auch, also zuerst die Tracking_Schema.sql und anschließend die Tracking_ Logic.sql.
!
!
!
ACHTUNG
Wir könnten für den Tracking und Persistenzmechanismus auch eine Datenbank gemeinsam benutzen. Achten Sie jedoch bei dieser Vorgehensweise darauf, dass Sie vor dem Start der WorkflowRuntime einen zusätzlichen Dienst mit der Bezeichnung SharedConnectionWorkflowCommitWorkBatchService hinzufügen!
Nach der Ausführung der SQL-Skripte werden Sie 20 neue Tabellen in Ihrer Datenbank wieder finden. Wir werden in diesem Buch nicht den Platz haben, um alle diese Tabellen detailliert besprechen zu können, jedoch ist das für die Anwendung auch nicht wichtig, denn wie wir nachher noch sehen werden, finden wir im Framework 3.0 ein Klassenmodell, zu dessen Anwendung man die Struktur der Datenbank nicht benötigt. Eine Tabelle mit dem Namen TrackingProfile möchte ich jedoch noch kurz herausgreifen. Denn in dieser Tabelle wird in der Spalte TrackingProfileXML gespeichert, welche Daten bzw. Ereignisse zu jedem Workflow von der WorkflowRuntime in die Datenbank extrahiert werden (Abbildung 4.22).
Abbildung 4.22: Tabelle DefaultTrackingProfile nach der Installation
269
Kapitel 4
Markieren und kopieren wir anschließend den ersten Datensatz der Spalte TrackingProfileXML in eine XML Datei, die wir im Visual Studio vorher hinzugefügt haben, sehen wir, welche Arten von Tracking-Daten die WorkflowRuntime unterscheidet.
Abbildung 4.23: TrackingProfile im Visual Studio 2005
Der erste TrackPoint mit dem Namen ActivityTrackPoint führt alle Ereignisse auf, die von allen enthaltenen Aktivitäten mitprotokolliert werden sollen. Standardmäßig ist das bei jedem ActivityTrackPoint jedes Event, also der gesamte Lebenszyklus einer Aktivität mit den Ereignissen Initialized, Executing, Compensating, Canceling, Closed und Faulting. Die zweite Schiene bei der Protokollierung bildet die Workflow-Ebene mit WorkflowTrackPoint, auf der alle Ereignisse eines Workflows wie z.B. Idle oder Loaded niedergeschrieben werden. Im unteren Drittel von Abbildung 4.23 sehen wir allerdings noch eine Schicht, die sich UserTrackPoint nennt. Als UserTrackPoints werden dabei alle benutzerdefinierten Daten gespeichert, die man der Tracking-Infrastruktur mittels des Methodenaufrufs TrackData explizit mitteilt.
270
Windows Workflow Foundation
Nun gibt es mehrere Möglichkeiten, wie man das Tracking-Profil nach seinen Wünschen anpasst. Man kann dieses Profil per Hand in einem Editor manipulieren, indem man die einzelnen Event-Zeilen herauslöscht bzw. neue hinzufügt und dann mit einem Update-Befehl in der ursprünglichen Tabelle wieder einspielt. Startet man anschließend einen Workflow, werden automatisch nur noch diejenigen Geschehnisse protokolliert, an denen man auch wirklich interessiert ist. Es gibt aber noch eine andere Variante, bei der man eine speziell für die Erstellung des XML-Profils im .NET Framework 3.0 vorhandene Klassen benutzt. Auf diese Klassen werden wir später zu sprechen kommen, wenn wir die mitgeschriebenen Daten wieder aus der Datenbank herausholen wollen. Falls Sie sich jedoch an dieser Stelle noch detaillierter mit den Profilklassen und dem Tracking-Mechanismus auseinandersetzen wollen, verweise ich hier auf das Windows SDK, das in der Rubrik Windows Workflow Tracking Services mit vielen Codebeispielen in dieser Richtung glänzt. Oder man benutzt für die Profilkonfiguration ein Tool, das in den Samples des Windows SDK unter der Rubrik Workflow Foundation zu finden ist und den Namen TrackingProfileDesigner trägt.
> >
>
HINWEIS
Vergessen Sie aber nicht, vor dem Starten des TrackingProfileDesigners den Connection-String in der app.config anzupassen!
Voraussetzung für die Verwendung dieses Tools ist, dass man als Projekt eine Workflow-Bibliothek gewählt hat und diese schon einmal in der Datenbank getrackt wurde. Denn erst nach dem ersten Start wird in der Tracking-Datenbank das Standardprofil für den aktuellen Workflow-Typ in der Tabelle TrackingProfile hinterlegt. Öffnet man danach im TrackingProfileDesigner mit OPEN/FILE À FROM SQL TRACKING DATABASE seinen speziellen Workflow-´Bezeichner mit der Angabe der Version und der dll, so erhält man eine visuelle Ansicht des Ablaufs, wo man mithilfe des Menüs die gewünschten Trackpoints hinzufügen oder entfernen kann. Als Übersicht sieht man im zweiten Reiter die textuelle Darstellung des XML-Profils. Um die Änderungen zu speichern, hat man durch den Menüpunkt FILE À SAVE À PROFILE TO SQL DATABASE die Möglichkeit, das aktualisierte XML-Tracking-Profil für seinen Workflow zu übernehmen. Doch nun schauen wir uns ein Beispiel für die Verwendung des Tracking Service an. Dazu erstellen wir uns ein neues Projekt vom Vorlagentyp SEQUENZIELLE WORKFLOWBIBLIOTHEK und ziehen uns eine WhileActivity und in diese eine CodeActivity auf den Designer (vgl. Abbildung 4.24). caNachrichtVersenden bietet dabei dem Anwender über das Konsolenfenster an, eine Nachricht zu verschicken (Listing 4.12).
271
Kapitel 4
Abbildung 4.24: SqlTrackingProfile-Designer aus den Windows SDK Samples
private void caNachrichtVersenden_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Bitte den Absender angeben:"); PersoenlicheNachricht.Absender = Console.ReadLine(); Console.WriteLine("Nun den Empfänger:"); PersoenlicheNachricht.Empfaenger = Console.ReadLine(); Console.WriteLine("Mitteilung"); PersoenlicheNachricht.Meldung = Console.ReadLine(); this.TrackData(PersoenlicheNachricht); Console.WriteLine("\nWollen Sie noch eine Nachricht verschicken? - J/N"); string eingabe = Console.ReadLine(); try { switch (eingabe.ToLower()) { case "j": mNochmal = true;
272
Windows Workflow Foundation break; case "n": mNochmal = false; break; default: break; } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } Listing 4.12: Anwender wird zum Verschicken einer Nachricht aufgefordert.
Die Variable PersoenlicheNachricht haben wir vorher im Workflow als Property vom Typ Nachricht deklariert und im Konstruktor nach dem InitializeComponent()-Aufruf instanziert. using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Collections; using System.Drawing; using System.Workflow.ComponentModel.Compiler; using System.Workflow.ComponentModel.Serialization; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Design; using System.Workflow.Runtime; using System.Workflow.Activities; using System.Workflow.Activities.Rules; namespace WorkflowNachrichtVersenden { public sealed partial class NachrichtVersenden: SequentialWorkflowActivity { public NachrichtVersenden() { InitializeComponent(); PersoenlicheNachricht = new Nachricht(); mNochmal = true; } private Nachricht mPersoenlicheNachricht; public Nachricht PersoenlicheNachricht { get { return mPersoenlicheNachricht; } set { mPersoenlicheNachricht = value; } }
273
Kapitel 4 private bool mNochmal; public bool Nochmal { get { return mNochmal; } set { mNochmal = value; } } //weiterer Code der Übersichtlichtkeit wegen ausgeblendet } Listing 4.13: Deklaration und Initialisierung von Workflow-Variablen
Die boolesche Variable Nochmal bietet dem User im Programm die Option, die WhileSchleife zu verlassen und somit die Anwendung zu beenden. Denn durch die öffentliche Deklaration von Nochmal kann man das Verlassen der While-Schleife von dieser abhängig machen, indem man als Bedingung eine Deklarative Regelbedingung und als Ausdruck den Wert this.Nochmal == true angibt. Die Klasse Nachricht habe ich in einer separaten Datei ausgelagert und wie in Listing 4.14 definiert. using System; using System.Collections.Generic; using System.Text; namespace WorkflowNachrichtVersenden { [Serializable] public class Nachricht { private string mAbsender; public string Absender { get { return mAbsender; } set { mAbsender = value; } } private string mEmpfaenger; public string Empfaenger { get { return mEmpfaenger; } set { mEmpfaenger = value; } } private string mMeldung; public string Meldung { get { return mMeldung; } set { mMeldung = value; } }
274
Windows Workflow Foundation public override string ToString() { return String.Format("Absender:{0}\nEmpfänger:{1}\nMeldung:\n{2}\n", Absender, Empfaenger, Meldung); } } } Listing 4.14: Definition der Klasse Nachricht
Das Versenden der Nachricht soll hier nur symbolisch angedeutet werden, uns interessiert in diesem Beispiel nur das Tracking. Mit dem Aufruf der Methode this.TrackData(PersoenlicheNachricht) in Listing 4.12 wollen wir dem TrackingService explizit mitteilen, dass er zusätzlich noch das übergebene Objekt PersoenlicheNachricht als UserTrackingRecord mitspeichern soll.
!
!
!
ACHTUNG
Beachten Sie bei diesem Vorgehen aber, dass das übergebene Objekt das [Serializable]-Attribut implementiert. Durch das Überschreiben der Methode ToString() können Sie dabei das gewünschte Ausgabeformat festlegen.
Allerdings haben wir jetzt noch ein kleines Problem. Wir benötigen noch ein Hostprogramm, um unsere Anwendung starten zu können. Da wir die Userinteraktion auch schon durch die Zuhilfenahme einer Konsole realisiert haben, fügen wir zu unserer Projektmappe ein neues Projekt vom Typ WINDOWS-KONSOLENANWENDUNG hinzu. Die automatisch hinzugefügte Program.cs beinhaltet die Main-Methode, die ich wie in den vorherigen Kapiteln auch mit der Implementierung der WorkflowRuntime versehen habe. Hinzu kommt jedoch noch der TrackingService, sodass wir am Ende einen Programmcode wie in Listing 4.15 vor uns haben. using System; using System.Collections.Generic; using System.Text; using System.Workflow.Runtime.Tracking; using System.Workflow.Runtime; using System.Threading; namespace WorkflowHost { public class Program { static AutoResetEvent _handle; static void Main(string[] args) { using (WorkflowRuntime wfRuntime = new WorkflowRuntime())
275
Kapitel 4 { _handle = new AutoResetEvent(false); wfRuntime.WorkflowCompleted += new EventHandler (wfRuntime_WorkflowCompleted); string con = @"Data Source=localhost\SQLEXPRESS;" + @"Integrated Security=SSPI;" + @"Initial Catalog=WorkflowTracking;"; SqlTrackingService tracking = new SqlTrackingService(con); wfRuntime.AddService(tracking); WorkflowInstance instance = wfRuntime.CreateWorkflow (typeof(WorkflowNachrichtVersenden.NachrichtVersenden)); instance.Start(); _handle.WaitOne(); } } static void wfRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { _handle.Set(); } } } Listing 4.15: Hinzufügen des SqlTrackingService
Vergessen Sie nicht, die notwendigen Assemblies ins Projekt mit einzubinden. Über PROJEKT/VERWEIS HINZUFÜGEN fügen Sie bitte die Assembly von WorkflowNachrichtVersenden hinzu und für die Verwendung der anderen Workflow-Klassen System. Workflow.Activities, System.Workflow.ComponentModel und System.Workflow.Runtime. Wenn wir jetzt noch als Startprojekt der Projektmappe die Konsolenanwendung WorkflowHost auswählen, startet der Workflow mit der Ausgabe im Konsolenfenster.
> >
>
HINWEIS
Sie können den Workflow auch in Form einer Bibliothek debuggen. Wählen Sie dazu bei den Projekteigenschaften die Kategorie Debuggen und aktivieren dort als Startaktion EXTERNES PROGRAMM STARTEN… Als ausführbare Datei hinterlegen Sie einfach die *.exe der Konsolenanwendung und geben zuletzt als Startprojekt der Projektmappe die Bibliothek selbst an. Ein Klick auf (F5), und schon klappt auch das Debuggen unserer Bibliothek!
276
Windows Workflow Foundation
»Das ist ja alles schön und gut«, werden Sie sich jetzt denken, aber ist denn überhaupt unser Ablauf mitprotokolliert worden, und wie können wir diese Informationen überhaupt zur Anzeige bringen? Dass unser Tracking Service funktioniert hat, können wir in Abbildung 4.25 erkennen:
Abbildung 4.25: Tracking-Daten der Tabellen ActivityInstance und UserEvent
Bedeutet das jetzt, dass wir uns mit der Tabellenstruktur auseinandersetzen und die Informationen mit Select-Befehlen extrahieren müssen? Nun ja, ein geübter Datenbankprogrammierer würde diesen Weg wohl bevorzugen. Für alle anderen jedoch, die mit SQL noch nicht so vertraut sind, besteht die Möglichkeit, auf die gespeicherten Datensätze mittels einer im .NET Framework 3.0 integrierten API zuzugreifen. Und das machen wir jetzt, indem wir eine Windows Konsolenapplikation zu unserer Projektmappe hinzufügen.
Datenextrahierung aus der Tracking-Datenbank Um das API für die Tracking-Informationen anwenden zu können, benötigen wir auch in diesem Projekt wieder die entsprechenden Verweise auf die Klassenbibliotheken. Anschließend instanzieren wir eine Klasse SqlTrackingQuery, der wir den Connectionstring zu unserer Datenbank im Konstruktor übergeben müssen. Zudem können wir diesem Objekt eine Art Filter in Gestalt der Klasse SqlTrackingQueryOptions übergeben, der zur Folge hat, dass nicht alle gefundenen Datensätze ausgegeben werden, sondern
277
Kapitel 4
nur diejenigen, die z.B. den Typ NachrichtVersenden betreffen. Auch weitere Eingrenzungen, die den Zeitpunkt der Workflow-Ausführung oder die Art des WorkflowStatus betreffen, können hier gemacht werden. Listing 4.16 zeigt dabei eine Ausgabe der getrackten Daten von NachrichtVersenden und kategorisiert nach den unterschiedlichen Arten der Tracking Points. Beachten Sie dabei v. a. die Ausgabe der UserTrackingRecords. Denn hätten wir die Methode ToString() in der Klasse Nachricht nicht dementsprechend überschrieben, so würden wir nur einen String mit dem Wert NachrichtVersenden.Nachricht auf der Konsole sehen. using using using using using
System; System.Collections.Generic; System.Text; System.Workflow.Runtime.Tracking; System.Workflow.Runtime;
namespace TrackingKonsole { class Program { static string abtrennung = "----------------------------"; static void Main(string[] args) { Console.ReadLine(); SqlTrackingQuery query = new SqlTrackingQuery( @"Data Source=localhost\SQLEXPRESS;" + @"Integrated Security=SSPI;" + @"Initial Catalog=WorkflowTracking;"); SqlTrackingQueryOptions options = new SqlTrackingQueryOptions(); options.WorkflowStatus = WorkflowStatus.Completed; options.WorkflowType = typeof(WorkflowNachrichtVersenden.NachrichtVersenden); IList workflowList = query.GetWorkflows(options); if ((workflowList.Count > 0) && (workflowList != null)) { foreach (SqlTrackingWorkflowInstance instance in workflowList) { Console.WriteLine("Workflow\nID:{0}\nStatus:{1}", instance.WorkflowInstanceId.ToString(), instance.Status.ToString()); IList workflowTrackingRecordlist = instance.WorkflowEvents; Console.WriteLine("\n" + abtrennung);
278
Windows Workflow Foundation Console.WriteLine("Workflow Tracking Records:"); Console.WriteLine(abtrennung + "\n"); if ((workflowTrackingRecordlist.Count > 0) && (workflowTrackingRecordlist != null)) { foreach (WorkflowTrackingRecord wfTrackingRecord in workflowTrackingRecordlist) { Console.WriteLine("Event: {0}\t Zeit:{1}", wfTrackingRecord.TrackingWorkflowEvent.ToString(), wfTrackingRecord.EventDateTime.ToLongTimeString()); } } IList activityTrackingRecordlist = instance.ActivityEvents; Console.WriteLine("\n" + abtrennung); Console.WriteLine("Activity Tracking Records:"); Console.WriteLine(abtrennung + "\n"); if ((activityTrackingRecordlist.Count > 0) && (activityTrackingRecordlist != null)) { foreach (ActivityTrackingRecord wfActivityTrackingRecord in activityTrackingRecordlist) { Console.WriteLine("Name: {0}\t Zeit: {1}\t Execution Status:" + "{2}",wfActivityTrackingRecord.QualifiedName, wfActivityTrackingRecord.EventDateTime.ToLongTimeString(), wfActivityTrackingRecord.ExecutionStatus.ToString()); } } IList userTrackingRecordlist = instance.UserEvents; Console.WriteLine("\n" + abtrennung); Console.WriteLine("User Tracking Records:"); Console.WriteLine(abtrennung); if ((userTrackingRecordlist.Count > 0) && (userTrackingRecordlist != null)) { foreach (UserTrackingRecord wfUserTrackingRecord in userTrackingRecordlist) { Console.WriteLine("{0}",wfUserTrackingRecord.UserData.ToString()); } }
279
Kapitel 4 Console.WriteLine(abtrennung); Console.WriteLine(abtrennung); Console.WriteLine(abtrennung); } } Console.ReadLine(); } } } Listing 4.16: Ausgabe der Tracking-Daten auf der Konsole
Starten wir nun mit (F5) das Projekt, erhalten wir eine Ausgabe wie in Abbildung 4.26:
Abbildung 4.26: Ausgabe der Tracking-Informationen auf der Konsole
4.4.3 Scheduling Service In der Windows Workflow Foundation werden die einzelnen Workflow-Instanzen von je einem Thread ausgeführt. Welcher Thread jetzt für die Ausführung eines Workflows zuständig ist, entscheidet dabei der Scheduling-Dienst.
280
Windows Workflow Foundation
DefaultWorkflowSchedulerService Wenn man wie in den vorherigen Kapiteln in diesem Buch einen Workflow startet, indem man die Start()-Methode einer Instanz aufruft, so hat man schon ohne es zu wissen per Default-Einstellung einen Scheduler-Dienst in die Workflow Runtime integriert, der den Namen DefaultWorkflowSchedulerService trägt. Dieser Dienst startet jeden Workflow in einer asynchronen Art und Weise, bedient sich dabei aus dem .NET Thread-Pool und weist jedem zu startenden Workflow einen Thread zu, solange die Anzahl der gerade ausgeführten Worfkflow-Instanzen eine bestimmte Obergrenze nicht überschreitet, die in der Property MaxSimultaneousWorkflows gespeichert ist. Diese Eigenschaft ist standardmäßig auf 5 gesetzt, kann jedoch durch das Hinzufügen eines angepassten Scheduling-Dienstes in die Höhe geschraubt werden. Dazu instanzieren wir einfach einen DefaultWorkflowSchedulingService und übergeben die maximale Anzahl (an Threads) dessen Konstruktor als Parameter: DefaultWorkflowSchedulerService workflowService = new DefaultWorkflowSchedulerService(10); workflowRuntime.AddService(workflowService); Listing 4.17: Hinzufügen eines angepassten DefaultWorkflowSchedulerService
!
!
!
ACHTUNG
Kommen Sie jetzt aber nicht auf die Idee, die Anzahl an Threads in unbekannte Höhen zu schrauben, denn standardmäßig bietet der .NET Thread Pool leider nur 25 Threads pro CPU.
Um die Arbeitsweise des Schedulers etwas besser zu verstehen, will ich Ihnen ein kurzes Beispiel zeigen. Nehmen wir an, die WorkflowRuntime startet z.B. sieben WorkflowInstanzen unmittelbar nacheinander, deren Sequenz eine WhileActivity beinhaltet, die nie verlassen wird. In dieser Schleife befindet sich eine SequenceActivity, die wiederum eine CodeActivity und darauffolgend eine DelayActivity mit der Dauer von einer Sekunde. Wie verteilt der DefaultWorkflowSchedulerService die einzelnen Threads auf die Workflow-Instanzen? Probieren wir es einfach aus. In Abbildung 4.27 sehen Sie die geschilderte Situation. Die WhileActivity waUnendlich ist dabei mit dem Ausdruck True einer Deklarativen Regelbedingung so konfiguriert, dass diese nie verlassen wird. Um zu erfahren, welcher Thread gerade mit der Ausführung beschäftigt ist, geben wir in der sich anschließenden CodeActivity caAusgabe die Workflow-ID zusammen mit der ManagedThreadId aus und blockieren die Aktivität mit der Methode Sleep der Thread-Klasse noch für vier Sekunden.
281
Kapitel 4
Abbildung 4.27: Unendliche While-Schleife
using using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules; System.Threading;
namespace WorkflowSchedulingService { public sealed partial class WorkflowThreadaufteilung: SequentialWorkflowActivity { public WorkflowThreadaufteilung() {
282
Windows Workflow Foundation InitializeComponent(); } private string mWorkflowInhaber; public string WorkflowInhaber { get { return mWorkflowInhaber; } set { mWorkflowInhaber = value; } }
private void caAusgabe_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("{0} befindet sich in Thread {1}", this.mWorkflowInhaber, Thread.CurrentThread.ManagedThreadId.ToString()); Thread.Sleep(4000); } } } Listing 4.18: Ausgabe der Workflow-ID und der ManagedThreadId der aktuellen Workflow-Instanz
Die von dem Host erhaltene Property WorkflowInhaber dient uns hier in Listing 4.18 als Workflow-ID. Wir hätten auch die GUID der Instanz nehmen können, jedoch finde ich einen einfachen Namen übersichtlicher bei der Ausgabe. In der Main-Methode von Program.cs durchlaufen wir anschließend ein mit Namen gefülltes String-Array und starten für jeden Eintrag eine Workflow-Instanz, der wir jeweils einen Array-Eintrag als Parameter übergeben: using using using using using using
System; System.Collections.Generic; System.Text; System.Threading; System.Workflow.Runtime; System.Workflow.Runtime.Hosting;
namespace WorkflowSchedulingService { class Program { static AutoResetEvent _handle; static void Main(string[] args) { using(WorkflowRuntime wfRuntime = new WorkflowRuntime()) { _handle = new AutoResetEvent(false); wfRuntime.WorkflowCompleted += new EventHandler
283
Kapitel 4 (wfRuntime_WorkflowCompleted); wfRuntime.WorkflowTerminated += new EventHandler (wfRuntime_WorkflowTerminated); DefaultWorkflowSchedulerService workflowService = new DefaultWorkflowSchedulerService(3); wfRuntime.AddService(workflowService); Dictionary mitarbeiter; string[] developer = { "Jürgen", "Torsten", "Markus", "Rouven", "Sebastian", "Simon","Rene"}; for (int i = 0; i < developer.Length; i++) { mitarbeiter = new Dictionary(); mitarbeiter["WorkflowInhaber"] = developer[i]; WorkflowInstance instance = wfRuntime.CreateWorkflow(typeof( WorkflowSchedulingService.WorkflowThreadaufteilung),mitarbeiter); instance.Start(); } _handle.WaitOne(); } } static void wfRuntime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine(e.Exception.Message); _handle.Set(); } static void wfRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { _handle.Set(); } } } Listing 4.19: Program.cs
Starten wir das Projekt mit (F5), erhalten wir einen Konsolenoutput wie in Abbildung 4.28. Darin sehen Sie auch, dass es hier wirklich nur drei unterschiedliche Threads gibt, welche die Ausführung der sieben gestarteten Workflow-Instanzen vorantreiben.
284
Windows Workflow Foundation
Abbildung 4.28: Threadaufteilung bei der Ausführung der Workflow-Instanzen
Die Wahl eines bestimmten Threads erfolgt dabei zufällig, wie Sie in Abbildung 4.28 unschwer erkennen können. Der Jürgen wird z.B. zuallererst von Thread 11 bearbeitet, dann von Nummer 12 und schließlich von Thread 3, je nachdem, welcher Thread im .NET Thread Pool gerade keine Arbeit verrichtet.
ManualWorkflowSchedulerService Doch was machen wir in anderen Szenarien, in denen z.B. wie in ASP.NET von dem .NET Thread Pool schon Gebrauch gemacht wird und wir mit unseren verbliebenen Ressourcen möglichst effizient umgehen wollen. In diesen Situationen bietet sich die Anwendung des synchronen ManualWorkflowSchedulerService an. Dabei wird der Workflow-Instanz zu dessen Ausführung der Thread der Applikation selbst übergeben. Folglich müssen wir mit der weiteren Abarbeitung unseres Hauptprogramms warten, bis der Workflow seine Aktivitäten abgearbeitet hat, und können zuletzt unsere Anwendung fortsetzen.
4.5 Kommunikation zwischen Host und Workflow Wie man in den bisherigen Beispielen schon gesehen hat, läuft der Workflow bei integriertem DefaultWorkflowSchedulerService asynchron in einem eigenen Thread ab, so dass es nicht so leicht ist, Daten zwischen Host und Workflow-Instanz auszutauschen. Doch auch an solche Szenarien hat das Workflow-Team gedacht und stellt uns mit dem
285
Kapitel 4
ExternalDataExchange ein Attribut zur Verfügung, das wir genau für diese Verwendung einsetzen können. Denn ausgestattet mit diesem Attribut kann eine Schnittstelle in Form ihrer Klassenimplementierung der WorkflowRuntime als Dienst hinzugefügt werden. Natürlich muss man für eine reibungslose Funktionalität noch ein paar Feinheiten beachten, aber diese schauen wir uns am besten wieder anhand eines Beispiels an. Im Beispielszenario wird dabei der Prozess eines Urlaubsantrags mithilfe eines Workflows realisiert werden. Aus didaktischen Gründen, um die Anwendung so einfach wie möglich zu halten, verwenden wir für unterschiedliche Personen dasselbe Formular, dargestellt in Abbildung 4.29, damit man sich besser auf die Datenübertragung zwischen Host und Workflow konzentrieren kann. Denn hätten wir unterschiedliche Projekte genommen, müssten wir die WorkflowRuntime in einem zentralen Prozess wie z.B. einem Web Service oder einer ASP.NET Web Service-Anwendung hosten. Dazu werden wir noch kommen, keine Angst. Jetzt soll jedoch die Kommunikation im Vordergrund stehen und alles andere wie z.B. das Layout oder der Formularaufbau in den Hintergrund treten.
Abbildung 4.29: Urlaubsformular auf Mitarbeiter- (links) und Managerseite (rechts)
Ein Mitarbeiter gibt also seine Wunschdaten für den nächsten geplanten Urlaub ein und sendet den Antrag mit Klick auf den Button mit der Beschriftung Absenden ab (Schritt 1). Der Manager erhält diesen Antrag auf der rechten Seite im oberen Drittel,
286
Windows Workflow Foundation
bestätigt oder lehnt ab, indem er nach dem Eintrag eines Kommentars die entsprechende Schaltfläche Bestätigen oder Ablehnen drückt (Schritt 2 und 3), worauf der Mitarbeiter eine sofortige Antwort auf der unteren Formularhälfte erhält (Schritt 4). Um diesen Urlaubsantrag zu realisieren, erstellen wir im Visual Studio 2005 eine neue Projektmappe und fügen eine Anwendung vom Typ SEQUENZIELLE WORKFLOW-BIBLIOTHEK und für die Oberfläche eine WINDOWS FORMS-APPLIKATION hinzu.
4.5.1 Schnittstelle definieren In der linken Spalte von Tabelle 4.3 sehen wir, welche Wege wir für eine erfolgreiche Umsetzung unseres Vorhabens überbrücken müssen, und auf deren rechten Seite, mit welchen Konstrukten wir dies bei der Verwendung der Workflow Foundation in die Tat umsetzen können. Zu überwindende Prozessschnittstellen
Realisierung
Urlaubsinformation aus dem Mitarbeiterformular dem Workflow beim Start übergeben
Parameterübergabe im Konstruktor der WorkflowRuntime
Erhaltene Urlaubsdaten im Workflow an den Manager weiterreichen
CallExternalMethodActivity
Antwort des Managers dem Workflow zusenden
HandleExternalEventActivity-Aktivität
Senden der Nachricht an den Mitarbeiter
CallExternalMethodActivity-Aktivität
Tabelle 4.3: Zu überbrückende Schnittstellen und ihre Realisierung in der zweiten Spalte
Da wir die zu implementierenden Schnittstellen jetzt schon kennen, können wir im Zuge des Contract-First-Ansatzes ein Interface deklarieren. Das machen wir, indem wir der Workflow-Bibliothek eine Schnittstelle mit dem Namen IWorkflowHostKommunikation hinzufügen und folgendermaßen ausprogrammieren: [ExternalDataExchange] public interface IWorkflowHostKommunikation { void Entscheiden(string nachricht); event EventHandler EntscheidungGetroffen; void Antworten(Entscheidung ent); } Listing 4.20: Schnittstellendeklaration für den Datenaustausch zwischen Workflow und Host
Falls Sie jetzt denken, ich habe den ersten Schritt in der Workflow-Korrespondenz vergessen, denken Sie daran, dass Parameter beim Start eines Workflows mit übergeben werden können! Die drei Zeilen des Interface in Listing 4.20 entsprechen also den letzten drei Kommunikationsbrücken in der richtigen Reihenfolge. Der Entscheidung-
287
Kapitel 4
EventArgs-Parameter ist dabei keine Klasse aus dem .NET Framework 3.0, sondern wurde zuvor deklariert und ist von ExternalDataEventArgs abgeleitet. [Serializable] public class EntscheidungEventArgs : ExternalDataEventArgs { private Entscheidung mEnt; public Entscheidung Ent { get { return mEnt; } set { mEnt = value; } } public EntscheidungEventArgs(Guid workflowInstanz, Entscheidung e ) : base(workflowInstanz) { Ent = e; } } [Serializable] public class Entscheidung { private bool mAntragBestaetigt; public bool AntragBestaetigt { get { return mAntragBestaetigt; } set { mAntragBestaetigt = value; } } private string mBegruendung; public string Begruendung { get { return mBegruendung; } set { mBegruendung = value; } } } Listing 4.21: Zu transportierende Informationen in Form der Klasse EntscheidungEventArgs
Zudem beinhaltet dieses Objekt eine Instanz der Klasse Entscheidung, mittels derer die Informationen in beide Richtungen fließen können. Die Ableitung von der Klasse ExternalDataEventArgs ist von sehr großer Bedeutung, da dieser eine WorkflowInstanz-ID im Konstruktor übergeben werden muss, wodurch die WorkflowRuntime in der beabsichtigten Workflow-Instanz das ausgelöste Ereignis EntscheidungGetroffen feuern kann.
288
Windows Workflow Foundation
> >
>
HINWEIS
Beachten Sie auch, dass alle in der Schnittstelle verwendeten Klassen serialisierbar sein müssen, um zwischen den Anwendungen hin und her transportiert werden zu können.
Da wir nun alle notwendigen Schnittstellen definiert haben, können wir mit der GUI, dem Graphical User Interface beginnen, ohne dass wir den Ablauf selbst in einem Workflow implementiert haben!
4.5.2 Erstellung der Host-Anwendung Doch zuerst erstellen wir die Workflow-Bibliothek neu und setzen in FormularUrlaubsantrag einen Verweis auf unseren Workflow (siehe Abbildung 4.30).
Abbildung 4.30: Projektmappe mit markiertem Verweis auf die Workflow-Bibliothek
Formular.cs in Abbildung 4.30 ist unser Formular, mit dem wir mit dem Anwender in Interaktion treten. Da diese Form auch zugleich der Host für die WorkflowRuntime ist, deklarieren wir innerhalb der Klasse Formular eine Eigenschaft mit der Bezeichnung Runtime, die für die Aufrechterhaltung der instanzierten WorkflowRuntime dient, und eine weitere Variable mit dem Namen WorkflowId und dem Datentyp GUID, in der die aktuell gestartete Workflow-Instanz-ID gespeichert wird.
289
Kapitel 4
Haben Sie die bisher beschriebenen Schritte verfolgt, also die Oberflächenelemente wie in Abbildung 4.29 erstellt und durch Doppelklick auf die jeweiligen Buttons die Ereignisrümpfe hinzugefügt, so erhalten wir eine Codeansicht des Formulars wie in Listing 4.22. using using using using using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms; System.Workflow.Runtime; System.Workflow.Activities; System.Threading; WorkflowUrlaubsantrag;
namespace FormularUrlaubsantrag { public partial class Formular : Form, WorkflowUrlaubsantrag.IWorkflowHostKommunikation { private WorkflowRuntime mRuntime; public WorkflowRuntime Runtime { get { return mRuntime; } set { mRuntime = value; } } private Guid mWorkflowId; public Guid WorkflowId { get { return mWorkflowId; } set { mWorkflowId = value; } } public Formular() { InitializeComponent(); } private void btnAbsenden_Click(object sender, EventArgs e) { } private void btnBestaetigen_Click(object sender, EventArgs e) { }
290
Windows Workflow Foundation private void btnAblehnen_Click(object sender, EventArgs e) { } } } Listing 4.22: Aktuelle Codeansicht von Formular.cs
Damit das Formular nun mit den Workflow-Instanzen kommunizieren kann, implementieren wir die zuvor im Workflow definierte Schnittstelle (den Verweis auf die dll haben wir ja schon gesetzt).
*
*
*
TIPP
Drücken Sie zur Ausgestaltung der Schnittstellenmethoden und Ereignisse (Strg) + (.), so öffnet sich dabei ein Kontextmenü, das die Implementierung der Schnittstellenmember für Sie übernimmt (siehe Abbildung 4.31).
Abbildung 4.31: Hilfe bei der Schnittstellenimplementierung im Visual Studio 2005
Nach dem Bestätigen werden die definierten Member der Schnittstelle in unserer Formklasse automatisch hinzugefügt (Listing 4.23). #region IWorkflowHostKommunikation Member public event EventHandler EntscheidungGetroffen; public void Entscheiden(string message) { }
291
Kapitel 4 public void Antworten(Entscheidung ent) { } #endregion Listing 4.23: Schnittstellenimplementierung in Formular.cs
Im Konstruktor rufe ich zusätzlich noch eine weitere Routine mit der Bezeichnung InitializeRuntime auf. public Formular() { InitializeComponent(); InitializeRuntime(); } Listing 4.24: Konstruktor der Klasse Formular
Initialisierung der Workflow Runtime In dieser finden wir eine unabdingbare Voraussetzung für die Nutzung der Schnittstellenmethoden. Zuerst fügen wir eine Instanz des ExternalDataExchangeService der WorkflowRuntime hinzu und daraufhin dem ExternalDataExchangeService selbst die Implementierung der Kommunikationsschnittstelle, hier unserer Formular-Klasse. Somit ist die Instanz unseres Formulars als Schnittstelle in der WorkflowRuntime integriert und in den Kommunikationsmechanismus mit einbezogen. private void InitializeRuntime() { mRuntime = new WorkflowRuntime(); ExternalDataExchangeService service = new ExternalDataExchangeService(); mRuntime.AddService(service); service.AddService(this); } Listing 4.25: Ermöglichen der Kommunikation zwischen Workflow und Host-Applikation
!
!
!
ACHTUNG
Beachten Sie bei dem Hinzufügen des ExternalDataExchangeService service unbedingt die Reihenfolge. Zuerst die Instanz eines ExternalDataExchangeService der WorkflowRuntime hinzufügen und anschließend die Schnittstellenimplementierung dem gerade hinzugefügten Service!
Absenden des Urlaubsantrags private void btnAbsenden_Click(object sender, EventArgs e) { Dictionary parameter = new Dictionary(); parameter.Add("Anfrage", txtAnfrage.Text);
292
Windows Workflow Foundation WorkflowInstance instance = mRuntime.CreateWorkflow( typeof(WorkflowUrlaubsantrag.SequentialWorkflowUrlaubsantrag),parameter); instance.Start(); WorkflowId = instance.InstanceId; } Listing 4.26: Start des Workflows beim Absenden des Urlaubsantrags
Beginnen wir mit der ersten Aktion in unserem Workflow. Nachdem der Anwender seinen Antrag schriftlich formuliert hat, betätigt er den Button Absenden. Das dadurch ausgelöste Ereignis btnAbsenden_Click in Listing 4.26 bedient sich im Hintergrund der vorher instanzierten WorkflowRuntime und startet einen Workflow vom Typ WorkflowUrlaubsantrag.SequentialWorkflowUrlaubsantrag und übergibt ihm die in die Textbox eingetragene Mitteilung in Gestalt eines Dictionarys, das sich aus SchlüsselWert-Paaren zusammensetzt.
> >
>
HINWEIS
Vergessen Sie nicht, dass der Schlüssel der übergebenen Parameter auf der Workflow-Seite der Bezeichnung einer öffentlichen Eigenschaft entsprechen muss.
Der Workflow hat also das Formular beim Start als Parameter entgegengenommen und leitet diese Daten an eine dafür zuständige Person weiter. In unserem Beispiel ist dieser Adressat durch die Managerseite auf der rechten Hälfte des Formulars abgebildet (Abbildung 4.29). Der Workflow sendet die Nachricht folglich über die Schnittstellenmethode Entscheiden an den Manager. public delegate void UpdateHandlerEntscheiden(string message); public void Entscheiden(string message) { if (this.InvokeRequired) { UpdateHandlerEntscheiden handler = new UpdateHandlerEntscheiden(Entscheiden); this.Invoke(handler, message); } else { lblAnfrage.Text = message; } } Listing 4.27: Workflow sendet über die Methode Entscheiden Daten an das Formular.
Das Schnittstellenmember Entscheiden wiederum bedient sich eines Delegaten mit dem Namen UpdateHandlerEntscheiden, der in der Klasse Formular schon vorher mit der Signatur der Methode Entscheiden deklariert wurde. Denn der Aufruf dieser Methode
293
Kapitel 4
durch den Workflow erfolgt aus einem anderen Thread als der UI-Thread und muss deshalb mittels eines Delegaten über die Formklasse selbst aufgerufen werden.
Entscheidung des Managers Der Manager wiederum auf der anderen Seite entscheidet sich entweder für die Bestätigung oder die Ablehnung des Antrags. Beide Ereignisse feuern dabei das Schnittstellenevent EntscheidungGetroffen, um den Workflow zu benachrichtigen, und übergeben in diesem Zusammenhang eine mit den entsprechenden Daten versehene Instanz der Klasse EntscheidungsEventArgs als Parameter: public event EventHandler EntscheidungGetroffen; private void btnBestaetigen_Click(object sender, EventArgs e) { Entscheidung managerEntscheidung = new Entscheidung(); managerEntscheidung.AntragBestaetigt = true; managerEntscheidung.Begruendung = txtManager.Text; EntscheidungEventArgs entscheidungsArgs = new EntscheidungEventArgs(mWorkflowId, managerEntscheidung); if (EntscheidungGetroffen != null) { this.EntscheidungGetroffen(null, entscheidungsArgs); } } private void btnAblehnen_Click(object sender, EventArgs e) { Entscheidung managerEntscheidung = new Entscheidung(); managerEntscheidung.AntragBestaetigt = false; managerEntscheidung.Begruendung = txtManager.Text; EntscheidungEventArgs entscheidungsArgs = new EntscheidungEventArgs(mWorkflowId, managerEntscheidung); if (EntscheidungGetroffen != null) { this.EntscheidungGetroffen(null, entscheidungsArgs); } } Listing 4.28: Entscheidungsmöglichkeiten des Managers in Formular.cs
294
Windows Workflow Foundation
Benachrichtigung des Antragstellers Nun ist der Workflow wieder an der Reihe, der bis zu diesem Zeitpunkt auf ein Ereignis des Typs EntscheidungGetroffen gewartet hat. Dieser wertet die Daten aus und ruft vor dessen Beendigung noch die Methode Antworten der implementierten Schnittstelle IWorkflowHostKommunikation mit den Antwortdaten als Methodenparameter auf. public delegate void UpdateHandlerAntworten(Entscheidung managerEntscheidung); public void Antworten(Entscheidung managerEntscheidung) { if (this.InvokeRequired) { UpdateHandlerAntworten handler = new UpdateHandlerAntworten(Antworten); this.Invoke(handler, managerEntscheidung); } else { switch (managerEntscheidung.AntragBestaetigt) { case true: lblAntwort.Text = "Antrag gestattet.\n"; break; case false: lblAntwort.Text = "Antrag abgelehnt.\n"; break; default: break; } lblAntwort.Text += managerEntscheidung.Begruendung; } } Listing 4.29: Mitarbeiter erhält die Antwort über die Methode Antworten in Formular.cs.
4.5.3 Erstellung der Workflow-Bibliothek Um die Informationen des Urlaubsantragsprozesses auf der Seite des Workflows an die jeweils passenden Empfänger zu senden, müssen wir spezielle Aktivitäten in den Ablauf mit einbinden, welche die Methoden und Ereignisse auf Basis der Schnittstellenkonfiguration verarbeiten können. Der Nachrichtenfluss von einer WorkflowAnwendung in Richtung der Host-Applikation wird dabei durch den Einsatz einer CallExternalMethodActivity in Form von Schnittstellenmethodenaufrufen realisiert. Kehren wir zurück zu unserem Szenario, sehen wir an erster Stelle des sequenziellen Verlaufs eine solche Aktivität mit der Bezeichnung cemaEntscheiden.
295
Kapitel 4
Abbildung 4.32: Designeransicht von SequentialWorkflowUrlaubsantrag
Der Workflow ruft also zuallererst die Schnittstellenmethode Entscheiden von der Workflow-Applikation aus auf. Mit diesem Baustein übermitteln wir die vom Urlaubsantragsteller eingegebenen Daten an seinen Vorgesetzten. Wie Sie noch wissen, haben wir für diese Transaktion die Methode Entscheiden in dem Interface IWorkflowHostKommunikation angelegt, der wir einen String als Mitteilung übergeben müssen. Geben wir diese Daten in den Eigenschaften an, erhalten wir automatisch neue Platzhalter für die Angabe der Parameterquelle. Hier wählen wir wiederum die öffentliche Eigenschaft Anfrage, die wir vorher auch in der Workflow-Klasse als Datentyp String deklarieren müssen, um den beim Start der Workflow-Instanz übergebenen Parameter speichern zu können: using using using using using using using using using using
296
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime;
Windows Workflow Foundation using System.Workflow.Activities; using System.Workflow.Activities.Rules; using System.Windows.Forms; namespace WorkflowUrlaubsantrag { public sealed partial class SequentialWorkflowUrlaubsantrag: SequentialWorkflowActivity { public SequentialWorkflowUrlaubsantrag() { InitializeComponent(); } private string mAnfrage; public string Anfrage { get { return mAnfrage; } set { mAnfrage = value; } } //restlicher Programmcode ausgeblendet } Listing 4.30: Öffentliche Eigenschaft Anfrage, um den Urlaubsantrag entgegenzunehmen
Den weiteren Verlauf kennen wir ja schon. Der Manager übernimmt die Aufgabe und fällt die Entscheidung, indem er den Antrag bestätigt oder nicht, was in dem hinterlegten Programmcode zum Auslösen des Events EntscheidungGetroffen führt. Und genau auf dieses Ereignis zu horchen haben wir dem Workflow befohlen, indem wir als nächste Aktivität eine HandleExternalEventActivity gesetzt haben, die wir gemäß Abbildung 4.33 entsprechend konfiguriert haben. Um die mitgelieferten Daten dieses Ereignisses speichern zu können, legen wir wiederum eine Property, dieses Mal jedoch vom Typ EntscheidungEventArgs und mit dem Namen EntscheidungsArgs in der Codeansicht der Workflow-Klasse an (Listing 4.31). public sealed partial class SequentialWorkflowUrlaubsantrag: SequentialWorkflowActivity { private EntscheidungEventArgs mEntscheidungsArgs; public EntscheidungEventArgs EntscheidungsArgs { get { return mEntscheidungsArgs; } set { mEntscheidungsArgs = value; } } //restlicher Programmcode ausgeblendet } Listing 4.31: Öffentliche Eigenschaft zur Speicherung der Managerentscheidung
297
Kapitel 4
Abbildung 4.33: Eigenschaften der handleExternalEventActivity heeaEntscheidungGetroffen
An dieser Stelle haben wir also die Entscheidung des Vorgesetzten und eine eventuelle Begründung in der Workflow-Instanz in der Eigenschaft EntscheidungsArgs gespeichert. Der letzte Schritt besteht jetzt darin, den Antragsteller über das Urteil zu benachrichtigen. Auch das erledigen wir wiederum mit einer CallExternalMethodActivity, in der wir das Schnittstellenmember angeben (Abbildung 4.34).
Abbildung 4.34: Eigenschaften der CallExternalMethodActivity cemaAntworten
298
Windows Workflow Foundation
Zwischen den letzten beiden Schritten habe ich noch eine IfElseActivity mit je einer integrierten CodeActivity einfließen lassen. Wenn die Eigenschaft EntscheidungsArgs.Ent.AntragBestaetigt den Wert True besitzt, wird in den linken Ast gesprungen, ansonsten in den rechten. Die in den Verzweigungen enthaltenen CodeActivities beinhalten jedoch keine Logik. Vielmehr wollte ich damit andeuten, dass an diesem Punkt des Workflows je nach Entscheidung weitere Workflow-Stufen eingebaut werden könnten, indem die Daten erst einmal ausgewertet und persistiert werden, bevor Sie weitergeleitet werden. Eine Übersicht über die Workflow-Klasse WFUrlaubsantrag sehen Sie in Listing 4.32. using using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules; System.Windows.Forms;
namespace WorkflowUrlaubsantrag { public sealed partial class SequentialWorkflowUrlaubsantrag: SequentialWorkflowActivity { public SequentialWorkflowUrlaubsantrag() { InitializeComponent(); } private string mAnfrage; public string Anfrage { get { return mAnfrage; } set { mAnfrage = value; } } private EntscheidungEventArgs mEntscheidungsArgs; public EntscheidungEventArgs EntscheidungsArgs { get { return mEntscheidungsArgs; } set { mEntscheidungsArgs = value; } }
299
Kapitel 4 private void caBestaetigt_ExecuteCode(object sender, EventArgs e) { //Hier könnten noch weitere Schritte erfolgen } private void caAbgelehnt_ExecuteCode(object sender, EventArgs e) { //Hier könnten noch weitere Schritte erfolgen } } } Listing 4.32: Komplette Codeansicht des Workflows
Abbildung 4.35: Workflow-Übersicht im Designer
Bedenken Sie nach wie vor, dass dieses Beispiel nur zu Anschauungszwecken in Bezug auf die Kommunikationstechniken dient und in dieser Art wohl kaum in einer Real World-Applikation Anwendung zu finden sein wird. Für eine brauchbare realistische Anwendung würde man wohl den Workflow in einem Windows-Dienst hosten oder ihn direkt als Web Service anbieten, was uns unmittelbar zum nächsten Kapitel bringt.
300
Windows Workflow Foundation
4.6 Workflow und Web Services Mit diesem Kapitel begeben wir uns zu einem etwas fortschrittlicheren Thema. Denn alle bisher vorgestellten Beispiele dienten der detaillierten Darstellung der einzelnen Möglichkeiten, die WF bietet, und könnten nur durch ein paar Modifikationen in einer wirklich verteilten Netzwerk-Architektur ausgeführt werden. Die beteiligten Personen müssten sich schon im selben Raum befinden, um einen Workflow erfolgreich durchführen zu können. Doch Spaß beiseite, um einen realistischen Workflow mit mehreren beteiligten Personen entwickeln zu können, müssen wir die WorkflowRuntime zentral auf einem Server hosten, der für mehrere Clients von außen erreichbar ist. Eine Möglichkeit für die Realisierung einer verteilten Workflow-Anwendung bietet in diesem Zusammenhang ein Web Service.
4.6.1 Workflow als Web Service publizieren Damit wir einen Workflow mit einem Klick als funktionierenden Web Service zur Verfügung stellen können, müssen wir einige Voraussetzungen für eine erfolgreiche Durchführung beachten: 1. Das zu veröffentlichende Projekt muss eine Bibliothek sein. 2. Die erste Aktivität ist vom Typ WebServiceInputActivity. 3. Die Eigenschaft IsActivating der ersten WebServiceInputActivity hat den Wert True. 4. Die im Workflow verwendeten Schnittstellenmethoden verwenden nur serialisierbare Datentypen als Parameter. Haben wir folgende Grundregeln beachtet, können wir uns mit der Definition der Schnittstelle auseinandersetzen, d.h., wir überlegen uns die Methoden, die von außen mittels eines Web Service-Aufrufes erreichbar sein sollen. Bei einem Web Service, der z.B. eine Formatierung eines Textes in ein bestimmtes Format anbietet, könnte diese Schnittstelle folgendermaßen aussehen: using System; using System.Collections.Generic; using System.Text; namespace WorkflowFormatieren { public interface IFormat { string FormatiereZuHTML(Formular antragsFormular); string FormatiereZuXML(Formular antragsFormular); string FormatiereZuCSV(Formular antragsFormular);
301
Kapitel 4 } [Serializable] public class Formular { private string mAbsender; public string Absender { get { return mAbsender; } set { mAbsender = value; } } private string mEmpfaenger; public string Empfaenger { get { return mEmpfaenger; } set { mEmpfaenger = value; } } } } Listing 4.33: Web Service-Schnittstelle und Formularklasse
Da wir nun die von außen erreichbaren Dienste festgelegt haben, können wir uns dem Workflow-Designer zuwenden. Auf diesen ziehen wir zuerst eine ListenActivity, denn wir wollen ja den Workflow durch mehrere Methoden aufrufen lassen können. In diese Aktivität hinein legen wir eine WebServiceInputActivity, CodeActivity und WebServiceOutputActivity und erhalten einen Designer wie in Abbildung 4.36. Was Sie in Abbildung 4.36 auch noch erkennen können, ist, dass die entsprechende Methode und das Interface selbst schon in den Eigenschaften unten rechts in dem Screenshot angegeben wurden. Außerdem müssen wir die Eigenschaft IsActivating auf True stellen, denn damit bezwecken wir, dass durch den Aufruf dieser Web ServiceMethode eine neue Workflow-Instanz gestartet wird. Doch das Kontextmenü des Smarttags der WebServiceInputActivity wsiaZuHTML zeigt uns noch einen Fehler an: Fehlende entsprechende WebServiceOutputActivity/WebServiceFaultActivity. Dieses kleine Problem haben wir aber schnell gelöst, indem wir bei der Eigenschaft InputActivityName der WebServiceOutputActivity wsoaHTML ihre Vorgänger-Web ServiceAktivität, hier wsiaZuHTML, angeben. Jetzt fehlen uns noch die übergebenen Parameter, in unserem Beispiel eine Klasse vom Typ Formular, die in Abbildung 4.36 auch schon in den Eigenschaften der WebServiceInputActivity auftaucht. Deshalb deklarieren wir in der Codeansicht von SequentialWorkflowFormatieren eine Property vom Typ Formular. Da der Rückgabewert der Web Service-Methode jedoch vom Typ String ist, benötigen wir noch eine zweite Variable, die im folgenden Listing 4.34 mit der Bezeichnung Output deklariert wird.
302
Windows Workflow Foundation
Abbildung 4.36: Designeransicht von WorkflowFormatieren
using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules;
namespace WorkflowFormatieren { public sealed partial class SequentialWorkflowFormatieren: SequentialWorkflowActivity { public SequentialWorkflowFormatieren() { InitializeComponent(); }
303
Kapitel 4 private Formular mFormular; public Formular Formular { get { return mFormular; } set { mFormular = value; } } private string mOutput; public string Output { get { return mOutput; } set { mOutput = value; } } private void caFormatieren_ExecuteCode(object sender, EventArgs e) { this.Output = String.Format( "
Absender | {0} |
" + "Empfänger | {1} |
", Formular.Absender, Formular.Empfaenger); } } } Listing 4.34: Codeansicht des Workflows SequentialWorkflowFormatieren
Selbstverständlich muss die Existenz dieser Variablen den Aktivitäten auch noch mitgeteilt werden. In Abbildung 4.36 sehen Sie, dass bei der Angabe des Schnittstellenmembers eine Eigenschaft im Eigenschaftsfenster mit der Bezeichnung antragsFormular hinzugekommen ist, welche die Instanz der Klasse Formular darstellt und beim Aufruf des Web Service übergeben wird. Hier geben wir die eben definierte Eigenschaft Formular an. Da wir einen String als Rückgabewert spezifiziert haben, markieren wir im Designer die WebServiceOutputActivity und geben als Wert Output an, dessen String wir in der Ausführung der dazwischenliegenden CodeActivity caFormatieren aus den Werten des übergebenen Formulars in HTML-Schreibweise zusammenbauen. Zu diesem Zeitpunkt dürften bis auf die eventDrivenActivity2 alle rot umkreisten weißen Ausrufezeichen verschwunden sein! Um die restlichen Schritte bei der Realisierung der anderen beiden Formatierungen XML und CSV so einfach wie möglich zu gestalten, markieren wir edaZuHTML, kopieren diese durch einen rechten Mausklick auf kopieren und fügen das Dreiergespann als Parallelzweig ein, indem wir die ListenActivity laFormatieren fokussieren und wiederum mit der rechten Maustaste auf EINFÜGEN verweisen. Wiederholen wir die letzten beiden Schritte noch einmal für die CSV-Methode und löschen die leer stehende eventDrivenActivity2, erhalten wir nach entsprechender Umbe-
304
Windows Workflow Foundation
nennung ein Dreiergespann von Web Service-Aktivitäten wie in Abbildung 4.37, dessen Methoden und Parameter es auf den speziellen Anwendungsfall anzupassen gilt.
Abbildung 4.37: Designeransicht des fertigen Workflows SequentialWorkflowFormatieren
Nachdem Sie die Korrektur der Methoden und Parameterwerte vorgenommen haben, können Sie nun den Workflow als Web Service veröffentlichen. Wählen Sie den Menüpunkt PROJEKT, wählen dort ALS WEBDIENST und warten, bis Sie eine Meldung wie in Abbildung 4.38 erhalten.
VERÖFFENTLICHEN
Abbildung 4.38: Meldung eines erfolgreichen Web Service Exports
305
Kapitel 4
Abbildung 4.39: Neu angelegtes Web Service-Projekt
Visual Studio 2005 legt nun ein neues Web Service-Projekt in der Projektmappe mit der Bezeichnung der Workflow-Bibliothek plus _WebService an. Eine Sache ändern wir jedoch noch: Setzen Sie in Eigenschaften des Web Service-Projekts Dynamische Anschlüsse auf false. Somit bleibt der anfangs eingestellte Port erhalten und bereitet uns nicht wie bei einem dynamischen Portwechsel irgendwelche Probleme. Legen wir in den Eigenschaften der Projektmappe das Web Service-Projekt als Startprojekt fest und starten die Anwendung, sehen wir die gewohnte HTML-Ausgabe im Browser mit der Angabe der enthaltenen Web Service-Methoden. Leider können wir den Service hier im Browser nicht sofort testen, da jede Methode einen Parameter vom Typ Formular erwartet. Deshalb fügen wir schnell ein neues Projekt vom Vorlagentyp KONSOLENANWENDUNG FÜR SEQUENZIELLE WORKFLOWS zu unserer Projektmappe hinzu, um unseren Workflow Web Service mit einer InvokeWebServiceActivity zu testen.
4.6.2 Web Service mit InvokeWebServiceActivity konsumieren In dieser eben hinzugefügten Workflow-Konsolenapplikation benutze ich eine CodeActivity, um die Daten vom Anwender für das Formular entgegenzunehmen, und eine weitere Aktivität des gleichen Typs, um den formatierten Formularinhalt auf der Konsole auszugeben. Dazwischen schalten wir nun eine InvokeWebServiceActivity, mittels der wir den Web Service aufrufen, das instanzierte Formular übergeben, den Output in einer Stringvariablen speichern und zuletzt wieder auf der Konsole ausgeben.
306
Windows Workflow Foundation
Abbildung 4.40: SequentialWorkflowFormatieren steht jetzt als Web Service zur Verfügung.
> >
>
HINWEIS
Beachten Sie, dass vor dem nächsten Schritt das zuvor erstellte Web Service-Projekt gestartet ist, da Visual Studio sonst keine Proxyklasse erstellen kann.
Ziehen wir eine Aktivität vom Typ InvokeWebServiceActivity auf den Designer, öffnet sich unmittelbar ein Assistent, dem wir die Lokalität unseres Dienstes in Form einer URL übergeben, und klicken auf GEHE ZU (Abbildung 4.41). Nun geben wir noch einen Webverweisnamen an, fügen den Verweis über die Schaltfläche VERWEIS HINZUFÜGEN hinzu und können den automatisch erzeugten Proxy nun in unserer Konsolenapplikation nutzen. Da wir den Verweis auf unseren Web Service gerade gesetzt haben, können wir die darin enthaltenen Methoden in unserer Konsolenanwendung bzw. in unserem Workflow nutzen. Da die Adresse und die Proxyklasse in den Eigenschaften unserer InvokeWebServiceActivity schon spezifiziert sind, müssen wir nur noch die aufzurufende Methode sowie die Variablen für die Übergabe- und Rückgabeparameter angeben.
307
Kapitel 4
Abbildung 4.41: Webverweis auf unseren Workflow Web Service hinzufügen
Abbildung 4.42: Eigenschaften der InvokeWebServiceActivity
Denn geben wir z.B. als Web Service-Methode FormatiereZuHTML an, müssen wir natürlich eine Instanz der Klasse Formular übergeben und auch den Rückgabewert in einer Stringvariablen speichern. Deshalb legen wir für Zeile 2 und 3 in Abbildung 4.42
308
Windows Workflow Foundation
eine Property in der Workflow-Klasse mit dem jeweils passenden Datentyp an und implementieren zugleich auch die Ausführungslogik der vor- und nachgeschalteten CodeActivity, deren Ausführungsmethode wir je durch einen Doppelklick auf die jeweilige Aktivität im Designer automatisch anlegen: using using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules; WorkflowKonsument.ProxyFormatieren;
namespace WorkflowKonsument { public sealed partial class WFKonsument:SequentialWorkflowActivity { public WFKonsument() { InitializeComponent(); this.Daten = new ProxyFormatieren.Formular(); } private ProxyFormatieren.Formular mDaten; public ProxyFormatieren.Formular Daten { get { return mDaten; } set { mDaten = value; } } private string mErgebnis; public string Ergebnis { get { return mErgebnis; } set { mErgebnis = value; } } private void caInput_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Empfänger:"); Daten.Absender = Console.ReadLine(); Console.WriteLine("Absender:");
309
Kapitel 4 Daten.Empfaenger = Console.ReadLine(); Console.WriteLine("Vielen Dank."); } private void caOutput_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("\nHier der Web Service HTML-Output:"); Console.WriteLine(Ergebnis.ToString()); Console.ReadLine(); } } } Listing 4.35: Codeansicht der WFKonsument-Klasse
Die fertige Designeransicht sehen Sie dabei in Abbildung 4.43.
Abbildung 4.43: Konsolenanwendung mit integrierter InvokeWebServiceActivity
Bevor Sie das Projekt nun als Startprojekt festlegen, achten Sie unbedingt darauf, dass im Infobereich Ihrer rechten unteren Bildschirmecke das ASP.NET DEVELOPMENT SERVER-Icon noch vorhanden ist(Abbildung 4.44).
310
Windows Workflow Foundation
Abbildung 4.44: ASP.NET Development Server-Icon
Falls nicht, geben Sie in den Eigenschaften der Projektmappe unter den ALLGEMEINEN EIGENSCHAFTEN als Startprojekt mehrere Projekte an. Einmal das WorkflowFormatieren_ WebService-Projekt und als zweites Projekt die soeben erstellte Konsolenapplikation.
Abbildung 4.45: Konsolenoutput nach der Verarbeitung durch den Web Service
4.6.3 Mehrere Web Service-Aufrufe innerhalb eines Workflows Angenommen, Sie möchten einen Workflow realisieren, in dem der Ablauf über Web Service-Schnittstellen realisiert werden muss, z.B. soll ein Formular einer Web ServiceMethode als String-Parameter übergeben werden. Daraufhin soll der gestartete Workflow warten, bis ein anderer Web Service-Aufruf das Dokument bestätigt und anschließend wieder mit seiner Ausführungslogik fortfahren. Hoffentlich fragen Sie sich jetzt: Aber woher weiß die WorkflowRuntime, an welche Workflow-Instanz sie die jeweiligen Web Service-Aufrufe weiterleiten soll? Dazu muss man wissen, dass die WorkflowRuntime mit Cookies arbeitet und diese die Workflow-Instanz über den Cookiecontainer der Web Service Proxyklasse ermittelt. Hier schauen wir uns am besten wieder ein kleines Beispiel an. Wir werden eine Workflow-Bibliothek als Web Service veröffentlichen und diese anschließend von einem Konsolenclient aus konsumieren (Abbildung 4.46). Fangen wir zuerst mit der Workflow-Bibliothek an und erstellen ein neues Projekt mit KONSOLENANWENDUNG FÜR SEQUENZIELLE WORKFLOWS als Vorlage. Diesem fügen wir eine Schnittstelle mit der Bezeichnung IWebServiceInterface hinzu und definieren in diesem die Methoden, durch deren Aufruf der Client die Möglichkeit erhalten soll, gezielt in den Workflow eingreifen zu können. Als Schnittstellenmember habe ich mir zwei Methoden mit dem Namen Registrieren und Bestaetigen überlegt.
311
Kapitel 4
Abbildung 4.46: Hintereinandergeschaltete WebServiceInput-Aktivitäten
using System; using System.Collections.Generic; using System.Text; namespace WorkflowWebServiceBibliothek { public interface IWebServiceInterface { Guid Registrieren(string benutzerName); string Bestaetigen(); } } Listing 4.36: Web Service-Schnittstellenmethoden
> >
>
HINWEIS
Wir hätten als Parameter auch komplexe, eigens definierte Datentypen verwenden können, aus Platzgründen haben wir jedoch darauf verzichtet. Falls Sie dies jedoch anstreben, z.B. in Form einer Klasse Formular, achten Sie darauf, dass die Klasse mit dem Attribut [Serializable] ausgestattet ist.
312
Windows Workflow Foundation
Da wir sowohl bei Registrieren als auch bei Bestaetigen einen Wert zurückgeben müssen, benötigen wir für beide Aufrufe eine WebServiceInput- und eine WebServiceOutput-Aktivität (Abbildung 4.46). Zwischen dem Aufruf und der Antwort der ersten Methode habe ich noch eine CodeActivity dazwischengeschaltet, die dafür sorgt, dass der Client die WorkflowInstanceId erhält. Denn die WorkflowRuntime kann dem Webdienst-Aufruf die entsprechende Workflow-Instanz nur zuordnen, wenn der Client die GUID der Instanz dem CookieContainer des Proxys hinzufügt.
*
*
*
TIPP
Wir könnten uns am Client auch eine spezielle Workflow-ID herauspicken, indem wir die Workflow-Datenbank befragen, die für die Persistierung verwendet wird. Hätten wir eine Persistenzdatenbank mit zusätzlichen informativen Spalten erweitert, würden wir etwas Licht in die Workflow Blackbox bringen und hätten die Möglichkeit, dem Anwender etwas mehr als die bloße GUID des Workflows anzubieten. using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules;
namespace WorkflowWebServiceBibliothek { public sealed partial class SequentialWorkflowWebService: SequentialWorkflowActivity { public SequentialWorkflowWebService() { InitializeComponent(); } private string mBenutzerName; public string BenutzerName { get { return mBenutzerName; } set { mBenutzerName = value; } } private Guid mInstanceID;
313
Kapitel 4 public Guid InstanceID { get { return mInstanceID; } set { mInstanceID = value; } } private void caErmittleGUID_ExecuteCode(object sender, EventArgs e) { InstanceID = this.WorkflowInstanceId; } } } Listing 4.37: Codeansicht von SequentialWorkflowWebService.cs
Die Werte der übergebenen Web Service-Parameter speichern wir natürlich wieder in den Properties der Workflow-Klasse. In BenutzerName nehmen wir den Parameter der Methode Registrieren entgegen, den wir beim zweiten Web Service-Aufruf dem Aufrufer wieder zurückgeben. Erhalten wir zuallerletzt den gleichen Wert, den wir zuvor eingegeben haben, können wir sichergehen, dass es sich um dieselbe Workflow-Instanz handelt.
!
!
!
ACHTUNG
Achten Sie wieder darauf, bei der ersten Aktivität die Eigenschaft IsActivating auf True umzustellen!
Wie man die Eigenschaften einer WebServiceInput- und WebServiceOutput-Aktivität konfiguriert, haben wir schon in den Kapiteln 4.6.1 und 4.6.2 besprochen. Deshalb gehen wir sofort zum nächsten Schritt über und publizieren unsere Bibliothek als Web Service über den Menüpunkt PROJEKT/ALS WEBDIENST VERÖFFENTLICHEN. Wir erhalten wieder ein Web Service-Projekt, dessen web.config schon mit den notwendigen Workflow-Diensten ausgestattet ist:
314
Windows Workflow Foundation Listing 4.38: Auszug aus der web.config des Workflow Web Service-Projektes
*
*
*
TIPP
Falls es sich um einen länger andauernden Workflow handelt, sollte man einen Persistenz-Service hinzufügen und die Eigenschaft UseActiveTimers mit dem Wert True dem ManualSchedulerService hinzufügen. Denn UseActiveTimers sorgt dafür, dass der Timermechanismus in einen zusätzlichen Thread ausgelagert wird und somit der Timer auch nach dem aktuellen HttpRequest aktiv bleibt. Listing 4.39: Eigenschaft UseActiveTimers in der web.config des Workflow Web Service
Jetzt können Sie das Web Service-Projekt für einen ersten Testlauf starten, indem Sie unter den Projektmappeneigenschaften das aktuelle Projekt als Startprojekt festlegen und (F5) drücken. Kopieren Sie sich dabei auch die URL aus der Adressleiste Ihres Internet-Browsers mit (Strg) + (C) in die Zwischenablage, denn diese benötigen wir später in unserer Konsolenapplikation. Um den Ablauf nun zu testen, fügen wir ein neues Projekt vom Typ WINDOWS-KONSOzu unserer Projektmappe hinzu. In diesem benötigen wir einen Webverweis auf unseren Workflow, dessen Location wir noch in der Zwischenablage haben.
LENANWENDUNG
Wir betätigen also nochmals wie in dem vorigen Kapitel auch den Menüpunkt PROJEKT/ WEBVERWEISE HINZUFÜGEN und fügen die kopierte Adresse mit (Strg) + (V) in die Adressleiste des aktuellen Fensters ein (Abbildung 4.47). Vergeben Sie nach einem Klick auf GEHE ZU noch einen aussagekräftigen Namen als Webverweis und fügen die Proxyklasse mit VERWEIS HINZUFÜGEN dem Projekt hinzu.
> >
>
HINWEIS
Nachdem Sie den Verweis hinzugefügt haben, finden Sie auch eine Datei mit dem Namen app.config in Ihrem Projekt, wo Sie die Adresse falls notwendig nochmals bearbeiten können (siehe Listing 4.40).
315
Kapitel 4
Abbildung 4.47: Webverweis in der Konsolenapplikation hinzufügen
http://localhost:1081/WorkflowWebServiceBibliothek_ WebService/ WorkflowWebServiceBibliothek.SequentialWorkflowWebService _WebService.asmx
316
Windows Workflow Foundation
Listing 4.40: Automatisch erstellte app.config nach Hinzufügen des Webverweises
Jetzt können wir den Web Service von unserer Program.cs aus ansprechen, indem wir die Proxyklasse instanzieren und deren gewünschte Methoden aufrufen. Die Proxyklasse dient sozusagen als Fernbedienung, mit der Sie den Aufruf der Web ServiceMethoden fernsteuern können. using using using using using
System; System.Collections.Generic; System.Text; System.Net; WorkflowKonsument.ProxyRegistrieren;
namespace WorkflowKonsument { class Program { static void Main(string[] args) { ProxyRegistrieren.SequentialWorkflowWebService_WebService proxy = new SequentialWorkflowWebService_WebService(); Console.WriteLine("Bitte den Namen eingeben:"); string name = Console.ReadLine(); Console.WriteLine("\nAufruf der Web Service Methode \"Registrieren\""); Guid workflowInstanz = proxy.Registrieren(name); Console.WriteLine("Rueckgabewert:" + workflowInstanz.ToString()); Console.WriteLine("\nBitte eine Taste drücken, um den" + " Workflow fortzusetzen ..."); Console.ReadLine(); Cookie workflowCookie = new Cookie( "WF_WorkflowInstanceId", workflowInstanz.ToString(), "/", "localhost"); proxy.CookieContainer = new System.Net.CookieContainer(); proxy.CookieContainer.Add(workflowCookie); Console.WriteLine("\nAufruf der Web Service Methode \"Bestaetigen\""); Console.WriteLine("Rueckgabewert:" + proxy.Bestaetigen());
317
Kapitel 4 Console.WriteLine("Taste druecken, um den Workflow zu beenden ..."); Console.ReadLine(); } } } Listing 4.41: Proxyklasse wird instanziert und ruft die Web Service-Methoden auf.
Zuerst fragen wir den Anwender nach dessen Namen und übergeben diesen der Methode Registrieren der Proxyklasse proxy als Parameter. Vor dem Aufruf der zweiten Methode Bestaetigen müssen wir jedoch der WorkflowRuntime die Instanz-ID mitteilen, dessen WebServiceInput-Aktivität diese ansteuern soll. Und das bewerkstelligen wir durch den Gebrauch des CookieContainer, der automatisch von der Proxyklasse zur Verfügung gestellt wird: Cookie workflowCookie = new Cookie( "WF_WorkflowInstanceId", workflowInstanz.ToString(), "/", "localhost"); proxy.CookieContainer = new System.Net.CookieContainer(); proxy.CookieContainer.Add(workflowCookie); Listing 4.42: Initialisierung des CookieContainers der Proxyklasse
Die Bezeichnung des Cookie-Namens ist dabei vorgegeben und lautet WF_WorkflowInstanceId. Testen wir das Projekt, sehen wir, dass die WorkflowRuntime die aktuelle Instanz wieder findet.
> >
>
HINWEIS
Vergewissern Sie sich vor dem Starten nochmals, dass der ASP.NET Development Server im Infobereich Ihres Monitors angezeigt wird und die Konsolenanwendung als alleiniges Startprojekt ausgewählt ist.
Abbildung 4.48: Konsolenanwendung kommuniziert mit dem Workflow Web Service.
318
Windows Workflow Foundation
4.7 Workflow und Markup Vielleicht haben Sie beim Hinzufügen neuer Elemente schon entdeckt, dass das Visual Studio 2005 zwei unterschiedliche Arten bei der Erstellung von Workflows zulässt: 1. Sequenzieller Workflow (Code) bzw. Statuscomputer-Workflow (Code) 2. Sequenzieller Workflow (mit getrenntem Code) bzw. Statuscomputer-Workflow (mit getrenntem Code) In den bisherigen Projekten ist dabei immer der erste Punkt zur Anwendung gekommen, bei der die Aktivitäten mittels einer Designercodedatei mit der Endung *.designer.cs deklariert, instanziert und auf der Oberfläche positioniert wurden. Die zweite Form bietet nun die Möglichkeit, einen Arbeitsablauf in einer XML-Datei mit der Dateinamenserweiterung *.xoml zu beschreiben. Anstatt der zusätzlichen Designerdatei finden wir jetzt eine XOML-Datei in unserem Projekt, in der die Aktivitäten nicht in gewohnter Programmierform hinzugefügt werden, sondern durch die Anordnung und Schachtelung vordefinierter XML-Tags. Ein kurzes Beispiel soll auch hier deren Verwendung verdeutlichen. Erstellen Sie dazu im Visual Studio 2005 ein neues Workflow-Projekt vom Typ KONSOLENANWENDUNG FÜR SEQUENZIELLE WORKFLOWS.
Abbildung 4.49: XOML-Workflow im Visual Studio
319
Kapitel 4
In Abbildung 4.49 sehen Sie, dass ich zusätzlich über PROJEKT/NEUES ELEMENT HINZUFÜGEN einen SEQUENZIELLEN WORKFLOW (MIT GETRENNTEM CODE) mit der Bezeichnung MarkupWorkflow.xoml hinzugefügt und eine CodeActivity auf den Designer gezogen habe, in welcher der Anwender über den Workflow-Typ benachrichtigt wird (Listing 4.43). using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules;
namespace MarkupWorkflow { public partial class MarkupWorkflow : SequentialWorkflowActivity { private void caWorkflowTyp_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Hallo! Hier spricht der MarkupWorkflow!"); } } } Listing 4.43: Codegerüst von MarkupWorkflow.xoml.cs
Bis auf die Anpassung der Konsolenausgabe vervollständigen wir in derselben Weise unseren CodeWorkflow.cs (Listing 4.44). using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules;
namespace MarkupWorkflow {
320
Windows Workflow Foundation public sealed partial class CodeWorkflow: SequentialWorkflowActivity { public CodeWorkflow() { InitializeComponent(); } private void caWorkflowTyp_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Hallo! Hier spricht der CodeWorkflow!"); } } } Listing 4.44: Codegerüst von CodeWorkflow.cs
Jetzt fehlt nur noch die Anpassung von Program.cs, um dem User die Wahl zu lassen, welchen Workflow-Typ er gerne gestartet hätte (Listing 4.45). using using using using using using
System; System.Collections.Generic; System.Text; System.Threading; System.Workflow.Runtime; System.Workflow.Runtime.Hosting;
namespace MarkupWorkflow { class Program { static void Main(string[] args) { using(WorkflowRuntime wfRuntime = new WorkflowRuntime()) { Console.WriteLine("Wollen Sie den Markup [0] oder CodeWorkflow [1] " "starten?"); int antwort = Convert.ToInt32(Console.ReadLine()); if (antwort == 0) { WorkflowInstance instance = wf.CreateWorkflow(typeof(MarkupWorkflow)); instance.Start(); } else { WorkflowInstance instance = wfRuntime.CreateWorkflow(typeof(CodeWorkflow)); instance.Start();
321
Kapitel 4 } Console.ReadLine(); } } } } Listing 4.45: Program.cs startet je nach Benutzereingabe einen speziellen Workflow-Typ.
Nach Start des Projektes mit (F5) haben Sie die Wahl, je nach Eingabe der 0 oder 1, entweder den MarkupWorkflow oder den CodeWorkflow zu starten. Die Anwendung einer bestimmten Darstellungsform eines Workflows ändert also nicht dessen Laufzeitverhalten oder die Vorgehensweise, wie diese Instanz über die WorkflowRuntime gestartet werden muss. XOML ist einfach nur eine andere Art der Objektinstanzierung, die im Zuge des .NET Frameworks 3.0 auch in der Windows Presentation Foundation, hier jedoch mit der Bezeichnung XAML, Einzug erhalten hat. Wie eine XOML-Datei aufgebaut ist, sehen Sie, wenn Sie eine solche Markup-Datei z.B. in einem Texteditor öffnen (Abbildung 4.50).
Abbildung 4.50: XOML-Ansicht in einem Texteditor
4.8 Eigene Aktivitäten Wie Sie in den bisherigen Kapiteln gesehen haben, gibt es in der Windows Workflow Foundation einige Aktivitäten out of the box, die Sie in Ihre Applikation einbinden und anpassen können. Diese Grundbausteine sind jedoch sehr allgemein gehalten und decken längst noch nicht alle Wünsche der Entwickler ab. Deshalb gibt es auch hier die Möglichkeit, von der Basisklasse Activity abzuleiten und durch Erweiterung mit zusätzlichen Membern seine eigens implementierte Funktionalität in einer Bibliothek zu kapseln. Da die Entwicklung eigener Aktivitäten schon zu den fortgeschritteneren, schwierigeren Themen von WF gehört und ihre Anwendung wohl eher bei darauf spezialisierten Entwicklern anzutreffen sein wird, werden Sie hier in diesem Abschnitt nur eine kleine Einsicht zu sehen bekommen, die sich auf die notwendigsten Schritte beschränkt.
322
Windows Workflow Foundation
4.8.1 Log-Aktivität Um die Entwicklung eigener Aktivitäten nachvollziehen zu können, erstellen wir in diesem Kapitel einen Baustein, der bestimmte Informationen in die Ereignisanzeige des Computers schreibt. Erstellen Sie dazu ein neues Projekt der Art WORKFLOW-AKTIVITÄTSBIBLIOTHEK im Visual Studio 2005, erhalten Sie eine Projektmappen-Exploreransicht wie in Abbildung 4.51.
Abbildung 4.51: Projektmappe nach Erstellung einer Workflow-Aktivitätsbibliothek
Wie Sie erkennen können, sind bei der Vorlage die notwendigen Verweise auf die Workflow Assemblies schon vorhanden, sodass wir direkt mit der Implementierung unserer Logik beginnen können. Benennen wir also Activity1.cs in LogAktivitaet.cs und wechseln mit der rechten Maustaste in die Codeansicht dieser Datei und beginnen mit dem Überschreiben der für uns wichtigsten Methode Execute.
*
*
*
TIPP
In Visual Studio 2005 erhalten Sie von IntelliSense automatisch ein Kontextmenü überschreibbarer Methoden der Basisklassen, wenn Sie die Zeile mit override beginnen und anschließend eine Leertaste drücken. Nach Drücken der Taste (¢_) wird die markierte Methode samt Signatur in die aktuelle Zeile eingefügt.
323
Kapitel 4
Denn wie uns der Name schon verrät, ist es diese Methode, die ausgeführt wird, wenn die Workflow-Instanz die Ausführung unserer Aktivität veranlasst. protected override ActivityExecutionStatus Execute(ActivityExecutionContext e xecutionContext) { EventLog.WriteEntry(this.WorkflowInstanceId.ToString(), Message, LogType); Console.WriteLine("Log geschrieben..."); return ActivityExecutionStatus.Closed; } Listing 4.46: Execute-Methode von LogAktivitaet
In der dritten Zeile von Listing 4.46 erfolgt dabei mit der Methode WriteEntry das Schreiben in die Ereignisanzeige, wobei wir die WorkflowInstanceId, eine Mitteilung und die Art der Anzeige als Variable übergeben. Vergessen Sie nicht, System. Diagnostics mithilfe der Using-Direktive einzubinden, um die Klassen für die Ereignisanzeige nutzen zu können. Als Rückgabewert müssen wir einen Eintrag aus der Enumeration ActivityExecutionStatus zurückgeben, damit die Workflow-Instanz weiß, ob die Ausführung der Aktivität beendet ist oder nicht bzw. ob sich irgendein Fehler ereignet hat. Kommen wir noch einmal auf die letzten beiden Parameter zurück, die der Methode WriteEntry übergeben wurden, denn es handelt sich hier um keine gewöhnlichen öffentlichen Variablen, sondern um DependencyProperties, deren Deklaration Sie in Listing 4.47 sehen. using using using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules; System.Diagnostics; System.Drawing.Drawing2D;
namespace LogAktivitaet { public partial class LogAktivitaet: SequenceActivity { public LogAktivitaet() {
324
Windows Workflow Foundation InitializeComponent(); } public string Message { get { return (string)GetValue(MessageProperty); } set { SetValue(MessageProperty, value); } }
public static readonly DependencyProperty MessageProperty = DependencyProperty.Register("Message", typeof(string), typeof(LogAktivitaet));
public EventLogEntryType LogType { get { return (EventLogEntryType)GetValue(LogTypeProperty); } set { SetValue(LogTypeProperty, value); } } public static readonly DependencyProperty LogTypeProperty = DependencyProperty.Register("LogType", typeof(EventLogEntryType), typeof(LogAktivitaet));
protected override ActivityExecutionStatus Execute (ActivityExecutionContext executionContext) { EventLog.WriteEntry(this.WorkflowInstanceId.ToString(), Message, LogType); Console.WriteLine("Log geschrieben..."); return ActivityExecutionStatus.Closed; } } } Listing 4.47: Selbst geschriebene Aktivität, die eine Meldung in die Ereignisanzeige schreibt
Aber keine Angst, denn auch hier bietet uns Visual Studio 2005 ein Codeschnipsel, durch dessen Gebrauch die Anwendung von DependencyProperties zum Kinderspiel wird. Schreiben Sie im Codeeditor propdp, betätigen Sie zweimal (ÿ), und springen Sie wiederum mit (ÿ) durch die farbig hinterlegten Codefelder. Aber warum brauchen wir eigentlich diese Art von Variablen? Nun ja, durch den Gebrauch von Variablen dieses Typs haben Sie im Bereich des .NET Frameworks 3.0 einige Vorteile auf Ihrer Seite. Denn wie Sie später im Eigenschaftsfenster von LogAktivitaet sehen werden, haben Sie anschließend auch in der Workflow-Designeransicht beim Setzen der Werte die Möglichkeit, diese von anderen Variablen im Workflow abhängig zu machen.
325
Kapitel 4
Die DependencyProperty MessageProperty übergibt in diesem Zusammenhang die zu protokollierende Nachricht, wohingegen LogTypeProperty eine Auswahl der Enumeration EventLogEntryType zur Verfügung stellt.
Abbildung 4.52: Designeransicht von LogAktivitaet
Somit haben wir unsere erste Aktivität erstellt und können diese nach der ersten fehlerfreien Kompilierung in anderen Projekten nutzen. Ein Beispiel hierzu sehen Sie im nächsten Kapitel mit dem Thema »Fehlerbehandlung in WF«.
> >
>
HINWEIS
Im Windows SDK unter der Rubrik WINDOWS WORKFLOW FOUNDATION SAMPLES/CUSTOM ACTIVITIES finden Sie ein Beispiel mit der Bezeichnung Basic Activity Designer Sample, das wertvolle Informationen über die Anpassung des Designerbildes einer Aktivität preisgibt.
4.9 Fehlerbehandlung in WF Ein weiteres nicht zu unterschätzendes Thema bildet die Fehlerbehandlung in WF. Natürlich können Sie wie gewohnt bei fehleranfälligen Codepassagen in einem Try-Catch-Block die geworfene Fehlermeldung entsprechend behandeln, doch bei dem Auftreten einer Exception, die sich nicht in der Execute-Methode abspielt, sondern in der Aktivität selbst, müssen Sie zu anderen Hilfsmitteln greifen. Nehmen wir das Beispielprojekt vom vorherigen Kapitel, in dem wir eine Aktivität selbst geschrieben haben, und fügen ein weiteres Projekt vom Typ KONSOLENANWENDUNG FÜR SEQUENZIELLE WORKFLOWS der Projektmappe hinzu (Abbildung 4.53). Den groben Verlauf sehen Sie in der Designeransicht in Abbildung 4.53. In caInput wird der Anwender aufgefordert, eine 1 oder eine 0 einzugeben. Je nach Eingabe wird darauf folgend entweder eine Ausnahme vom Typ ApplicationException oder Exception ausgelöst, die in der von der Workflow Foundation integrierten Fehlerbehandlung abgefangen wird. caOutput soll abschließend verdeutlichen, dass trotz des ausgelösten Fehlers die Workflow-Instanz mit ihrem gewohnten Ablauf fortfährt und schließlich ohne Absturz beendet wird.
326
Windows Workflow Foundation
Abbildung 4.53: Designeransicht des Workflow-Konsolenprojekts
Ziehen Sie also zwei Codeaktivitäten auf den Designer, und implementieren Sie deren Execute-Methode wie folgt: using using using using using using using using using using using using
System; System.ComponentModel; System.ComponentModel.Design; System.Collections; System.Drawing; System.Workflow.ComponentModel.Compiler; System.Workflow.ComponentModel.Serialization; System.Workflow.ComponentModel; System.Workflow.ComponentModel.Design; System.Workflow.Runtime; System.Workflow.Activities; System.Workflow.Activities.Rules;
namespace WorkflowKonsole { public sealed partial class WorkflowExceptions: SequentialWorkflowActivity { public WorkflowExceptions() {
327
Kapitel 4 InitializeComponent(); AppException = new ApplicationException("Das war eine ApplicationException."); Exc = new Exception("Das war eine normale Exception."); } private int mExceptionNumber; public int ExceptionNumber { get { return mExceptionNumber; } set { mExceptionNumber = value; } } private ApplicationException mAppException; public ApplicationException AppException { get { return mAppException; } set { mAppException = value; } } private Exception mExc; public Exception Exc { get { return mExc; } set { mExc = value; } } private void caInput_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Suchen Sie sich eine Exception aus:\n" + "[0] ApplicationException oder [1] normale Exception"); try { ExceptionNumber = Convert.ToInt32(Console.ReadLine()); } catch (Exception ex) { throw ex; } } private void caOutput_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Nach der Exception ..."); } } } Listing 4.48: Ausführungsmethoden der beiden Codeaktivitäten
328
Windows Workflow Foundation
In caInput_ExecuteCode wird der Anwender aufgefordert, entweder eine 0 oder eine 1 für die entsprechende Exception einzugeben. Kann der darauffolgende Input in der Integervariablen ExceptionNumber gespeichert werden, so wird in der nachfolgenden conditionedActivityGroup cagExceptions je nach Eingabe die entsprechende Ausnahme ausgelöst.
> >
>
HINWEIS
Die ConditionedActivityGroup ist eine Art Container, in dem man Regeln definieren muss, wann und wie oft die darin enthaltenen Aktivitäten ausgeführt werden sollen.
Um nun die entsprechende Ausnahme im Workflow auszulösen, ziehen wir eine ThrowActivity in die obere Zeile von cagExceptions und definieren die Regel, wann diese Aktivität feuern soll, indem wir im Eigenschaftsfenster der ThrowActivity als WhenCondition eine deklarative Regelbedingung angeben (Abbildung 4.54).
Abbildung 4.54: Editor für die Regel von ThrowActivity taApplicationException
Wir erzeugen uns also wie in Abbildung 4.54 ersichtlich eine neue Regel, in diesem Fall mit der Bezeichnung cApplicationException, und legen fest, dass die Aktivität ausgeführt werden soll, wenn der Input des Users in Form der Variable ExceptionNumber den Wert 0 hat.
329
Kapitel 4
Abbildung 4.55: Eigenschaften von taApplicationException
Im Eigenschaftsfenster von taApplicationException legen wir noch den Exception-Typ mit System.ApplicationException fest und binden die Eigenschaft Fault an die anfangs deklarierte und im Konstruktor instanzierte Variable AppException. Ähnlich gehen wir jetzt in den folgenden Schritten bei der zweiten ThrowActivity taException vor, die wir wiederum dem Container der ConditionedActivityGroup per Drag & Drop hinzufügen und deren Eigenschaften entsprechend anpassen (Abbildung 4.56). Natürlich fordern wir dieses Mal den Wert 1 von ExceptionNumber und eine zu werfende Ausnahme des Fault-Typs System.Exception in Gestalt der Variablen Exc (Abbildung 4.56). In Abbildung 4.57 sehen Sie in der Mitte das Kontextmenü der ConditionedActivityGroup mit FEHLERHANDLER ANZEIGEN als aktuelle Auswahl. Denn genau dieses benötigen wir, um festzulegen, was bei dem Auftreten eines Fehlers passieren soll.
> >
>
HINWEIS
Falls Sie diese Form der Fehlerbehandlung mit einem Try-Catch-Block vergleichen, dann stellt die Ansicht FEHLERHANDLER sozusagen den Catch-Abschnitt dar, in dem Sie gezielt auf bestimmte Ausnahmetypen reagieren können.
Um die auftretenden Ausnahmen abfangen zu können, ziehen wir in der Ansicht des Fault-Handlers eine Aktivität mit der Bezeichnung FaultHandler in das obere Fenster von cagExceptions und geben bei FaultType System.ApplicationException als zu behandelnde Exception an.
330
Windows Workflow Foundation
Abbildung 4.56: Eigenschaftsfenster von taException unten rechts
Abbildung 4.57: Kontextmenü einer ConditionedActivityGroup
331
Kapitel 4
Den gerade hinzugefügten FaultHandler markiert, können wir jetzt im unteren Abschnitt derselben Aktivität festlegen, welche Arbeitsschritte folgen sollen, falls der erste Fault-Handler fhaApplicationException zur Ausführung kommt. Erinnern Sie sich an die LogAktivität, die wir im vorherigen Kapitel erstellt haben? Jetzt wollen wir doch mal sehen, ob sie auch funktioniert, und ziehen diese in das untere Fenster.
> >
>
HINWEIS
Haben Sie Kapitel 4.8 noch nicht gelesen, können Sie genauso gut auch andere Aktivitäten in diesen Bereich ziehen.
Als zu protokollierende Nachricht binden wir die Eigenschaft Message der im Workflow deklarierten ApplicationException-Variablen AppException und wählen als LogType den Wert Error. Somit haben wir uns um die Ausnahme vom Typ ApplicationException gekümmert und können uns um die zweite Fehlerart einer allgemeinen Exception kümmern.
Abbildung 4.58: Ansicht der Fehlerbehandlung der ConditionedActivityGroup
332
Windows Workflow Foundation
Wie in Abbildung 4.58 schon angedeutet, fügen wir dazu einen weiteren FaultHandler hinzu, geben als Fehlertyp System.Exception an und konfigurieren wiederum mit Anpassung der Property Message die LogAktivitaet, die wir dazu wieder auf den Designer ziehen. Haben wir jetzt die Projektmappe fehlerfrei kompiliert und WorkflowKonsole als Startprojekt ausgewählt, ist es Zeit für einen Test, den wir durch Betätigen der Tastenkombination (Strg) + (F5) starten.
Abbildung 4.59: Konsolenausgabe mit der Wahl einer ApplicationException
Abbildung 4.60: Eintrag der LogAktivitaet in die Ereignisanzeige
4.10 Kompensationsvorgänge Aber Moment mal – was passiert eigentlich, wenn eine Ausnahme auftritt und die vorher durchgeführten Ereignisse oder nur eine bestimmte Aktion wieder rückgängig gemacht werden sollen? Nun, haben wir an die Ausnahme gedacht und einen Faulthandler für diesen Vorfall implementiert, so könnten wir z.B. mit einer CodeActivity dementsprechend reagieren. Viel schöner aber wäre es, wenn wir die eingebauten grafischen Mechanismen der Workflow Foundation nutzen, indem wir in solchen Situa-
333
Kapitel 4
tionen auf Activities zurückgreifen, die das Interface ICompensatableActivity implementieren, wie z.B. CompensatableSequenceActivity oder CompensatableTransactionScopeActivity. Falls Sie bei der zuletzt genannten Aktivität nur an eine Datenbanktransaktion denken, so muss ich Sie in diesem Kapitel leider enttäuschen. CompensatableTransactionScopeActivity und auch TransactionScopeActivity fassen zwar enthaltene Datenbankabfragen in einer Transaktion zusammenfassen, so wie Sie das schon bei der Verwendung des Namensraumes System.Transactions her kennen. Die CompensatableTransactionScopeActivity kann jedoch mehr als das und versorgt uns wie die CompensatableSequenceActivity mit einem Kompensationsblock, in dem wir der Workflow Runtime mitteilen können, was beim Eintreten eines späteren Fehlers rückgängig gemacht werden muss. Dazu jetzt aber ein einfaches Beispiel, in dem wir in einem sequenziellen Workflow eine Datei erzeugen und diese beim Auftreten eines darauffolgenden Fehlers anschließend wieder durch den Einsatz eines Kompensationsblocks einer CompensatableSequenceActivity löschen.
Abbildung 4.61: Designeransicht mit markierter CompensatableSequenceActivity
334
Windows Workflow Foundation
Abbildung 4.61 zeigt uns die Designeransicht von KompensationsWorfklow.cs. Die erste Aktivität saDateiSchreiben ist eine SequenceActivity, die eine CompensatableSequenceActivity csaSequenz beinhaltet. Eine CompensatableSequenceActivity kann mehrere Aktivitäten umfassen, in unserem Fall ist es jedoch nur eine CodeActivity caSchreibeDatei, in der wir programmatisch eine Datei im Anwendungsverzeichnis anlegen (Listing 4.49). private void caSchreibeDatei_ExecuteCode(object sender, EventArgs e) { StreamWriter sWriter = new StreamWriter("GeschriebeneDatei.txt", false); try { sWriter.Write("Ich bin noch da ...!"); sWriter.Flush(); sWriter.Close(); Console.WriteLine("Datei \"GeschriebeneDatei.txt\" geschrieben...\n"); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } Listing 4.49: Schreiben der Datei GeschriebeneDatei.txt auf die Festplatte
Vergessen Sie in Listing 4.49 jedoch nicht, zuvor den Namensraum System.IO mit einzubinden, in dem sich die Klasse StreamWriter zum Schreiben der Datei befindet. Weiter im Anschluss sehen Sie in Abbildung 4.61 die ThrowActivity taApplicationException, mit der wir eine Ausnahme vom Typ ApplicationException werfen, denn wir wollen ja aufgrund einer später aufgetretenen Ausnahme die zuvor erstellte Datei wieder von der Festplatte löschen. Doch wie bewerkstelligen wir das? Um diesem Problem begegnen zu können, kommt jetzt endlich der Kompensationshandler ins Spiel. O.k., falls Sie jetzt Einspruch erheben und behaupten, wir könnten natürlich auch wie in Kapitel 4.9 in dem FaultHandler der SequenzActivity oder des Workflows selbst die Datei mittels der Ausführungsmethode einer CodeActivity löschen, so haben Sie natürlich recht. Im Nachhinein werden Sie aber feststellen, dass es bedeutend übersichtlicher und auch wieder verwendbarer ist, wenn wir der Ausnahme mit einer CompensateActivity begegnen und somit die Problembewältigung im Designer versteckt hinterlegen. Dazu wählen wir im Kontextmenü der CompensatableSequenceActivity den Eintrag KOMPENSATIONSHANDLER ANZEIGEN und ziehen eine CodeActivity in diesen hinein (Abbildung 4.62).
335
Kapitel 4
Abbildung 4.62: Kompensationshandler von csaSequenz
Dieser Block soll nun genau dann ausgeführt werden, wenn eine Ausnahme auftritt und wir das Erstellen der Datei wieder rückgängig machen wollen (Listing 4.50). Momentan jedoch würde beim Ausführen der Workflow-Instanz nur eine ApplicationException ausgelöst werden und die zuvor gespeicherte Datei GeschriebeneDatei.txt wäre nach wie vor vorhanden, denn wie alle Aktivitäten braucht auch der Kompensationshandlers einen Anstoß, um ihn zur Ausführung zu bewegen. private void caLoescheDatei_ExecuteCode(object sender, EventArgs e) { try { File.Delete("GeschriebeneDatei.txt"); Console.WriteLine("Datei \"GeschriebeneDatei.txt\" wieder gelöscht...\n") ; } catch (Exception ex)
336
Windows Workflow Foundation { Console.WriteLine(ex.ToString()); } } Listing 4.50: Ausführungsmethode von caLoescheDatei
Abbildung 4.63: Fehlerhandleransicht von der SequenceActivity saDateiSchreiben
In Abbildung 4.63 sehen Sie schon des Rätsels Lösung mit der Bezeichnung compensateDateiSchreiben. Wählen Sie den Kontextmenüeintrag FEHLERHANDLER ANZEIGEN unserer SequenceActivity saDateiSchreiben, und implementieren Sie die Behandlung einer ApplicationException. Wie das funktioniert, haben Sie ja schon in Kapitel 4.9 gelernt, wo wir mit unserer selbst erstellten LogAktivität den Fehler in der Ereignisanzeige mitprotokolliert haben. In dieser Ausführung der FaultHandlerActivity erkennen Sie jedoch zwei Aktivitäten mit der Bezeichnung caKonsolenAusgabe und compensateDateiSchreiben. Die CompensateActivity ist dabei die zentrale Aktivität, zumal die CodeActivity nur dazu dient, den aktuellen Informationsstand auf der Konsole auszugeben (Listing 4.51). Mit der CompensateActivity können Sie jetzt steuern, welcher Kompensationsblock ausgeführt werden soll. Dazu hilft Ihnen auch Visual Studio
337
Kapitel 4
2005, indem es im Eigenschaftsfenster bei der Property TargetActivityName auch nur diejenigen Aktivitäten auflistet, die eine derartige Funktion bereitstellen. private void caKonsolenAusgabe_ExecuteCode(object sender, EventArgs e) { Console.WriteLine("Ausnahme vom Typ \"ApplicationException\" aufgetreten...\n"); } Listing 4.51: Codeblock von caKonsolenAusgabe
Starten wir jetzt das Beispiel mit (F5), werden Sie beobachten, dass der Kompensationsblock greift und somit die notwendigen Aufräumarbeiten durchführt.
Abbildung 4.64: Konsolenausgabe unmittelbar nach dem Start der Anwendung
4.11 Zusammenfassung Glauben Sie jetzt endlich, dass die Windows Workflow Foundation eine hervorragende Technologie ist? Nun ja, bei den ersten Kapiteln werden Sie die Projektbeispiele wohl noch mit kritischen Blicken verfolgt haben. Sie werden sich gefragt haben: »Warum soll ich diese Workflow-Engine benutzen, wenn ich das mit den bisherigen Möglichkeiten genauso realisieren kann?« Und wahrscheinlich werden Sie sogar schneller sein, weil Sie sich nicht in die neue Technologie einarbeiten müssen. Doch denken Sie einen Schritt weiter, z.B. an Kapitel 4.6, wo der Workflow mittels Web Services zur Verfügung gestellt wird und nun mehrere Personen an einem Ablauf beteiligt sind. Wie schon einmal anfangs erwähnt, bei kleineren Applikationen werden Sie mit Sicherheit recht haben, wenn Sie auf Ihre bisherige Weise der Programmierung beharren. Doch was passiert, wenn die Anwendungen weitreichender werden, sich eine Transaktion auf mehrere Anwender bezieht, die auf der Welt verteilt zu unterschiedlichen Zeitpunkten ihre Geschäfte regeln möchten? Und es nicht nur eine, sondern mehrer Hunderte oder sogar Tausende Transaktionen gleichzeitig gibt, die über mehrere Tage hinweg einen bestimmten Zustand aufrechterhalten müssen? Haben Sie dann auch an die Performance gedacht? Und an die Eskalation, haben Sie an einen derartigen Mechanismus gedacht, falls was schiefgeht?
338
Windows Workflow Foundation
Wenn Sie sich einmal in die Workflow Foundation eingearbeitet haben, kann ich Ihnen versichern, dass Sie bei keiner dieser Fragen nicht nur im Ansatz nachdenklich werden, denn um all das kümmert sich vornehmlich die Workflow Runtime, vorausgesetzt Sie haben diese mit den notwendigen Diensten ausgestattet. Das ist das Schöne an der Workflow Runtime. Man sagt ihr, welchen Geschäftsprozess Sie ausführen soll, gibt ihr womöglich noch ein paar Objekte mit und lässt sie mal machen. Ein eigenständiger Agent sozusagen, dem Sie befehlen, er soll mit bestimmten Personen in Interaktion treten und anschließend mit einer beliebigen Aufgabe fortfahren. Ist das nicht schön?! Dann haben Sie auch endlich wieder mehr Zeit für Sachen, die sich nicht automatisieren lassen oder die Sie nicht automatisiert haben wollen! Ja, auch ich bin mir der Tatsache bewusst, dass nicht alles automatisiert werden kann oder muss, dass nicht alles gut ist, was automatisiert ist, und menschliche Kommunikation noch eine der wichtigsten Geschäftsaktivitäten ist. Jedoch müssen wir auch erkennen, was ich anfangs in der Einleitung schon angesprochen habe. Unsere geschäftlichen Aktivitäten und alles, was damit zusammenhängt, werden nicht einfacher, sondern im Gegenteil. Als global tätiger Unternehmer ist der Druck der Konkurrenz groß, und was die Folgen davon sind, erfahren Sie ja mittlerweile vielleicht schon am eigenen Leib. Klar, für uns alle als Konsumenten eine hervorragende Sache, doch als Geschäftsinhaber bedeutet das entweder die Insolvenz oder der Übergang zu noch mehr Automatisierung, um noch ein paar Punkte mehr von der alltäglichen To-do-Liste streichen zu können und sich wieder verstärkt auf die wesentlichen Aktivitäten im Unternehmensumfeld zu konzentrieren. Und vergessen Sie dabei nicht, dass die Workflow Foundation noch am Anfang ihrer Karriere steht. Also auf geht’s, springen Sie auf den Zug, bevor er Ihnen zu schnell wird!
339
5
CardSpace
CardSpace ist zwar mit Blick auf die Klassenbibliotheken eine etwas kleinere Säule des .NET Frameworks 3.0, jedoch darf man dieses Gebiet nicht außer Acht lassen, vor allem dann, wenn es sich bei den zu entwickelnden Applikationen um über das Internet verteilte Anwendungen handelt, die besondere Sicherheitsmaßnahmen benötigen und nur speziellen Personen Zugang gewähren sollen. Wie Microsoft Passport versucht auch Windows CardSpace als dessen Nachfolger, die Identifikation im Internet für den Endanwender sicherer und leichter zu gestalten. Doch Windows CardSpace geht im Vergleich zu Microsoft Passport einen Schritt weiter, indem es keine proprietären Sicherheitsformate von Microsoft hervorbringt, sondern als eine Art Basisplattform auf der Seite des Clients eine Möglichkeit dafür bietet, die bisherigen im Internet eingesetzten Sicherheitsstandards und Technologien miteinander kommunizieren zu lassen.
5.1 Authentifizierung heute – ein Chaos? Für den Anwender gestaltet sich die Authentifizierung im Internet immer schwieriger. Immer mehr seitenspezifische Benutzer-Passwort-Kombinationen muss man sich merken, wobei als Benutzername einmal die E-Mail benutzt wird, das andere Mal ein frei gewählter Name oder vielleicht ein zusammengesetztes Wort aus dem ersten Buchstaben des Vornamens plus dem vollen Nachnamen?
Kapitel 5
Haben wir denn überhaupt noch eine Übersicht, bei welchen Anbietern und mit welchen E-Mail-Accounts wir uns schon registriert haben? Und geben wir vielleicht deshalb immer öfter dasselbe Passwort für unterschiedliche Webseiteninhalte an, um unsere möglichen Identitätskombinationen ein wenig einzuschränken? Und wem übergebe ich überhaupt meine sensiblen Daten, wie z.B. meine Kontonummer, oder die Kreditkartennummer. Sollte sich der Webseitenbetreiber mir gegenüber nicht ausweisen und umgekehrt? Sie sehen schon, dass damit auch der Identitätsklau im Internet für Hacker mit bösartigen Absichten immer leichter zu sein scheint. Einmal das Passwort eines Benutzers herausgefunden, kann er sich in die Webmail des Opfers einloggen und erhält somit Informationen über dessen besuchte Internetseiten, ja er kann sich sogar bei den meisten Websites das dort hinterlegte Passwort zuschicken lassen! Diesem Trend versucht Microsoft nun mit der Entwicklung eines einheitlichen MetaSicherheitsframeworks entgegenzuwirken, um die Authentifizierung für den Endbenutzer einfacher und sicherer zu gestalten und Phishing-Attacken und anderweitigen Identitätsbetrügereien ein Ende zu setzen.
5.2 Laws of Identity Ein solches Metasystem zu schaffen, ist natürlich nicht einfach, denke man nur an die unterschiedlichen Sicherheits- und Kommunikationstechnologien, die derzeit als vom Markt akzeptiert rund um das Internet ihre Anwendung finden oder in Zukunft eingesetzt werden. Microsoft war sich dieses Problems bewusst und hat sich vor der Implementierung Gedanken gemacht, welchen Ansprüchen ein solches System der Systeme überhaupt genügen muss, um in einem derartigen, heterogenen Technologieumfeld auf Dauer akzeptiert und angewandt zu werden. Entstanden sind dabei die sogenannten Laws of Identity, die auf der Website www.identityblog.com als Download angeboten werden und die notwendigen Eigenschaften eines erfolgreichen Identitätssystems beschreiben.
5.3 Erste Vorstellung von Windows CardSpace Wie macht sich nun CardSpace beim Endanwender bemerkbar? Nun, bei vorhandenem .NET Framework 3.0 finden Sie in der Systemsteuerung einen neuen Eintrag mit der Bezeichnung Windows CardSpace. Wählt man diese Rubrik mit einem Doppelklick, so öffnet sich Ihnen ein neuer geschützter Desktop, auf dem man seine digitalen Karten verwalten kann.
342
CardSpace
Abbildung 5.1: CardSpace-Desktop zur Verwaltung der digitalen Karten
Sie können sich diesen Desktop als virtuellen Geldbeutel vorstellen. Denn wie Sie sich im realen Leben gegenüber den unterschiedlichsten Institutionen mit einer Karte ausweisen können, so können Sie sich auf diese Weise auch im Internet mit den einzelnen Cards aus dem CardSpace-Portfolio identifizieren, indem Sie eine passende Karte auswählen. Nehmen wir an, Sie möchten sich bei Ihrer Bank in Ihrem Online-Depot einloggen, das auch eine CardSpace-Anmeldung akzeptiert. Statt nun wie bisher Ihre Bankdaten in die vorhandenen Formularfelder einzutippen, zücken Sie nun eine von Ihren Karten, um in Ihren geschützten Online-Bereich einzutreten, und sind angemeldet. Aber welchen Zweck oder welche Vorteile hat eine derartige Form der Authentifizierung gegenüber der bisherigen? Nun, dem Endanwender fällt wahrscheinlich als erster Punkt auf, dass mit dieser Anmeldungsform das Eintippen der Benutzer- und Passwortdaten entfällt, doch um die entscheidenden Vorteile dieses Systems zu erkennen, werden wir uns in den nächsten Kapiteln mit dem Grundaufbau von CardSpace und den dahinter eingesetzten Technologien auseinandersetzen.
343
Kapitel 5
5.4 Rollen im Authentifizierungsprozess Um den Vorgang der Authentifizierung besser verstehen zu können, weisen wir den einzelnen Akteuren drei Rollen in Bezug auf ihr Verhalten zu: Der Identitätsforderer, im Englischen auch Relying Party genannt, stellt einen geschützten Bereich einer Internetseite oder eines aufgerufenen Webdienstes dar. Der Internetbenutzer versucht sich gegenüber dem Identitätsforderer zu authentifizieren. Der Identitätsaussteller (Identity Provider) bescheinigt die Identität des Internetnutzers gegenüber des Authentitätsforderers.
5.5 Digitale Karten Wie im richtigen Leben auch besitzt der Anwender bei der Verwendung von Windows CardSpace Visitenkarten, nur dass sich diese nicht real in seinem Portemonnaie in der Form von Plastikkarten befinden, sondern in dessen Systemsteuerung. Die Kartenform ist natürlich nur eine abstrakte Darstellung, denn in Wirklichkeit beinhalten diese Karten nicht die Informationen der Identität selbst, sondern nur dessen Metadaten. Die eigentlichen Daten, welche die Identität des Anwenders bezeugen, befinden sich bei dem zur Karte zugehörigen Identitätsaussteller und werden in der Gestalt eines Security Tokens übermittelt. Ob diese Identitätsdaten nun auf einem externen Rechner oder lokal auf der Festplatte des Anwenders gespeichert sind, hängt von der Kartenart ab, wobei CardSpace zwei grundsätzliche Kartenformen unterscheidet.
Abbildung 5.2: Kartenarten in Windows CardSpace
344
CardSpace
5.5.1 Persönliche Karten erstellen Die Identitätsdaten der Persönlichen Karten werden lokal auf der Festplatte des Rechners gespeichert und in einem geschützten Bereich von einem in CardSpace befindlichen Identitätsaussteller verwaltet. Vergleichbar mit der Registrierung auf einer Community Internetseite, kann man sich selbst innerhalb des CardSpace-Desktops mittels PERSÖNLICHE KARTE ERSTELLEN eine Persönliche Karte ausstellen. Unterstützt wird bei dieser Anmeldungsart eine feste Liste von unkritischen Daten, wie z.B. Vorname, Nachname, Adresse und Telefonnummern. Einmal erstellt, kann man diesen virtuellen Ausweis bei mehreren Internetseiten verwenden, falls der jeweilige Seitenanbieter die Anmeldung mit CardSpace gestattet und die ausgefüllten Felder den bei einer Registrierung angeforderten Daten entsprechen.
Abbildung 5.3: Erstellen einer persönlichen Karte
345
Kapitel 5
In Abbildung 5.3 habe ich mir ein Profil mit der Bezeichnung Community Seiten erstellt, dessen Daten ich gewillt bin, über meine Person preiszugeben. Nach der Speicherung erscheint die erstellte Karte in der Übersicht (vgl. Abbildung 5.1) und kann von nun an verwendet werden. Um ein kurzes Szenario mit einer persönlichen Karte aufzuzeigen, registriere ich mich mit dieser eben erstellten Karte auf der Community-Seite http://sandbox.netfx3.com/.
Abbildung 5.4: Anlegen eines Benutzeraccounts mittels CardSpace
Dort habe ich die Wahl zwischen einer bisher üblichen Formularanmeldung auf der linken Seite und der Anmeldung mit einer Information Card auf der rechten (Abbildung 5.4). Wählt man die neue Art der Registrierung, so erscheint der CardSpace-Desktop, wo wir zuerst eine Information über das Zertifikat der Webseite erhalten (Abbildung 5.5). Wählen wir JA, EINE KARTE ZUM SENDEN AUSWÄHLEN, so schlägt uns CardSpace genau diese Karten zur Auswahl vor, deren Daten die Kriterien des Registrierungsprozesses erfüllen (Abbildung 5.6).
346
CardSpace
Abbildung 5.5: Informationen über die Webseite vor dem Senden der Karte
Abbildung 5.6: Vorschlag der zutreffenden Karten
347
Kapitel 5
Wählt man nun die eben erstellte Information Card mit der Bezeichnung Community Seite, so wird die Karte bzw. das entsprechende Security Token vom lokalen Identitätsaussteller zur Seite gesendet und von dieser entgegengenommen. Nachdem wir auf der Seite einen noch nicht vorhandenen Benutzernamen gewählt haben, erhalten wir die Information der erfolgreichen Registrierung.
Abbildung 5.7: Bestätigung einer erfolgreichen Registrierung
Bei einer erneuten Anmeldung gehen wir einfach wieder auf das Login, wählen die CardSpace-Variante, entscheiden uns bei dem erscheinenden CardSpace-Desktop für die bei dieser Seite registrierte Karte und sind anschließend wieder eingeloggt (Abbildung 5.8).
Abbildung 5.8: Signed in as Steckermeier
Listing 5.1: CardSpace-Authentifizierung in HTML-Form
In HTML sieht das Procedere übrigens wie in Listing 5.1 aus. CardSpace rufen wir dabei mittels eines -Tags des Typs application/x-informationcard auf, wobei in einem
348
CardSpace
Parameter der Typ des Security Tokens gefordert wird, wie hier SAMLV1.1, und im zweiten Parameter die Informationen, die innerhalb dieses Tokens zu finden sein sollen: Givenname = Vorname Surname = Nachname Emailaddress Privatepersonalidentifier
5.5.2 Verwaltete Karten Neben den persönlichen Karten gibt es nun noch eine zweite Art, die sogenannten Verwalteten Karten. Diese sind dabei auch für die Ausstellung von wertvolleren, sensiblen Identitäten geeignet, die sich aus sensiblen Daten zusammensetzen, wie z.B. Kreditkarteninformationen oder Kontodaten. Der Identitätsaussteller wäre in diesem Fall die Bank oder ein von dieser beauftragtes Unternehmen und selbst für Aushändigung der Verwalteten Karten ihrer Kunden zuständig. Hat der Kunde die Verwaltete Karte seiner Bank erhalten, kann er sie anschließend in Windows CardSpace importieren und für alle weiteren Transaktionen auf der Webseite der Bank verwenden. Damit nun derartig sensible Daten an eine dritte Entität sicher weitergeleitet werden können, bedarf es natürlich mehrerer komplizierter Verschlüsselungsverfahren, denn sonst könnte ja jede beliebige Person versuchen, sich ein Security Token in meinem Namen ausstellen zu lassen. Wie man sich nun gegenüber dem Identitätsaussteller ausweist, dass man wirklich diejenige Person ist, für die man sich ausgibt, hängt von der Policy des jeweiligen Ausstellers des Sicherheitstoken ab. Herkömmliche Benutzer-Passwort Abfragen, KerberosAuthentifizierung, digitale Zertifikate, persönliche Karten oder auch physische mit dem Rechner verbundene SmartCards sind dabei denkbare Mechanismen für die Bescheinigung seiner selbst.
5.6 CardSpace als Meta-Identitätssystem Das vorhin in Abschnitt 5.5.1 geschilderte Geschehen war natürlich stark vereinfacht dargestellt und lässt in keiner Weise die dahinterliegenden Technologien erkennen. Man könnte den Gedanken haben, dass Microsoft versucht, den Kunden mit betriebssystemspezifischen Kartenformaten und Authentifizierungsmechanismen wieder an sich zu binden. Doch auch hier geht Microsoft wieder einen Schritt mehr in Richtung Interoperabilität und benutzt keine Microsoft-proprietären Austauschmechanismen, sondern offene
349
Kapitel 5
Standards für die Kommunikation und präsentiert CardSpace als ein Metasystem, das als Dolmetscher unterschiedliche Identifikationssysteme miteinander kommunizieren lässt. Genauer gesagt basiert der Informationsaustausch auf Web Service *-Protokollen, wobei WS-SecurityPolicy die Regeln für die Tokenausgabe verkündet, WS-MetaDataExchange diese Regeln versteht und ausliest, WS-Trust die Definition für den Tokenaustausch vorgibt und mittels SOAP die eigentlichen Nachrichten letztendlich verschickt werden.
5.7 Genereller Ablauf Natürlich kann im Rahmen dieses CardSpace-Kapitels nur ein kleiner Einblick in die Hintergrundtransaktionen der Technologie gewährt werden. Dennoch soll Abbildung 5.9 mit anschließender Erläuterung Ihnen helfen, sich einen generellen Überblick über die Authentifizierung mittels Windows CardSpace zu verschaffen.
Abbildung 5.9: Genereller Ablauf der Authentifizierung mittels CardSpace
Abbildung 5.9 zeigt uns die Schritte, die bei einer Authentifizierung mittels CardSpace vollzogen werden. 1. Angenommen, ein Internetuser möchte sich, wie in Schritt 1 angedeutet, mit seinem Rechner an einem geschützten Bereich einer Webseite authentifizieren, die eine Anmeldung mit Windows CardSpace akzeptiert. 2. Die Webseite, d.h. der Identitätsforderer in Abbildung 5.9, verweigert berechtigterweise den Zutritt und teilt dem Client mit, welches Security Token-Format er vorweisen muss, um in den gesicherten Bereich eintreten zu können.
350
CardSpace
3. CardSpace prüft nun, welche installierten Karten auf dem Computer die Erfordernisse erfüllen, und schlägt diese dem Benutzer in einem neuen geschützten Desktop vor. 4. Der Benutzer wählt daraufhin die für ihn am besten geeignete Karte und klickt auf SENDEN im Control Panel Applet des CardSpace Desktops. 5. Intern fordert CardSpace nach der Wahl der Karte nun die darin enthaltenen Identitätsdaten vom betreffenden Identitätsaussteller an. Handelt es sich bei der gewählten Karte um eine PERSÖNLICHE KARTE, wird der in CardSpace integrierte Identitätsaussteller kontaktiert, im Falle einer VERWALTETEN KARTE der Security Token Service (STS) des jeweiligen Providers.
> >
>
HINWEIS
Der Benutzer muss natürlich auch dem Identitätsaussteller gegenüber beweisen, dass er diejenige Person ist, für die er sich ausgibt. Der Mechanismus dafür hängt dabei vom Identitätsaussteller selbst ab und erfolgt z.B. durch eine Benutzername-Passwort-Überprüfung, Kerberos-Authentifizierung oder X.509-Zertifikate, um die gängigsten Methoden aufzuführen.
6. Hat sich der Benutzer nun erfolgreich gegenüber dem Identitätsaussteller verifiziert, so übermittelt der Security Token Service des Providers diesem ein Security Token, das die geforderten Daten beinhaltet. 7. Der Benutzer bleibt aber weiterhin der kontrollierende Mitspieler und wird in Schritt 7 nochmals aufgefordert, das Absenden des Token zu bestätigen. 8. Bei einer Bestätigung wird das Security Token im letzten Schritt an den Identitätsforderer gesandt, wonach er entweder Zutritt zu dem geschützten Bereich erhält oder nicht.
5.8 Sichern und Wiederherstellen von Karten Hat man die Karten einmal in seinem CardSpace-Depot installiert, kann man diese beliebig oft verwenden und gegen Aufforderung vorweisen. Doch was passiert, wenn wir unseren Rechner neu aufsetzen? Oder falls man sein Notebook gerade einmal nicht zur Hand hat? Für diesen Fall hat man die Option, eine seiner digitalen Karten auf ein Speichermedium, z.B. einen USB-Stick, zu sichern und in ein anderes CardSpace bzw. in einen anderen Rechner wieder zu importieren. Das Exportieren von Karten gestaltet sich in CardSpace durch wenige Schritte, die ohne großen Aufwand auch von einem Computerlaien durchgeführt werden können. Dazu wählen Sie KARTEN SICHERN und fahren mit einem Klick auf WEITER fort.
351
Kapitel 5
Abbildung 5.10: Sichern von CardSpace-Karten
1. Karte(n) auswählen und WEITER 2. Speicherort, Dateinamen angeben und WEITER 3. Kennwort zum Schutz der exportierten Karten angeben und SICHERN (Abbildung 5.11)
Abbildung 5.11: Kennwort für den Schutz der exportierten Karten eingeben
352
CardSpace
Schon wieder ein Kennwort? Muss man das jetzt bei jedem Vorzeigen der Identität eingeben? Entfällt bei CardSpace nicht die Passworteingabe? Nun ja, in diesem Fall kommt man wohl nicht ohne einen Passwortschutz aus. Die gesicherten Karten werden natürlich nicht in Klartext außerhalb von CardSpace gespeichert, denn das wäre ja genauso fahrlässig, als ob Sie auf der Rückseite Ihrer Bankkarte den PIN-Kode vermerkt hätten. Deshalb sorgt das bei dem Sicherungsvorgang angegebene Kennwort dafür, dass die Karte je nach Kennwortstärke dementsprechend gut verschlüsselt wird. Abbildung 5.12 zeigt einen Ausschnitt des Karteninhalts, aufgezeigt im Windows-Editor.
Abbildung 5.12: Verschlüsselte Kartenmetadaten
Angenommen, wir haben unsere Karten auf einen USB-Stick oder eine externe Festplatte exportiert (vgl. Abbildung 5.13), so können wir diese mit einem Doppelklick und durch die Angabe des zu dieser(n) Karte(n) zugehörigen Passworts auf einen anderen Rechner importieren (Abbildung 5.14) und erhalten am Ende ein Duplikat der in diesem Backup enthaltenen Karten.
Abbildung 5.13: Exportierte CardSpace-Karten
353
Kapitel 5
Abbildung 5.14: Eingabe des vorher vergebenen Kennworts
354
A
Projektbeschreibung
In diesem Buch haben wir versucht, Ihnen einen Einblick in die neuesten Technologien, die das .NET Framework 3.0 mit sich bringt, zu geben. Wir haben uns bemüht, Windows Presentation Foundation, Windows Communication Foundation und Workflow Foundation so zu präsentieren, dass Sie als Leser nicht nur einen Einstieg in diese Technologie bekommen, sondern vielleicht auch, dass der Enthusiasmus, den wir in der Arbeit mit .NET 3.0 erfahren haben, ein klein wenig auf Sie übergeht. In der Ankündigung dieses Buches ist von einem technologieübergreifenden Beispiel die Rede, und Sie haben sich vielleicht bisher gefragt, warum denn die einzelnen Kapitel immer wieder unterschiedliche Beispiele in verschiedensten Kontexten präsentieren. Die Antwort darauf ist sehr einfach, denn ein durchgängiges Beispiel wäre in der Erläuterung der einzelnen Technologien eher hinderlich gewesen. Zum einen wären dadurch die Beispiele zu sehr aufgebläht worden, und zum anderen hätten die Beispiele dann vielleicht nicht genau die Interessen aller Leser getroffen. Wir wollten einen Leser, der gerade nur über WPF Bescheid wissen will, nicht dazu zwingen, sich auch in den anderen beiden Technologien Grundwissen anzueignen. Das zusammenhängende Beispiel haben wir uns für den Schluss aufgehoben, und wir werden Ihnen jetzt präsentieren, wie wir WPF, WCF, und WF in einer Applikation unter einen Hut gebracht haben.
Anhang A
Jede der drei Technologien hat ihre typischen Eigenheiten, und so sollen sie auch wieder einzeln vorgestellt werden, um dann doch noch in den späteren Erklärungen als zusammenarbeitende Einheit enttarnt zu werden. Dennoch sollen Sie zuerst den groben Überblick über das gesamte Beispiel bekommen. Wir haben lange diskutiert, welches Thema wir behandeln sollen und wie umfangreich das Thema abgearbeitet werden sollte. Wir geben zu, anfangs haben auch wir zuerst einmal in der großen Windows SDK-Kiste gesucht, ob wir in der Rubrik Samples/Cross Technology Samples à Integration Samples for .NET Framework 3.0 Features vielleicht ein paar Anregungen dazu finden können. Doch wenn Sie diese Beispiele einmal kurz betrachten, werden Sie sehen, dass dort nicht wirklich viele brauchbare Beispiele für den Einsatz in der Praxis zu finden sind, die sowohl WPF, WCF und WF benutzen. Wir wollen Ihnen damit sagen und auch vielleicht ans Herz legen: Nur weil Sie jetzt alle Technologien des .NET Frameworks 3.0 kennengelernt haben und vielleicht auch sogar beherrschen, heißt das jetzt noch lange nicht, dass Sie in alle Ihre zukünftigen Applikationen WPF, WCF und WF gleichzeitig einbinden müssen. Grenzt sich zwar WPF von Grund auf von seinen anderen .NET 3.0er-Kollegen ab, indem es für die Oberflächengestaltung zuständig ist, so ist die Grenze zwischen WCF und WF bezüglich der Anwendungsgebiete nicht mehr ganz so eindeutig wie z.B. zwischen WPF und WCF oder WPF und WF. Den Umfang der Applikation betreffend hat sich sehr schnell ein Konsens herauskristallisiert, dessen Inhalt war, dass die Applikation überschaubar und in der Anzahl der Klassen und Codezeilen ohne Weiteres nachvollziehbar sein sollte. Dennoch sollte der Anspruch der Applikation etwas über den der einzelnen erklärenden Buchteile hinausgehen und die Offroad-Tauglichkeit von .NET 3.0 unter Beweis stellen. Als Themen standen die Implementierung einer UrlaubsantragsgenehmigungsAnwendung, die Verwendung eines Beispiels aus unseren produktiven Kundenapplikationen und die Realisierung einer Chat-Anwendung zur Diskussion. Dadurch, dass wir uns beim Umfang auf das Attribut überschaubar geeinigt haben, ist die Wahl auf den Chat gefallen. Nicht dass wir nicht auch die Urlaubsapplikation in einer knackigen verteilten Anwendung hätten zeigen können, nein, wir wollten ein sehr greifbares und alltägliches Beispiel implementieren, das dennoch gut in das Dreigestirn der Technologien passt. Keine Angst, Sie müssen diese Anwendung nicht alleine zusammenbauen, denn auch dieses Beispiel finden Sie, wie alle anderen, auf der beiliegenden Buch-CD.
356
Projektbeschreibung
A.1 Projektaufbau Der Chat wurde von uns in seiner Logik sehr einfach gehalten. Als Funktionalität sind nur eine Registrierung, ein Login und das Versenden und Empfangen von Nachrichten vorgesehen. Und selbst hier haben wir unter dem Motto Überschaubarkeit den Ball sehr flach gehalten und auf ein Speichern der Benutzerdaten (am besten noch verschlüsselt) in einer Datenbank oder ein Versenden einer E-Mail als Registrierungsbestätigung verzichtet und uns fast nur auf die im Buch beschriebenen Technologien konzentriert. Den zentralen Part stellt in diesem Beispiel ein WCF-Service dar. Der WCF-Service stellt dabei Funktionalität für den WPF-Client bereit. Aus der ChatApplikation können dabei die Methoden Login, Register, SendMessage und Logout aufgerufen werden. Die Kommunikation erfolgt dabei auf der Basis von TCP, und über Callbacks (Duplexkommunikation) ist es auch dem WCF-Service möglich, Aufrufe zum Verteilen der gesendeten Nachrichten an alle registrierten Clients zurückzusenden. Sowohl für die Funktionalität des Logins wie auch des Registrierens eines Users wird vom WCF-Service ein Workflow gehostet, der die Funktionalität zum Anmelden und Registrieren ausführt. Somit kommuniziert der WPF-Client mittels eines Proxyobjekts mit dem WCF-Service, der zur besseren Nachvollziehbarkeit in einer Konsolenanwendung gehostet wird. Der WCF-Service wiederum lädt die Workflow-Bibliothek und hostet die entsprechende Workflow-Funktionalität. Unsere Beispielanwendung besteht dabei aus drei unterschiedlichen Projekten: ChatWorkflow Stellt die Workflow-Funktionalität zum Registrieren und Login eines Users zur Verfügung. Es handelt sich dabei um eine Klassenbibliothek. ChatServer Stellt mittels WCF den Service in einer Konsolenanwendung bereit. Die WorkflowFunktionalität ist über einen Verweis auf die Klassenbibliothek des Projektes ChatWorkflow eingebunden. Der Server muss einmal gestartet sein, damit die ChatGUIProjekte laufen. ChatGUI Implementiert die Oberfläche des Chats. Diese Applikation kann mehrfach gestartet werden, um mehrere Chatter zu simulieren. Die Schnittstellen des WCF-Service wurden mittels einer Proxyklasse, die mit dem svcutil-Tool erstellt wurde, dem Client bekannt gegeben.
357
Anhang A
A.2 ChatGUI Der Windows Presentation Foundation-Teil der Chat-Applikation ist das, was der Benutzer schließlich und endlich zu Gesicht bekommt. Dabei galt es, eine Loginmaske, ein Fenster für die Registrierung und das Chat-Fenster an sich zu entwerfen. Wir benutzen hier explizit den Ausdruck entwerfen, denn die Oberflächen wurden mit Microsoft Expression Blend erstellt und nach ein paar kleinen Anpassungen in Visual Studio 2005 übernommen.
A.2.1 GUI Die Loginmaske gestaltet sich sehr einfach und geht wie nicht anders zu erwarten mit dem Trio Label, TextBox (PasswordBox) und Button einher. Listing A.1: Die Maske für das Login
Wie Sie in Listing A.1 auf den ersten Blick erkennen werden, sind im XAML-Dokument Angaben für die prozentuale Höhe einer Zeile im Grid oder der Einzug von Elementen teils auf die, lassen Sie mich zuerst zählen, 15. Nachkommastellen genau festgelegt. So eine fast schon übertriebene Genauigkeit ist immer ein Hinweis auf designergeneriertes XAML. In Abbildung A.1 sehen Sie das Ergebnis als Loginmaske.
Abbildung A.1: Maske für den Login zum Chat
359
Anhang A
Auf den ersten Blick scheint alles klar zu sein, aber wenn Sie genau hinschauen, erkennen Sie in Abbildung A.1 vielleicht einen Farbverlauf als Hintergrund des Fensters und der beiden Buttons. Hier kommt der Verweis auf eine statische Ressource in einer XAML-Extension zum Tragen: Background="{StaticResource lgbFlow}">
Die Hintergrundeigenschaften der beiden Buttons und des Fensters werden durch eine statische Ressource bestimmt. Frage & Antwort Die Frage, die sich jetzt stellt, ist, wenn die Ressource nicht im XAML-Dokument in Listing A.1 zu finden ist, woher beziehen die Elemente dann ihren Farbverlauf? Die Antwort ist in der applikationsweit verfügbaren App.xaml-Datei zu finden. Dort ist als Ressource ein Farbverlauf in einem LinearGradientBrush und ein Trigger in einem Style abgelegt.
360
Projektbeschreibung Listing A.2: App.xmal mit Ressource
Die Ressourcen aus dem App.xaml-Dokument stehen allen Elementen der grafischen Benutzeroberflächen in diesem Projekt zur Verfügung. Auch das Fenster für die Registrierung bezieht sich auf diese Ressourcen. Da sich die Oberfläche für die Registrierung in ihrem Aufbau dem Login sehr ähnlich darstellt, soll sie daher nur kurz als Screenshot in Abbildung A.2 gezeigt werden.
Abbildung A.2: Die Oberfläche der Registrierung für den Chat
Ist der Anwender entweder registriert oder angemeldet, kann er mit anderen Benutzern kommunizieren. Dazu haben wir uns ein Fenster ausgedacht, das vom Aufbau dem Windows Messenger nachempfunden ist; so zu sehen in Abbildung A.3.
361
Anhang A
Abbildung A.3: Der Chat in Aktion
Sendet einer der Chatter eine Nachricht, so wird diese an alle Beteiligten geschickt. Das missmutige gelbe Gesicht ist das Resultat einer Animation, die dann durchlaufen wird, wenn der Sender vor dem Absenden der Nachricht auf einen der kleinen Smileys oberhalb der Texteingabe klickt. Die Oberfläche kommt wieder zustande, indem in einem Fenster Elemente angeordnet werden. Das hat auch in diesem Fall Expression Blend übernommen. Dennoch möchte ich noch kurz auf die ListBox eingehen, die zur Anzeige der Nachrichten verwendet wurde. Listing A.3: ListBox zur Anzeige der Chat-Nachrichten
Eine ListBox ist in unserem Chat dafür geeignet, die Chat-Nachrichten als ListItems anzuzeigen. In der Arbeit mit einer gewöhnlichen ListBox wird Ihnen dabei aber schnell auffallen, dass bei sehr breiten Einträgen, in unserem Fall langen Nachrichten, der Eintrag nicht umbrochen wird, sondern die ListBox einen horizontalen Scrollbalken anzeigen wird. Das gilt es, für unseren Chat zu verhindern, aber leider ist man bei der ListBox vergeblich auf der Suche nach einer Eigenschaft oder Methode, die den Scrollbalken unterdrücken würde. Die Lösung liefert dafür ein Style als Ressource, der das Verhalten unserer ListBox festlegt. Listing A.4: Style, um den Scrollbalken der ListBox zu unterdrücken
In Listing A.4 wird als Vorlage für das ItemsPanel, welches das Layout unserer ListBox bestimmt, ein StackPanel festgelegt und in einem zweiten Setter die Sichtbarkeit des Scrollbalkens verhindert. Und schon bricht langer Text in den Einträgen der ListBox um, anstatt einen Scrollbalken zu verursachen. Die Animation, die sich in Abbildung A.3 schon angedeutet hat, definiert eine Ellipse, deren Layout durch ein ImageBrush bestimmt wird. In Abhängigkeit von dem Smiley, der in der Nachricht beim Chat-Fenster angekommen ist, wird ein Bild mit gutem oder bösem Smiley in die Ellipse gezeichnet. Die Vergrößerung und Verschiebung der Ellipse übernehmen wieder je eine DoubleAnimation.
363
Anhang A private void DrawSmiley(Emoticon smiley) { // Ellipse mit Umrandungsfarbe und Stärke Ellipse ellipseSmiley = new Ellipse(); ellipseSmiley.Stroke = new SolidColorBrush(Colors.Black); ; ellipseSmiley.StrokeThickness = 3; //Beim Start ist die Ellipse sehr klein ellipseSmiley.Width = 0.0; ellipseSmiley.Height = 0.0; this.canvasMitte.Children.Add(ellipseSmiley); //Die Mitte des Canvas ist der Ausgangspunkt Animation double zentrumY = this.canvasMitte.ActualHeight / 2.0; ellipseSmiley.SetValue(Canvas.LeftProperty, 150.0); ellipseSmiley.SetValue(Canvas.TopProperty, zentrumY); // Die Dauer der Animation double Dauer = 1.0; DoubleAnimation groessenAnimation = new DoubleAnimation ( 0.0, 150.0, new Duration(TimeSpan.FromSeconds(Dauer))); //Die Ellipse wird groß und wieder klein groessenAnimation.AutoReverse = true; //Start der Animation für die Höhe und Breite der Ellipse ellipseSmiley.BeginAnimation(Ellipse.WidthProperty, groessenAnimation); ellipseSmiley.BeginAnimation(Ellipse.HeightProperty, groessenAnimation); // Verschiebung der Ellipse wird vorbereitet TranslateTransform verschiebung = new TranslateTransform(); string pfad; //Das Bild wird ausgesucht, //das die Grundlage des Brush für die Ellipse darstellt if (smiley == Emoticon.Smiley) { pfad = @"IMAGES\goodSmiley.jpg"; } else { pfad = @"IMAGES\badSmiley.jpg"; } // Bild wird geholt BitmapImage theImage = new BitmapImage(new Uri(pfad, UriKind.Relative)); //Die Pinsel als ImageBrush für die Farbgebung (eher Bildgebung) //der Ellipse. ImageBrush myImageBrush = new ImageBrush(theImage); ellipseSmiley.Fill = myImageBrush; //Das Bild des Brush wird auf die Ellipsenumrandung beschnitten. ellipseSmiley.ClipToBounds = true; //Animation für die Translation wird bereitgestellt DoubleAnimation richtungsAnimation = new DoubleAnimation (0.0, -75.0, new Duration(TimeSpan.FromSeconds(Dauer))); // Die Verschiebung wird in X- und Y-Richtung gestartet
364
Projektbeschreibung verschiebung.BeginAnimation(TranslateTransform.XProperty, richtungsAnimation); verschiebung.BeginAnimation(TranslateTransform.YProperty, richtungsAnimation); ellipseSmiley.RenderTransform = verschiebung; } Listing A.5: Die Animation für die Emoticons
Die restliche GUI des Chats ist sehr eingängig und dennoch in manchen Teilen sehr anspruchsvoll. Jetzt gilt es, nur noch zu wissen, wie es von den Events, die beim Klicken der Buttons der GUI gefeuert werden, zum richtigen Chatten kommt. Sie sehen bislang nur, dass verschiedene Methoden des Proxyobjektes aufgerufen werden. Darüber soll der nun folgende Einblick in den Service Aufschluss geben.
A.2.2 Kommunikation mit dem WCF-Service Ein besonderes Augenmerk liegt noch im Formular ChatWindow. Betrachten wir uns kurz die Definition der Klasse: public partial class ChatWindow : System.Windows.Window, IPtChatCallback
Dass eine Form abgeleitet ist von der Basisklasse System.Windows.Window, ist wohl nicht sehr verwunderlich, aber was bedeutet die Implementierung des Interface IPtChatCallback? Dieses Interface besteht aus einer Methode ReceiveMessage, die einen Parameter vom Typ ChatMessageResponse entgegennimmt. Innerhalb dieser Methode wird die ListBox mit den versendeten Nachrichten befüllt. Wo wird aber diese Methode eigentlich aufgerufen? Im Client werden Sie den Aufruf nicht finden, denn tatsächlich wird diese Funktionalität über einen Callback vom WCF-Service direkt aufgerufen. Der WCF-Service stellt über ein Proxy-Objekt dem Client seine Funktionalität zum Aufruf zur Verfügung. Die zugehörige Datei client.cs wurde dabei vom Serviceentwickler mit dem svcutil-Tool erstellt, ebenso wie die app.config-Konfigurationsdatei. Um diesen Proxy zu instanzieren, muss man dem Konstruktor des Proxy ein Objekt übergeben, das einen Kontext auf ein Objekt besitzt, welches das Interface IPtChatCallback implementiert. Auf diese Weise wird sichergestellt, dass der WCF-Service die eingehenden Nachrichten an alle registrierten Clients verteilen kann. Den Code in Listing A.6, in dem der Proxy instanziert wird, finden Sie sowohl im Register- wie auch im Loginformular.
365
Anhang A //Das Chatfenster wird instanziert ChatWindow cw = new ChatWindow(); // Der Kontext wird erstellt, mit der Übergabe des Chatfensters InstanceContext context = new InstanceContext(cw); // Der Proxy wird mit passendem Kontext instanziert PtChatClient proxy = new PtChatClient(context); Listing A.6: Instanzierung des Proxy zur Kommunikation mit dem Chatserver
Der Proxy stellt dabei einen Kommunikationskanal zur Verfügung, der später dem ChatWindow zur Verfügung gestellt werden muss, damit die Kommunikation über denselben Kontext läuft (ist später für das Sessionhandling am Server entscheidend). Dazu wurde in der Klasse ChatWindow eine Property Proxy implementiert, die nach einem erfolgreichen Login bzw. nach einer erfolgreichen Registrierung mit dem instanzierten proxy-Objekt belegt wird. cw.Proxy = proxy;
A.3 Chatserver Beim Chatserver handelt es sich um die zentrale Komponente innerhalb unserer abschließenden Beispielanwendung. Zum einen nimmt er die Aufrufe des Clients entgegen, verteilt Nachrichten unter den Clients und hostet zum anderen schließlich auch noch die benötigten Workflow Foundation-Komponenten.
A.3.1 Datendefinitionen In der Datei ChatMessages.cs sind dabei alle für die Kommunikation mit dem Client benötigten Objekte definiert. Dabei sind alle Klassen mit dem Attribut DataContract versehen, für das als Namespace http://primetime-software.de/ChatKommunikation angegeben wurde. Jede Klasse implementiert außerdem das Interface IExtensibleDataObject, um für eventuell geplante Aktualisierungen Kompatibilität mit älteren Clients zu gewährleisten. Sämtliche Properties der in dieser Datei definierten Klassen sind mit dem Attribut DataMember versehen, sodass alle Eigenschaften bei der Datenübertragung zwischen Server und Client auch serialisiert werden können.
366
Projektbeschreibung
Außerdem befindet sich hier auch die Definition einer Enumeration Emoticon. Um die enthaltenen Enumerationswerte serialisieren zu können, müssen die einzelnen Werte der Enumeration mit dem Attribut EnumMember versehen werden. Im Folgenden sehen Sie eine Auflistung der in dieser Datei definierten Typen: Emoticon Enumeration für verschiedene unterschiedliche Emoticons. ChatMessage Klasse, die eine Nachricht abbildet, die ein Client zum Server überträgt. ChatMessageResponse Klasse, welche die Daten einer an den Server gesendeten Nachricht an alle registrierten Clients weiter überträgt. UserData Klasse, in der die beim Login übergebenen User-Credentials gehalten werden. RegisterData Klasse, in der die bei der Registrierung eingegebenen Daten übertragen werden. LoginMessageResponse Rückgabewert der Login-Operation. RegisterMessageResponse Rückgabewert der Register-Operation. LoginFault Klasse, die beim Auftreten einer Exception eine ausführliche Fehlermeldung beinhaltet.
A.3.2 ServiceContract-Definition Die Datei IPtChat.cs innerhalb des Projektes definiert den ServiceContract unseres WCFService. Die Datei besteht aus zwei Interfaces IPtChat und IPtChatCallback, die beide mit dem Attribut ServiceContract versehen sind. Der Namespace von ServiceContract ist identisch mit dem Namespace von DataContract. Innerhalb des Interface IPtChat sind sämtliche Operationen definiert, die unser Service zur Verfügung stellt.
367
Anhang A
In Listing A.7 sehen Sie die Definition des Interface. [ServiceContract(Namespace = "http://primetime-software.de/ ChatKommunikation", SessionMode = SessionMode.Required, CallbackContract = typeof(IPtChatCallback))] public interface IPtChat { [OperationContract(IsInitiating = false,IsOneWay=true)] void SendMessage(ChatMessage message); [OperationContract(IsInitiating = true)] [FaultContract(typeof(LoginFault))] RegisterMessageResponse Register(RegisterData user); [OperationContract(IsInitiating = true)] [FaultContract(typeof(LoginFault))] LoginMessageResponse Login(UserData user); [OperationContract(IsInitiating = false,IsTerminating = true)] void Logout(UserData user); } Listing A.7: Definition des Interface IPtChat
Sie sehen hier, dass dem Interface als SessionMode der Wert SessionMode.Required zugewiesen ist. Dies bedeutet, dass dieser Service Sessions benötigt, dazu aber gleich mehr. Auch alle OperationContract-Attribute wurden mit benannten Parametern bezüglich des Sessionhandlings versehen. Außerdem wird für den ServiceContract noch ein weiterer benannter Parameter CallbackContract angegeben.
Da wir einen Duplexkontrakt definieren wollen, der Server soll ja die Nachrichten im Netzwerk verteilen und muss deswegen in der Lage sein, die registrierten Clients aufzurufen, müssen wir einen CallbackContract definieren. Dieser CallbackContract gibt dabei ein Interface an, das am Client implementiert sein muss und somit dem Server Operationen für einen Callback zur Verfügung stellt. In unserem Fall wird dabei als ContractCallback das Interface IPtChatCallback angegeben, das Sie in Listing A.8 sehen. [ServiceContract (Namespace = "http://primetime-software.de/ChatKommunikation")] public interface IPtChatCallback { [OperationContract(IsOneWay = true)] void ReceiveMessage(ChatMessageResponse message); } Listing A.8: Definition des Interface IPtChatCallback
368
Projektbeschreibung
Die beiden Operationen Register und Response besitzen noch zusätzlich das Attribut FaultContract. Damit wird ausgezeichnet, falls innerhalb dieser Operationen eine Exception auftritt, dass die ausführliche Fehlerbeschreibung in der Klasse LoginFault serialisiert zum Client übertragen wird. Die Implementierung des Interface IPtChat findet dabei in der Klasse PtChat statt, während die Implementierung des zweiten Interface IPtChatCallback auf dem Client liegt.
A.3.3 Konfiguration Die gesamte Konfiguration unseres Service geschieht in der applikationsspezifischen Konfigurationsdatei app.config, die Sie in Listing A.9 sehen. Listing A.9: Konfigurationsdatei app.config
369
Anhang A
Innerhalb des services-Elements wird unser Service mit einem Endpoint bekannt gegeben. Unserem Service wird dabei auch ein behaviorConfiguration zugewiesen, das am Anfang der Applikationsdatei steht. Hier wird lediglich definiert, dass unser Service über HTTP Metadaten zur Verfügung stellt und im Fehlerfall eine ausführliche Beschreibung ausgibt.
> >
>
HINWEIS
Auch wenn wir für diese Applikation ein TCP-Binding verwenden, so können die Metadaten trotzdem über HTTP veröffentlicht werden.
Innerhalb der Definition des Endpoints finden wir wieder unser hoffentlich bereits bekanntes Endpoint-ABC. Neben der Adresse und dem Contract wird netTcpBinding als Binding angegeben. Wir haben uns dabei für dieses Binding entschieden, da es alle Features implementiert (Duplexkommunikation, Sessions), die wir für die Applikation benötigen. Mittels des XML-Attributs bindingConfiguration geben wir dem Endpunkt an, dass er die Bindingkonfiguration DuplexBinding, die vor dem service-Element definiert ist, implementiert. Hier wird zusätzlich noch definiert, dass für die Kommunikation zwischen Server und Client eine reliable Session verwendet wird. Das bedeutet, dass zwischen den beiden Endpoints eine Session aufgebaut wird, die der Definition der WS-RM (Reliable Messages) entspricht.
A.3.4 Instanzierung und Sessionmanagement Eine Fragestellung, die es noch zu klären gibt, bezieht sich auf die Instanzierung der Klasse PtChat. Wir haben bislang nirgends gesehen, ob diese Klasse nur ein einziges Mal instanziert, pro Client einmal instanziert oder sogar bei jedem Aufruf neu instanziert wird. Die Antwort zu dieser Frage sehen Sie im Attribut ServiceBehavior, das unserer Klasse PtChat zugewiesen wurde. [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class PtChat:IPtChat {…}
Hier wird angegeben, dass für jede erzeugte Session eine Instanz der Klasse am Server gehalten wird. Bei der Definition des Interface IPtChat wurde festgelegt, dass Sessions zwingend notwendig (SessionMode.Required) sind.
370
Projektbeschreibung
Frage & Antwort Wie oder wer erzeugt denn aber nun die Sessions? Sämtliche Sessions werden dabei vom Client initialisiert. Bei der Definition des Interface IPtChat wurden den Operationen mittels Attributen Eigenschaften bezüglich des Sessionverhaltens zugewiesen.
Die beiden Operationen Login und Register wurden dabei mit IsInitailizing = true versehen. Das bedeutet, dass beim Aufruf dieser Methode automatisch eine neue Session gestartet wird. Somit ist es zum Beispiel nicht möglich, dass die Operation SendMessage aufgerufen wird, ohne dass mittels der beiden Operationen eine Sessioninitialisierung stattgefunden hat. Die Operation Logout schließlich beendet die Session (IsTerminating = true). Die Variable member innerhalb der Klasse PtChat beinhaltet dabei den Nicknamen des Chatters für jede Session. Anhand dieser Variablen können Sie sehr gut sehen, wie sich Änderungen an den Attributen auf das Verhalten der Sessions auswirken.
A.3.5 ClientCallbacks Jetzt sollten wir noch erläutern, wie der Callback zum Client kommt, damit eingehende Nachrichten auch an alle im Chat angemeldeten User weitergeleitet werden. Die Operation SendMessage wird dabei von einem Client aufgerufen, und es wird eine Nachricht vom Typ ChatMessage an den Server geschickt. Diese Methode wurde als OneWay im OperationContract definiert, das bedeutet, es wird kein Funktionsergebnis zum Client zurück übertragen. Stattdessen ruft der Server mittels der Operation ReceiveMessage des Interface IPtChatCallback sämtliche Clients zurück (Callback), um die Nachricht im Netzwerk zu verteilen. Dem Delegaten PtChatEvent wird dabei beim Login/Register eine Adresse auf die Funktion ResponseEventHandler hinzugefügt (siehe Listing A.10). PtChat ist dabei als static deklariert, damit jede Instanz von PtChat auf diesen Delegaten zugreifen kann. public static event PtChatEventHandler PtChatEvent;
Die Variable callback (vom Typ IPtChatCallback) ist nicht als statische Variable definiert. Somit besitzt jede Instanz der Klasse PtChat (eine Instanz pro Session) über die Variable callback einen Verweis auf den Aufrufer der Operation Login oder Register. Der Aufrufer muss dabei vom Typ IPtChatCallback sein, was unser Clientformular ChatWindow auch ist.
371
Anhang A
> >
>
HINWEIS
Vielleicht erinnern Sie sich noch an die Instanzierung des Proxys am Client. Hier wurde dem Konstruktor ein InstanceContext übergeben. Dieser wird hier ausgelesen und der Variablen callback zugewiesen. //Hinzufügen der Adresse ResponseEventHandler für jede Instanz //(pro Session eine Instanz) PtChatEvent += new PtChatEventHandler(ResponseEventHandler); //Hier steckt pro Session die tatsächliche Callbackadresse zum Client dahinter callback = OperationContext.Current.GetCallbackChannel(); Listing A.10: Hinterlegen der Callback-Adressen
Innerhalb der Methode SendMessage wollen wir jetzt jede eingehende Nachricht an alle angemeldeten Clients weiterleiten. Dazu rufen wir einfach jede dem Delegate PtChatEvent zugeordnete Funktionsadresse explizit auf, wie Sie dies in Listing A.11 sehen. if (PtChatEvent != null) { //für jeden bereits dem Event zugeordneten Handler foreach (PtChatEventHandler handler in PtChatEvent.GetInvocationList()) { //wird die zugeordnete Methode aufgerufen //und das Responseobjekt weitergeleitet //in diesem Beispiel ist das die Routine //ResponseEventHandler, die die Nachricht //an den Client weiterleitet handler.Invoke(this, response); } } Listing A.11: Schleife über alle registrierten Callbacks
Dabei wird der Event nicht direkt aufgerufen, weil gewährleistet sein muss, dass der Kontext der Variablen callback, die im Eventhandler (siehe Listing A.12) angesprochen wird, sich auf die jeweilige Instanz von PtChat bezieht. Ansonsten würde die Nachricht nur an den Aufrufer zurückgesendet werden. private void ResponseEventHandler(object sender, ChatMessageResponse e) { //Aufruf des callbacks am Client callback.ReceiveMessage(e); } Listing A.12: Tatsächlicher Aufruf zum Client
372
Projektbeschreibung
A.3.6 Aufruf des Workflows Die Schnittstelle zu WF gestaltet sich in diesem Beispiel genau so, wie Sie es bisher in den meisten anderen WF-Projektbeispielen in diesem Buch mitverfolgen konnten. Da wir unsere Workflow-Logik in einer SEQUENZIELLEN WORKFLOWBIBLIOTHEK gekapselt haben, können wir auf diese innerhalb der WCF-Anwendung zugreifen, indem wir einen Verweis auf die Bibliothek ChatWorkflow setzen und gegebenenfalls noch den Namensraum ChatWorkflow mittels using-Direktive einbinden. Die nächsten Schritte werden Ihnen schon aus den vorhergehenden Beispielen bekannt sein. Wir definieren die Variablen, die wir innerhalb einer Workflow-Instanz benötigen, und übergeben diese in Form eines generischen Dictionarys der Methode CreateWorkflow unserer Workflow-Runtime als zweiten Parameter. Genauer gesagt wären das bei dem Login-Prozess der Nickname und das Passwort des Users, hier in der Klasse LoginData zusammengefasst. //Instanzierung der benötigten WorkflowRuntime using (WorkflowRuntime wf = new WorkflowRuntime()) { //Sperre, damit die Methode Register erst fertig durchlaufen wird //wenn der Workflow abgearbeitet ist //da es sich um einen sehr schnellen Workflow handelte //wurde an dieser Stelle auf einen asynchronen Aufruf verzichtet _handle = new AutoResetEvent(false); //Erstellen eines Dictionarys mit den Daten des zu registrierenden //Users zur Übergabe an den Workflow Dictionary dic = new Dictionary(); //Erstellen eines Objektes, das die Credentials des Chatters aufnimmt ChatWorkflow.LoginData ld = new ChatWorkflow.LoginData(); ld.NickName = user.User; ld.Password = user.Password; //Hinzufügen der Daten zum Dictionary dic["User"] = ld; //Instanzieren eines Workflows vom Typ Login mit Übergabe der //Daten aus dem Dictionary WorkflowInstance wi = wf.CreateWorkflow(typeof(ChatWorkflow.Login), dic); //Setzen eines Ereignishandlers (zum Auslesen des Rückgabewertes //und Freigeben der Sperre) wf.WorkflowCompleted += new EventHandler (wf_WorkflowLoginCompleted); //Starten des Workflows wi.Start(); //Blockieren der Routine Login, bis das Handle //in WorkflowLoginCompleted wieder freigegeben wird _handle.WaitOne(); … }
373
Anhang A static void wf_WorkflowLoginCompleted(object sender, WorkflowCompletedEventArgs e) { //Ergebnis der Registrierung wird aus den OutputParametern ausgelesen resultLogin = (LoginResult)e.OutputParameters["Result"]; //Sperre wird wieder freigegeben, ursprüngliche Login-Routine //wird weiter abgearbeitet _handle.Set(); } Listing A.13: Auslagerung des Login-Prozesses in einen Workflow
Voraussetzung für eine erfolgreiche Parameterübergabe in Listing A.13 ist natürlich wieder, dass es innerhalb der Workflow-Klasse Login eine öffentliche Eigenschaft gibt, die den gleichen Namen trägt wie der dem Parameterwert zugewiesene Schlüssel User in dem Dictionary. Nach dem Start der Workflow-Instanz rufen wir noch die WaitOne()-Methode der statischen Variablen _handle vom Typ AutoResetEvent auf. Dies sorgt dafür, dass der aktuelle Ausführungsthread so lange blockiert wird und an dieser Stelle stoppt, bis er von einem anderen Thread ein Signal erhält. Denn da wir keinen ManualWorkflowSchedulerService, sondern den DefaultWorkflowSchedulerService innerhalb der Workflow-Runtime benutzen, müssen wir die Ausführung der WCF-Methode anhalten, bis der Workflow seinen Ablauf beendet hat. Und genau das erledigen wir mit dem EventHandler wf_WorkflowLoginCompleted, in dem die Set()-Methode von _handle für die Freigabe des blockierten Threads sorgt und zuvor noch die relevanten Daten der Workflow-Instanz mittels OutputParameter extrahiert.
A.3.7 Hosting des Service Das Hosting unseres WCF-Service geschieht in der Konsolenapplikation. In der MainRoutine wird dazu, wie Sie in Listing A.14 sehen, einfach ein ServiceHost generiert, der ein Objekt vom Typ PtChat hostet, und der entsprechende Kommunikationskanal geöffnet. Die entsprechenden Konfigurationseinstellungen werden dabei aus der applikationsspezifischen Datei app.config gelesen. using using using using
System; System.Collections.Generic; System.Text; System.ServiceModel;
namespace ChatServer { class Program {
374
Projektbeschreibung /// /// Startroutine des Hostprogramms /// ServiceHost wird geöffnet und stellt den Typ PtChat /// mit den in der app.config gemachten Einstellungen zur Verfügung /// /// nicht ausgewertet static void Main(string[] args) { using (ServiceHost sh = new ServiceHost(typeof(PtChat))) { sh.Open(); Console.WriteLine("Chat bereit..."); Console.ReadLine(); } } } } Listing A.14: Program.cs
A.4 ChatWorkflow Wie kann man in einem Chat die Workflow Foundation integrieren? Als Antwort zu dieser Frage gibt es wohl mehrere Möglichkeiten, die mit Sicherheit den Einsatz von WF in einer Chat-Anwendung rechtfertigen. Wir haben uns in diesem Fall dafür entschieden, den Login-Prozess und die Registrierung in einer Workflow-Bibliothek abzubilden und anschließend von WCF aus zu nutzen. In Abbildung A.4 sehen Sie die Projektmappe. Sowohl bei dem Login als auch bei der Registrierung haben wir uns auf einen sequenziellen Workflow geeinigt, dessen verwendete Klassen in der Datei Data.cs zu finden sind.
Abbildung A.4: Projektmappe von ChatWorkflow
375
Anhang A
A.4.1 Registrierung als Workflow Der Einsatz des Workflows Registrate (vgl. Abbildung A.4) kommt dann zum Einsatz, wenn sich ein neuer User an dem Chat beteiligen möchte. Intern werden dabei die Daten des Benutzer, also der Nickname, das Passwort und die E-Mail-Adresse, von der WCF-Applikation an dem Workflow übergeben, der anschließend entscheidet, ob der Registrierungsvorgang durchgeführt werden kann oder nicht. Die Designeransicht des Workflows sehen Sie in Abbildung A.5. caUserExists prüft dabei, ob der gewünschte Nickname des aktuellen Benutzers schon in unserer Datenbank (der Einfachheit halber eine XML-Datei) vorhanden ist. Ist der Name noch nicht vorhanden, wird der entsprechende Registrierungsvorgang in Form der CodeActivity caRegistrate durchgeführt, andernfalls der Workflow mit einer entsprechenden Fehlermeldung mittels caCancel beendet (Abbildung A.5).
Abbildung A.5: Registrierung wird in einen Workflow ausgelagert.
376
Projektbeschreibung
A.4.2 Benutzer-Login als Workflow Der Login-Workflow gestaltet sich noch einfacher als der zuvor aufgezeigte Ablauf der Registrierung. Hier müssen wir nur überprüfen, ob der Nickname mit dem angegebenen Passwort auch so in der hinterlegten Benutzerdatenbank auftaucht. Diese Überprüfung erledigen wir, wie in Abbildung A.6 zu sehen, mit einer einzelnen CodeActivity, hier mit der Bezeichnung caSuccessfulOrNot, deren ExecuteCode Sie in Listing A.15 betrachten können.
Abbildung A.6: Workflow-Login mit einer einzelnen CodeActivity
private void caSuccessfulOrNot_ExecuteCode(object sender, EventArgs e) { //XMLPath gibt den Pfad zu der XML-Datei zurück, in der die Benutzer//daten gespeichert werden if (File.Exists(XMLPath)) { DataSet ds = new DataSet(); try { ds.ReadXml(this.XMLPath); } catch (Exception ex) { EventLog.WriteEntry("LoginWorkflow", ex.ToString()); throw ex; } DataView dv = new DataView(ds.Tables[0]);
377
Anhang A dv.RowFilter = "Name = '" + User.NickName + "' AND Password = '" + User.Password + "'"; if (dv.Count == 0) { this.Result = LoginResult.WrongUsernameOrPassword; dv = new DataView(ds.Tables[0]); dv.RowFilter = "Name='" + User.NickName + "'"; if (dv.Count == 0) { this.Result = LoginResult.UsernameNotExists; } else { this.Result = LoginResult.Successful; } } else { XmlDocument xmlDoc = new XmlDocument(); XmlDeclaration declaration = xmlDoc.CreateXmlDeclaration("1.0", "utf-8", null); xmlDoc.InsertBefore(declaration, xmlDoc.DocumentElement); XmlElement wurzelKnoten = xmlDoc.CreateElement("users"); xmlDoc.AppendChild(wurzelKnoten); try { xmlDoc.Save(this.XMLPath); } catch (Exception ex) { EventLog.WriteEntry("XML-Datei" + this.XMLPath + " erzeugen fehlgeschlagen", ex.ToString()); throw ex; } } } } Listing A.15: Ausführungsmethode von CodeActivity caSuccessfulOrNot
Je nachdem, ob ein entsprechender Eintrag in der XML-Datei gefunden wurde oder nicht, wird das entsprechende Ergebnis in der Property Result gespeichert und so mittels OutputParameter der WCF-Applikation zur Verfügung gestellt.
378
Index ! .NET 3.0 15, 18 .NET 3.5 19 .NET Framework 19 .NET Framework 3.0 17 .NET Remoting 237
A>>> ACID 231 ActiveCaptionBrushKey 84 Activity 245, 322 ActivityTrackPoint 270 AddService 265 AddServiceEndpoint 211 Adresse 140 Advanced Web Services 213 Aktivitäten 245 Als Webdienst veröffentlichen 305 Animatable 120 Animationen 126 BeginAnimation 132 BeginTime 132 für Entwickler 129 RepeatBehavior 132 Storyboards 127 Timing 127 AnimationTimeline 127 Annotations 115 app.config 315 ApplicationCommands 59 Applikationskonfigurationsdatei 161 ArcSegment 123 Counterclockwise 124 IsLargeArc 124 SweepDirection 124 ASP.NET Ajax 19 AspNetCompatibilityRequirementsMode 204 ASPNet-Kompatibilitätsmodus 200 Attribute 145
AuditLogLocation 227 Ausnahme 194 AutoResetEvent 248
B>>> BeginAnimation 132 BeginTime 132 Behavior 159 Benannte Parameter 144, 170 BezierSegment 123 Binding 95, 103, 140, 145, 146, 151, 210 Path 108 SetBinding 95 Binding-Elemente 146 Binding-Informationen 210 BP 1.1 213 Brushes 125 Build-Prozess 32 Button 50
C>>> CallExternalMethodActivity 287, 295 Canvas 35, 46 Alignment 48 Canvas.Bottom 46 Canvas.Left 46 Canvas.Right 46 Canvas.Top 46 Resizing 46 ZIndex 48 CardSpace 341 ChannelFactory 211 Checked 55 Claims 225 ClientChannel 164 CodeActivity 248, 254, 255, 271, 299, 302 Code-Behind Datei 30 CodeBehind-Attribut 201
379
Index
COM+-Applikationen 237 CombinedGeometry 120 ComboBox 52 CommandBinding 59 Common Language Runtime 19 CompletedScope 263 CompletedStateName 253 ComSvcConfig 237 ConcurrencyMode 230 ConditionedActivityGroup 245, 329, 330 Content 65 ContentControl 50, 53 Content-Datei 60 ContextMenu 52 Contract 140, 142, 151, 168 ContractFirst 142 Control Panel Applet 351 Cookiecontainer 311 Cookies 311 CreateChannel 212 CurrentStateName 261 Custom Activities 326 Custom Binding 147
Datenserialisierung 168 Datenstrukturen 140 DCOM 138 Debuggen 256, 276 DefaultWorkflowSchedulerService 281, 285 Deklarative Regelbedingung 250, 255, 274, 281, 329 DelayActivity 262, 266 Dependency Properties 39 abhängige Eigenschaften 39 DependencyProperty 324, 326 Deserialisierer 185 Dictionary 251 Digitale Karten 342, 344 Digitale Zertifikate 349 Direktive 200 DockPanel 35, 43 DockPanel.Dock 43 DocumentViewer 114 Domain Specific Language 140 DoubleAnimation 128 Duplex 141 Dynamische Anschlüsse 306
D>>> DataContext 108 DataContract 168, 185 datacontractonly 171 DataContractSerializer 168 DataMember 168 DataTemplates 102 Datenbindung 91 Default 94 ElementName 93 Explicit 94 IsSynchronizedWithCurrentItem 108 LostFocus 94 Master-Detail 104 ObjectDataProvider 96 OneTime 92 OneWay 92 OneWayToSource 92 Path 93 PropertyChanged 94 TwoWay 92 UpdateSourceTrigger 94
380
E>>> Eigene Aktivitäten 322 Einsatzgebiete 242 Einzug 36 EllipseGeometry 120 Rect 121 Encoder 145 Endpoint 139, 154, 168 EndpointBehavior 218 Endpunkt 139, 148, 200 Enterprise Services 138 Ereignisbehandlung 33 Ereignisprotokoll 228 EventDrivenActivity 304 Exceptionobjekt 193 Execute 323 Expression 21 eXtensible Application Markup Language 29 ExtensionData 177 ExtensionDataObject 177
Index
Extensions 17 ExternalDataEventArgs 288 ExternalDataExchange 286 ExternalDataExchangeService 292
F>>> Farbübergang 125 Fault 330 Fault Handling 326 FaultContract 194 Faulted-Event 205 FaultException 195 FaultHandler 333 Fehlerbehandlung 192 in WF 326 Fehlerhandler anzeigen 330 Figure 113 Fixdokumente 109 Fixed Documents 113 FixedDocumentSequence 114 Floater 113 FlowDocument 110 FlowDocumentPageViewer 112 Flussdokumente 109 FontSize 40 Frames 126 FrameworkElement 50
G>>> g.cs 33 Geometry 119 GeometryGroup 120 GradientBrush 125 GradientStops 125 Grid 35 Grid.Column 41 Grid.Row 41 Margin 37 relative Positionierung 35 ShowGridLines 36
H>>> Haltepunkt 256 HandleExternalEventActivity 254, 287, 297 HeaderdItemsControl 54 Hello World 246 sequenziell 246 statuscomputer 251 Host 196 Hosting 156, 246, 262 HyperLink 66
I>>> ICompensatableActivity 263 Identitätsaussteller 344 Identitätsforderer 344, 350 Identity Model 225 Identity Provider 344 IExtensibleDataObject 177, 179, 182 IfElseActivity 245, 255, 256, 299 IIS 198 IIS-Prozessmodell 209 Indigo 137 Information Card 346 InitializeComponent 35 InitialStateName 253 Inline 68 Inline Codierung 30 INotifyCollectionChanged CollectionChanged 97 INotifyCollectionChanged 97 InputActivityName 302 InputGestureText 54 Installerklasse 207 installutil 208 InstanceState 263, 266 InstancingContextMode 230 Interaktion XAML und Code-Behind 30 Interface 143, 163 Internet-Informationsdienste 198 InvokeWebServiceActivity 306, 307 IsActivating 301, 314 IsCheckable 55 IsolationLevels 231
381
Index
IsRequired 188 ItemsControl 95
K>>> Karten sichern 351 Kerberos-Authentifizierung 349 Known Types 185 KnownType-Attribut 185 Komplexere Datenstrukturen 168 Konfigurationsdateien 148 Kontextmenü 53, 56
L>>> Label 50 language-Parameter 163 Laws of Identity 342 Layout 35 Lebenszeit 208 Line 122 LineGeometry 120, 122 LineSegment 123 LINQ 19 ListBox 51, 52, 95 ListBoxItem 52 ListenActivity 302, 304 Listener 232 ListView 52 LoadComponent 33 LocBaml 68, 72, 73 out 74 parse 74 Satelliten-Assemblies 68 Logging 232 Lokalisierung 68 .restext 69 CultureInfo 71 LocBaml 68, 72 Projektdatei PropertyGroup 72 ResourceManager 71 GetString 70, 71 Satelliten-Assemblies 68 Uid 72 updateid 72 Lose Koppelung 140
382
M>>> ManualSchedulerService 315 ManualWorkflowSchedulerService 285 Margin 35, 37 Master-Detail 104 MaxSimultaneousWorkflows 281 MediaElement 59 Clock Mode 61 Independent Mode 61 LoadedBehavior 60 MediaOpened 64 MediaState 60 Pause 62 Play 61, 62 Source 60 Stop 62 MenuBase 52 Menüleisten 54 Menüs 53 ContextMenu 56 MenuItem.Command 54 Shortcuts 54 MenuItem 54 MessageAuthenticationAuditLevel 227 MessageClientCredentialType 223 MessageLogging 232 Messages 139, 217 Metadaten 159 Microsoft Expression 21 Microsoft Expression Blend 21, 134 Microsoft Expression Design 21, 22 Microsoft Expression Media 21 Microsoft Expression Web 21 Microsoft Message Queues 138 Microsoft Passport 341 Microsoft Windows SDK 18 MSBuild 72 MSMQ 218 MTOM 214
N>>> Nachrichten 139 Nachrichtenwarteschlangen 147 NetDataContractSerializer 169 NT Services 205
Index
O>>> OASIS 213 ObjectDataProvider 96 ObjectType 96 ObservableCollection 98, 104 OLE Transactions 231 OnPropertyChanged 104 String.Empty 101 OnStart 205 OnStop 205 OperationBehavior 218 OperationBehaviorAttribut 219 OperationContract 143 Operationen 139 Orcas 18
P>>> Padding 35 Page 29 Panel ZIndex 48 Paragraph 110 ParallelTimelines 127 Parameterübergabe 251 PasswordBox 51 PasswordChar 51 Path 120 PathFigure IsClosed 124 PathGeometry 120, 123 Persistence Service 262 Persönliche Karten 345 PolyBezierSegment 123 PolyLineSegment 123 PolyQuadraticBezierSegment 123 Port 206 Positionierung 35 PrincipalPermission-Attribut 225 Projektvorlagen 242 Sequenzieller Workflow 242 Statuscomputer-Workflow 242 propdp 325 PropertyChangedEventHandler 99 String.Empty 99
PropertyGroup 72 Prozessrecycling 209
Q>>> QuadraticBezierSegment 123
R>>> RadioButton 66 read-only 30 Rectangle 121 Rect 122 RectangleGeometry 120, 121 Regeleditor 256 Relying Party 344 RenderTransform 133 RePaint 83 RepeatBehavior 132 Request/Reply 141 Request-Parameter 188 ResourceManager 70 Resources 78, 79, 81 FindResource 82 Key 79 Response-Parameter 189 RessourceKey 84 Round Tripping 178, 182 Roundtrip 182 RoutedPropertyChangedEventArgs NewValue 63 OldValue 63 Run 68
S>>> Satelliten-Assemblies 68 Scheduling Service 280 Schnittstellen 142 schon 19 SDK 17 Security Policy 349 Security Token 344, 348 Security Token Service 351 SelectedItem 52 Selfhosting 196
383
Index
SequenceActivity 281 SequentialWorkflowActivity 253 Sequenzielle Workflow-Bibliothek 287 Sequenzieller Workflow 242, 244 Serialisierern 172 Serializable 275, 312 Service Configuration Editor 148, 153, 159 Service Control Manager 205 Service Trace Viewer 236 ServiceAuthorizationAuditLevel 228 ServiceBehavior 218 ServiceBehaviorAttribut 219 ServiceContract 143, 151 ServiceHost 157, 200, 205 serviceHostingEnvironment 204 Servicemodell 140 ServiceModelReg 198 Serviceorientierte Architektur 139 serviceSecurityAudit 227 Serviceverhalten 158 Session 144, 228 SessionMode 229 SetStateActivity 256, 257, 260 Shape 120 PathFigure StartPoint 123 SharedConnectionWorkflowCommitWor kBatchService 269 Simplex 141 Slider 63 RoutedPropertyChangedEventArgs 6 3 SmartCards 349 Smarttag 302 SOA 139, 239 SOAP 350 SOAP-Nachricht 146 Software Factory 139 SolidColorBrush 81, 125 Sparcle 134 SQL Tracking Service 268 SqlTrackingQuery 277 SqlTrackingQueryOptions 277 SqlWorkflowPersistenceService 263
384
StackPanel 35 FlowDirection 41 Grid.Column 41 Orientation 41 Startprojekte 166 State hinzufügen 253 StateActivity 253 StateInitializationActivity 253, 257, 260 Statemachine 244 StateMachineWorkflowActivity 253 27 StatusBar 52 Statuscomputer Workflow 242, 244 Storyboards 127, 128 TargetName 128 StreamGeometry 120 Stroke 121 Style 85, 89 EventSetter 85 Setter 85 TargetType 87 SuppressAuditFailure 228 svc 199 svclog 235 svcutil 158, 171 System.Diagnostics 232 System.Runtime.Serialization 168, 177 System.ServiceModel 140, 193 System.ServiceModel Trace Source 232 System.ServiceModel.Activation 204 System.ServiceModel.MessagingLogging Trace Source 232 System.Windows 27 System.Windows.Annotations 115 System.Workflow.Activities 276 System.Workflow.Component Model 276 System.Workflow.Runtime 276
T>>> TargetName 128 TargetStateName 256 TargetType 87, 89 Templates 78 TextBlock 66
Index
TextBox 50 TextElement 68 Thickness 37 ThrowActivity 329 TileBrush 125 Timelines 127 Timermechanismus 315 Tokens 225 Toolbar 57 TraceLevel 232 Tracesource 232 Tracing 232 TrackData 270, 275 Tracking Service 268 TrackingProfile 269, 271 TrackingProfileDesigner 271 TrackingProfileXML 270 TransactionScope 231 Transaktion 231 TranslateTransform 132 XProperty 132 YProperty 132 Transportprotokoll 146, 151 TreeView 52 Trigger 89
U>>> UnChecked 55 Unterstützte Betriebssysteme 17 UpdateSourceTrigger Path 94 UseActiveTimers 315 UserTrackingRecord 275, 278 UserTrackPoint 270
V>>> Versionieren 192 Versionierung 188 Verwaltete Karten 349 Verweis hinzufügen 315 Vista 157 Visual Studio 2005 Erweiterungen 30 Visual Studio Orcas 19 Vorlagen 78
W>>> WAS 209 Web Service 138, 301 Web Service *-Protokolle 350 Web Service Description Language 141 web.config 200, 314 Webdienste 139 Proxy 307 WebServiceInputActivity 301, 302, 313 WebServiceOutputActivity 302, 313 Werkzeugleisten 58 WF_WorkflowInstanceId 318 WhileActivity 245, 248, 250, 271, 281 Window 27 FontSize 40 Windows Activation Service 209 Windows CardSpace 16, 342 Windows Communication Foundation 15, 137 Windows Communication Foundation Activation Components 198 Windows Presentation Foundation 15 Windows Presentation Foundation/ Everywhere 20 Windows SDK 18, 147, 236 Windows Vista 137 Windows Workflow Foundation 16 Windows-Dienst 205 Workflow-Aktivitätsbibliothek 323 WorkflowCompleted 248, 265 WorkflowIdled 265 WorkflowLoaded 265 WorkflowPersisted 265 WorkflowRuntime 246, 248, 251, 255, 262, 269, 281, 286, 289 WorkflowTrackPoint 270 WPF/E 20 WPF/E SDK 20 WPFEPAD 21 WS-* 213 WS-AT 231 WS-Basic Profile 213 WSDL 141, 214 WSE 3.0 238 WS-I 213 WS-MetaDataExchange 350
385
Index
WS-Security 213 WS-SecurityPolicy 350 WS-Trust 350
X>>> X.509-Zertifikate 351 XAML 25 Compiler 32 Extension 80 Tools 134
386
XAMLPad 29 XML-Konfigurationsdatei 148 XmlSerializer 168 XOML 319 XpsDocument 114 xsd-Schemadatei 171
Copyright Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell angebotenen eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook stellen wir lediglich als persönliche Einzelplatz-Lizenz zur Verfügung! Jede andere Verwendung dieses eBooks oder zugehöriger Materialien und Informationen, einschliesslich •
der Reproduktion,
•
der Weitergabe,
•
des Weitervertriebs,
•
der Platzierung im Internet, in Intranets, in Extranets,
•
der Veränderung,
•
des Weiterverkaufs
•
und der Veröffentlichung
bedarf der schriftlichen Genehmigung des Verlags. Insbesondere ist die Entfernung oder Änderung des vom Verlag vergebenen Passwortschutzes ausdrücklich untersagt! Bei Fragen zu diesem Thema wenden Sie sich bitte an:
[email protected] Zusatzdaten Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei. Die Zurverfügungstellung dieser Daten auf unseren Websites ist eine freiwillige Leistung des Verlags. Der Rechtsweg ist ausgeschlossen. Hinweis Dieses und viele weitere eBooks können Sie rund um die Uhr und legal auf unserer Website
http://www.informit.de herunterladen