(
KOMPENDIUM
)
Visual C# 2008
Kompendium Kompetent aufbereitetes PC-Know-how für alle Die KOMPENDIEN aus dem Markt+Technik Verlag stehen seit mehr als 20 Jahren für anerkanntes Expertenwissen und bieten wertvolle Praxistipps in allen Fragen rund um den PC. Das Portfolio der Handbücher reicht von der übersichtlichen Vorstellung diverser Programmiersprachen bis hin zur umfangreichen Beschreibung kompletter Betriebssysteme: Mit mehr als 500 Titeln seit Bestehen der Reihe wurde nahezu jede Fragestellung der Computerpraxis abgedeckt. Ob als Lehrbuch für den ambitionierten Einsteiger oder Nachschlagewerk für den erfahrenen Anwender: Die übersichtlichen, klar strukturierten KOMPENDIEN helfen jedem schnell weiter und auch komplexe Sachverhalte werden mit praxisnahen Beispielen übersichtlich illustriert und verständlich gemacht. Ein detailliertes Inhaltsverzeichnis und ein umfangreicher Index ermöglichen dem Leser außerdem schnellen Zugriff auf die gesuchten Informationen. Technisch anspruchsvoll und präzise, dabei jedoch immer praxisbezogen und klar verständlich: Das sind die KOMPENDIEN, die mit mehr als 6 Millionen Lesern zu den erfolgreichsten Computerfachbüchern auf dem deutschsprachigen Markt gehören.
Visual C# 2008 Windows-Programmierung mit dem .NET Framework 3.5 JÜ R G E N B AYE R
(
KOMPENDIUM Einführung | Arbeitsbuch | Nachschlagewerk
)
Bibliografische Information Der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Die Informationen in diesem Buch werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen 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 Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
10 9 8 7 6 5 4 3 2 1 10 09 08 ISBN 978-3-8272-4339-3
© 2008 by Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Covergestaltung: Thomas Arlt,
[email protected] Bildagentur: Masterfile Deutschland GmbH, Düsseldorf Titelfoto: Blurred View of People on Escalators, Fotograf: Mike Dobel Fachlektorat: Heiko Lehnert,
[email protected] Lektorat: Sylvia Hasselbach,
[email protected] Korrektorat: Simone Meißner,
[email protected] Herstellung: Elisabeth Prümm,
[email protected] Satz: Reemers Publishing Services GmbH, Krefeld Druck und Verarbeitung: Kösel, Krugzell (www.KoeselBuch.de) Printed in Germany
Überblick
Überblick
Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
Grundlagen
33
Kapitel 1
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
Kapitel 2
Einführung in die Arbeit mit Visual Studio 2008 . . . . . . . . . . . . . . . . . .
87
Kapitel 3
Die Sprache C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
141
Kapitel 4
Grundlegende OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
231
Kapitel 5
Weiterführende OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
281
Kapitel 6
OOP-Specials . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
341
Kapitel 7
Arrays und Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
373
Kapitel 8
Grundlegende Programmiertechniken . . . . . . . . . . . . . . . . . . . . . . . . .
441
Kapitel 9
Teil 1
Fehler debuggen, testen und protokollieren . . . . . . . . . . . . . . . . . . . .
543
Kapitel 10 Arbeiten mit Dateien, Ordnern und Streams . . . . . . . . . . . . . . . . . . . . .
599
Kapitel 11
LINQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
649
Teil 2
Anwendungen entwickeln
721
Kapitel 12 WPF-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
723
Kapitel 13 WPF-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
785
Kapitel 14 Wichtige WPF-Techniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
867
Kapitel 15 Konfiguration, Ressourcen und Lokalisierung . . . . . . . . . . . . . . . . . . . .
935
Kapitel 16 Windows-Anwendungen verteilen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
969
Teil 3
Daten verwalten
Kapitel 17
Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1007
1005
Kapitel 18 XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1031 Kapitel 19 Datenbanken mit LINQ to SQL bearbeiten . . . . . . . . . . . . . . . . . . . . . . . 1079 5
Überblick
Teil 4
Fortgeschrittene Programmierung
1137
Kapitel 20 Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1139 Kapitel 21 Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1205 Kapitel 22 Assemblys, Reflektion und Anwendungsdomänen . . . . . . . . . . . . . . . . 1229 Kapitel 23 Sicherheitsgrundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1257
Teil 5
Anhang
Kapitel A
Glossar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1283
Kapitel B
Die ersten 255 Zeichen des Unicode-Zeichensatzes. . . . . . . . . . . . . . . . 1299
Kapitel C
Meine Namenskonvention für Steuerelemente . . . . . . . . . . . . . . . . . . . 1301
1281
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1305
6
Inhalt
Inhalt
Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
Teil 1
Grundlagen
33
Kapitel 1
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
1.1
Zum Buch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Zielgruppe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Inhalt dieses Buchs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Inhalt der Buch-DVD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Buch-Blog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typografische Konventionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35 35
1.2
Das Glossar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
1.3
Die Installation von Visual Studio 2008 und der Express-Editionen . . . . . . . . . Die Visual-Studio-2008-Editionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Betriebssystem und Speicheranforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . Vorinstallationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Installation der Express-Editionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Installation von Visual Studio 2008 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Weitere hilfreiche Installationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der erste Start von Visual Studio 2008 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46 46
1.4
1.5
Wie erhalten Sie Hilfe zu Problemen? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die .NET Framework-Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Codebooks und Kochbücher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Suchen im Internet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wichtige .NET-Websites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wichtige Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objekte und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ereignisorientiertes Programmieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36 43 43 44
47 47 47 49 50 51 52 52 52 53 53 57 59 59 60 61
7
Inhalt
Zeichencodierung, Unicode, UCS-2, UCS-4, UTF-8, UTF-16, UTF-32 . . . . . . . . . . . . . XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6
1.7
1.8
62 63
Das .NET Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Was ist .NET? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Was ist das .NET Framework? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CIL-Code und der Just-In-Time-Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Common Language Runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Garbage Collector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klassenbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Tools des .NET Framework und des Microsoft Windows SDK für das .NET Framework
67 68
Die Möglichkeiten von .NET-Programmen . . . . . . . . . . . . . . . . . . . . . . . . . . »Normale« Windows-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Komponentenbasierte Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Client/Server-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Webanwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenbankzugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anwendungen für mobile Geräte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
75 75
68 70 71 72 72 73
76 76 76 77 77
Typen, Namensräume, Assemblys und Module . . . . . . . . . . . . . . . . . . . . . . . Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namensräume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Assemblys und Module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Assemblys mit und ohne starkem Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Global Assembly Cache (GAC) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Referenzierung von Assemblys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Versionsverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verwaltung der Zugriffsrechte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
77 77
1.9
Der .NET-Reflector: Ein wichtiges Tool zur Erforschung von Assemblys . . . . . . .
85
Kapitel 2
Einführung in die Arbeit mit Visual Studio 2008 . . . . . . . . . . . . . . . . . . . . . .
87
2.1
Die (wichtigen) C#-Projekttypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
88
2.2
Projekte und Projektmappen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
2.3
Der Start von Visual Studio und das Erzeugen bzw. Öffnen von Projekten . . . . .
91
2.4
Einstellung der Entwicklungsumgebung . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
2.5
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel . . . . . . . Positionierung der Fenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Projektmappen-Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klassenansicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Code-/Designer-Bereich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95 96
8
78 78 80 81 83 84 85
96 98 98
Inhalt Die Toolbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Formular-Designer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Eigenschaftenfenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Code-Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IntelliSense . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testen der Beispielanwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6
99 100 101 103 105 106
Weitere wichtige Fenster der IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die dynamische Hilfe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Aufgabenliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Objektbrowser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
106 107
Kompilieren und Ausführen einer Projektmappe . . . . . . . . . . . . . . . . . . . . . Kompilieren einer Projektmappe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Projektmappen ausführen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
108 109
Entwicklung einer einfachen Windows-Anwendung . . . . . . . . . . . . . . . . . . . Das Projekt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Formular . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Programmierung in einer ersten Version . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Programmierung in einer fehlerfreien und benutzerfreundlichen Version . . . . . . Verwenden von anderen Ereignissen am Beispiel eines Längenumrechners . . . . . . .
111 112
Entwicklung einer Konsolenanwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmierung an der Konsole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testen von Konsolenanwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
119 119
Grundlagen zum Debuggen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kompilierfehler beseitigen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausnahmen debuggen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Debuggen logischer Fehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
122 123
2.11
Grundlagen zum Verteilen einer Anwendung . . . . . . . . . . . . . . . . . . . . . . . .
126
2.12
Optionen der Entwicklungsumgebung. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
126
2.13
Projekteigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
127
2.14
Weitere Features von Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lesezeichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quellcode-Verknüpfungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Codeausschnitte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Codeausschnitte in der Toolbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wichtige weitere Features des Code-Editors . . . . . . . . . . . . . . . . . . . . . . . . . . . Makros und Add-Ins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
128 128
2.7
2.8
2.9
2.10
107 108
110
112 113 114 116
122
123 125
128 129 130 130 131 132
9
Inhalt
Server-Explorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Editoren für XML, HTML, CSS, Ressourcen, Bitmaps, Cursor und Icons . . . . . . . . . . . . Suchen mit Ergebnisliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inkrementelle Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flexible Suche mit regulären Ausdrücken und Platzhaltern . . . . . . . . . . . . . . . . . Der Zwischenablagering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Vorlagen für Projekte und Projekt-Elemente . . . . . . . . . . . . . . . . . . . . . .
132
Kompilieren ohne Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Kommandozeilencompiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . SharpDevelop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
135 135
Neues in Visual Studio 2008 und in den neuen Express-Editionen . . . . . . . . . . Neue Projekttypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Neue Features für die Arbeit mit Daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Neues im Code-Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sonstige Neuerungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
137 137
Kapitel 3
Die Sprache C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
141
3.1
Die Grundlage einer C#-(Windows-)Anwendung . . . . . . . . . . . . . . . . . . . . . Konsolenanwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Windows.Forms-Windows-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . .
141 142
3.2
Assemblys und Namensräume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
144
3.3
Bezeichner und Schlüsselwörter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
145
3.4
Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Elementare Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anweisungsblöcke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sichere und unsichere Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Aufruf von Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
147 147
(Daten)Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typsicherheit und der Typ Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wert- und Referenztypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Standardwerte der verschiedenen Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nullables: Werttypen mit der Möglichkeit nichts (null) zu speichern . . . . . . . . . . . . Freigeben von Objekten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Typ String als Ausnahme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersicht über die Standardtypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Instanzmethoden der Standardtypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
155 155
2.15
2.16
3.5
10
133 133 133 133 134 134
136
138 138 139
143
148 149 150 152
156 164 164 167 170 171 171 172
Inhalt Klassenmethoden und -eigenschaften der Standardtypen . . . . . . . . . . . . . . . . . . Integer-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fließkommatypen und der Typ decimal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Über- und Unterläufe und spezielle Werte . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datumswerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichen und Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Typ Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konvertierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufzählungen (Enumerationen) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6
173 174 175 177 181 182 185 188 192
Variablen und Konstanten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Deklaration von Variablen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Array-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implizit typisierte lokale Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
195 196
3.7
Namensrichtlinien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
200
3.8
Ausdrücke und Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Typ eines Ausdrucks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arithmetische Ausdrücke und Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bitoperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zuweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vergleiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Logische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ?:, ??, typeof, is, as, sizeof und => . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
201 201
Verzweigungen und Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die if-Verzweigung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die switch-Verzweigung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die while-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die do-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die for-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die foreach-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
216 216
3.10
Präprozessor-Direktiven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
228
Kapitel 4
Grundlegende OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
231
4.1
Klassen und Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Unterschiede . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Deklaration von Klassen und Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . .
231 231
4.2
Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
235
4.3
Kommentieren der Elemente eines Typs . . . . . . . . . . . . . . . . . . . . . . . . . . .
237
3.9
197 197 198
202 205 207 208 211 212
218 223 224 225 226
233
11
Inhalt
4.4
Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Deklaration von Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der this-Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überladene Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ref- und out-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variable Argumente mit params . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rekursive Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
239 239
Eigenschaften: Kapselung von Daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kapselung in Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schreibgeschützte Felder und Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigenschaften mit unterschiedlichen Gültigkeitsbereichen für das Schreiben und das Lesen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Automatisch implementierte Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . Lesegeschützte Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstante Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
250 250
4.6
Der Gültigkeitsbereich von Klassen, Strukturen und deren Elementen . . . . . . .
259
4.7
Anonyme Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
260
4.8
Indexer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
263
4.9
Konstruktoren, Finalisierer, Dispose und using . . . . . . . . . . . . . . . . . . . . . . . Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Finalisierer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Close- und die Dispose-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
266 266
Statische Klassenmember und statische Klassen . . . . . . . . . . . . . . . . . . . . . . Statische Felder und Eigenschaften (Klasseneigenschaften) . . . . . . . . . . . . . . . . . Statische Klassen: Globale Daten in statischen Eigenschaften . . . . . . . . . . . . . . . . Statische Methoden (Klassenmethoden) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statische Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
273 273
277
4.11
Organisieren von Typelementen mit Hilfe von Regionen . . . . . . . . . . . . . . . .
278
Kapitel 5
Weiterführende OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
281
5.1
Partielle Klassen, Strukturen (und Schnittstellen) . . . . . . . . . . . . . . . . . . . . .
281
5.2
Verschachtelte Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
283
5.3
Vererbung und Polymorphismus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ableiten von Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erweitern von Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Neudefinieren von Methoden und Eigenschaften und Zugriff auf geerbte Elemente . . Polymorphismus, virtuelle Methoden und virtuelle Eigenschaften . . . . . . . . . . . . .
284 285
4.5
4.10
12
240 242 244 247 248
254 256 258 259 259
269 271
275 276
285 288 292
Inhalt Polymorphismus extrem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung von Konstruktoren und Finalisierern . . . . . . . . . . . . . . . . . . . . . . . .
295
5.4
Abstrakte Klassen, Eigenschaften und Methoden . . . . . . . . . . . . . . . . . . . . .
299
5.5
Versiegelte Klassen und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
303
5.6
Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Entwurfsrichtlinien für Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schnittstellen deklarieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schnittstellen implizit implementieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Explizite Implementierung der Member einer Schnittstelle . . . . . . . . . . . . . . . . . . Mit Schnittstellen arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Polymorphismus über Schnittstellen im Vergleich zu Polymorphismus über Vererbung Schnittstellenelemente überschreiben und neu definieren . . . . . . . . . . . . . . . . .
303 304
Operatoren für eigene Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unäre und binäre Operatoren überladen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operatoren für Konvertierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
312 314
5.8
Wichtige Methoden für eigene Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
317
5.9
Delegaten, anonyme Methoden und Lambda-Ausdrücke . . . . . . . . . . . . . . . . Delegaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anonyme Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vordefinierte Delegaten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lambda-Ausdrücke für Delegaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kovarianz und Kontravarianz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Multicast-Delegaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Delegaten im Vergleich zu Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
320 320
Benachrichtigung des Aufrufers über Ereignisse und partielle Methoden . . . . . Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Partielle Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Unterschied zwischen Delegaten, Ereignissen und partiellen Methoden . . . . . . .
329 330
Klassenbibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassenbibliotheken entwickeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassenbibliotheken referenzieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
337 338
Kapitel 6
OOP-Specials . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
341
6.1
Erweiterungsmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erweiterungsmethoden und Polymorphismus . . . . . . . . . . . . . . . . . . . . . . . . . Erweiterungsmethoden für Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
341 344
5.7
5.10
5.11
298
304 306 307 309 310 310
316
322 323 324 326 327 329
334 337
339
344
13
Inhalt
6.2
Generische Typen und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Typen im Vergleich zu normalen Typen . . . . . . . . . . . . . . . . . . . . . . Ableiten von generischen Klassen und Schnittstellen . . . . . . . . . . . . . . . . . . . . . Generische Methoden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Automatischer Typrückschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typparameter einschränken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das default-Schlüsselwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Schnittstellen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Delegaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
345 345
Lambda-Ausdrücke und Ausdrucksbäume . . . . . . . . . . . . . . . . . . . . . . . . . . Lambda-Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausdrucksbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
356 356
Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vordefinierte Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
366 367
Kapitel 7
Arrays und Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
373
7.1
Grundsätzliche Unterschiede . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Assoziative Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Spezielle Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Individualisierbare Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
374 374
Übersicht über die aktuellen Auflistungen (inkl. Performance-Vergleich) . . . . . Merkmale und Performance der aktuellen Auflistungen . . . . . . . . . . . . . . . . . . . Das Ergebnis meines Performance-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
376 376
Die Schnittstellen der Arrays und Auflistungen . . . . . . . . . . . . . . . . . . . . . . . IEnumerable, IEnumerable, IEnumerator und IEnumerator . . . . . . . . . . . . ICollection und ICollection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IList und IList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IDictionary und IDictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . .
380 381
6.3
6.4
7.2
7.3
347 348 348 349 349 353 353 354
357
370
374 375 376 376
379
382 383 384
7.4
Eigene Implementierung eines Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . .
385
7.5
Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arrays erzeugen und verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arrays initialisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implizit typisierte Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
389 389
14
390 391 391
Inhalt 7.6
7.7
Arrays aus Arrays (Jagged Arrays) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zuweisungen von Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden und Eigenschaften eines Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . .
392
Die »normale«, aktuelle Auflistung List . . . . . . . . . . . . . . . . . . . . . . . . . Erzeugen und Füllen einer List-Auflistung . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf die verwalteten Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Durchgehen einer List-Auflistung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Entfernen von Objekten aus der Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sortieren einer Auflistung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
396 399
393 394
401 401 402 404
Suchen in Auflistungen (am Beispiel der List-Auflistung) . . . . . . . . . . . . . Suchen mit IndexOf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene sequenzielle Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Suchen mit BinarySearch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Suchen mehrerer Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Prädikatbasierte Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Performance-Vergleich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
405 406
7.8
Bearbeiten aller Objekte eines Arrays oder einer List-Auflistung mit ForEach
411
7.9
Optimierung einer Auflistung mit vielen Objekten . . . . . . . . . . . . . . . . . . . .
412
7.10
Aktuelle assoziative Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Schlüssel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erzeugen und Füllen einer assoziativen Auflistung . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf die verwalteten Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Durchgehen der verwalteten Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Entfernen von Objekten aus der Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Assoziative Auflistungen mit speziellen Schlüssel-Vergleichsobjekten . . . . . . . . . . .
413 414
Spezielle aktuelle Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ReadOnlyCollection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HashSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . LinkedList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . BitArray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . BitVector32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
422 422
Individualisierbare Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . KeyedCollection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
434 434
7.11
7.12
7.13
Übersicht über die weniger interessanten, alten Auflistungen des .NET Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
406 407 407 408 410
415 417 418 420 421
423 424 425 428 429 431
435 437
15
Inhalt
Kapitel 8
Grundlegende Programmiertechniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
441
8.1
Arbeiten mit Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Standard-Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Ausnahmeprinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Werfen einer Ausnahme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Direktes Abfangen von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Weiterwerfen einer Ausnahme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Auswerten innerer Ausnahmen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Stack-Trace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schachteln von Ausnahmebehandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ignorieren von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der finally-Block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Globale Ausnahmebehandlung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
441 442
Arbeiten mit Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strings vergleichen und Teilstrings suchen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Teilstrings extrahieren, Strings kürzen und auffüllen . . . . . . . . . . . . . . . . . . . . . Teilstrings ersetzen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strings einfügen und löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strings in Groß- und Kleinschreibung umwandeln . . . . . . . . . . . . . . . . . . . . . . Trennen und Zusammensetzen von Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . Die StringBuilder-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
460 464
Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Prinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Musterzeichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strings auf ein Muster testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kompilieren eines regulären Ausdrucks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Performancevergleich der statischen mit den Instanzmethoden und mit vorkompilierten Assemblys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fundstellen auswerten: Das Match-Objekt. . . . . . . . . . . . . . . . . . . . . . . . . . . . Festlegen, dass ein String mit einem Muster beginnen und/oder enden muss . . . . . . In mehrzeiligen Strings suchen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Groß- und Kleinschreibung ignorieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gruppierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Faule Quantifizierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rückreferenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ersetzen von Teilstrings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flexibles Ersetzen über einen Match-Evaluator . . . . . . . . . . . . . . . . . . . . . . . . . Strings splitten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einige Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
472 473
8.2
8.3
16
444 446 448 450 452 454 455 456 456 458
466 466 467 467 468 469
474 478 479 481 482 483 483 484 484 488 489 490 492 492 492
Inhalt 8.4
Formatierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zahlformatierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datumsformatierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kulturspezifisches Formatieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
493 494
Eingaben überprüfen und parsen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eingaben überprüfen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parsen von Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parsen spezifischer Datumsangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
501 501
Arbeiten mit Datumswerten und Zeitspannen . . . . . . . . . . . . . . . . . . . . . . . Der Offset zur UTC-Zeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wichtige Eigenschaften und Methoden der DateTimeOffset-Struktur . . . . . . . . . . . . Grundlegende Arbeit mit DateTimeOffset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konvertieren nach DateTime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datumswerte vergleichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mit Zeitspannen arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
506 506
Mathematische Berechnungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersicht über die Methoden und Eigenschaften der Math-Klasse . . . . . . . . . . . . . Winkelberechnungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zahlen runden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
513 513
8.8
Meldungen mit Hilfe der MessageBox-Klasse ausgeben . . . . . . . . . . . . . . . . .
521
8.9
Wichtige Diagnose-Hilfsmittel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
526
8.10
Auswerten von Befehlszeilenargumenten . . . . . . . . . . . . . . . . . . . . . . . . . .
527
8.11
Dokumentation der Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Elemente der C#-Dokumentationskommentare . . . . . . . . . . . . . . . . . . . . . . Erstellen der Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
529 529
Umgang mit dem Garbage Collector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Arbeitsweise des Garbage Collectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vorkehrungen zur Optimierung des Speicherverbrauchs . . . . . . . . . . . . . . . . . . . Manuelles Aufrufen des GC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
538 538 540
Kapitel 9
Fehler debuggen, testen und protokollieren . . . . . . . . . . . . . . . . . . . . . . . .
543
9.1
Fehler suchen und beseitigen (Debugging). . . . . . . . . . . . . . . . . . . . . . . . . . Voraussetzungen und Grundeinstellungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . Anhalten des Programms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Basis-Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Debugging-Fenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bedingte Haltepunkte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Haltepunkte mit Trefferanzahl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
543 543
8.5
8.6
8.7
8.12
497 500
503 505
508 510 510 511 512
516 520
533
540
545 547 550 554 555
17
Inhalt
Weitere Features von Haltepunkten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Debuggen des fehlerhaften Setzens von Eigenschaften und Feldern . . . . . . . . . . . . Haltepunkte mit Debugger.Break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Debuggen bei vorhandener Ausnahmebehandlung . . . . . . . . . . . . . . . . . . . . . . Debuggingausgaben mit Debug.Print . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Annahmen mit Debug.Assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Debugging des .NET Framework-Quellcodes . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2
555 555 556 557 558 558 560
Automatisches Testen mit Unit-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen zu Unit-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unit-Tests in Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Testergebnis und die Testkonfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . Debuggen von Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Initialisieren von Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testreihen und die Test-Reihenfolge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Test-Kontext . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datengetriebene Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
564 564
Protokollieren während der Ausführung einer Anwendung . . . . . . . . . . . . . . Protokollieren mit TraceSource . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flexibles, konfigurierbares Protokollieren mit log4net . . . . . . . . . . . . . . . . . . . .
575 575
Kapitel 10
Arbeiten mit Dateien, Ordnern und Streams . . . . . . . . . . . . . . . . . . . . . . . . .
599
10.1
Mit dem Dateisystem arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dateioperationen über File und FileInfo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ordneroperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ermitteln der Pfade von Systemordnern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pfade über die Path-Klasse bearbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Laufwerkinformationen über die Klasse DriveInfo . . . . . . . . . . . . . . . . . . . . . . . Überwachen des Dateisystems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
600 600
Einführung in die Arbeit mit Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Stream-Konzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Methoden und Eigenschaften der Basisklasse Stream . . . . . . . . . . . . . . . . . . . MemoryStream: Ein Beispiel für das Stream-Konzept . . . . . . . . . . . . . . . . . . . . . Die Standard-Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stream-Adapter und Dekorator-Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das richtige Schließen und der interne Puffer . . . . . . . . . . . . . . . . . . . . . . . . . . StringWriter und StringReader. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
620 620
10.3
Dateien binär über eine FileStream-Instanz lesen und schreiben . . . . . . . . . .
630
10.4
Spezielles binäres Schreiben und Lesen über BinaryWriter und BinaryReader . .
633
9.3
10.2
18
565 570 571 571 572 572 573
585
607 612 614 616 618
621 623 623 624 625 628
Inhalt 10.5
Textdateien lesen und schreiben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
634
10.6
Daten komprimieren und komprimierte Daten entpacken . . . . . . . . . . . . . . .
636
10.7
Richtlinien zum Speichern von Daten im Dateisystem . . . . . . . . . . . . . . . . . .
641
10.8
Arbeiten mit isoliertem Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen von isoliertem Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schreiben und Lesen von isoliertem Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . Auflisten der Dateien in einem isolierten Speicher . . . . . . . . . . . . . . . . . . . . . . . Löschen von Ordnern und Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
642 643
Kapitel 11
LINQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
649
11.1
Grundlegendes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlegende Abfragen über die LINQ-Erweiterungsmethoden . . . . . . . . . . . . . . Sequenzen und Einzeldaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Aufrufkette . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Originaldaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . C#-Abfrageausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gemischte Verwendung von Abfrageausdrücken und Erweiterungsmethoden . . . . . Wann Abfrageausdrücke und wann Erweiterungsmethoden? . . . . . . . . . . . . . . . . Aufgeschobene Ausführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lokale und interpretierte Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Performance-Überlegungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
649 651
Die LINQ-Erweiterungsmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen zu der in den folgenden Abschnitten verwendeten Syntaxbeschreibung der LINQ-Erweiterungsmethoden . . . . . . . . . . . . . . . . . . . . Einschränken mit Where . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sortieren mit OrderBy, OrderByDescending, ThenBy und ThenByDescending . . . . . . . Projektionen mit Select . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gruppierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verknüpfen von Sequenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden zur Ermittlung einzelner Elemente . . . . . . . . . . . . . . . . . . . . . . . . . . Aggregat-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden zur Ermittlung, ob Objekte in einer Sequenz enthalten sind . . . . . . . . . . Spezielle Filter-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konvertierungsmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erzeugungsmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mengen-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
663
11.2
11.3
Komplexe Abfragen mit into, let und mehreren from-Klauseln . . . . . . . . . . . . Das into-Schlüsselwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abfragen schachteln. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
645 646 647
652 653 653 653 655 656 657 660 662
666 667 668 669 671 679 686 690 694 696 698 699 699 703 703 705
19
Inhalt
Das let-Schlüsselwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Performance-Vergleich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abfragen mit mehreren from-Klauseln: Kreuzprodukt-Abfragen und spezielle Abfragen wie Ungleichheits-Verknüpfungen . . . . . . . . . . . . . . . . . . . . Unterabfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
705
Einige Tipps und Tricks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Progressive und dynamische Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ungleichheits-Verknüpfungen (Non-Equi Joins) . . . . . . . . . . . . . . . . . . . . . . . . Kreuzprodukt-Verknüpfungen (Cross Joins) . . . . . . . . . . . . . . . . . . . . . . . . . . . Kommaseparierte Dateien (CSV-Dateien) verarbeiten . . . . . . . . . . . . . . . . . . . . .
714 714
Teil 2
Anwendungen entwickeln
721
Kapitel 12
WPF-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
723
12.1
WPF versus Windows.Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassische Windows-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . WPF-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die wesentlichen Unterschiede zwischen Windows.Forms- und WPF-Anwendungen . WPF oder Windows.Forms? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
724 724 725
12.2
Die Möglichkeiten, die Sie mit WPF haben . . . . . . . . . . . . . . . . . . . . . . . . . .
729
12.3
WPF-Anwendungen in Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das WPF-Projekt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen zum Erzeugen eines neuen WPF-Projekts . . . . . . . . . . . . . . . . . . . . Einstellung der Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Gestaltung der Oberfläche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Programmierung des Beispiels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Debuggen von WPF-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Architektur einer mit Visual Studio erzeugten WPF-Anwendung . . . . . . . . . . . .
730 730
XAML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundaufbau und Namensräume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parsen und Kompilieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objekterzeugung und Initialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Markuperweiterungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die XAML-Schlüsselwörter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XAML mit integriertem Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
748 748
Die grundlegenden WPF-Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der prinzipielle Unterschied zwischen WPF-Fenstern und WPF-Seiten . . . . . . . . . . Ressourcen, Stile, Vorlagen, Dekoratoren, Skins und Themen . . . . . . . . . . . . . . . .
761 762
11.4
12.4
12.5
20
706 707 712
716 718 719
726 729
732 734 737 739 741 745
751 752 756 760 761
762
Inhalt Logische und visuelle Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abhängigkeitseigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Angefügte Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Geroutete Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Angefügte Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die grundlegende WPF-Klassenhierarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . .
765 768 776 777 781 782
Kapitel 13
WPF-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
785
13.1
Grundlegendes zur Anwendungsentwicklung unter WPF . . . . . . . . . . . . . . . . Die WPF-Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herunterfahren einer Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Standarddialoge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Systemparameter auslesen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
785 786
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen. . . . . . . . . . . . . Die Einheit von Positions- und Größenangaben . . . . . . . . . . . . . . . . . . . . . . . . Farbangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Vorder- und Hintergrund eines Fensters oder Steuerelements: Die Brush-Klasse . Schriftangaben: Verschiedene Schriftart-Klassen . . . . . . . . . . . . . . . . . . . . . . . . Wichtige allgemeine Eigenschaften der WPF-Fenster und Steuerelemente . . . . . . . . Die wichtigen allgemeinen Ereignisse der Steuerelemente und Fenster . . . . . . . . . . Die wichtigen allgemeinen Methoden der Steuerelemente und Fenster . . . . . . . . . .
797 797
13.2
13.3
13.4
Der Umgang mit WPF-Fenstern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die wichtigen Eigenschaften eines Fensters . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Tabulatorreihenfolge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (Modales und unmodales) Öffnen weiterer Fenster . . . . . . . . . . . . . . . . . . . . . . Schließen eines Fensters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reaktion auf das Schließen eines Fensters . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dialoge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Initialisieren eines Fensters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tipps und Tricks zu Fenstern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die WPF-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Basisklassen der Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersicht über die Steuerelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Inhalt eines Steuerelements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Größe und Position von Steuerelementen . . . . . . . . . . . . . . . . . . . . . . . . . . Inhalts-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Listen-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bereichssteuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Text-Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
788 790 796
799 800 803 804 806 809 810 810 814 814 818 818 819 822 822 824 825 825 827 830 835 842 849 851
21
Inhalt
Menüs, Symbolleisten und Statuszeilen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Layout-Steuerelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.5
854 858
Einige abschließende Tipps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abfrage der Tastatur außerhalb von Tastaturereignissen . . . . . . . . . . . . . . . . . . . Umgang mit der Zwischenablage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
864 865
Kapitel 14
Wichtige WPF-Techniken. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
867
14.1
Ressourcen in WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die unterschiedlichen Ressourcenarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Binäre Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ressourcenangaben: Normale URIs und Paket-URIs . . . . . . . . . . . . . . . . . . . . . . Wörterbuch-Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
868 869
Befehle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Was ist ein Befehl? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein eigener Befehl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verknüpfen eines Befehls mit WPF-Steuerelementen . . . . . . . . . . . . . . . . . . . . . Vordefinierte Befehle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vordefinierte Befehle mit Steuerelementen einsetzen . . . . . . . . . . . . . . . . . . . . . Befehlsbindungen an Ereignishandler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Befehlsbindungen für eigene Befehle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verknüpfen von Befehlen mit Tastenkombinationen oder mit der Maus . . . . . . . . .
887 887
Trigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen zu Triggern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigenschaftentrigger. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Multi-Eigenschaftentrigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datentrigger und Multi-Datentrigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ereignistrigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
899 900
14.4
Dekoratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
904
14.5
Stile, Vorlagen, Skins und Themen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vorlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Skins und Themen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
905 905
Datenbindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einfache Datenbindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fehler bei der Datenbindung auswerten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Relative Datenbindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Beispiel für die folgenden Themen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bindung an Auflistungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
922 922
14.2
14.3
14.6
22
865
869 871 877
887 890 892 893 895 896 898
901 902 903 903
913 919
924 924 925 926
Inhalt Der Datenkontext . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenvorlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konvertieren der gebundenen Werte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nicht besprochene Features der Datenbindung . . . . . . . . . . . . . . . . . . . . . . . . .
928
Einige weitere WPF-Techniken am Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . Transformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Animationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
932 932
Kapitel 15
Konfiguration, Ressourcen und Lokalisierung . . . . . . . . . . . . . . . . . . . . . . . .
935
15.1
Konfiguration einer Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Basis: machine.config . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Basis-Konfiguration einer Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen zu anwendungsspezifischen Konfigurationsdaten . . . . . . . . . . . . . . . Konfigurationsdaten im appSettings-Element . . . . . . . . . . . . . . . . . . . . . . . . . . Konfiguration einer Anwendung mit den Standard-Features von Visual Studio . . . . . Verbindungszeichenfolgen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nicht näher besprochene Konfigurations-Features . . . . . . . . . . . . . . . . . . . . . . .
935 936
Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Binäre, eingebettete Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .resx-Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
947 948
Lokalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen der Lokalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lokalisierung einer Windows.Forms-Anwendung . . . . . . . . . . . . . . . . . . . . . . . Lokalisierung einer WPF-Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ansätze zu einem Lokalisieren in der Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . .
953 953
Kapitel 16
Windows-Anwendungen verteilen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
969
16.1
Vorbereitung der Verteilung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Version der Anwendung und weitere Assembly-Informationen . . . . . . . . . . . . Die notwendigen Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
969 969 971
16.2
XCopy-Verteilung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
972
16.3
Erstellung eines Setup mit Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Setup-Projekt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Einstellung des Setup-Projekts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benutzerdefinierte Aktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Weitere Features von Visual-Studio-Setup-Projekten, die hier nicht besprochen werden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
972 973
14.7
15.2
15.3
929 930 931
933
938 940 940 941 946 947
948
959 960 967
975 977 979 987
23
Inhalt
16.4
ClickOnce-Verteilung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 988 Die Basis-Dateien eines ClickOnce-Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 990 Die Rechte einer ClickOnce-Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 991 Erstellung eines ClickOnce-Setup mit Visual Studio . . . . . . . . . . . . . . . . . . . . . . . 992 Verteilung und Installation auf einem Webserver . . . . . . . . . . . . . . . . . . . . . . . 998 Aktualisierung einer ClickOnce-Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . 1002 Nicht besprochene ClickOnce-Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1002
Teil 3
Daten verwalten
Kapitel 17
Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1007
17.1
Grundlagen der Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1007
17.2
Wann welche Serialisierung? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1009
17.3
XML-Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Serialisieren über einen XmlSerializer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Deserialisieren über einen XmlSerializer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Versions-Unterstützung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Steuern der Serialisierung über Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Weitere Beispiele auf der DVD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1009 1010
Binäre/SOAP-Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Binäres Serialisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das binäre Deserialisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Attribute für die binäre Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Versionierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1015 1015
Datenvertrag-Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Definition des Datenvertrags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Serialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Deserialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objekte binär serialisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenvertrag-Serialisierung und Polymorphismus . . . . . . . . . . . . . . . . . . . . . . . Serialisieren von Objekt-Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Versionierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nicht besprochene Features der Datenvertrag-Serialisierer . . . . . . . . . . . . . . . . . .
1019 1019
17.4
17.5
1005
1012 1012 1013 1014
1017 1017 1018
1021 1022 1023 1024 1025 1027 1029
Kapitel 18
XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1031
18.1
Noch nicht behandelte XML-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . 1031 Das Document Object Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1031
24
Inhalt XML-Schema (XSD), XPath und XSLT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1035 Die XML-APIs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1041 18.2
Lesen und Schreiben mit XElement, XDocument und LINQ to XML . . . . . . . . . . . Laden und Parsen von einfachen XML-Dokumenten . . . . . . . . . . . . . . . . . . . . . Erzeugen eines XML-Dokuments ohne Namensraum . . . . . . . . . . . . . . . . . . . . . Speichern von Nullwerten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gemischte Inhalte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Serialisieren und Speichern eines X-DOM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erzeugen von XML-Dokumenten mit Namensraum . . . . . . . . . . . . . . . . . . . . . . Navigieren in einem XML-Dokument (ohne Namensräume) . . . . . . . . . . . . . . . . . Navigieren in einem XML-Dokument mit Namensräumen . . . . . . . . . . . . . . . . . . Lesen von Textinhalten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ändern eines XML-Dokuments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klasse XDocument . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . LINQ to XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abfragen mit LINQ to XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erzeugen von XML-Dokumenten über LINQ . . . . . . . . . . . . . . . . . . . . . . . . . . . Transformation von XML-Dokumenten mit LINQ to XML . . . . . . . . . . . . . . . . . . . . Nicht besprochene Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1041 1042 1043 1045 1046 1047 1047 1050 1054 1055 1057 1060 1061 1061 1065 1067 1069
18.3
Schnelles Lesen und Schreiben mit einem XmlReader bzw. XmlWriter . . . . . . . 1069 Lesen über einen XmlReader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1069 Schreiben über einen XmlWriter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1073
18.4
XML-Dokumente über XSD validieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1075
Kapitel 19
Datenbanken mit LINQ to SQL bearbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . 1079
19.1
Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Begriff »Entität« . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Beispiel-Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersicht über die Möglichkeiten, in .NET 3.5 Datenbanken zu bearbeiten . . . . . . . . Einschränkungen von LINQ to SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1080 1080
Erstellen eines Datenbankmodells . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erstellen eines Datenbankmodells mit Visual Studio auf der Basis einer vorhandenen Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erstellen eines komplett neuen Datenbankmodells mit Visual Studio . . . . . . . . . . . Das grundlegende Datenbankmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erste Nachbearbeitung des Datenbankmodells . . . . . . . . . . . . . . . . . . . . . . . . . Die Einstellungen des Datenkontextes, der Entitäts-Klassen und ihrer Eigenschaften . Zweite Nachbearbeitung des Datenbankmodells . . . . . . . . . . . . . . . . . . . . . . . . Die Klassen des Modells und der Datenkontext . . . . . . . . . . . . . . . . . . . . . . . . .
1087
19.2
1080 1084 1086
1087 1089 1090 1090 1092 1094 1096
25
Inhalt
Erzeugen des Datenkontextes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1097 Das LINQ-to-SQL-Protokoll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1099 19.3
Abfragen mit LINQ to SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einfache Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abfragen mit LIKE und dynamische Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . Einschränkungen von LINQ to SQL gegenüber LINQ . . . . . . . . . . . . . . . . . . . . . . . Aufgeschobene Ausführung mit LINQ to SQL und das Erzeugen von SQL . . . . . . . . . . Auflösen von Beziehungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abfragen an die Oberfläche binden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (Verzicht auf) Verknüpfungen in LINQ to SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . Gruppierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Effizienz der Abfrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verzögertes Laden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1099 1099 1100 1101 1102 1103 1105 1106 1108 1109 1109 1111
19.4
Bearbeiten von Daten über das Objektmodell . . . . . . . . . . . . . . . . . . . . . . . . 1113 Anfügen, Ändern und Löschen von Entitäten . . . . . . . . . . . . . . . . . . . . . . . . . . 1114 Fehlerbehandlung beim Anfügen, Ändern und Löschen . . . . . . . . . . . . . . . . . . . 1119
19.5
Business-Objekt-Modelle oder: Die partiellen Klassen und eine Reaktion auf Datenereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1122
19.6
Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1126
19.7
Behandlung von Konflikten beim Aktualisieren . . . . . . . . . . . . . . . . . . . . . . 1128 Die Aktualisierungs-Überprüfung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1128 Behandeln der ChangeConflictException . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1129
19.8
Wichtige, nicht behandelte Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1134
Teil 4
Fortgeschrittene Programmierung
Kapitel 20
Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1139
20.1
Einführung in Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Was ist ein Thread? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Was ist Multithreading?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads aus technischer Sicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threadsicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mögliche Probleme beim Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wann sollten Sie Multithreading einsetzen? . . . . . . . . . . . . . . . . . . . . . . . . . . . Nachteile des Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Möglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
1137
1140 1140 1140 1141 1142 1142 1144 1145 1146
Inhalt 20.2
Zugriff auf die Benutzeroberfläche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1147
20.3
Asynchrones Ausführen von Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die asynchron auszuführende Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Delegat für die Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Callback-Delegat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der asynchrone Aufruf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Prozessorlast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1148 1149 1150 1150 1152 1152
20.4
Einfaches Multithreading mit der BackgroundWorker-Klasse . . . . . . . . . . . . . 1152
20.5
Einfache Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads erzeugen und starten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads debuggen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Ende von Threads signalisieren, Ergebnisse zurückgeben und Argumente einfacher übergeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Priorität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads abbrechen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein praxisnahes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1156 1157
Die ThreadPool-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ThreadPool verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Anzahl der Threads im Pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Arbeitsweise von ThreadPool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Interne Verwendung der ThreadPool-Klasse . . . . . . . . . . . . . . . . . . . . . . . . Weitere interessante Features von ThreadPool, die hier nicht näher besprochen werden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1173 1173
20.6
1159 1160 1163 1164 1166
1174 1174 1175 1175
20.7
Ausnahmen in Threads und beim asynchronen Aufruf von Methoden . . . . . . . 1176
20.8
Das ereignisbasierte asynchrone Entwurfsmuster und asynchrone Methoden . . 1178 Das ereignisbasierte asynchrone Entwurfsmuster . . . . . . . . . . . . . . . . . . . . . . . . 1179 Asynchrone Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1181
20.9
Threads synchronisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads blockieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Den Zugriff auf globale Daten und Ressourcen sperren . . . . . . . . . . . . . . . . . . . . Prozessübergreifendes Sperren mit einem Mutex . . . . . . . . . . . . . . . . . . . . . . . . Signalisierungs-Konstrukte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Atomare Anweisungen und die Klasse Interlocked . . . . . . . . . . . . . . . . . . . . . . .
1183 1184 1186 1193 1195 1202
20.10
Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1203
20.11
Weitere interessante Themen beim Multithreading . . . . . . . . . . . . . . . . . . . . 1204
27
Inhalt
Kapitel 21
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten . . . . 1205
21.1
Das Windows-API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Aufruf von API-Funktionen über PInvoke . . . . . . . . . . . . . . . . . . . . . . . . . . Umsetzen von Datentypen (Marshalling) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die .NET-Entsprechungen der API-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Boolesche Argumente, Felder und Rückgabewerte . . . . . . . . . . . . . . . . . . . . . . . Strings übergeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Rückgabe von Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Referenzargumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Übergabe von Strukturen und feste Strings . . . . . . . . . . . . . . . . . . . . . . . . . API-Konstanten-Werte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Umgehen mit API-Fehlern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Umsetzen von API-Fehlercodes in passende Fehlerbeschreibungen . . . . . . . . . . . .
1206 1206 1208 1209 1210 1211 1213 1214 1215 1216 1217 1218
21.2
Arbeiten mit COM-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1220 COM-Komponenten mit früher Bindung verwenden . . . . . . . . . . . . . . . . . . . . . 1221 COM-Komponenten mit später Bindung verwenden. . . . . . . . . . . . . . . . . . . . . . 1224
Kapitel 22
Assemblys, Reflektion und Anwendungsdomänen. . . . . . . . . . . . . . . . . . . . . 1229
22.1
Wichtige weitere Möglichkeiten beim Umgang mit Assemblys . . . . . . . . . . . . . Das Signieren und der Name einer Assembly . . . . . . . . . . . . . . . . . . . . . . . . . . Vorkompilieren von Assemblys in den nativen Abbild-Cache . . . . . . . . . . . . . . . . Assemblys, die nicht im Anwendungsordner direkt verwaltet werden . . . . . . . . . .
1229 1230
Reflektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Evaluieren von Typen und Assemblys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dynamisches Instanzieren und Verwenden von Typen . . . . . . . . . . . . . . . . . . . . Dynamisches Erzeugen von Typen und Assemblys . . . . . . . . . . . . . . . . . . . . . . .
1237 1238
Anwendungsdomänen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Sinn von Anwendungsdomänen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anwendungsdomänen erzeugen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausführen einer ausführbaren Assembly in der Anwendungsdomäne . . . . . . . . . . Entladen von Anwendungsdomänen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausführen von eigenem Programmcode in einer separaten Anwendungsdomäne . . . Dynamisches Erzeugen und Verwenden von Typen einer Assembly in einer separaten Anwendungsdomäne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenaustausch zwischen Anwendungsdomänen . . . . . . . . . . . . . . . . . . . . . . . Optimieren des Ladeverhaltens von Assemblys . . . . . . . . . . . . . . . . . . . . . . . . . Weitere Features, die hier nicht näher besprochen werden . . . . . . . . . . . . . . . . .
1248 1249
22.2
22.3
28
1235 1237
1241 1246
1250 1250 1251 1251 1252 1253 1255 1255
Inhalt Kapitel 23
Sicherheitsgrundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1257
23.1
Codezugriffssicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Arbeitsweise der Codezugriffssicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Administration der Codezugriffssicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . Codezugriffssicherheit auf Assembly-Ebene . . . . . . . . . . . . . . . . . . . . . . . . . . .
1258 1258
Verschlüsseln und Entschlüsseln von Daten . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Windows-Verschlüsselung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Ver- und Entschlüsseln von Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hashcodes berechnen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Daten symmetrisch verschlüsseln. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Daten asymmetrisch verschlüsseln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Digitale Signaturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1262 1262
23.2
1260 1261
1265 1266 1267 1270 1274 1279
23.3
Das sichere Ende dieses Buchs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1280
Teil 5
Anhang
Kapitel A
Glossar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1283
Kapitel B
Die ersten 255 Zeichen des Unicode-Zeichensatzes. . . . . . . . . . . . . . . . . . . . . 1299
Kapitel C
Meine Namenskonvention für Steuerelemente . . . . . . . . . . . . . . . . . . . . . . . 1301
1281
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1305
29
Vorwort
Das .NET Framework 3.5 bietet ein enormes Potenzial für die Entwicklung von Anwendungen. Und in jeder Version kommt Neues hinzu. Diese vielen, sehr hilfreichen Möglichkeiten führen aber auch dazu, dass die Lernkurve ansteigt. Der Einstieg in die neuen Versionen von .NET und C# wird deswegen immer schwieriger, auch weil viele alte Technologien neben neuen stehen. Deswegen benötigen Sie ein Buch als Wegweiser. Und ein solches halten Sie in Ihren Händen ☺. Dieses Buch umfasst den nahezu kompletten Bereich der (WPF-)Windows-Programmierung mit dem .NET Framework 3.5 und C# 3.0. Das Buch behandelt die Sprache C# (inklusive der OOP), geht ausführlich auf die grundlegenden .NET FrameworkKlassen ein und behandelt schließlich fortgeschrittene Themen wie LINQ, WPF, LINQ to SQL und Multithreading. Ich lege in diesem Buch mehr Wert auf die Grundlagen und auf die Praxis als auf die (ausführliche) Beschreibung aller Technologien oder aller Möglichkeiten, die Sie mit den einzelnen Technologien haben. Ein 1300-Seiten-Buch kann heutzutage leider nicht mehr den kompletten Bereich der Programmierung unter .NET behandeln. Deshalb habe ich die für die (WPF-)Windows-Programmierung wichtigen Themen in das Buch aufgenommen, behandle die Grundlagen recht ausführlich und lasse einige speziellere Themen weg. Leider gehört dazu auch die Webprogrammierung (mit ASP.NET), aber dafür war einfach kein Platz mehr. Ich weise im Buch an den entsprechenden Stellen auf die Themen hin, die ich nicht behandle. Einige nicht behandelte Themen (wie z. B. Windows.Forms) habe ich auch in Artikel ausgelagert, die Sie auf meiner Website finden. Einfach war die Entscheidung nicht, welche Themen ins Buch kommen und wie tief diese behandelt werden. Technologien wie WPF und WCF bieten so viele Features, dass es eigentlich schade ist, diese nicht (WCF) oder nicht alle (WPF) besprechen zu können. Ich denke aber, dass es in einem Kompendium sinnvoll ist, die Grundlagen der Sprache, des .NET Framework und der für die normale Praxis wichtigen Technologien (wie z.B. WPF) ausführlich zu behandeln und spezielle Themen (wie die Programmierung eigener 3-D-Anwendungen) spezialisierten Büchern zu überlassen.
31
Index
Eine andere Idee meines Buchs ist die Nähe zur Praxis. Ich bin selbst ein erfahrener Entwickler, der schon alle möglichen Arten von Anwendungen erstellt hat. Und ich bin jemand, der es nicht leiden kann, wenn irgendetwas nicht funktioniert (oder nicht so, wie es in anderen Büchern beschrieben ist). Deswegen suche ich manchmal Stunden (oder sogar Tage) nach der Lösung eines Problems, die ich im Buch dann lediglich mit einem oder zwei Sätzen erwähne. Aber für Sie bedeutet dies, dass Sie nicht nach der Lösung dieses Problems suchen müssen.
Vorwort
Bei diesem Buch haben einige Leute sehr hilfreich mitgewirkt. Dazu möchte ich zunächst der Firma AUTOonline danken, die mich beim Schreiben des Buches sehr unterstützt hat. Mein Fachlektor, Heiko Lehnert hat mit so manchen Anmerkungen sehr zur Qualität dieses Buchs beigetragen. Gleiches gilt für Volker Wehrhahn, dessen hochqualifizierte Kommentare zu einigen Verbesserungen im Buch geführt haben. Tanja Gliege, der Mutter meiner potenziellen Kinder, danke ich für ihre endlose Geduld und ihre Aufmunterungen (»du schaffst das …«). Meiner bevorzugten Korrektorin Simone Meißner danke ich für die einmal wieder hervorragende Korrektur und die Spaß machende Zusammenarbeit. Und nicht zuletzt danke ich den fünf Tibetern dafür, dass sie mir beim Schreiben dieses Buches so viel Kraft gegeben haben. Ihr Jürgen Bayer www.juergen-bayer.net
[email protected]
32
Teil 1 Grundlagen 35
Einführung
1
87
Einführung in die Arbeit mit Visual Studio 2008
2
141
Die Sprache C#
3
231
Grundlegende OOP
4
281
Weiterführende OOP
5
341
OOP-Specials
6
373
Arrays und Auflistungen
7
441
Grundlegende Programmiertechniken
8
543
Fehler debuggen, testen und protokollieren
9
599
Arbeiten mit Dateien, Ordnern und Streams
10
649
LINQ
11
Inhalt
1
Einführung 1
Dieses Kapitel beschreibt zunächst den Inhalt dieses Buchs und die verwendeten Konventionen. Nachdem Sie dann erfahren haben, wie Sie Visual Studio installieren, zeige ich Wege zur Lösung von Programmier-Problemen auf (da dieses Buch nicht alle Probleme lösen kann ☺). Beendet wird das Kapitel mit der Erläuterung von Begriffen, die bei der Beschäftigung mit .NET und C# immer wieder vorkommen, einer kurzen Einführung in XML und den Grundlagen des .NET Framework.
2
1.1
4
3
Zum Buch
Dieses Buch behandelt die .NET-Programmierung mit C# in allen für die Entwicklung von Windows-Anwendungen (mit WPF) wichtigen Aspekten. Sie erfahren alles Wichtige zu den Grundlagen von .NET und von C#, Wichtiges zu der Arbeit mit dem .NET Framework und Sie lernen Windows-Anwendungen zu entwickeln. Das Buch geht aber über die Grundlagen hinaus und behandelt auch erweiterte Themen wie Multithreading oder die Windows Presentation Foundation (WPF).
5
6
Ich beschreibe die einzelnen Themen dabei möglichst ausführlich, aber mit knappen Worten. Nur so bestand die Chance, die für die Praxis wichtigen Themen in der begrenzten Seitenzahl des Buchs behandeln zu können. Aber ich denke, das ist auch in Ihrem Sinne ☺.
Die Zielgruppe 8
Dieses Buch ist für alle Entwickler gedacht, die mit C# und dem .NET Framework programmieren wollen. Es setzt keine .NET- oder C#-Kenntnisse voraus, geht aber davon aus, dass Sie die Grundlagen der Programmierung (Anweisungen, Variablen, Funktionen/Methoden, Abfragen, Schleifen) kennen. OOP-Kenntnisse werden allerdings nicht vorausgesetzt, da die OOP-Themen recht ausführlich behandelt werden (ohne allerdings zu tief in die Theorie der OOP einzusteigen).
9
Ich lege in diesem Buch sehr viel Wert auf die Grundlagen. Das erkennen Sie schon alleine daran, dass die entsprechenden Kapitel mehr als ein Drittel des Buchs ausmachen. Wegen der begrenzten Seitenzahl und der Mächtigkeit einzelner Themen können die fortgeschrittenen Themen wie WPF aber nicht mehr umfassend behandelt werden. Einige Themen wie WCF werden sogar gar nicht behandelt. Wie soll auch ein Buch über WCF oder WPF mit jeweils etwa 600 Seiten in ein Kompendium über C# gepresst werden? Deswegen behandle ich bei den fortgeschrittenen Themen die Grundlagen und einige wichtige Erweiterungen. Bei WPF erfahren Sie z. B., wie WPF arbeitet und wie WPF-Anwendungen prinzipiell entwickelt werden, für die speziellen WPF-Features wie Animationen, 2D-Grafiken, 3D-Grafiken, Spracherkennung etc. war aber kein Platz.
10
11
35
Index
1.1.1
7
Einführung
Das Buch richtet sich deswegen mehr an Anfänger als an Profis. Aber auch für fortgeschrittene C#-Programmierer, die die Neuigkeiten in C# 3.0 bzw. im .NET Framework 3.5 kennenlernen oder spezifische Themen wie Multithreading erlernen wollen, ist das Buch gut geeignet. Schließlich reicht mir das in diesem Buch vermittelte Wissen als jahrelangem .NET-Entwickler in der Praxis zu 90% aus (die restlichen 10% hole ich aus meinem Codebook ☺).
1.1.2
Der Inhalt dieses Buchs
Das Buch behandelt alle grundlegenden und für die Programmierung von WindowsAnwendungen wichtigen Themen der Programmierung mit C#. Dazu gehören neben den wichtigen C#- und Visual-Studio-Grundlagen auch fortgeschrittene Themen wie WPF, Multithreading und Sicherheit. Da die Entwicklung mit .NET mittlerweile aber extrem vielfältig ist und wesentlich mehr Möglichkeiten bietet, als in einem Buch mit ca. 1300 Seiten beschrieben werden können, konnte ich leider nicht alles behandeln. Zu den nicht behandelten Themen gehört auch die Webprogrammierung, die in typischen Büchern selbst teilweise wesentlich mehr als 1000 Seiten (»ASP.NET 3.5 Unleashed«, 1796 Seiten ohne und 1920 Seiten mit Index) beansprucht. Im Abschnitt »Nicht und eingeschränkt behandelte Themen« sind alle Themen aufgeführt, die in diesem Buch nicht beschrieben werden. Bei der Auswahl der Themen habe ich aber meine vielfältige Praxiserfahrung zu Rate gezogen und die speziellen, in der Praxis eher selten verwendeten Features weggelassen. Zu diesen biete ich aber an gegebener Stelle, wenn möglich, weitere Informationsquellen an. In jeder .NET-Version wurden zudem neue Technologien eingeführt. Dazu gehören z. B. generische Typen (.NET 2.0), WPF, WCF, WF (. NET 3.0) und LINQ (.NET 3.5). Teilweise werden damit alte Technologien mehr oder weniger ersetzt. Generische Auflistungen sind z. B. wesentlich einfacher, schneller und sicherer als die alten nicht generischen. Der Datenbankzugriff über LINQ to SQL ist wesentlich einfacher und weniger aufwändig als der Datenzugriff über das »alte« ADO.NET. Als innovativer Mensch (☺) beschreibe ich vorwiegend die neuen Features. Die alten werden erwähnt und teilweise auch kurz vorgestellt, aber nicht tiefer gehend beschrieben. Die nicht oder nur eingeschränkt behandelten Themen erläutere ich auf Seite 41. Das Buch ist so aufgebaut, dass grundlegende Themen möglichst vor Themen beschrieben werden, die die Grundlagen voraussetzen. So behandelt das Buch erst die Grundlagen der Sprache, dann die OOP, um danach auf Arrays und Auflistungen einzugehen. Das mag in Ihren Augen vielleicht an einigen Stellen eigenartig erscheinen, macht aber deswegen Sinn, da einige Themen (wie z. B. Arrays und Auflistungen) andere (wie die OOP) voraussetzen, um (praxisorientiert-)umfassend behandelt werden zu können. Am Anfang jedes Kapitels steht eine kurze Zusammenfassung des Kapitels. Außerdem weise ich dort auf die in C# 3.0 bzw. dem .NET Framework 3.5 neuen Features hin, mit Verweis auf die Seiten, auf denen diese beschrieben werden. Diese Verweise sind für Sie hilfreich, wenn Sie für einzelne Themen nur die neuen Features lernen wollen.
Teil 1: Grundlagen Der erste Teil des Buchs beschäftigt sich mit den Grundlagen der C#-Programmierung.
36
Zum Buch
Kapitel 1: Einführung In der Einführung, die Sie gerade lesen, beschreibe ich den Inhalt des Buchs und wie Sie Visual Studio installieren. Außerdem zeige ich einige Wege auf, weitere Informationen zu erhalten, erläutere wichtige Begriffe, die in den folgenden Kapiteln immer wieder verwendet werden, und gebe einen Überblick über das .NET Framework.
Kapitel 2: Einführung in die Arbeit mit Visual Studio 2008
1
Visual Studio 2008 ist die wichtigste Entwicklungsumgebung für die Entwicklung von .NET-Anwendungen. Das liegt zum einen an dem großen Umfang an Features und an der Flexibilität. Zum anderen ist Visual Studio wohl die im professionellen Bereich meistverwendete Entwicklungsumgebung für .NET (unter Windows). Ein weiterer wichtiger Grund ist, dass die (eingeschränkten) Express-Editionen frei verfügbar sind. Deshalb gab es prinzipiell keinen Grund, eine andere Entwicklungsumgebung für dieses Buch zu beschreiben – außer dem, dass Visual Studio leider (noch) nicht für andere Betriebssysteme als Windows verfügbar ist.
2
3
Um also dem wahrscheinlichen Großteil der Leser gerecht zu werden, setze ich in diesem Buch (wie auch in meiner Programmier-Praxis) Visual Studio 2008 ein. Kapitel 2 beschreibt deswegen den grundsätzlichen Umgang mit dieser Entwicklungsumgebung. Es zeigt die grundsätzlichen Möglichkeiten, beschreibt die wichtigsten Fenster, Befehle und Tastenkombinationen und zeigt, wie Sie mit Visual Studio (zunächst nur einfache) (Windows-)Anwendungen entwickeln.
4
5
Das Kapitel zeigt auch, wie Sie eine einfache Windows-Anwendung mit Hilfe der Windows.Forms-Bibliothek erstellen. Ich habe hier bewusst nicht das neue WPF gewählt, weil WPF zum einen separat behandelt wird. Zum anderen ist Windows.Forms für den Anfang einfacher als WPF. Das Buch beschreibt Windows.Forms, jedoch nicht näher, weil ich denke, dass die Zukunft in WPF liegt.
6
Die weiteren Möglichkeiten von Visual Studio 2008, wie z. B. die Entwicklung von Webanwendungen oder das Testen und Debuggen, werden teilweise in folgenden Kapiteln beschrieben. Alle Features dieser sehr mächtigen Entwicklungsumgebung kann das Buch allerdings nicht behandeln, es handelt sich schließlich um ein Buch über C#, und nicht um ein Buch über Visual Studio ☺.
7
8
Kapitel 3: Die Sprache C# Dieses Kapitel behandelt die Grundlagen von C#, also die Grundlagen der Sprache, mit der Sie Ihre Programme schreiben (werden). Es behandelt den Umgang mit Datentypen, das Schreiben von Anweisungen, Grundlagen zu Ausdrücken, die verschiedenen Operatoren, Schleifen, Verzweigungen und einiges mehr.
9
Methoden werden übrigens noch nicht in diesem Kapitel, sondern erst in Kapitel 4 behandelt.
10
Kapitel 4: Grundlegende OOP C# ist eine objektorientierte Programmiersprache. Wie so viele Frameworks ist auch das .NET Framework objektorientiert. Um mit C# und mit dem .NET Framework optimal arbeiten zu können, müssen Sie die Grundlagen der OOP verstehen.
11
Kapitel 4 behandelt aber weniger die theoretischen Grundlagen (diese können Sie in einem Artikel auf meiner Website nachlesen), sondern zeigt eher, wie Sie die Basiskonzepte der OOP in C# umsetzen. Dazu gehören u. a. das Entwickeln einfacher Klassen mit Eigenschaften und Methoden, die Möglichkeiten, die Sie bei der Ent-
37
Einführung
wicklung von Methoden besitzen, das wichtige Konzept der Kapselung, das Initialisieren von Objekten über Konstruktoren und statische Klassen.
Kapitel 5: Weiterführende OOP Mit den in Kapitel 4 behandelten OOP-Grundlagen können Sie schon eine Menge erreichen. Besonders aber bei der Entwicklung von größeren Projekten und bei der Arbeit im Team sind weiterführende OOP-Konzepte, wie Vererbung, Schnittstellen, darauf basierender Polymorphismus (wenn Sie diesen Begriff noch nicht kennen: Er hört sich schlimmer an, als er ist ☺), Delegaten, Ereignisse und partielle Typen wichtig. Kapitel 5 setzt sich deswegen (recht intensiv) mit diesen Themen auseinander.
Kapitel 6: OOP-Specials C# erweitert die klassische OOP um einige spezielle Features wie Erweiterungsmethoden, generische Typen und Methoden, Lambda-Ausdrücke und Ausdrucksbäume. Neben diesen Punkten behandelt Kapitel 6 das mit der OOP in Verbindung stehende Thema »Attribute« (Metainformationen).
Kapitel 7: Arrays und Auflistungen .NET bietet eine große Vielfalt an Möglichkeiten, Daten (bzw. Objekte) im Arbeitsspeicher in einer Liste zu verwalten. Neben den klassischen Arrays gibt es verschiedene Auflistungen, die jeweils einen eigenen Einsatzbereich besitzen. Wegen der Vielfalt der Möglichkeiten habe ich diesem Thema ein eigenes Kapitel gewidmet.
Kapitel 8: Grundlegende Programmiertechniken Dieses Kapitel behandelt die (in meinen Augen) wichtigen grundlegenden Programmiertechniken. Dazu gehören z. B. das Erzeugen und Abfangen von Ausnahmen, der Umgang mit Strings und Datumswerten, reguläre Ausdrücke, mathematische Berechnungen, Programm-Meldungen in einer MessageBox und das Dokumentieren eines Projekts.
Kapitel 9: Fehler debuggen, testen und protokollieren Die wenigsten Programme werden fehlerfrei entwickelt. Je größer eine Anwendung wird, desto mehr Fehler schleichen sich ein. Die Programmier-Standards, die ich in den vorhergehenden Kapiteln immer wieder eingestreut habe (wie vernünftige Benennung von eigenen Typen, Eigenschaften, Methoden und Variablen und der größtmögliche Verzicht auf globale Daten), helfen zwar, viele Fehler zu vermeiden. Fehlerfrei wird eine Anwendung aber nur selten sein. Deshalb zeigt dieses Kapitel zunächst, wie Sie nach logischen Programmfehlern oder nach der Ursache unerwarteter Ausnahmen suchen (wie Sie »debuggen«). Um Fehler von vornherein zu vermeiden, können Sie so genannte Unit-Tests verwenden. Diese Tests überprüfen Teile Ihrer Anwendung auf korrekte Ausführung. Unit-Tests können Sie nach jeder Änderung der Anwendung oder Klassenbibliothek, die Sie entwickeln, immer wieder neu ausführen, um sicherzustellen, dass alle Teile der Anwendung (noch) zuverlässig funktionieren. Dieser Ansatz, der als »Test Driven Development« (Test-getriebene Entwicklung) bezeichnet wird, ist sehr interessant und wird deshalb in Kapitel 9 auch behandelt. Da in der Praxis trotzdem noch Fehler auftreten und meist erst beim Kunden, spielt die Protokollierung in Anwendungen ebenfalls eine große Rolle. Kapitel 9 geht des-
38
Zum Buch
wegen (kurz) auf die in .NET integrierten Möglichkeiten ein und beschreibt zudem die zum allgemeinen Standard gewordene Protokollierungskomponente log4net.
Kapitel 10: Arbeiten mit Dateien, Ordnern und Streams Kapitel 10 setzt sich mit der Arbeit im Dateisystem und mit Streams auseinander. Es beschreibt zunächst, wie Sie Dateien und Ordner überprüfen und manipulieren können und wie Sie mit Laufwerken arbeiten. Nach einer Einführung in das Stream-Konzept zeigt dieses Kapitel dann, wie Sie binäre und Textdateien lesen und schreiben, und zeigt den Umgang mit isoliertem (geschütztem) Speicher (der z. B. für die Ausführung von .NET-Anwendungen wichtig sein kann, die aus dem Internet geladen werden).
1 2
Kapitel 11: LINQ Die Language Integrated Query (LINQ) ist eines der neuen Konzepte von .NET 3.5 und C# 3.0. Mit LINQ können Sie mit ein und derselben Syntax verschiedene Datenquellen, wie z. B. Auflistungen, XML-Dateien oder Datenbanken abfragen. Da dies wesentlich einfacher ist, als die »klassischen« .NET-Klassen zum Zugriff auf Datenquellen (die sich für die einzelnen Datenquellen sehr voneinander unterscheiden) zu verwenden, und weil LINQ häufig mehr Features bietet, behandelt Kapitel 11 das Thema LINQ sehr ausführlich.
3
4
Teil 2: Anwendungen entwickeln
5
Der zweite Teil des Buchs behandelt die Entwicklung von (WPF-)Windows-Anwendungen. Aus Platzgründen wird das ältere Windows.Forms nicht mehr im Buch behandelt. Ich denke (wie auch andere Autoren), dass die Zukunft in WPF liegt. WPF ist allerdings mit seinen vielfältigen Möglichkeiten so umfangreich, dass eine Beschreibung sehr viel Platz in Anspruch nimmt. Für Windows.Forms bleibt da im Buch leider kein Platz mehr. Im WPF-Grundlagenkapitel 12 finden Sie übrigens einen Vergleich von Windows.Forms und WPF. Unter der Adresse www.juergen-bayer.net/artikel/csharp/windows.forms/windows. forms.aspx finden Sie das ausgelagerte Kapitel zu Windows.Forms in Form eines Artikels. Diesen können Sie auch als PDF-Dokument herunterladen.
6 TIPP
7
8 REF
9
Kapitel 12: WPF-Grundlagen Dieses Kapitel behandelt die Grundlagen der Windows Presentation Foundation (WPF), der aktuellen Technologie zur Entwicklung von Anwendungen, die als Windows-Anwendung oder in einem Browser (Internet Explorer, Firefox etc.) ausgeführt werden können. WPF ist relativ neu und liegt lediglich in einer ersten Version vor. Mit seinen Möglichkeiten übertrifft es aber die klassische Art, Windows-Anwendungen über Windows.Forms zu entwickeln, bereits jetzt in vielen Bereichen. Die zusätzliche Möglichkeit, WPF-Anwendungen (als Teil einer Webanwendung) über Microsoft Silverlight im Browser auszuführen und die Einschränkungen von HTML damit zu umgehen, gibt weitere Pluspunkte für WPF.
10
11
In Kapitel 12 finden Sie deswegen alle für die Arbeit mit WPF grundlegenden Themen (die sehr umfangreich sind).
39
Einführung
Kapitel 13: WPF-Anwendungen Nachdem Sie in Kapitel 12 die grundlegenden Konzepte kennengelernt haben, die WPF verwendet, behandelt Kapitel 13 nun die zur Entwicklung einfacher WindowsAnwendungen notwendigen Dinge. Dazu gehört neben den Grundlagen der Anwendungsentwicklung unter WPF auch der Umgang mit Fenstern und mit den WPFSteuerelementen.
Kapitel 14: Wichtige WPF-Techniken Dieses Kapitel behandelt die für die Entwicklung von Standardanwendungen wichtigen WPF-Techniken wie Ressourcen, Befehle, Trigger, Stile und Vorlagen. Spezielle Themen wie 2D- und 3D-Grafiken werden nicht behandelt.
Kapitel 15: Konfiguration, Ressourcen und Lokalisierung Dieses Kapitel setzt sich zunächst mit der Konfiguration des .NET Framework und der Konfiguration von Anwendungen auseinander. Danach wird das allgemeine Einbinden und Verwenden von Ressourcen (Bilder, Texte etc.) behandelt (das Einbinden unter WPF wurde bereits in Kapitel 14 besprochen) und die damit zusammenhängende Lokalisierung (Mehrsprachigkeit) einer Anwendung.
Kapitel 16: Windows-Anwendungen verteilen Dieses Kapitel behandelt die Möglichkeiten der Verteilung von Windows-Anwendungen. Neben einer einfachen »Copy & Paste«-Verteilung wird die Erstellung einer Installationsanwendung beschrieben. Als letzte Variante behandelt Kapitel 16 die Verteilung von Anwendungen (u. a. über das Internet) in Form von ClickOnce (inkl. der Beschreibung der Einschränkungen).
Teil 3: Daten verwalten Das Speichern und Übertragen von Daten ist Thema des dritten Teils des Buchs. Dazu gehören das Serialisieren von Objekten, die Arbeit mit XML-Dokumenten und die Arbeit mit Datenbanken. Dabei werden hauptsächlich die neueren Technologien wie LINQ to XML und LINQ to SQL behandelt.
Kapitel 17: Serialisierung Serialisierung heißt, Objekte in eine Form zu bringen, die speicherbar oder über ein Netzwerk übertragbar ist. Das .NET Framework stellt verschiedene Techniken zur Verfügung, dies zu erreichen. Kapitel 17 setzt sich mit diesen auseinander.
Kapitel 18: XML Dieses Kapitel zeigt, wie Sie XML-Dokumente lesen und schreiben. Es führt zunächst grundlegend in noch nicht behandelte XML-Grundlagen wie XML-Schemas ein und beschreibt dann den Umgang mit XML über LINQ to XML und den Klassen XmlReader und XmlWriter.
Kapitel 19: Datenbanken mit LINQ to SQL bearbeiten In diesem Kapitel geht es um die Arbeit mit Datenbanken. Dazu können Sie im Prinzip auch ADO.NET verwenden, aber die Arbeit mit LINQ to SQL (und dem zurzeit noch nicht erschienenen ADO.NET Entity Framework) ist zum einen moderner (weil objektorientiert) und zum anderen wesentlich einfacher.
40
Zum Buch
Teil 4: Fortgeschrittene Programmierung Im letzten Teil des Buchs werden fortgeschrittene Programmierthemen wie Multithreading, der Aufruf von API-Funktionen, Reflektion und Sicherheit behandelt.
Kapitel 20: Multithreading Viele Anwendungen führen Aufgaben im Hintergrund aus. Wenn Sie z. B. in Microsoft Word einen Text drucken, wird dieser (normalerweise) im Hintergrund gedruckt, während Sie den Text im Vordergrund weiter bearbeiten können.
1
Wenn auch Sie Anwendungen programmieren wollen, die Aufgaben im Hintergrund ausführen, müssen Sie sich mit Multithreading auseinander setzen. Kapitel 20 behandelt deswegen alle für die Praxis wichtigen Aspekte des Multithreading. Es beschreibt u. a. das einfache Multithreading über einen BackgroundWorker, die asynchrone Ausführung von Methoden und das »echte« Multithreading. Es geht aber auch (gleich am Anfang) auf die Unterschiede dieser Möglichkeiten ein (die sonst kaum beschrieben werden ☺).
2
3
Kapitel 21: Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
4
Obwohl das .NET Framework eine riesige Anzahl an Klassen und Strukturen liefert, sind nicht alle Möglichkeiten, die Windows bietet, verfügbar. Für einige Features müssen Sie auf die API-Funktionen (Application-Interface-Funktionen) zurückgreifen, die Windows (oder eine Anwendung) über verschiedene klassische DLLDateien anbietet. Andere Features sind über etwas modernere COM-Komponenten verfügbar. So können Sie Office-Anwendungen zurzeit lediglich über deren COMKomponenten fernsteuern. Kapitel 21 zeigt deshalb, wie Sie mit klassischen DLLDateien und mit COM-Komponenten arbeiten.
5
6
Kapitel 22: Assemblys, Reflektion und Anwendungsdomänen
7
Dieses Kapitel behandelt alles Wichtige, was Sie über Assemblys, Reflektion und Anwendungsdomänen wissen müssen. Dazu gehört das Signieren von Assemblys zur Erzeugung eines starken Namens, das Evaluieren von Typen in der Laufzeit, das dynamische Erzeugen von Typen und die Bedeutung von und der Umgang mit Anwendungsdomänen.
8
Kapitel 23: Sicherheitsgrundlagen 9
Sicherheit ist ein wichtiges Thema in allen Arten von Anwendungen. Kapitel 23 setzt sich deswegen mit diesem Thema in Windows-Anwendungen auseinander. Dazu gehören Themen wie die Sicherheit von Assemblys und das Verschlüsseln von Daten.
10
Nicht und eingeschränkt behandelte Themen Einige Themen haben leider nicht mehr in das Buch gepasst, andere Themen konnte ich nur eingeschränkt behandeln. Schauen Sie gelegentlich auf meiner Website in dem Blog vorbei, den ich für dieses Buch verwalte (oder abonnieren Sie den RSS-Feed). Ich werde im Laufe der Zeit immer wieder Artikel zu den im Buch nicht oder nicht umfassend beschriebenen Themen verfassen und dort veröffentlichen. Den Blog finden Sie an der Adresse www.juergenbayer.net/kompendium.
11
REF
41
Einführung
Die nicht behandelten »großen« Themen sind: ■
■
■
■
■
■
■
■
42
ADO.NET: Die »klassische .NET-Art«, Datenbanken über die Klassen von ADO.NET zu bearbeiten, ist u. U. für einige Anwendungen interessant, auch wenn LINQ to SQL und das ADO.NET Entity Framework wesentlich einfacher und flexibler sind. ADO.NET ist aber immer noch die performanteste Lösung, wenn es um die Bearbeitung von Datenbanken geht. Und nicht für alle Datenbanksysteme stehen Provider für LINQ to SQL zur Verfügung. Für ADO.NET blieb im Buch aber leider kein Platz. Sie finden das ausgelagerte Kapitel jedoch auf meiner Website an der Adresse www.juergen-bayer.net/artikel/csharp/ ado.net/ado.net.aspx. Das ADO.NET Entity Framework: Dieses ist sehr interessant für die Arbeit mit Datenbanken in großen Systemen und steht in Konkurrenz zu LINQ to SQL (das eher für kleinere Anwendungen empfohlen wird). Leider war das ADO.NET Entity Framework zu dem Zeitpunkt, als ich dieses Buch geschrieben habe, noch nicht erschienen (bzw. erst in einer Beta-Version). Die Webprogrammierung mit ASP.NET: ASP.NET ist ein sehr interessantes, aber leider auch sehr umfangreiches Thema. Ursprünglich hatte ich geplant, ASP.NET mit in das Buch zu nehmen. Für die ca. 400 Seiten, die alleine die Grundlagen in Anspruch nehmen würden, hat das Buch aber leider keinen Platz. Aktuelle ASP.NET-3.5-Bücher, die den gesamten Themenbereich umfassen, sind wesentlich umfangreicher. Das Buch »ASP.NET 3.5 Unleashed« von Stephen Walter, das sich auf ASP.NET 3.5 konzentriert (nicht auf die .NET- und C#-Grundlagen!), umfasst z. B. unglaubliche 1796 Seiten (ohne den umfangreichen Index). Webdienste: Webdienste gehören zwar zu ASP.NET, weil diese aber auch in vielen Fällen zur Kommunikation zwischen Windows-Anwendungen und externen Systemen genutzt werden, nenne ich dieses eigentlich wichtige, aber nicht behandelte Thema separat. WCF: Die Windows Workflow Foundation, die mit .NET 3.0 erschienen ist, ist eine wichtige Komponente zur Entwicklung von Anwendungen, die über beliebige Netzwerke miteinander kommunizieren. Dieses Thema ist leider auch sehr umfangreich. Das Buch »Programming WCF Services« von Juval Löwy (O'Reilly Verlag) umfasst z. B. 582 Seiten mit Informationen (ohne Index). WCF ist leider zu umfangreich, um im Visual C# 2008-Kompendium behandelt werden zu können. WF: Wie WCF ist die Workflow Foundation eine sehr interessante Komponente für die Gestaltung von Workflows und Statusmaschinen. Leider ist auch dieses Thema zu umfangreich für das Visual C# 2008-Kompendium. Die Enterprise Services: Die Enterprise Services sind Werkzeuge, die Microsoft über das so genannte COM+ mit dem Betriebssystem zur Verfügung stellt. COM+ basiert zwar auf dem alten COM (Component Object Model), mit dem .NET-Programmierer glücklicherweise nicht mehr allzu viel Kontakt haben. COM+ bietet aber leistungsfähige Features, die in Unternehmen gerne genutzt werden. Dazu gehören z. B. Just-In-Time-Aktivierung von Komponenten, verteilte Transaktionen und Objektpooling. Für die Behandlung der komplexen Grundlagen der Enterprise Services war in diesem Buch leider kein Platz. Windows.Forms: Wie bereits gesagt, befasst sich dieses Buch mit Windows.Forms nur noch sehr grundlegend in der Einführung in Visual Studio 2008 in Kapitel 2 und setzt Windows.Forms-Beispiele teilweise in den Kapiteln vor dem WPF-Kapitel ein. Meinen Windows.Forms-Artikel finden Sie an der Adresse www.juergen-bayer.net/artikel/csharp/windows.forms/windows.forms.aspx.
Zum Buch
■
■
Remoting: Remoting ermöglicht, dass eine Anwendung mit einer anderen über ein Netzwerk oder das Internet kommuniziert. Remoting wurde in .NET 3.0 von WCF ersetzt. Die Erstellung von Berichten über Crystal Reports und die in .NET integrierten Berichts-Features.
Neben den »großen« werden auch einige »kleine« Themen nicht behandelt: ■ ■ ■ ■ ■
1
Das Protokollieren mit den Klassen des Namensraums System.IO.Log. Die Programmierung von Makros und Add-Ins für Visual Studio. Die Integration von .NET-Assemblys in den SQL Server. Die Entwicklung von (Web-)Anwendungen für mobile Geräte (PDAs, MDAs etc.). Die Programmierung von Windows-Diensten.
2
Einige Themen werden nur eingeschränkt behandelt: ■ ■
■
3
Die nicht generischen Auflistungen werden nur in den Fällen behandelt, in denen es kein generisches Äquivalent gibt. WPF wird zwar recht ausführlich grundlegend behandelt. Für spezielle Themen wie Animationen, 2D-Grafik, 3D-Grafik, Sprachausgabe, Spracherkennung, Dokumenten-Support, navigationsbasierte Anwendungen, Silverlight-Anwendungen und Interoperabilität mit Windows.Forms war aber kein Platz in diesem Buch. Das Thema Sicherheit wird in Kapitel 23 grundlegend behandelt. Dazu gehört neben Grundlagen zur Codezugriffssicherheit auch das Verschlüsseln von Daten. Dabei blieb aber kein Platz für die Programmierung spezieller Anwendungen, die die Features der Codezugriffssicherheit nutzen. Spezielle Themen wie Membership, Obfuskatoren und das Verhindern von SQL Injection werden in diesem Buch nicht behandelt.
1.1.3
4
5
6
Der Inhalt der Buch-DVD 7
Auf der dem Buch beiliegenden DVD finden Sie alle im Buch verwendeten Beispiele als Visual-Studio-2008-Projekt. Die Datei Links.html enthält alle im Buch angesprochenen Internet-Links. Die Datei Tastenkombinationen.pdf beschreibt die wichtigen Visual-Studio-Tastenkombinationen. In der Datei RegEx-VS-Suchausdrücke.pdf finden Sie eine Übersicht über die wichtigen Bestandteile der regulären Ausdrücke, die beim Suchen und Ersetzen in Visual Studio verwendet werden können. Außerdem finden Sie auf der Buch-DVD die Installationsversion der für das Buch wichtigen Visual C# Express-Edition und zusätzliche Komponenten und Informationen zu C#. Falls Sie dieses Buch als PDF-Version gekauft haben, können Sie diese wichtige DVD bei Markt und Technik (www.mut.de) gegen eine Schutzgebühr nachbestellen. Auf der Buchseite bei Markt und Technik (siehe Link auf der Seite www.juergen-bayer.net/ buecher/csharpkompendium) finden Sie einen entsprechenden Link.
1.1.4
8
9
10 TIPP
11
Der Buch-Blog
An der Adresse www.juergen-bayer.net/kompendium finden Sie einen Blog zum Buch, in dem ich eventuelle Fehler im Buch korrigiere und neue Artikel (die im Buch nicht behandelte Themen beinhalten) veröffentliche. Diesen Blog können Sie natürlich auch über einen RSS-Reader (z. B. den im Firefox integrierten) abonnieren, um über Neuigkeiten informiert zu werden.
43
Einführung
1.1.5
Typografische Konventionen
Dieses Buch verwendet einige typografische Konventionen, die jedoch nicht allzu sehr vom allgemeinen Standard abweichen.
Syntaxbeschreibungen Zur Beschreibung der Syntax einer Deklaration wurde eine kompakte und übersichtliche Form gewählt, wie im folgenden Beispiel: int IndexOf({string | char} value [, int startIndex] [, int count])
Normale Wörter sind sprachspezifische Schlüsselwörter. Kursive Wörter sind Platzhalter für Eingaben, die Sie spezifizieren müssen. Die in hervorgehobenen eckigen Klammern stehenden Elemente sind optional. Diese Elemente können Sie, müssen Sie jedoch nicht angeben. Die hervorgehobenen eckigen Klammern sind nur Teil der Syntaxbeschreibung und werden nicht im Programmcode angegeben. Wenn an einer Stelle mehrere Varianten möglich sind, werden diese in der Syntaxbeschreibung durch ein hervorgehobenes | (das bitweise Oder-Zeichen in C#) voneinander getrennt. Handelt es sich dabei um eine nicht optionale Angabe, wird diese in hervorgehobene geschweifte Klammern eingeschlossen. Diese Klammern werden dabei natürlich auch nicht mit angegeben. Sie müssen bei den Syntaxbeschreibungen ein wenig aufpassen: Wenn Sie mit Arrays und Auflistungen (oder anderen Objekten, die Indexer anbieten) arbeiten, gehören eckige Klammern zur Syntax. Geschweifte Klammern werden ebenfalls in C#-Code häufig eingesetzt. Sie müssen die Syntaxbeschreibungsklammern also von den sprachspezifischen Klammern unterscheiden. Leider blieb mir keine andere Möglichkeit, die teilweise umfangreichen Deklarationen der Elemente mancher Klassen übersichtlich und kompakt darzustellen. Damit Sie die eckigen und geschweiften Klammern, die Teil der Syntaxbeschreibung sind, von den Klammern, die Teil der Syntax sind, visuell trennen können, werden diese hervorgehoben dargestellt.
Beispiel-Listings Beispiel-Listings werden folgendermaßen dargestellt: string now; now = DateTime.Now.ToShortDateString(); MessageBox.Show("Heute ist der " + now);
Exkurse Exkurse werden wie dieser Text dargestellt. EXKURS
Typografische Konventionen im Fließtext Im normalen Text werden sprachspezifische Schlüsselwörter in der Schriftart Courier dargestellt. Wörter in KAPITÄLCHEN im normalen Text bezeichnen Teile der Benutzerschnittstelle, wie z. B. Menübefehle und Schalter. Menübefehle, die in einem Menü untergeordnet sind, werden mit den übergeordneten Menüs angegeben, wobei die einzelnen Menüebenen durch einen Schrägstrich getrennt werden (z. B. DATEI / BEENDEN). Datei- und Ordnernamen werden kursiv formatiert. Internetadressen werden folgendermaßen gekennzeichnet: www.juergen-bayer.net.
44
Das Glossar
Marginaltexte fassen wichtige Aussagen, die im Fließtext stehen, zusammen. Marginaltexte gehören nicht zum Text und können überlesen werden. Sie helfen aber u. U. beim Finden wichtiger Themen.
Marginaltexte fassen wichtige Aussagen zusammen
Icons Die Kompendium-Reihe setzt verschiedene Icons zur Kennzeichnung besonderer Abschnitte ein. Ich verwende im Buch die folgenden:
1
Ein solches Icon kennzeichnet besondere Hinweise zum aktuellen Thema. INFO
2
HALT
3
TIPP
4
REF
5
STEPS
6
Dieses Icon kennzeichnet Warnungen, die Sie auf jeden Fall beachten sollten, da deren Nichtbeachtung zu Fehlern oder Problemen führen könnte. Das Tipp-Icon bezeichnet Tipps (was auch sonst), z. B. Hinweise auf Webseiten, die weitere Informationen oder Komponenten zum jeweiligen Thema anbieten. Dieses Icon steht an Texten, die auf weiterführende Informationen verweisen, z. B. in anderen Büchern oder im Internet. Neben diesem Icon stehen (relativ selten genutzte) Schritt-für-Schritt-Anleitungen, die Ihnen erläutern, wie bestimmte Probleme gelöst werden können oder wie Sie ein Beispiel nachvollziehen. Das Disc-Icon bezeichnet Verweise auf Dateien, die Sie auf der beiliegenden DVD finden.
7
DISC
Dieses Icon steht neben Texten, die Features beschreiben, die in der C#-Version 3.0 oder der .NET-Version 3.5 im Vergleich zur vorhergehenden Version neu sind.
8
NEU
1.2
Das Glossar 9
Obwohl ich es versucht habe: Ich konnte in den einzelnen Kapiteln nicht auf Begriffe verzichten, von denen ich annehmen muss, dass nicht alle Leser diese kennen. An vielen Stellen habe ich diese Begriffe kurz in Klammern erläutert oder Fußnoten angebracht. Das war jedoch nicht immer möglich.
10
Deswegen können Sie alle diese Begriffe im Glossar des Buchs nachlesen, das Sie im Anhang finden.
1.3
11
Die Installation von Visual Studio 2008 und der Express-Editionen
Ich gehe in diesem Buch davon aus, dass Sie Ihre C#-Anwendungen mit Visual Studio 2008 entwickeln. Sie könnten zwar (wenn Sie sehr viel Zeit haben …) nur das .NET Framework SDK (Software Development Kit) installieren, mit einem einfachen Text-
45
Einführung
Editor arbeiten und Ihre Programme mit dem C#-Compiler kompilieren, der dem SDK beiliegt. Oder Sie könnten (ebenfalls, nachdem Sie das .NET Framework SDK installiert haben) die freie Entwicklungsumgebung Sharp Develop verwenden (www.icsharpcode.net/OpenSource/SD). Da Microsoft aber Visual Studio 2008 in Form der (etwas eingeschränkten) Express-Editionen kostenfrei zur Verfügung stellt, denke ich, dass diese in vielen Firmen verwendete Entwicklungsumgebung die beste Wahl darstellt. Leider bleiben Linux-Benutzer damit zunächst außen vor.
TIPP
REF
Für dieses Buch reicht Visual C# 2008 Express grundsätzlich aus. Das einzige Thema dieses Buchs, das mit Visual C# 2008 Express nicht umsetzbar ist, ist die Erstellung eines normalen Setup (ein ClickOnce-Setup ist allerdings möglich). Ansonsten laufen alle Beispielprojekte des Buchs auch mit der Express-Edition. Wenn Sie eingeschriebener Student sind, können Sie die Professional Edition von Visual Studio 2008 kostenfrei von Microsoft beziehen. Nähere Informationen finden Sie auf der »Microsoft DreamSpark«-Seite an der Adresse downloads.channel8.msdn.com.
1.3.1
Die Visual-Studio-2008-Editionen
Visual Studio existiert in mehreren Editionen, die zwar dieselben Basis-Features aufweisen, sich aber in den erweiterten Features (und natürlich im Preis) unterscheiden: ■
■
■
■
46
Visual Studio 2008 Team Suite Edition: Die Team Suite Edition bietet alle Features der Professional Edition und zusätzliche Komponenten, die bei der Arbeit im Team verwendet werden. Dazu gehört z. B. eine Quellcodeverwaltung, die es ermöglicht, Projektdateien so auszuchecken, dass andere Entwickler diese nicht mehr bearbeiten können, und die die einzelnen Versionen einer Datei in Ihrer Datenbank speichert. Visual Studio 2008 Professional Edition: Diese Edition ist für professionelle Entwickler gedacht, die hauptsächlich Software für Kunden entwickeln. Sie bietet alle Features, die ein einzelner Entwickler benötigt, allerdings fehlen die Komponenten, die bei der Entwicklung im Team benötigt werden. Visual Studio 2008 Standard Edition: Diese Edition ist für Entwickler gedacht, die weniger professionell arbeiten. Ihr fehlen im Vergleich zur Professional Edition die folgenden Features: Anwendungen für Office entwickeln, Anwendungen für mobile Geräte entwickeln, der Klassendesigner, Crystal Reports (flexibles Bericht-Tool), der Server Explorer und die Unit-Test-Features. Visual Studio 2008 Standard Edition besitzt außerdem einfachere und etwas eingeschränkte Menüs, ähnlich der Express-Edition und verfügt nur über eine OnlineDokumentation. Express-Editionen: Die kostenfreien separaten Express-Editionen von Visual Studio bietet lediglich Basis-Features, die jedoch für die meisten ProgrammierAufgaben ausreichen. Microsoft hat für die einzelnen Programmier-Umgebungen (C#, Visual Basic, Webanwendungen) einzelne Entwicklungsumgebungen erstellt, und bietet zudem eine Express-Edition des SQL Servers. Kapitel 2 beschreibt die Möglichkeiten der Anwendungsentwicklung mit Visual Studio 2008 und informiert über die Verfügbarkeit des jeweiligen Projekttyps in den Express-Editionen.
Die Installation von Visual Studio 2008 und der Express-Editionen
1.3.2
Betriebssystem und Speicheranforderungen
Visual Studio 2008 lässt sich unter Windows XP, XP Media Edition, XP Tablet PC Edition SP2, Windows Server 2003 und unter Vista installieren. Microsoft empfiehlt als Mindestanforderung einen Prozessor mit 1,6 GHz Taktfrequenz und 384 MB Arbeitsspeicher. Ich würde eher einen Prozessor mit mindestens zwei GHz Taktfrequenz und einem GB Arbeitsspeicher empfehlen. Festplattenplatz sollte reichlich vorhanden sein, da Visual Studio 2008 (ohne Visual C++) mit der (dringend) empfohlenen MSDNDokumentation ca. 7,3 GB (Visual Studio mit .NET Framework und Tools ohne C++: ca. 4,8 GB; Dokumentation: ca. 2.5 GB) und die hier im Buch beschriebenen ExpressEditionen (inkl. SQL Server Express und der Dokumentation) etwa 1,5 GB belegen. Daneben benötigt die Installation zusätzlich temporären Festplattenplatz.
1.3.3
1 2
Vorinstallationen
Vor der Installation des .NET Framework oder von Visual Studio 2008 sollten Sie die Internet-Informationsdienste installieren. Visual Studio 2008 verwendet zwar einen eigenen Webserver für die Ausführung von Webanwendungen im Debugger. Es ist aber immer sinnvoll, eine erstellte Website in den Internet-Informationsdiensten zu testen, bevor diese veröffentlicht wird. Die in den Internet-Informationsdiensten dazu notwendigen Dateiendungs-Verknüpfungen werden von der Visual-StudioInstallation nur dann vorgenommen, wenn die Internet-Informationsdienste bereits existieren, wenn Visual Studio installiert wird. Eine nachträgliche Registration der Dateiendungen (.aspx, .asmx etc.) ist nur über das Kommandozeilenprogramm aspnet_regiis.exe möglich. Wenn Sie die Internet-Informationsdienste auf Ihrem System ausführen, ist Ihr Rechner ein potenzieller Angriffspunkt für Viren und Hackerangriffe. Verwenden Sie deshalb eine gute und aktuelle Firewall und installieren Sie gegebenenfalls das Security Toolkit, das Sie bei www.microsoft.com/security finden, um Ihren Rechner so gut es geht abzusichern.
Installieren Sie die Internet-Informationsdienste
4
5
6 HALT
7
Unter Windows XP und Vista installieren Sie die Internet-Informationsdienste über die Windows-Komponenten. Öffnen Sie die Systemsteuerungen, wählen Sie den Eintrag SOFTWARE und klicken Sie dann auf WINDOWS-KOMPONENTEN HINZUFÜGEN/ENTFERNEN. Installieren Sie dort die Internet-Informationsdienste, idealerweise mit allen Unteroptionen.
1.3.4
3
8
9
Die Installation der Express-Editionen
Wenn Sie die Express-Editionen installieren wollen, sollten Sie die folgenden Programme installieren: ■
Visual C# 2008 Express-Edition: Diese Edition ermöglicht die Programmierung von Windows-Anwendungen und Klassenbibliotheken mit C#.
■
Visual Web Developer 2008 Express-Edition: Mit dieser Entwicklungsumgebung entwickeln Sie Webanwendungen und Webdienste (mit ASP.NET).
10
11
47
Einführung
Die Installationsdateien der genannten Editionen finden Sie auf der dem Buch beiliegenden DVD. Sie können diese aber auch bei Microsoft an der Adresse www.microsoft.com/express downloaden. Falls Sie nicht die Version auf der BuchDVD verwenden wollen, können Sie bei Microsoft eine Online-Installation herunterladen. In diesem Fall laden Sie zunächst lediglich ein kleines Setup-Programm herunter. Wenn Sie dieses starten, werden die restlichen Installationsdateien sukzessiv nachgeladen. Die Installationsdateien sind recht groß (um die 400 MB). Die Installation kann deswegen etwas dauern. Sie haben aber auch die Möglichkeit, die Setup-Dateien komplett herunterzuladen. Unverständlicherweise bietet Microsoft diese nicht in einem Archiv gepackt an, sondern als Image-Datei. Diese Image-Datei können Sie in einer Brenn-Anwendung wie Nero öffnen, auf CD bzw. DVD brennen oder damit ein virtuelles CD-Laufwerk simulieren (über Nero Image Drive oder das von Microsoft nicht offiziell unterstützte Tool »Virtual CD-ROM Control Panel for Windows XP«, das Sie im Internet finden, wenn Sie bei Google den Namen eingeben).
TIPP
Wenn Sie Nero Image Drive verwenden, um die Image-Dateien in virtuelle CD-Laufwerke zu mounten, ändern Sie die Endung der Dateien von .img in .iso. Nero Image Drive zeigt per Voreinstellung nur Nero-Images (.nrg-Dateien) und ISO-Images an. Bei der .img-Datei handelt es sich um ein ISO-Image, weswegen dieser »Trick« funktioniert. Alternativ können Sie Image-Tools wie IsoBuster (www.isobuster.com) verwenden um die Image-Datei in einen Ordner zu extrahieren. Zur Installation der Visual C# 2008 Express-Edition starten Sie die Datei setup.exe (für die Offline-Installation) bzw. vcssetup.exe (für die Online-Installation). Nach den ersten zwei Willkommens-Schritten können Sie in Schritt 3 einige zusätzliche Optionen auswählen (die Verfügbarkeit der Optionen hängt davon ab, ob die entsprechenden Features bereits auf Ihrem System installiert sind): ■
■
■
48
MSDN EXPRESS LIBRARY FÜR VISUAL STUDIO 2008: Wenn Sie nicht gerade über sehr wenig Festplattenplatz verfügen, sollten Sie diese wichtige Dokumentation mit installieren. Alternativ können Sie aber auch, wie ich im Abschnitt »Die vollständige Visual-Studio-Dokumentation und zusätzliche Tools« beschreibe, später die (größere) vollständige Visual-Studio-Dokumentation installieren. MICROSOFT SQL SERVER 2005 EXPRESS-EDITION: Die Microsoft SQL Server ExpressEdition ist ein prinzipiell vollständiger SQL Server mit einigen kleinen Einschränkungen. Dieser SQL Server ist für die Verwendung durch einen einzigen Benutzer gedacht (allerdings können auch mehrere Benutzer gleichzeitig darauf zugreifen). Im Vergleich zum »richtigen« SQL Server fehlen der Express-Edition auch die Administrier-Tools (die aber bei Microsoft für die Express-Edition heruntergeladen werden können, wie ich in Kapitel 19 beschreibe). Sie sollten die Microsoft SQL Server Express-Edition auf jeden Fall installieren. MICROSOFT SILVERLIGHT-RUNTIME: Silverlight ist ein Browser-Add-In, das die Ausführung von WPF-Seiten in verschiedenen Browsern ermöglicht. WPF (Windows Presentation Foundation) ermöglicht reichhaltige Oberflächen, u. a. mit 3D-Integration, Animationen und Medien-Integration. Silverlight ist prinzipiell vergleichbar mit Adobe Flash und ist notwendig, wenn Sie WPF-Seiten für die Verwendung in Webanwendungen programmieren oder sich Internetseiten anschauen wollen, die mit WPF arbeiten.
Die Installation von Visual Studio 2008 und der Express-Editionen
Nachdem Sie im nächsten Schritt den Installationsort festgelegt haben (den Sie nur dann ändern können, wenn noch keine anderen Visual-Studio-Komponenten installiert sind), können Sie im letzten Schritt die Installation starten. Die Installation der Visual Web Developer 2008 Express-Edition läuft ähnlich ab. Starten Sie zur Installation die Datei setup.exe (Offline-Installation) bzw. vnssetup.exe (Online-Installation).
1
Die vollständige Visual-Studio-Dokumentation und zusätzliche Tools Die Express-Editionen enthalten nur eine eingeschränkte Hilfe, der z. B. die Dokumentation des .NET Framework fehlt. Diese steht zwar online zur Verfügung (indem Sie in der Hilfe nach entsprechenden Begriffen suchen), aber nicht für die kontextsensitive Hilfe (die aufgerufen wird, wenn Sie (F1) auf einem Schlüsselwort oder Bezeichner im Quellcode betätigen) und auch nicht im Index der Hilfe. Um die vollständige Dokumentation des .NET Framework (inkl. der Dokumentation des Windows-API) zu erhalten, sollten Sie nach der Installation der Express-Editionen die »Vollständige MSDN Bibliothek für Visual Studio 2008« (2,17 GB) installieren, die Sie auf der Buch-DVD und an der Adresse www.microsoft.com/germany/express/download/msdn.aspx finden. Damit erhalten Sie auch wichtige Tool-Programme (im Ordner C:\Programme\Microsoft SDKs\Windows\v6.0A\bin), die bei der professionellen Arbeit mit dem .NET Framework wichtig sein können. Beenden Sie vor der Installation alle geöffneten ExpressEditionen, damit die Dokumentation beim Neustart derselben korrekt integriert wird (was übrigens recht viel Zeit in Anspruch nimmt). Damit haben Sie in den Express-Editionen prinzipiell dieselben Hilfe- und Tool-Möglichkeiten wie in Visual Studio 2008.
1.3.5
2
3
4
5
Die Installation von Visual Studio 2008 6
Starten Sie zur Installation von Visual Studio 2008 die Datei Setup.exe. Nachdem Sie die Lizenzbestimmungen bestätigt haben, können Sie auswählen, welche Art der Installation Sie ausführen wollen. Hier sollten Sie die benutzerdefinierte Installation wählen, um auswählen zu können, welche Features Sie installieren wollen. Im nächsten Schritt können Sie dann die einzelnen zu installierenden Bestandteile auswählen. Wenn Sie nur mit C# entwickeln wollen, können Sie bei den Sprachtools Visual C++ und Visual Basic abwählen. Visual C# und den Visual Web Developer (den Teil der Entwicklungsumgebung, mit der Webanwendungen entwickelt werden) sollten Sie aber auf jeden Fall ausgewählt lassen.
7
8
Alle anderen Installationsoptionen sind für die (professionelle) Entwicklung relativ wichtig und sollten gewählt bleiben. Falls Sie Festplattenplatz sparen müssen, können Sie die für Sie unwichtigen Optionen natürlich auch abwählen. Die folgende Auflistung gibt Ihnen eine Übersicht über die einzelnen Optionen: ■
9
DOTFUSCATOR COMMUNITY EDITION: Der Dotfuscator ist ein externes Tool, über das der in einer Assembly gespeicherte CIL-Code so durcheinandergebracht werden kann, dass er zwar noch problemlos ausgeführt wird, aber nicht mehr zu einem sinnvollen Quelltext dekompiliert werden kann. Aufgrund des ZwischencodePrinzips ist es nämlich bei ungeschützten Assemblys leider (oder in den Fällen, in denen ich zu Studienzwecken Assemblys dekompiliert habe, glücklicherweise ☺) möglich, den CIL-Code z. B. in C#-Code zurückzuführen. Auch wenn dabei (natürlich) keine Kommentare im Code stehen, kann ein Programmierer den Code für seine Zwecke nutzen. Ein Dotfuscator verhindert dies wirksam. Die Community-Edition ist gegenüber der Vollversion des Dotfuscators oder gegenüber anderen ähnlichen Tools etwas eingeschränkt, sodass Sie für professionelle Anwendungen besser einen vollständigen Dotfuscator installieren sollten.
10
11
49
Einführung
■
■
■
■
■
TOOLS FÜR DAS VERTEILEN VON ANWENDUNGEN: Diese Tools sind wichtig, wenn Sie Setup-Programme erstellen. Auf die Grafikbibliothek können Sie ggf. verzichten, die Mergemodule benötigen Sie aber in einigen Fällen. KOMPONENTENTESTTOOLS: Die ab der Professional-Edition von Visual Studio verfügbaren Tools für Komponententests (Unit Tests) ermöglichen das Testen von einzelnen Einheiten Ihrer Programme. Unit Testing ist ein spezielles Thema, das in Kapitel 9 besprochen wird. Sie sollten diese wichtigen Tools installieren, wenn Sie professionell entwickeln wollen. MICROSOFT SQL SERVER 2005 EXPRESS-EDITION: Die Microsoft SQL Server 2005 Express-Edition ist ein prinzipiell vollständiger SQL Server mit einigen kleinen Einschränkungen. Dieser SQL Server ist für die Verwendung durch einen einzigen Benutzer gedacht (allerdings können auch mehrere Benutzer gleichzeitig darauf zugreifen). Im Vergleich zum »richtigen« SQL Server fehlen der ExpressEdition auch die Administrier-Tools (die aber bei Microsoft für die Express-Edition heruntergeladen werden können, wie ich in Kapitel 19 beschreibe). Sie sollten die Microsoft SQL Server Express-Edition auf jeden Fall installieren, auch wenn Sie bereits den SQL Server installiert haben. Die Gründe dafür sind, dass die Schnellstart-Lernprogramme den SQL Server Express verwenden und dass nur die Express-Edition das dynamische Einbinden einer SQL-Server-Datenbank erlaubt, die nicht im SQL Server registriert ist. CRYSTAL REPORTS BASIC FÜR VISUAL STUDIO 2008: Crystal Reports ist ein externer, aber in .NET integrierter, sehr umfangreicher Berichtsgenerator, der in vielen Firmen Standard ist. Obwohl die Arbeit mit Crystal Reports eine Menge Lernarbeit erfordert, können Sie damit alle Arten von Berichten erzeugen. Und wenn Sie einmal das Prinzip von Crystal Reports verstanden haben, geht dies sogar recht schnell. Crystal Reports wird in diesem Buch nicht behandelt. CRYSTAL REPORTS LANGUAGE PACK FÜR VISUAL STUDIO 2008: Enthält u. a. die deutschen Texte der Visual-Studio-Version von Crystal Reports.
Starten Sie dann die Installation über den nächsten Schritt. Wenn das .NET Framework 3.5 installiert werden muss, weil dieses noch nicht auf Ihrem Rechner vorhanden ist, müssen Sie nach dieser Installation den Rechner neu starten. U. U. müssen Sie die Gesamt-Installation über die Setup.exe-Datei danach erneut starten und Ihre Auswahl wiederholen. Nach der Installation von Visual Studio sollten Sie die Dokumentation installieren. Nach meinen Erfahrungen ist die beste Lösung eine komplette Installation. Sie können auch nur Teile installieren oder die Dokumentation ganz weglassen. In diesem Fall greift Visual Studio dann aber beim Öffnen der Dokumentation auf die OnlineDokumentation im Internet zu, was in der Regel zu viel Zeit in Anspruch nimmt (die wir ja alle nicht haben, weil wir neben dem Programmieren ja auch noch windsurfen, snowboarden oder sonst was machen wollen).
1.3.6
Weitere hilfreiche Installationen
Wenn auf Ihrem System nicht der vollständige SQL Server installiert ist, sondern nur der SQL Server Express, besitzen Sie normalerweise kein Werkzeug zur Administration des SQL Servers. Wenn Sie den SQL Server installieren, können Sie dazu das SQL Server Management Studio mit installieren. Dem SQL Server Express fehlt ein solches Tool.
50
Die Installation von Visual Studio 2008 und der Express-Editionen
Microsoft bietet jedoch auch das SQL Server Management Studio in einer Express-Edition an. Sie können die aktuelle Version aber über die Adresse msdn2.microsoft.com/ en-us/express/bb410791.aspx downloaden. Sie sollten diese Anwendung installieren, wenn Sie ernsthaft mit dem SQL Server Express arbeiten wollen. Eine Beschreibung des SQL Server Management Studio sprengt jedoch den Rahmen dieses Buchs. Zu dem gesondert herunterladbaren Windows SDK möchte ich Ihnen noch ein paar Informationen geben. Dieses SDK enthält neben der (englischen) Dokumentation des .NET Framework u. a. einige Beispiele zu verschiedenen Technologien. Sie finden das zurzeit aktuelle »Microsoft Windows Software Development Kit (SDK) for Windows Server® 2008 and .NET Framework 3.5« an der Adresse msdn2.microsoft.com/en-us/ windowsvista/bb980924.aspx. Der etwas irreführende Name rührt daher, dass das aktuellste SDK immer die aktuellste Windows-Version berücksichtigt. Zur Zeit des Schreibens dieser Zeilen war das Windows Server 2008. Das SDK enthält aber auch alle Informationen für ältere Windows-Versionen. Die Downloadgröße ist leider enorm (mehr als ein GB). Der auf der Festplatte benötigte Platz ist mit 2 bis 3 GB auch nicht gerade gering.
1 2
3
Sie benötigen das Windows SDK nicht wirklich, da das SDK, das mit Visual Studio mitgeliefert wird, alles zum Programmieren Notwendige enthält. Das Windows SDK könnte aber ggf. interessant sein, wenn Sie einmal die englischsprachige Dokumentation lesen wollen oder weil es Beispiele zu den neueren Technologien (wie WCF und WPF) enthält.
4
5
Bei der Installation können Sie die Optionen abwählen, die »Win32« oder »COM« im Namen tragen, wenn Sie sich auf die Entwicklung unter .NET konzentrieren wollen. Das Windows-API und das ältere COM-Modell werden bei der Entwicklung von .NET-Anwendungen nur noch sehr selten benötigt. Informationen darüber finden Sie auch im Internet (www.pinvoke.net) Außerdem können Sie die Option VISUAL C++ COMPILERS abwählen, da Sie diese für C# nicht benötigen.
1.3.7
6
7
Der erste Start von Visual Studio 2008
Beim ersten Start von Visual Studio 2008 (nicht der Express-Editionen) werden Sie aufgefordert, die Einstellung der Entwicklungsumgebung vorzunehmen. Visual Studio 2008 kann nämlich für verschiedene Zwecke so eingestellt werden, dass die für den Anwender wichtigen Features im Vordergrund stehen bzw. unwichtige Features ausgeblendet werden. Sie sollten hier die allgemeinen Entwicklungseinstellungen wählen, da diese alle Features zur Verfügung stellen. Anderen Einstellungen fehlen z. B. bestimmte u. U. wichtige Menüeinträge.
8
9
Wenn Sie Visual Studio dann über den entsprechenden Schalter starten, wird noch ein wenig konfiguriert und Sie können mit der Entwicklungsumgebung arbeiten. Wie das geht, zeigt Kapitel 2. Falls Sie eine falsche Einstellung ausgewählt haben, wählen Sie im EXTRAS-Menü von Visual Studio den Eintrag EINSTELLUNGEN EXPORTIEREN UND IMPORTIEREN, wählen im folgenden Dialog die Option ALLE EINSTELLUNGEN ZURÜCKSETZEN und können im Folgenden dann die ALLGEMEINEN ENTWICKLUNGSEINSTELLUNGEN (oder eine von Ihnen bevorzugte Einstellung) festlegen.
10
11 TIPP
51
Einführung
1.4
Wie erhalten Sie Hilfe zu Problemen?
In der Programmier-Praxis treten (natürlich) immer wieder Probleme auf. Am Anfang sind dies meist Fragen wie »Wie versende ich eine E-Mail?«, später dann eher vielleicht »Wie kann ich dafür sorgen, dass ein Thread so lange wartet, bis ein anderer Thread seine Arbeit erledigt hat?«. Die meisten dieser Programmierprobleme lassen sich über die verschiedenen Informationsquellen lösen, die ich hier vorstelle.
1.4.1
Die .NET Framework-Dokumentation
Die äußerst umfangreiche .NET Framework-Dokumentation finden Sie im Startmenü von Windows im Ordner PROGRAMME / MICROSOFT VISUAL STUDIO 2008, wenn Sie Visual Studio haben. Klicken Sie dort auf den Eintrag MICROSOFT VISUAL STUDIO 2008 DOKUMENTATION. Für den Fall, dass Sie nur die Express-Editionen installiert haben, finden Sie keinen Menüeintrag und müssen die (eingeschränkte) Dokumentation über das HILFE-Menü der Anwendung starten. Haben Sie allerdings wie von mir empfohlen die »Vollständige MSDN Bibliothek für Visual Studio 2008« installiert (Seite 49), haben Sie die .NET Framework-Dokumentation im Startmenü im Ordner MICROSOFT DEVELOPER NETWORK. In dieser Dokumentation finden Sie die Referenz der Klassen des .NET Framework unter .NET-ENTWICKLUNG / DOKUMENTATION ZU .NET FRAMEWORK SDK / .NET FRAMEWORK. Unter ENTWICKLUNGSTOOLS UND SPRACHEN / VISUAL STUDIO / VISUAL C# finden Sie die C#-Referenz. Diese Dokumentationen können Sie innerhalb von Visual Studio auch kontextsensitiv nutzen, indem Sie den Eingabecursor auf einen Bezeichner in Ihrem Quelltext setzen und dann in der dynamischen Hilfe (im Fenster unten rechts) ein passendes Hilfethema auswählen. Leider funktioniert die kontextsensitive Hilfe in den ExpressEditionen nicht wie in Visual Studio und zeigt nicht die Dokumentation des Typs oder Typ-Elements an, das sich unter dem Cursor befindet, sondern lediglich eine Seite mit allgemeinen Informationen. Sie können aber im Index der Hilfe suchen (wenn Sie die »Vollständige MSDN Bibliothek für Visual Studio 2008« installiert haben), um die Dokumentation eines Typs oder eines Typ-Elements zu finden.
1.4.2
Die Beispiele
Wenn Sie Visual Studio installiert haben, finden Sie im Ordner Samples\1031 im Visual-Studio-Ordner (normalerweise C:\Programme\Microsoft Visual Studio 9.0) einige ZIP-Archive mit verschiedenen Beispielen. Das Archiv CSharpSamples.zip enthält C#-Projekte. Haben Sie eine der Express-Editionen installiert, finden Sie die Beispiele leider nicht auf Ihrer Festplatte. Sie können diese aber aus dem Internet an der Adresse code.msdn.microsoft.com/csharpsamples/Release/ProjectReleases.aspx herunterladen. Diese aktualisierten Beispiele sind natürlich auch dann interessant, wenn Sie Visual Studio 2008 installiert haben. Wenn Sie das Windows SDK installiert haben, enthält der Ordner Samples des Windows-SDK-Ordner (normalerweise C:\Programme\Microsoft SDKs\Windows\vX.X) Beispiele zu den neueren Technologien (WCF, WF, WPF, Windows Cardspace).
52
Wie erhalten Sie Hilfe zu Problemen?
1.4.3
Codebooks und Kochbücher
Eine hervorragende Quelle für die Suche nach Problemlösungen sind die Kochbücher, Cookbooks und Codebooks, die es in vielfältiger Form von verschiedenen Verlagen gibt. Ein mit sehr viel Enthusiasmus geschriebenes Exemplar dieser Spezies ist das C# 2008 Codebook, das (zur Zeit des Schreibens dieser Zeilen quasi parallel) von einem Autor geschrieben wird, den Sie kennen, da Sie eines seiner Bücher in Händen halten ☺. Sie finden es unter der ISBN 978-3-8273-2576-1.
1
In diesen Büchern finden Sie in der Regel einfache Lösungen für viele, mehr oder weniger komplizierte Praxisprobleme in Form von »Rezepten«. Das Lesen einer Textdatei gehört dabei genauso dazu wie das Senden einer E-Mail oder das Lesen der Tags einer MP3-Datei. Dieser Bücher sind in der Regel sehr übersichtlich und effizient. Suchen Sie bei Amazon (www.amazon.de) nach »C# Kochbuch«, um eine Auswahl der verschiedenen verfügbaren Kochbücher, Cookbooks und Codebooks zu erhalten.
2
3
1.4.4
Suchen im Internet
Wenn Sie bei der Programmierung das eine oder andere Problem lösen müssen, können Sie dieses (natürlich) meist über das Internet erreichen. Eine der ersten Adressen ist in meiner Praxis immer die Newsgroup-Suche von Google, deren erweiterte Version Sie an der Adresse groups.google.com/advanced_group_search finden.
4
In dieser speziellen Google-Suche können Sie nach aktuellen und archivierten Newsgroup-Beiträgen suchen. Die meisten Fragen, die ich in meiner Praxis und beim Schreiben meiner Bücher hatte und habe, konnte und kann ich allein durch eine einfache Google-Recherche lösen.
5
6
In dem Feld NEWSGROUP der Google-Suche können Sie Ihre Suche auf eine oder mehrere Newsgroups einschränken (mehrere trennen Sie einfach durch Kommata oder über das Schlüsselwort Or). Wenn Sie den Namen der Newsgroup nicht kennen oder in mehreren Gruppen suchen wollen, können Sie den Namen mit Wildcards maskieren. Ich verwende eigentlich immer die Maske *dotnet*, weil alle .NET-Newsgroups diesen Begriff im Namen tragen.
7
Abbildung 1.1: Google-Newsgroup-Suche
8
9
10
11
53
Einführung
Abbildung 1.2: Ergebnis der Google-Newsgroup-Suche
Abbildung 1.3: Gefundener NewsThread mit dem gesuchten Thema
Microsoft stellt eine sehr große Anzahl an Informationen in seinem MDSN (Microsoft Developer Network) und in der Knowledge Base zur Verfügung. Eine kombinierte Suche ist (u. a.) über die Seite search.msdn.microsoft.com/search möglich, die Sie bei Bedarf auf Deutsch umstellen können. Hier können Sie auch mit deutschen Begriffen suchen. Achten Sie darauf, dass Sie im Suchbegriff immer ».NET« oder »C#« angeben, damit Sie nur Artikel finden, die etwas mit C# oder .NET zu tun haben.
54
Wie erhalten Sie Hilfe zu Problemen?
Abbildung 1.4: Suche in der Knowledge Base und im MSDN von Microsoft
1 2
3
4
5 Abbildung 1.5: Ergebnis der MSDNSuche von Microsoft
6
7
8
9
10
11 Eine andere Möglichkeit ist die Suche auf der englischsprachigen Microsoft-Seite, die nach meinen Erfahrungen häufig andere bzw. bessere Ergebnisse liefert. Diese Suche erreichen Sie über den Link search.microsoft.com/advancedsearch.aspx? mkt=en-US. Geben Sie als Suchseiten die Knowledge Base und das MSDN an.
55
Einführung
Abbildung 1.6: Suche in der englischsprachigen Microsoft-Knowledge Base und im MSDN
Abbildung 1.7: Ergebnis der Knowledge Base-/MSDNSuche bei Microsoft
DISC
Microsoft verwirrt allerdings mit weiteren Suchseiten, wie z. B. der Seite www.microsoft.com/germany/msdn/search.aspx, die im »MSDN-Portal« sucht. Probieren Sie die Microsoft-Suchen aus, Sie werden sehen, dass die Ergebnisse der einzelnen Suchseiten recht unterschiedlich sind. Die Links zu den Suchseiten finden Sie in der Datei Links.html auf der Buch-DVD. Wenn Sie selbst in Newsgroups Fragen stellen oder Antworten geben wollen, empfehle ich die Newsgroup microsoft.public.dotnet.languages.csharp. In dieser englischsprachigen Newsgroup wird sehr viel gepostet. In der deutschen Newsgroup microsoft.public.de.german.entwickler.dotnet.csharp finden Sie dagegen nur relativ selten neue Beiträge.
56
Wie erhalten Sie Hilfe zu Problemen?
1.4.5
Wichtige .NET-Websites
Viele Websites im Internet beschäftigen sich mit .NET. Ich kenne natürlich nicht alle, während meiner Arbeit mit .NET habe ich aber einige sehr gute Websites entdeckt, die ich hier kurz vorstelle. Auf den meisten dieser Websites finden Sie neben BeispielQuellcodes auch kompakte Artikel, die spezifische und teilweise komplexe Themen (meist) verständlich erläutern.
1
Die Links zu den Websites, die ich hier beschreibe, finden Sie auch in der Datei Links.html auf der Buch-DVD. DISC
2
WindowsClient.net Diese zu Microsoft gehörende Seite (windowsclient.net) stellt eine Vielzahl an Artikeln, Beispielen, Foren etc. für die Entwicklung von Windows-Anwendungen zur Verfügung. Viele Artikel stammen aus dem MSDN, sind aber hier übersichtlich zusammengefasst.
3
ASP.net
4
Die ebenfalls zu Microsoft gehörende Website ASP.net (www.asp.net) liefert eine Menge allgemeine und spezifische Informationen zu der Webprogrammierung mit ASP.NET.
5
Codezone Die deutschsprachige Codezone (www.codezone.de) ist ein »Developer Knowledge Network rund um Microsoft Technologien«. Hier finden Sie eine große Anzahl an Ressourcen zu .NET-Themen. Die hervorragende Idee von Codezone ist, dass dort jeder Entwickler seine Ressourcen veröffentlichen kann. Eine ebenfalls gute Idee ist, dass die Einträge bei Codezone neben einer kurzen Beschreibung lediglich einen Link auf die entsprechenden Webseiten oder den Download beinhalten. Auf diese Weise ist die Veröffentlichung von Artikeln oder anderen Ressourcen in der jeweiligen Community sehr einfach (und wird z. B. von mir genutzt ☺).
6
7
dotnetjunkies
8
Bei den dotnetjunkies (www.dotnetjunkies.com) finden Sie viele, gute und hervorragend formatierte How-To-Artikel, Tutorial-Artikel und Beispiel-Codes, hauptsächlich zu ASP.NET und dessen Umfeld.
9
The Code Project Das Code Project (www.codeproject.com) veröffentlicht auf seiner Website im Vergleich zu anderen Websites zwar nicht allzu viele Artikel. Diese sind dafür aber qualitativ hochwertig und behandeln auch spezielle Themen.
10
Auf der Seite www.codeproject.com/dotnet finden Sie allgemeine .NET-Artikel. Die Seite www.codeproject.com/csharp verweist auf Artikel zu C#. Andere Seiten, die Sie über das Menü der Einstiegsseite erreichen, beschäftigen sich (u. a.) mit speziellen .NET- und C#-Themen. Daneben finden Sie aber auch Seiten zu wichtigen WindowsTechnologien wie z. B. DirectX und dem Windows-API.
11
57
Einführung
.NET 247 .NET 247 (www.dotnet247.com) versteht sich als »erste unabhängige .NET-Programmierer-Referenz« und verweist in gut geordneten Kategorien auf Artikel anderer Websites zu wichtigen .NET-Referenz- und -Programmierthemen. Neben diesen finden Sie auf der Startseite Links zu den wichtigsten Namensräumen des .NET Framework und auf der jeweiligen Seite die im Namensraum enthaltenen Klassen. Die Seite einer .NET-Klasse enthält wiederum Links zu Ressourcen, Artikeln, Beispielen, Diskussionen etc., die mit der Klasse in Zusammenhang stehen. Das Ganze ist sehr hilfreich, wenn Sie nicht wissen, wie eine Klasse in der Praxis angewendet wird.
C# Corner Die »C#-Ecke« (www.c-sharpcorner.com) enthält sehr viele Artikel, Quellcodes und Links zu C#-Themen. Die einfache Navigation und die Unterteilung der Inhalte in gut sortierte Kategorien machen diese Website für .NET-Programmierer sehr wertvoll. Die Artikel sind meist kurz gefasst, enthalten aber alle wesentlichen Informationen (was nicht bei allen .NET-Seiten der Fall ist).
C# Help Auf der Website von C# Help (www.csharphelp.com) finden Sie viele Artikel zu C#, die sich teilweise mit Grundlagen, aber auch mit fortgeschrittenen Themen beschäftigen. Leider existierte zur Zeit der Drucklegung dieses Buchs kein Verzeichnis aller Artikel (nur die aktuellen waren auf der Startseite verlinkt). Sie können aber natürlich nach archivierten Artikeln suchen.
.NETheute .NETheute (www.dotnetheute.com) ist eine hervorragende deutschsprachige Website mit vielen Artikeln zu .NET und dem Umfeld.
dotnet-snippets.de Auf der deutschen Seite dotnet-snippets.de veröffentlichen .NET-Programmierer einzelne Codeschnipsel zur Lösung aller möglichen Probleme. Die Seite bietet die Möglichkeit, die einzelnen Codeschnipsel zu bewerten, sodass Sie erkennen können, was andere darüber denken. Sie ist außerdem gut organisiert, bietet die Möglichkeit zu suchen und einen RSS-Feed, über den Sie über die neuen Codeschnipsel informiert werden.
devX Der .NET-Bereich der umfangreichen devX-Website (www.devx.com/dotnet) enthält viele Artikel zu .NET, die teilweise frei verfügbar sind, teilweise aber (leider) zum kostenpflichtigen »Premier Content« gehören. Die vielen und guten Artikel von devX sind leider nicht allzu einfach zu finden (klicken Sie auf den Link MORE C# ARTICLES, um eine Liste anzuzeigen). Sie können aber natürlich (leider mit einer eingeschränkten Google-Suche) auch nach Artikeln suchen. Der E-Mail-Newsletter, den Sie über die .NET-Startseite abonnieren können, ist hingegen hervorragend. Er enthält eine Kurzbeschreibung der neuesten Artikel und ermöglicht deren direkten Aufruf über den E-Mail-Client.
58
Wichtige Begriffe
ONDotnet.com Auf der .NET-Website des Verlags O’Reilly (www.ondotnet.com) finden Sie eine Vielzahl an interessanten Artikeln, die teilweise Grundlagen-, aber auch spezifische Themen behandeln.
Lutz Roeder’s PROGRAMMING.NET
1
Lutz Roeder veröffentlicht im .NET-Bereich seiner Website (www.aisto.com/ roeder/dotnet) einige sehr interessante .NET-Komponenten wie z. B. einen »Reflector«, über den Sie .NET-Komponenten und Assemblys erforschen können, und eine CommandBar-Komponente, über die Sie verschiedene Befehls-Symbolleisten in Ihre Windows.Forms-Anwendung integrieren können.
2
George Shepherd’s Windows Forms FAQ Auf der Seite von SyncFusion, einem Hersteller von .NET-Komponenten, veröffentlicht George Shepherd ein umfangreiches FAQ, das sich nicht nur mit den Windows.Forms-Komponenten, sondern auch mit Themen wie GDI+, dem Windows-API, COM und allgemeinen Framework-Tipps beschäftigt. Die im FAQ enthaltenen Tipps und Tricks wurden dabei verschiedenen Newsgroups und MailingListen entnommen bzw. stammen von Mitarbeitern von SyncFusion. Sie finden dieses FAQ an der Adresse www.syncfusion.com/faq/winforms.
1.5
3
4
5
Wichtige Begriffe
C# und .NET sind hoch entwickelte Systeme, die auf vielen grundlegenden Technologien und Konzepten aufbauen. Sie sollten die wichtigsten Begriffe dieser Technologien und Konzepte kennen, da Sie an vielen Stellen bereits sehr frühzeitig damit in Kontakt kommen.
6
1.5.1
7
Objekte und Klassen
C# ist eine rein objektorientierte Programmiersprache. Das Buch behandelt die OOP in Kapitel 4. Zum Verständnis der vorhergehenden Kapitel sollten Sie aber bereits die wichtigen Begriffe der OOP kennen: ■
■
■
■
8
Klassen: Eine Klasse enthält einzelne Teilprogramme einer objektorientierten Anwendung. OOP-Anwendungen bestehen aus mindestens einer, meist aber aus mehreren Klassen. Klassen enthalten zusammengehörige Methoden und Eigenschaften (und Ereignisse). Methoden: Eine Methode ist eine gespeicherte Folge von Programmanweisungen, die über den Namen der Methode aufgerufen werden kann. Methoden stellen somit einen einzelnen, elementaren Programmteil dar, der an verschiedenen Stellen innerhalb der Anwendung durch den Aufruf der Methode ausgeführt werden kann. Eigenschaften: Eine Eigenschaft dient dem Speichern von Daten innerhalb der Anwendung. Eigenschaften sind prinzipiell so etwas wie Variablen, gehören aber wie Methoden zu einer Klasse. Objekte, Instanzen: Um die in einer normalen Klasse enthaltenen Eigenschaften und Methoden verwenden zu können, müssen Sie eine Instanz dieser Klasse erzeugen. Eine Instanz einer Klasse wird auch als Objekt bezeichnet. Die Klasse dient dann quasi als Bauplan für das erzeugte Objekt. Ein Objekt wird normaler-
9
10
11
59
Einführung
■
■
weise über eine Variable verwaltet. Über diese Variable können Sie die Methoden und Eigenschaften verwenden, die in der Klasse implementiert sind. Dazu geben Sie den Namen der Objektvariablen gefolgt von einem Punkt und dem Namen der Eigenschaft bzw. Methode an. Die Methoden und Eigenschaften, die nur über Instanzen von Klassen verwendet werden können, werden auch als Instanzmethoden bzw. Instanzeigenschaften bezeichnet. Statische Klassenelemente: Einige Klassen besitzen statische Methoden und Eigenschaften. Diese werden ohne Instanz der Klasse verwendet. Dazu müssen Sie lediglich den Klassennamen getrennt durch einen Punkt vor den Namen der Methode oder Eigenschaft setzen. Statische Methoden werden auch als Klassenmethoden, statische Eigenschaften als Klasseneigenschaften bezeichnet. Klassenbibliotheken: Klassenbibliotheken enthalten vorgefertigte Klassen in kompilierter Form. Klassenbibliotheken können Sie sehr einfach in Ihren Programmen einsetzen, um die Funktionalität der enthaltenen Klassen zu nutzen. Das .NET Framework enthält eine Vielzahl an Klassenbibliotheken mit sehr vielen Klassen für die unterschiedlichsten Aufgaben.
1.5.2
Ereignisorientiertes Programmieren
Viele Objekte besitzen neben Eigenschaften und Methoden auch Ereignisse. Über Ereignisse gibt ein Objekt dem Programmierer die Möglichkeit, auf Anwendereingaben oder andere Ereignisse zu reagieren. Diese Art der Programmierung wird ereignisorientierte Programmierung genannt. Im Gegensatz zu Konsolenanwendungen1 laufen echte Windows-Anwendungen nicht ab einem definierten Einsprungpunkt sequenziell bis zum Ende ab, sondern warten darauf, dass etwas passiert. Passiert nichts, warten diese Programme ewig (bzw. bis das Programm beendet wird). Wenn Sie mit modernen Programmierumgebungen eine Windows-Anwendung schreiben, erzeugen Sie für alle Ereignisse, die Ihr Programm auswerten soll, Ereignisbehandlungsmethoden. Diese Methoden werden dann ausgeführt, wenn das Ereignis auf dem betreffenden Objekt eingetreten ist. Ist die Methode abgearbeitet, wartet Ihre Anwendung (oder um genauer zu sein: das aktive Fenster) auf weitere Ereignisse. Die Steuerelemente und Formulare, mit denen Sie die Oberfläche einer Anwendung gestalten, stellen Ihnen zu diesem Zweck meist gleich mehrere Ereignisse zur Verfügung. Eine TextBox (zur Ein- und Ausgabe von Texten) besitzt beispielsweise (u. a.) die Ereignisse Enter (der Anwender hat den Eingabecursor in die Textbox gesetzt), Leave (der Anwender hat den Eingabecursor aus der Textbox herausbewegt), TextChanged (der Anwender hat den Text geändert) und DoubleClick (der Anwender hat in der Textbox doppelt geklickt). Obwohl die meisten Steuerelemente und Formulare eine große Anzahl Ereignisse besitzen, reichen meist einige wenige zum Programmieren aus. Visual Studio hilft bei der Erzeugung einer Ereignisbehandlungsmethode, dadurch, dass diese automatisch beim Doppelklick auf ein Steuerelement oder einen der Ereignis-Einträge im Eigenschaftenfenster erzeugt wird. Visual Studio beschreibe ich noch ausführlicher in Kapitel 2.
1
60
Eine Konsolenanwendung verwendet die Konsole für Eingaben und Ausgaben (in Textform). Die Konsole ist vergleichbar mit der alten DOS-Eingabeaufforderung oder der Powershell von Vista.
Wichtige Begriffe
Die meisten Ereignisse bei Steuerelementen betreffen Anwendereingaben. Im Prinzip gilt, dass alles, was ein Anwender (mit der Anwendung) anstellen kann, in einem Ereignis ausgewertet werden kann. So können Sie z. B. bei einem TextBox-Steuerelement darauf reagieren, dass der Anwender das Steuerelement verlassen will, indem Sie den eingegebenen Text auf Gültigkeit überprüfen. Am wichtigsten sind jedoch oft der OK- und der Schließen-Schalter auf einem Fenster, bei denen Sie im Click-Ereignis darauf reagieren, dass der Anwender den Schalter betätigt hat.
1.5.3
1
Schnittstellen 2
Schnittstellen sind ein prinzipiell einfaches, aber schwer zu verstehendes Konzept der OOP, das in Kapitel 5 noch genauer erläutert wird (machen Sie sich also nichts daraus, wenn Sie den Sinn von Schnittstellen jetzt noch nicht verstehen ☺). Für das Verständnis des .NET Framework ist es allerdings wichtig, die Bedeutung von Schnittstellen zu kennen.
3
Eine Schnittstelle (englisch: Interface) beschreibt einen Satz von Methoden, Eigenschaften und anderen Elementen. Eine Schnittstelle zum Drucken könnte z. B. eine Methode Print beschreiben, die als Argument den zu druckenden Text erhält. Die Schnittstelle könnte zudem eine Eigenschaft PrinterName beschreiben, die den Namen des Druckers angibt, auf dem ausgedruckt werden soll.
4
5
Die Schnittstelle beschreibt aber nur, wie die enthaltenen Methoden und Eigenschaften von außen aufgerufen werden. Sie implementiert die Methoden und Eigenschaften nicht! Die Implementierung wird in einer Klasse (oder Struktur) vorgenommen, die mit der Schnittstelle deklariert wird. Wie das geht, zeigt Kapitel 5. Über eine Instanz der Klasse können dann die Methoden und Eigenschaften der Schnittstelle verwendet werden.
6
Das (zunächst) Besondere an Schnittstellen ist, dass verschiedene Klassen oder Strukturen dieselbe Schnittstelle implementieren können. Instanzen dieser Typen können über eine Referenz (eine Variable) vom Typ der Schnittstelle angesprochen werden, unabhängig vom Typ des Objekts selbst. Das hat einige Vorteile, die allerdings erst in Kapitel 5 besprochen werden. Einer der Vorteile ist, dass eine Anwendung bzw. ein Entwickler nur die Schnittstelle kennen muss, um mit deren Elementen zu arbeiten, unabhängig vom implementierenden Objekt.
7
Eine Schnittstelle ist damit so etwas wie ein Vertrag (und wird deswegen auch so bezeichnet), den verschiedene Typen implementieren können. Typen, die diesen Vertrag erfüllen (also die die Schnittstelle implementieren), können beliebig gegeneinander ausgetauscht werden.
9
Das .NET Framework setzt Schnittstellen in vielen Bereichen ein. Viele der Klassen zur Verwaltung von listenförmigen Daten implementieren z. B. die Schnittstelle ICollection (Schnittstellen werden immer mit einem führenden »I« benannt). Diese Schnittstelle beschreibt u. a. eine Methode Add zum Hinzufügen von Objekten zur Liste, eine Methode Remove zum Entfernen von Objekten und eine Eigenschaft Count, die die Anzahl der aktuell verwalteten Objekte zurückgibt. Unabhängig davon, ob Sie eine SortedList-, eine Queue- oder eine Stack-Auflistung zur Verwaltung einer Liste von Objekten (Daten) verwenden, können Sie immer die Methoden und Eigenschaften der ICollection-Schnittstelle verwenden. Und das ist wie gesagt nur einer der Vorteile von Schnittstellen. Für das anfängliche Verständnis reicht dieses Wissen aber aus.
10
8
11
61
Einführung
1.5.4
Zeichencodierung, Unicode, UCS-2, UCS-4, UTF-8, UTF-16, UTF-32
Der Begriff »Unicode« kommt im Zusammenhang mit .NET-Programmen und Zeichenketten, XML-Dateien oder Textdateien immer wieder vor. Deswegen sollten Sie wissen, was Unicode ist. Unicode ist eine von vielen Zeichenkodierungen (bzw. Zeichensätzen). Eine Zeichenkodierung bestimmt die numerischen Werte der einzelnen darstellbaren Zeichen. Das große A besitzt z. B. in allen Standard-Zeichensätzen den Wert 65. Neben Unicode existieren noch weitere, meist ältere Zeichensätze. Die Zeichensätze ASCII, ANSI, ISO 8859-1 und Windows-1252 sind Beispiele dafür.
Ältere 8-Bit-Zeichensätze ASCII ist einer der ursprünglichen Zeichensätze. Er verwaltet die darstellbaren Zeichen in lediglich sieben Bit, weswegen nur 128 Zeichen enthalten sind. ANSI und andere 8-Bit-Zeichensätze wie ISO 8859-1 verwenden allerdings acht Bit und können demnach 256 Zeichen verwalten. ISO 8859-1 ist ein relativ aktueller Zeichensatz, der auch als Latin-1 bezeichnet wird und hauptsächlich im Internet verwendet wurde bzw. wird. In den ersten 128 Zeichen sind alle (Windows-)Zeichensätze identisch. Die ersten 31 Zeichen sind Steuerzeichen. Die wichtigsten davon sind die Zeichen 9 (Horizontaler Tabulator), 10 (Zeilenvorschub bzw. Line Feed) und 13 (Wagenrücklauf bzw. Carriage Return). Die Zeichen 32 bis 127 und 160 bis 255 sind darstellbare Zeichen (128 bis 159 sind wieder Steuerzeichen). Das Zeichen 'a' besitzt z. B. immer den Wert 97, 'b' den Wert 98. Wichtige Zeichen sind auch das Leerzeichen (32) und das geschützte Leerzeichen (255). Die Zeichen des ISO-8859-1-Zeichensatzes, der in Unicode enthalten ist, finden Sie im Anhang. Die Zeichen nach den Standardzeichen, zu denen z. B. auch unsere Umlaute gehören, sind in den einzelnen Zeichensätzen meist etwas unterschiedlich kodiert. Das führt bei der Interpretation von Text- oder XML-Daten häufig zu Problemen: Wenn Texte gespeichert oder über ein Netzwerk übertragen werden, müssen die einzelnen Zeichen entsprechend eines Zeichensatzes in eine binäre Form übertragen werden. Wird beim Lesen dieser Daten nicht derselbe oder ein kompatibler Zeichensatz verwendet, werden Zeichen falsch interpretiert. Das Resultat sind dann inkorrekte Texte mit ggf. falschen Zeichen an den Stellen, an denen Zeichen mit einem Wert größer 127 verwendet werden.
Unicode Um dieses Problem zu lösen wurde Unicode eingeführt. Diese Zeichenkodierung kann Zeichen in bis zu vier Byte verwalten und ermöglicht damit bis zu 4.294.967.296 Zeichen. In der aktuellen Version 5.0 dieses Standards sind allerdings nicht alle Zeichen definiert. Unicode ist in mehrere logische Ebenen (engl.: Planes) unterteilt. Normalerweise wird nur die erste Ebene verwendet, die als Basic Multilingual Plane (BMP) bezeichnet wird. Diese Ebene setzt zwei Byte für ein Zeichen ein, womit 65.536 Zeichen möglich sind. Mit der BMP werden alle wichtigen Sprachen abgebildet. Neben Zeichen aller westlichen Sprachen gehören auch griechische, kyrillische, hebräische, arabische, indische, japanische, chinesische und Zeichen anderer Sprachen dazu. Satzzeichen und Symbole sind ebenfalls Bestandteil der BMP.
62
Wichtige Begriffe
Die weiteren Unicode-Ebenen verwalten selten benötigte Schriftzeichen wie altägyptische Hieroglyphen und selten verwendete chinesische Schriftzeichen. Einige Ebenen sind auch für private Zwecke reserviert. Unicode existiert in zwei Varianten. Eine wurde vom Unicode Consortium entwickelt, die andere ist im ISO-Standard 10646 definiert. In den ersten 17 Ebenen sind beide Standards gleich. Erst ab Ebene 18, die eigentlich gar nicht in Gebrauch ist (siehe www.unicode.org/versions/Unicode5.0.0/appC.pdf#G4436 und www.unicode.org/ versions/Unicode5.0.0/ch02.pdf#G16433), unterscheiden sich beide Standards.
1
Die Codierung der Basic Multilingual Plane wird im Unicode-Consortium-Standard als UCS-2 (Universal Character Set 2) bezeichnet. In dieser Codierung wird ein Zeichen immer in zwei Bytes verwaltet. Sollen auch die weiteren Ebenen verwaltet werden, wird als Codierung UCS-4 verwendet, bei der ein Zeichen in vier Bytes verwaltet wird.
2
3
Der ISO-Standard verwendet für die Zeichen der ersten 17 Ebenen zwar denselben Wert, setzt aber teilweise eine andere Codierungstechnik ein. Die Codierung UTF-32 (Unicode Transformation Format – 32 Bit) ist dabei (bis auf die Ebenen ab Ebene 18) mit UCS-4 identisch. In der UTF-16-Codierung (UCS Transformation Format for 16 Planes of Group 00.) werden aber je nach Wert des Zeichens zwei oder vier Bytes eingesetzt. Liegt der Wert des Zeichens im Bereich der BMP, wird dieses in zwei Bytes verwaltet. Zeichen im Bereich der Ebene 2 bis 16 werden allerdings in vier Bytes verwaltet. Zeichen ab Ebene 17 kann UTF-16 nicht verwalten, da für die Kennzeichnung der Länge der für ein Zeichen eingesetzten Datenwörter einige Bits benötigt werden.
4
5
Um diese Zeichen darstellen zu können werden Zeichen in vier Byte verwaltet, wobei die ersten zwei Byte für UCS-2 bzw. UTF-16 vorgesehen sind. Die Zeichencodierung wird dann als UCS-4 bzw. UTF-32 bezeichnet.
6
UFT-8 7
Neben UTF-16 und UTF-32 existiert noch die spezielle, aber wichtige Codierung UTF-8. UTF-8 kann alle Zeichen von UTF-16 und UTF-32 verwalten. Die einzelnen Zeichen werden allerdings dynamisch in bis zu sieben (in der Regel allerdings maximal vier) 8-Bit-Datenwörtern (bzw. Bytes) codiert. Die Länge der einzelnen ByteKetten ist von dem Wert des Zeichens abhängig. Zeichen mit einem Wert bis 127 werden in einem Byte verwaltet. Zeichen im Bereich von 128 bis 32767 (7FFF) in zwei Bytes, Zeichen im Bereich von 32786 (8000) bis 65535 (FFFF) in drei Bytes etc. Die Bytes einer zu einem Zeichen gehörenden Kette werden an Hand der ersten Bits erkannt, die eine spezielle Funktion haben. Auf diese Weise ist es möglich, alle Unicode-Zeichen (aller Ebenen!) verwalten zu können, dabei (zumindest für westliche Zeichen) aber möglichst viel Platz zu sparen. UTF-8 ist deswegen der Standard für die Übertragung von Texten im Internet und für die Speicherung von XML-Daten.
8
9
10
Weitere Informationen zu Unicode erhalten Sie im Internet, z. B. an den Adressen www.unicode.org und unicode.e-workers.de/about_unicode.php. UTF-8 wird bei Wikipeda sehr gut beschrieben: de.wikipedia.org/wiki/Utf-8.
1.5.5
11
XML
Wenn unter .NET Daten in einfachen Dateien gespeichert oder über ein Netzwerk versendet werden, verwenden die entsprechenden .NET-Tools dazu eigentlich immer XML. Bereits im Kapitel 2, bei der Beschreibung von Visual Studio, wird der
63
Einführung
Begriff »XML« verwendet, weil Visual Studio verschiedene Projekt- und Konfigurationsinformationen in solchen Dateien speichert. Deshalb sollten Sie wissen, was XML grundsätzlich ist. XML (Extensible Markup Language) ist eine Familie von Techniken zur Speicherung und Verarbeitung von strukturierten Daten in Textform. XML-Dokumente können Sie sich als Datenbanken vorstellen (die ja auch strukturierte Daten speichern). XML besitzt aber Vorteile gegenüber Datenbanken. Die wichtigsten sind, dass die Daten in Textform gespeichert werden und die Struktur der Daten absolut flexibel ist (in Datenbanken ist die Struktur festgelegt). Hinzu kommt, dass verschiedene XMLTechnologien die Arbeit mit den gespeicherten Daten erleichtern. Der eigentliche Vorteil ist aber, dass prinzipiell jedes Programm und auch ein Mensch XML-Daten lesen und weiterverarbeiten kann. Und das sogar über Betriebssystem-Grenzen hinweg. Der XML-Standard besteht nicht nur aus der Festlegung, wie Daten gespeichert werden sollen, sondern definiert auch spezielle Techniken, die die Arbeit mit XMLDaten vereinfachen. XSL-Stylesheets werden z. B. dazu eingesetzt, XML-Daten in eine andere Form (z. B. in HTML) zu übertragen. XML-Schemata werden verwendet, um die Struktur eines XML-Dokuments zu beschreiben (damit ein XML-fähiges Programm diese Struktur erkennen kann). Diese Techniken beschreibe ich allerdings (teilweise) erst im XML-Kapitel 18.
Die Grundstruktur eines XML-Dokuments XML-Dokumente bestehen aus einzelnen Elementen, die über Tags definiert werden. Ein Tag besitzt einen Namen und wird in spitze Klammern eingeschlossen. Ein komplettes XML-Element wird mit einem Start-Tag eingeleitet und durch einen Ende-Tag beendet. Das Ende-Tag sieht prinzipiell aus wie das Start-Tag, nur dass dem Tagnamen ein Schrägstrich vorangestellt wird. Der Inhalt des Elements stellt die gespeicherten Daten dar: Inhalt
Das folgende Element speichert z. B. einen Buchtitel: C#
Ein XML-Element kann nicht nur einen Inhalt besitzen, sondern auch Attribute, die zusätzliche Informationen zum Element liefern. Attribute werden im Start-Tag definiert: Inhalt
Der Wert eines Attributs wird immer in Anführungszeichen eingeschlossen. Einzelne Attribute werden einfach durch Leerzeichen voneinander getrennt (das [...] in der Syntaxbeschreibung deutet an, dass Sie mehrere Attribute im Start-Tag unterbringen können). Soll z. B. der Preis eines Buchs gespeichert werden, kann ein Attribut die Währung definieren: 6.97
INFO
64
XML wird immer unter Beachtung der Groß- und Kleinschreibung ausgewertet. Achten Sie also darauf, dass Sie die Namen der Elemente und der Attribute in der korrekten Schreibweise angeben, wenn Sie XML-Dokumente bearbeiten.
Wichtige Begriffe
Für XML-Elemente ohne Inhalt kann auch eine Kurzform verwendet werden, bei der das Start- und das Ende-Tag zusammengefasst sind:
Sinnvoll ist diese Kurzform für Elemente, die lediglich über Attribute definiert sind. XML-Elemente können geschachtelt werden. Untergeordnete Elemente gehören dann zum übergeordneten Element. Ein XML-Dokument kann damit mehrere »Datensätze« speichern und baut sich auf wie ein Baum. So können Sie z. B. die Daten mehrerer Buchtitel in einem XML-Dokument speichern.
1
Der Standard verlangt, dass ein XML-Dokument in der ersten Zeile in einer speziellen Anweisung seinen Typ und die verwendete XML-Version deklariert. Optional können Sie in dieser Zeile noch weitere Attribute des Dokuments, wie z. B. den zu verwendenden Zeichensatz definieren. Die Angabe des Zeichensatzes ist eigentlich immer notwendig, da ansonsten das lesende Programm die gespeicherten Zeichen u. U. nicht korrekt auswertet. In unseren Breitengraden werden normalerweise der Unicode-Zeichensatz UTF-8 oder der ASCII-Zeichensatz ISO-8859-1 verwendet (auch bekannt als »Western Latin 1«). Zusätzlich dazu verlangt der Standard, dass ein XML-Dokument immer nur ein einziges Wurzel-Element (root element) enthält.
2
3
4
Ein XML-Dokument, das Buchinformationen speichert, sieht dann z. B. so aus wie im folgenden Listing:
5
A Long Way Down 6.97 Nick Hornby
6
7
State of Fear 8.45 Michael Crichton
8
9
Die Datei speichert die Daten von zwei Büchern. Für jedes Buch ist im Attribut isbn die ISBN-Nummer gespeichert, damit das Buch eindeutig identifizierbar ist. Die ISBN-Nummer könnte alternativ auch in einem eigenen Element gespeichert werden, das dem book-Element untergeordnet ist. Das Element author besitzt zwei untergeordnete Elemente, die die Daten des Autors speichern.
10
XML-Namensräume
11
XML-Namensräume haben eine ähnliche Bedeutung wie die Namensräume in .NET: Sie trennen Elemente auf einer übergeordneten Ebene voneinander. Ein Element a, das dem Namensraum x zugeordnet ist, ist ein vollkommen anderes Element als ein Element a, das dem Namensraum y zugeordnet ist. Bedeutung haben XML-Namensräume beim Zusammenführen von verschiedenen XML-Dokumenten. Dabei kann es vorkommen, dass die zusammengeführten Dokumente auf derselben Ebene gleich-
65
Einführung
namige Elemente beinhalten. Gehören diese unterschiedlichen Namensräumen an, ist das aber kein Problem, da die Elemente über ihren Namensraum adressiert werden. XML-Namensräume können alles Mögliche sein. In der Praxis werden häufig URIs (Uniform Resource Identifier = Identifizierer für Ressourcen wie z. B. eine E-MailAdresse oder eine Web-Adresse) und GUIDs (Global Unique IDs = Weltweit eindeutige IDs in Hexadezimalform) verwendet. URIs müssen dabei nicht auf eine wirklich existierende Ressource im Internet verweisen, sondern können vollkommen fiktiv sein. Zum Hinzufügen von Namensräumen gibt es zwei Möglichkeiten: Die einfachste ist, einem übergeordneten Element (in der Regel ist das das Root-Element) über das xmlns-Attribut einen Namensraum zuzuordnen: A Long Way Down 6.97 Nick Hornby State of Fear 8.45 Michael Crichton
In diesem Fall werden alle untergeordneten Elemente automatisch ebenfalls dem angegebenen Namensraum zugeordnet (sofern diese nicht explizit einem Namensraum zugeordnet sind). Die book-Elemente im Beispiel gehören also genau wie das books-Element dem Namensraum http://www.mut.de/c#-kompendium an. Die andere Möglichkeit ist, bei der Deklaration des Namensraums einen Präfix anzugeben und mit diesem alle Elemente zu kennzeichnen, die dem Namensraum zugeordnet werden sollen: A Long Way Down 6.97 Nick Hornby ...
66
Das .NET Framework
Diese Variante ist deutlich schwieriger und fehleranfälliger und sollte nur dann angewendet werden, wenn in einem XML-Dokument mit mehreren Namensräumen gearbeitet wird. Ein typischer Fehler wäre z. B. untergeordnete Elemente nicht mit dem Präfix zu versehen: A Long Way Down 6.97 Nick Hornby
1 2
...
3
In diesem Beispiel ist nur das Element books dem Namensraum zugeordnet. Alle anderen Elemente gehören keinem Namensraum an.
4
Namensräume mit Präfix werden häufig in Kombination mit einem globalen Namensraum verwendet, um Teile der XML-Daten als zu einem anderen Namensraum gehörig zu kennzeichnen:
5
4 A Long Way Down 6.97 Nick Hornby
6
7
5 State of Fear 8.45 Michael Crichton
8
9
Das Ganze mag Ihnen sehr komplex und möglicherweise auch unsinnig vorkommen, im weiteren Verlauf des Buchs – besonders im WPF-Kapitel 12 – wird die Bedeutung von Namensräumen aber klarer. In Kapitel 18 behandle ich das Lesen und Schreiben von XML-Dokumenten, natürlich auch mit Namensräumen.
10
11
1.6
Das .NET Framework
Das .NET Framework ist die Basis aller .NET-Programme. Es beinhaltet im Wesentlichen eine riesige Bibliothek mit einer Vielzahl an Typen für die Lösung der meisten Programmier-Probleme und die Common Language Runtime (CLR), die .NET-Pro-
67
Einführung
gramme ausführt. Sie sollten das .NET Framework grundsätzlich kennen, wenn Sie gute .NET-Anwendungen entwickeln oder spezielle Features wie die Codezugriffssicherheit (siehe Kapitel 23) verstehen wollen. Deswegen stelle ich in diesem Abschnitt zunächst kurz die .NET-Idee vor und beschreibe danach die Grundlagen des .NET Framework.
1.6.1
Was ist .NET?
.NET (gesprochen als »Dotnet«) ist eine Strategie von Microsoft zur Erzeugung und Verteilung von Software. Die Grundlage von .NET ist das .NET Framework. Das .NET Framework ist eine umfangreiche (aber einfach anzuwendende) Infrastruktur, in der Sie Anwendungen programmieren, kompilieren, ausführen und verteilen können. Das .NET Framework unterstützt die Programmierung von Windows- und Webanwendungen. .NET-Anwendungen können auf einfache Weise über das Internet (oder über andere Netze) mit anderen Anwendungen oder Komponenten kommunizieren, die auf entfernten Rechnern ausgeführt werden. Das dazu in der Regel verwendete Protokoll, SOAP, basiert auf HTTP und XML und kann deswegen von den verschiedensten Systemen und auch über Firewall-Grenzen hinweg verwendet werden. Mit ASP.NET können Sie auf einfache Weise Anwendungen entwickeln, die auf einem Webserver ausgeführt werden und die HTML-Code erzeugen, der in einem Browser dargestellt werden kann. Eine andere Möglichkeit von .NET sind WCF- und Webdienste. Diese Dienste werden in einer Anwendung oder auf einem Webserver ausgeführt und können von einer auf einem entfernten Rechner laufenden Anwendung relativ einfach verwendet werden, auch wenn es sich dabei um eine Nicht-.NET-Anwendung handelt. Yahoo bietet z. B. einen Webdienst, über den eine Anwendung die aktuellen Kurse von Aktien abrufen kann. Neben dem für Programmierer wichtigen .NET Framework steht .NET aber auch für eine Vision. Basierend auf Webdiensten und den damit verbundenen Technologien verfolgt Microsoft mit .NET eine Idee, die als »Software as a Service« bezeichnet wird. Anwender sollen in Zukunft ihre Programme nicht mehr lokal auf dem Rechner speichern, sondern mehr oder weniger aus einzelnen WCF- oder Webdiensten und/oder den Funktionen von (Microsoft-)Produkten zusammenstellen. Die neuen Versionen der einzelnen Microsoft-Produkte werden deshalb (leider sehr langsam) mit .NET-Support ausgestattet. So ist es z. B. sehr einfach, die Fähigkeiten des SQL Servers direkt in eigenen .NET-Programmen zu nutzen, indem einfach die Klassenbibliotheken referenziert werden, die auch der SQL Server selbst nutzt (z. B. um ein Backup einer Datenbank anzufertigen).
1.6.2
Was ist das .NET Framework?
Das .NET Framework ist der grundlegende Teil von .NET. Dieses Framework stellt .NET-Anwendungen (in der Regel) alle benötigten Dienste zur Verfügung (in einigen Fällen muss eine Anwendung aber auch auf eine der »alten« Technologien zugreifen, um bestimmte Probleme zu lösen). Das unterscheidet die .NET-Programmierung erheblich von der klassischen Windows-Programmierung, bei der Programmierer häufig auf vollkommen unterschiedliche Dienste, wie das Windows-API oder COMKomponenten, zugreifen mussten.
68
Das .NET Framework
Das .NET Framework besteht hauptsächlich aus: ■ ■ ■
einer umfangreichen Klassenbibliothek, die Klassen für alle grundlegenden Problemlösungen enthält, der CLR (Common Language Runtime), einem Dienst, der die grundlegenden Technologien zur Verfügung stellt und .NET-Programme ausführt, und verschiedenen Compilern für unterschiedliche Programmiersprachen.
1
Abbildung 1.8 zeigt eine Übersicht über das .NET Framework (ohne Compiler). Abbildung 1.8: Übersicht über das .NET Framework (ohne Compiler)
2
3
4
5
6
7
8
9
10
11
69
Einführung
Das Betriebssystem liefert natürlich die grundlegenden Dienste für alle Programme (bei Windows in Form des Windows-API). Diese Systemdienste werden allerdings von der Common Language Runtime gekapselt. Und die Typen (Klassen, Strukturen) der Klassenbibliothek bauen schließlich auf den Diensten der CLR auf. Die aktuelle Version des .NET Framework (3.5) baut auf zwei älteren Versionen auf, die auf einem System, das das .NET Framework 3.5 einsetzt, ebenfalls installiert sein müssen. Die Version 2.0 ist die Basis. Sie enthält die meisten Typen der Basis-Bibliothek, die Typen der Windows.Forms- und der ASP.NET-Bibliothek und die CLR. Das .NET Framework 3.0 enthält die Typen von WCF, WPF und WF und Typen zum Zugriff auf das Vista-Cardspace-Feature. Die Version 3.5 schließlich erweitert die Basis-Bibliothek um Typen zur Unterstützung neuer Features, wie z. B. LINQ, und um neue Compiler, die diese neuen Features unterstützen. Deswegen sollten Sie sich nicht wundern, wenn auf Ihrem System eigentlich drei Frameworks vorhanden sind.
1.6.3
CIL-Code und der Just-In-Time-Compiler
.NET-Programme bestehen nicht aus Maschinencode, der vom Betriebssystem direkt ausgeführt werden kann, sondern aus einem speziellen Microsoft-Zwischencode, der als Common Intermediate Language (CIL) oder Managed Code bezeichnet wird. Diesem Zwischencode liegt ein allgemeines, abstraktes Prozessor-Modell zugrunde. Er besteht aus einzelnen, Maschinencode-ähnlichen Assembler-Befehlen, die aber nicht für eine bestimmte CPU, sondern eben für die abstrakte, allgemeine CPU entwickelt wurden. Die verschiedenen .NET-Compiler erzeugen aus dem Quellcode eines Programms immer CIL-Code. Microsoft stellt Compiler für C++, C#, Visual Basic und J# zur Verfügung. Viele andere Hersteller bieten Compiler für die verschiedensten Sprachen, wie z. B. Eiffel oder Pascal (Delphi). Welche Sprache Sie zur Erzeugung von .NETAnwendungen verwenden, ist eigentlich nur noch Geschmackssache (natürlich ist aber C# die beste aller Sprachen ☺). Der vom Compiler erzeugte Zwischencode kann nicht direkt vom Betriebssystem ausgeführt werden, weil das Betriebssystem mit den speziellen Befehlen nichts anfangen kann. Um den Code ausführbar zu machen, wird dieser beim Aufruf von einem Just-In-Time-Compiler (JIT) in Betriebssystemcode umgewandelt. Der JustIn-Time-Compiler kompiliert dabei immer nur die Methode, die gerade angesprochen wurde, behält den kompilierten Code für den nächsten Aufruf dann aber in seinem Cache (deswegen »Just-In-Time«). Daraus ergeben sich einige Vorteile: ■ ■ ■
CIL-Programme können auch auf anderen Betriebssystemen ausgeführt werden, sofern das .NET Framework auf dem System verfügbar ist, die CLR, die die Programme ausführt, kann diese daraufhin überprüfen, ob die Ausführung sicher ist, der Just-In-Time-Compiler kann den CIL-Code für den jeweiligen Prozessor optimieren.
Bereits die prozessoroptimierte Just-In-Time-Kompilierung bewirkt, dass .NETAnwendungen sehr performant sind. Um diese zusätzlich zu optimieren, können Sie den CIL-Code bei der Installation oder nachträglich über ein spezielles Tool des .NET Framework, den »Native Code Generator« (ngen.exe), auch dauerhaft in nativen Maschinencode umwandeln.
70
Das .NET Framework
1.6.4
Die Common Language Runtime
Die Common Language Runtime (CLR) ist eine Laufzeitumgebung für alle .NET-Programmiersprachen. Sie ■
■
■ ■
■ ■ ■
führt den CIL-Code aus, der von einer beliebigen .NET-Programmiersprache erzeugt wurde. Vor der Ausführung wird der CIL-Code daraufhin überprüft, ob er sicher ist (d. h. keine Speicherbereiche überschreibt, die nicht zum aktuell ausgeführten Programmteil gehören). Abstürze, die durch ein versehentliches Überschreiben von fremden Speicherbereichen entstehen, sind unter der CLR nicht mehr möglich, weil diese unsicheren Programmcode erst gar nicht ausführt. Sie erzeugt stattdessen eine Ausnahme (Exception), die im Programm abgefangen werden kann, überprüft vor der Ausführung eines Programmteils, ob dieser Rechte anfordert, die das Programm (vereinfacht gesagt) nicht besitzt. Das .NET Framework integriert dazu eine erweiterte Sicherheit, die Codezugriffssicherheit, die in Kapitel 23 besprochen wird. Besitzt das Programm die angeforderten Rechte nicht, bricht die CLR die Ausführung mit einer Ausnahme ab, bietet den Programmen eine große Anzahl an Standarddatentypen, die den Anforderungen moderner Programmiersprachen genügen, definiert mit dem Common Type System (CTS) Regeln, die festlegen, wie neue Typen (Strukturen, Klassen etc.) konstruiert werden müssen. Diese Regeln müssen von allen .NET-Programmiersprachen eingehalten werden, die CTS-kompatible Programme erzeugen. Damit ist es problemlos möglich, in einer Sprache neue Typen zu definieren, die in Programmen verwendet werden, die in einer anderen Sprache geschrieben werden, übernimmt das Management von Objekten und deren automatische Zerstörung (über einen Garbage Collector, siehe Seite 72), übernimmt den Aufruf von Methoden in Objekten, und das Handling von Ausnahmen (Exceptions).
1 2
3
4
5
6
7
Keine .NET-Anwendung sollte die Dienste des Betriebssystems direkt verwenden (was aber über den Aufruf von API-Funktionen oder in C++-Programmen auch möglich ist). Der Zugriff sollte immer (mindestens) über die CLR erfolgen.
8
Weil die von der CLR angebotenen Basisfeatures von allen .NET-Programmiersprachen verwendet werden, ist es mit .NET möglich, dass ein in einer Programmiersprache geschriebenes Programm (bzw. eine Komponente) ohne Probleme von einem in einer anderen Programmiersprache geschriebenen Programm verwendet wird. Die in der alten Welt bestehenden Probleme der Zusammenarbeit von Programmen, die in unterschiedlichen Sprachen geschrieben wurden (inkompatible Datentypen, unterschiedliches Handling von Exceptions etc.) gehören damit endgültig der Vergangenheit an.
9
10
Wenn Sie z. B. in C# eine Komponente entwickeln, diese kompilieren und an andere Programmierer weitergeben, können diese die Klassen Ihrer Komponente in Visual Basic ohne Probleme verwenden. Die Technik zur Verwendung von Objekten und zum Aufruf von Methoden und die verwendeten Datentypen sind eben identisch. Der Visual-Basic-Programmierer kann sogar von Ihren Klassen neue Klassen ableiten (sofern Sie das nicht unterbinden). Löst Ihre Komponente bei bestimmten Ausnahmezuständen (oder im Fehlerfall) eine Ausnahme aus, kann der Visual-BasicProgrammierer diese ganz einfach in einem Try-Catch-Block abfangen.
11
71
Einführung
1.6.5
Der Garbage Collector
Alle .NET-Programme, die auf der CLR aufsetzen, besitzen einen Thread2, der als Garbage Collector (»Müllsammler«) bezeichnet wird. Immer dann, wenn die Anwendung gerade nicht beschäftigt ist, geht der Garbage Collector durch den Speicher und entfernt Objekte, die nicht mehr verwendet werden. Er verwendet dabei einen ausgereiften Algorithmus, der dazu führt, dass das Programm durch die Entsorgung nicht wesentlich belastet wird. Ein .NET-Programmierer muss sich deswegen nicht um die Speicherbereinigung kümmern. Programmfehler und Speicherlöcher, wie diese in Sprachen wie C++ oder Delphi möglich sind (die eine explizite Freigabe der Objekte erfordern), sind damit ausgeschlossen. Der Garbage Collector hat jedoch auch seine kleinen Probleme, die auf seinem Algorithmus beruhen. So werden Objekte nicht sofort, sondern erst zeitverzögert zerstört. Das kann in Sonderfällen3 schon einmal Probleme wegen fehlendem Arbeitsspeicher verursachen. Außerdem kann es vorkommen, dass kleine Objekte nicht freigegeben werden, weil der Garbage Collector dieses als nicht lohnenswert ansieht. Das kann bei der Arbeit mit Technologien, die nur eine begrenzte Anzahl von Instanzen eines Typs zulassen (wie bei GDI+), Probleme bereiten. In Kapitel 4 gehe ich deswegen intensiver auf die Freigabe von Objekten und in Kapitel 8 auf den Garbage Collector ein.
1.6.6
Die Klassenbibliothek
Die Klassenbibliothek des .NET Framework ist in mehreren Dateien gespeichert, die größtenteils auf Ihrem System im Ordner Assembly verwaltet werden. Wenn Sie diesen Ordner im Windows-Explorer öffnen, wird allerdings nicht der Ordner selbst geöffnet, sondern ein Tool, das den Inhalt des Assembly-Ordners in einer anderen Form darstellt. Darüber erfahren Sie mehr im Abschnitt »Der Global Assembly Cache (GAC)« ab Seite 81. Die Klassenbibliothek des .NET Framework ist sehr umfangreich und kann in einem Buch kaum beschrieben werden. Ich erläutere hier deswegen nur die wichtigsten Dateien der Klassenbibliothek: Tabelle 1.1: Die wichtigsten Dateien der .NET FrameworkKlassenbibliothek
Datei
Inhalt
mscorlib.dll
Die »Kern-Bibliothek« enthält die grundlegenden Typen des .NET Framework. Ist quasi der erste Teil des System-Namensraums: mscorlib.dll enthält wie System.dll Typen, die im Namensraum System organisiert sind.
System.dll
enthält (mit mscorlib.dll) die Typen des .NET Framework, die bei der täglichen Programmierung am meisten benötigt werden.
System.Core.dll
enthält die neuen Typen von .NET 3.5 inkl. LINQ.
System.Data.dll
enthält grundlegende Typen zum Zugriff auf Datenbanken und spezialisierte Typen zum Zugriff auf SQL-Server-Datenbanken.
2 3
72
Ein Thread (Programmfaden) ist ein Programmteil, der parallel und quasi gleichzeitig zu anderen Programmteilen ausgeführt wird. Speicherprobleme treten dann auf, wenn Objekte externe Ressourcen verwenden und diese nicht, wie eigentlich gefordert, in einer speziell zur Freigabe vorgesehenen Dispose-Methode freigeben, sondern lediglich in Ihrem Finalisierer. In Kapitel 4 erfahren Sie mehr darüber.
Das .NET Framework
Datei
Inhalt
System.Data.Linq.dll
enthält LINQ to SQL.
System.Data.OracleClient.dll
enthält die für Oracle-Datenbanken spezifischen Datenbankzugriff-Typen.
System.Drawing.dll
enthält Typen zum Zeichnen und zur Arbeit mit Bildern.
System.Net.dll
enthält Typen für die Arbeit im Netzwerk (u. a. auch zum Senden von E-Mails).
System.Security.dll
enthält Typen für Sicherheitsthemen.
System.Web.dll
enthält Typen, die in Webanwendungen verwendet werden.
System.Web.Services.dll
enthält Typen, die in Webdiensten verwendet werden.
System.Windows.Forms.dll
enthält die Typen, die in Windows.Forms-Anwendungen verwendet werden.
System.Xml.dll
enthält Typen zur Arbeit mit XML-Dokumenten.
System.Xml.Linq.dll
enthält Typen zur Arbeit mit XML-Dokumenten über LINQ to XML.
Im Internet finden Sie interessante Poster, die eine gute Übersicht über das .NET Framework bieten. Die Firma bbv stellt z. B. über die Seite www.bbv.ch/?site=offers/ dotnet_news.html#dotnet_poster eigene, sehr übersichtliche Poster zur Verfügung, die Sie ordern können und dann (kostenfrei) per Post erhalten. Sehr nett von bbv ☺.
Tabelle 1.1: Die wichtigsten Dateien der .NET FrameworkKlassenbibliothek (Forts.)
1 2
3
4
5 TIPP
6 Die originalen Microsoft-Poster erhalten Sie direkt (u. a. als volles oder geteiltes PDFDokument) aus dem Internet. Auf der Seite blogs.msdn.com/brada/archive/2008/ 01/12/net-framework-3-5-namespace-poster-updated.aspx finden Sie die entsprechenden Links.
1.6.7
7
Die Tools des .NET Framework und des Microsoft Windows SDK für das .NET Framework
8
Das .NET Framework und das Microsoft Windows SDK für das .NET Framework (das von Visual Studio 2008 teilweise automatisch installiert wird) bieten einige teilweise wichtige Tools. Die mit Visual Studio mitgelieferten Tools sind in den Ordnern Microsoft.NET\Framework\v2.0.50727 im Windows-Ordner und Microsoft SDKs\Windows\v6.0A\bin im Programme-Ordner gespeichert. Der erstgenannte Ordner verwaltet die Werkzeuge, die standardmäßig zum .NET Framework gehören. Der zweite Ordner verwaltet die SDK-Werkzeuge. Sie erreichen diese Tools an der Kommandozeile, wenn Sie den VISUAL STUDIO COMMAND PROMPT öffnen. Diese spezielle Konsole (in der die Path-Umgebungsvariable so eingestellt ist, dass diese auf die für Visual Studio und .NET wichtigen Ordner verweist) können Sie über den Eintrag VISUAL STUDIO TOOLS / VISUAL STUDIO 2008 COMMAND PROMPT im Visual-Studio-Menü im Startmenü öffnen.
9
10
11
Viele der .NET Framework- und SDK-Tools sind nur notwendig, wenn Sie nicht mit Visual Studio entwickeln. Andere hingegen sind auch interessant, wenn Sie Visual Studio einsetzen. Diese Tools beschreibt Tabelle 1.2.
73
Einführung
REF
Tabelle 1.2: Die wichtigen Tools des Microsoft Windows SDK
Eine komplette Übersicht erhalten Sie in der Dokumentation des .NET Framework unter .NET-ENTWICKLUNG / .NET FRAMEWORK SDK / .NET FRAMEWORK / .NET FRAMEWORK-WERKZEUGE. Tool
Bedeutung
Herkunft
al.exe
Der »Assembly Linker« ermöglicht die Zusammenführung mehrerer Assemblys zu einer einzigen.
SDK
aspnet_regiis.exe
Dieses Tool erlaubt die nachträgliche Registration von ASP.NET im IIS Framework (Internet Information Server bzw. Internet-Informationsdienste). Es ist für die Fälle hilfreich, bei denen auf einem System der IIS erst nach dem .NET Framework installiert wurde. In diesen Fällen fehlen nämlich wichtige Konfigurationen im IIS. Mit aspnet_regiis.exe -i können Sie diese nachholen.
aspnet_regsql.exe
Das »ASP.NET SQL Server Registration Tool« erlaubt die Einrichtung einer Framework SQL-Server-Datenbank für die ASP.NET-Features Membership, Profilverwaltung, Rollenverwaltung, Webparts und Webereignisse.
CasPol.exe
CasPol (Code Access Security Policy Tool) erlaubt die Einstellung der Code- Framework zugriffssicherheit an der Konsole und wird von Administratoren genutzt, die auf einem Rechner die Sicherheit für .NET-Anwendungen einstellen müssen. Zur Codezugriffssicherheit erfahren Sie mehr in Kapitel 23.
74
CertMgr.exe
Das »Certificate Manager Tool « erlaubt die Ansicht und Bearbeitung der SDK auf einem System verwalteten Zertifikate.
csc.exe
Über den »CSharp Compiler« können Sie C#-Programme an der Konsole kompilieren (was bei komplexeren Projekten allerdings nicht allzu einfach ist).
Framework
disco.exe
Über das »Web Services Discovery Tool« können Sie die auf einem Webserver verwalteten Webdienste erforschen.
SDK
gacutil.exe
Über das »Global Assembly Cache Utility« ermöglicht die Ansicht und die SDK Bearbeitung des Global Assembly Cache (GAC) und des Download Cache (in dem .NET-Programme verwaltet werden, die über einen InternetLink ausgeführt werden).
ildasm.exe
Über den »MSIL Disassembler« können Sie eine Assembly in MicrosoftSDK Intermediate-Language-Assemblercode zurückführen. ildasm.exe ist interessant um zu sehen, was der C#-Compiler aus einem C#-Quellcode in Wirklichkeit macht.
mage.exe mageUI.exe
Das »Manifest Generation and Editing Tool« erlaubt die Bearbeitung der SDK Manifeste, die für die Verteilung von Anwendungen über das Internet (oder Intranet) über ClickOnce benötigt werden. ClickOnce wird in Kapitel 16 behandelt. mage.exe ist ein reines Kommandozeilen-Tool, mageUI.exe besitzt eine Windows-Oberfläche.
makecert.exe
Über das »Certificate Creation Tool« können Sie X.509-Zertifikate für Testzwecke erzeugen. Solche Zertifikate werden zur Signierung von Assemblys und von Daten verwendet um deren Authentizität zu belegen.
SDK
Die Möglichkeiten von .NET-Programmen
Tabelle 1.2: Die wichtigen Tools des Microsoft Windows SDK (Forts.)
Tool
Bedeutung
Herkunft
mscorcfg.msc
Dieses Tool ist ein Snap-In für die Microsoft-Management-Konsole, über das Sie die grundlegende Konfiguration des .NET Framework (inklusive der Codezugriffssicherheit) bearbeiten können. Dieses Werkzeug wird in Kapitel 23 angesprochen (aber nicht behandelt, da es leider seit .NET 3.5 nicht mehr Bestandteil von .NET ist).
SDK – Wird leider nicht von Visual Studio 2008 installiert!
ngen.exe
Der »Native Code Generator« ermöglicht das Erzeugen eines nativen Framework Image aus .NET-Assemblys und führt damit zur Beschleunigung des Aufrufs von Anwendungen, die diese Assemblys verwenden (bzw. die diese Assemblys sind). NGen wird in Kapitel 22 behandelt.
Regasm.exe
Das »Assembly Registration Tool« ermöglicht die Registration von .NET- Framework Assemblys als COM-Komponenten, die dann auch von älteren Programmen, die nicht .NET-fähig sind, verwendet werden können.
SecUtil.exe
Dieses Tool erlaubt die Extraktion von Namens-Informationen und des öffentlichen Schlüssels aus einer Assembly.
SDK
SignTool.exe
Über das »File Signing Tool« können Sie eine Assembly nachträglich mit einem digitalen Zertifikat signieren.
SDK
4
SqlMetal.exe
Über das »Code Generation Tool« können Sie LINQ-to-SQL-MappingSDK Dateien für eines der unterstützten Datenbanksysteme erzeugen. SqlMetal.exe ist zurzeit der einzige Weg zur Erzeugung eines Mapping für eine SQL-Server-Compact-Edition-Datenbank.
5
StoreAdm.exe
Das »Isolated Storage Tool« ermöglicht die Anzeige oder das Löschen aller Bereiche für die isolierte Speicherung von Daten für den aktuellen Benutzer. Isolierte Speicherung wird in Kapitel 10 behandelt.
wincv.exe
Der »Windows Forms Class Viewer« ist recht interessant, da er Ihnen eine SDK – Wird Übersicht über die Namensräume der Bibliotheken des .NET Framework leider nicht gibt. von Visual Studio 2008 installiert!
xsd.exe
Das »XML Schema Definition Tool« ermöglicht die Erzeugung von XML-Schemadefinitions-Dateien (XSD-Dateien) aus XML-, XDR- und XSD-Dateien.
1 2
3
SDK
6
7
8
SDK
9
1.7
Die Möglichkeiten von .NET-Programmen
Mit einer .NET-Programmiersprache können Sie alle modernen Softwarearchitekturen realisieren. Dazu gehören zunächst einfache Windows- und Webanwendungen. Sie können aber auch komponentenbasierte und verteilte Anwendungen entwickeln und (natürlich) Anwendungen, die auf Datenbanken zugreifen.
1.7.1
10
11
»Normale« Windows-Anwendungen
»Normale« Windows-Anwendungen erstellen Sie mit Hilfe der Klassen der Windows Presentation Foundation oder der Klassen der Windows.Forms-Bibliothek. Dazu stehen Ihnen Formulare (Windows-Fenster) und eine umfangreiche Anzahl von Steuerelementen zur Verfügung.
75
Einführung
.NET-Windows-Anwendungen können in der Regel über ein einfaches Kopieren auf einem Rechner »installiert« werden. Sie können natürlich auch Setup-Programme erstellen, über die Ihre Anwendungen komfortabler installiert werden können (wie ich in Kapitel 16 zeige). Windows-Anwendungen können zudem über das so genannte Click Once über das Internet (über einen Link auf einer Website z. B.) automatisch auf einem Rechner installiert werden. Näheres zu diesem interessanten Thema finden Sie in Kapitel 16.
1.7.2
Komponentenbasierte Anwendungen
Komponentenbasierte Anwendungen sind nicht in einer einzelnen ausführbaren Datei gespeichert, sondern nutzen zusätzlich eine oder mehrere Komponenten. Der Begriff »Komponente« ist ein eher allgemeiner Begriff. Unter C# sind mit Komponenten eigentlich Klassenbibliotheken gemeint. Über Klassenbibliotheken können Sie Programmteile so auslagern, dass Sie diese in anderen Projekten bzw. Programmen problemlos wiederverwenden können. Die Programmierung und Anwendung von Klassenbibliotheken ist unter C# (und unter .NET im Allgemeinen) sehr einfach. Ich beschreibe Klassenbibliotheken in Kapitel 5.
1.7.3
Client/Server-Anwendungen
Anwendungen können über Windows Communication Foundation (WCF), über Remoting oder über Webdienste über Rechner-Grenzen hinweg kommunizieren. Eine zentrale (Server-)Anwendung kann gleich mehrere (Client-)Anwendungen auf unterschiedlichen Rechnern bedienen. Der Client muss dazu lediglich die (IP-)Adresse des Servers kennen, die verwendete Technologie unterstützen und natürlich passend zu den gebotenen Diensten programmiert sein. Client/Server-Anwendungen können zur Kommunikation ein binäres Protokoll verwenden, das Daten sehr schnell überträgt, oder SOAP (Simple Object Access Protocol). SOAP ist ein einfaches, textbasiertes Protokoll für den Zugriff auf Objekte, die in entfernten Komponenten verwaltet werden. SOAP verwendet für den Transport von Daten zwischen einer Anwendung und einer Komponente normalerweise XML. Da SOAP auf dem HTTP-Protokoll basiert, können (Client-)Anwendungen die Dienste von Server-Anwendungen auch über das Internet nutzen (die Verwendung des binären Protokolls schränkt die Verwendung in der Regel auf Intranets ein). Dabei ist WCF die modernere Art, Dienste nach außen verfügbar zu machen. WCF ermöglicht es, Server-Anwendungen als Windows-Anwendung oder als WindowsDienst zu entwickeln oder diese im IIS zu hosten, damit die Dienste problemlos über das Internet verfügbar sind. Sie können WCF-Dienste über die verschiedensten Kanäle verfügbar machen, z. B. als Webdienst oder als Remoting-Dienst.
1.7.4
Webanwendungen
Webanwendungen sind Anwendungen, die in einem Browser ausgeführt werden und die über das Internet oder ein Intranet geladen werden. Webanwendungen kennen Sie wahrscheinlich sehr gut, weil Sie im Internet immer wieder mit solchen Anwendungen in Kontakt kommen. Amazon oder Ebay sind gute Beispiele für Webanwendungen.
76
Typen, Namensräume, Assemblys und Module
ASP.NET ermöglicht Ihnen auf einfache Weise, Webanwendungen zu erzeugen (allerdings müssen Sie die Grundlagen verstehen). Da Webanwendungen im Vergleich zu Windows-Anwendungen stark eingeschränkt sind4, benötigen Sie zur Entwicklung von benutzerfreundlichen (»reichen«) Webanwendungen aber zusätzliche Funktionalitäten, die Ihnen Microsoft in Form des Ajax.NET Framework zur Verfügung stellt. Beide Technologien werden in diesem Buch nicht behandelt. Sie können aber auch Webanwendungen entwickeln, die auf WPF (Windows Presentation Foundation) basieren. Diese Anwendungen werden über ein BrowserPlugin, das als Silverlight bezeichnet wird, im Browser ausgeführt. WPF-Anwendungen im Browser unterliegen zwar wie normale Webanwendungen Sicherheitseinschränkungen, dafür kann die Oberfläche einer solchen Anwendung prinzipiell so flexibel gestaltet werden wie die einer Windows-Anwendung. WPF wird in Kapitel 12, 13 und 14 behandelt.
1
1.7.5
3
2
Datenbankzugriff
Das .NET Framework erlaubt über LINQ to SQL, über das Entity-Framework und über ADO.NET eine sehr einfache Entwicklung von Anwendungen, die mit nahezu beliebigen Datenbanken kommunizieren. In Kapitel 19 zeige ich, wie Sie mit LINQ to SQL auf Datenbanken zugreifen.
4
1.7.6
5
Anwendungen für mobile Geräte
Für mobile Geräte wie PDAs und MDAs stellt Microsoft das .NET Compact Framework zur Verfügung. Dieses ist nahezu identisch zum »großen« .NET Framework, ist aber auch etwas eingeschränkt (besonders in allen Dingen, die das Betriebssystem betreffen). Trotzdem ist es, besonders über die in Visual Studio integrierte Unterstützung, sehr einfach, .NET-Anwendungen für mobile Geräte zu entwickeln. Einzige Voraussetzung ist, dass das .NET Compact Framework auf dem Gerät zur Verfügung steht. Möglich ist dies zurzeit für die Plattformen Pocket PC 2003, Windows CE 5.0, Windows Mobile 5.0 Pocket PC SDK und Windows Mobile 5.0 Smartphone PC SDK.
1.8
6
7
Typen, Namensräume, Assemblys und Module
8
Bei der Arbeit mit .NET begegnen Ihnen immer wieder die Begriffe »Typ«, »Namensraum« (Namespace), »Assembly« und »Modul«. Damit Sie wissen, worum es sich dabei handelt, beschreibe ich diese Begriffe kurz.
1.8.1
9
Typen
10
Jeder Datentyp wird in .NET als Typ (Type) bezeichnet. Dazu gehören Klassen, Strukturen, Aufzählungen (Enumerations), Schnittstellen und Arrays. Die Standarddatentypen wie Integer, Double und String sind in .NET übrigens grundsätzlich Objekte (genauer: Instanzen von Strukturen).
4
11
Eingeschränkt deswegen, weil diese auf HTML beruhen und in einem Browser ausgeführt werden. HTML bietet lange nicht die Möglichkeiten, die eine Windows-Anwendung besitzt. Die Ausführung in einem Browser schränkt Anwendungen zusätzlich ein, da diese aus Sicherheitsgründen viele Aufgaben, wie z. B. den Zugriff auf das Dateisystem, nicht ausführen können.
77
Einführung
1.8.2
Namensräume
Das .NET Framework fasst Typen in so genannten Namensräumen (Namespaces) zusammen. Ein Namensraum ist eine logische Gruppierung von Typen. Der Namensraum System.IO enthält z. B. alle Typen, die für die Arbeit mit dem Dateisystem verwendet werden. Diese Gruppierung vereinfacht die Suche nach Typen, erleichtert deren Zuordnung und verhindert Kollisionen von gleichnamigen Typen. Über Namensräume ist es meist recht einfach, in der Klassenbibliothek des .NET Framework einen Typ zu finden, der eine bestimmte Funktionalität bietet. Dazu müssen Sie prinzipiell nur, beginnend beim System-Namensraum, die einzelnen Namensräume durchgehen. Namensräume werden normalerweise geschachtelt. Der System-Namensraum des .NET Framework enthält z. B. einige Typen, aber auch wieder Namensräume. In System finden Sie z. B. die Namensräume Data (Klassen zur Arbeit mit Daten) und Drawing (Klassen zum Zeichnen von grafischen Elementen). Der Data-Namensraum enthält wiederum (u. a.) die Namensräume OleDb (die Klassen der zum Zugriff auf Datenbanken verwendeten ActiveX Data Objects, deren Basistechnologie OLEDB ist) und SqlClient (Klassen zum direkten Zugriff auf den SQL Server). Die Dokumentation zum .NET Framework stellt diese Namensräume sehr übersichtlich dar (Abbildung 1.9). Abbildung 1.9: Die Dokumentation der Namensräume des .NET Framework
Wenn Sie selbst Klassen entwickeln, können Sie diese in eigenen Namensräumen anlegen und Ihre Namensräume ebenfalls schachteln.
1.8.3
Assemblys und Module
Eine Assembly ist die Basiseinheit für die Verwaltung von kompiliertem Programmcode und für die Verteilung von Anwendungen. Eine Assembly enthält den kompilierten Code einer oder mehrerer Klassen und anderer Typen (Auflistungen, Strukturen etc.), alle Ressourcen (Zeichenketten, Bilder etc.), die von diesen Typen verwendet werden, und Metadaten, die die Assembly beschreiben.
78
Typen, Namensräume, Assemblys und Module
Normalerweise enthält eine Assembly alle Klassen eines (untergeordneten) Namensraums. Die Klassen des Namensraums System.Net sind z. B. in der Datei system. net.dll gespeichert. Eine Anwendung ist eine Kombination mehrerer Assemblys. Auch wenn Sie in Ihren Anwendungen selbst keine Assemblys einsetzen, verwenden Sie zumindest die Assemblys des .NET Framework. Normalerweise enthalten einzelne Assemblys Typen und Ressourcen, die thematisch zueinander passen. Die Assembly System.IO.dll enthält z. B. Klassen und andere Typen für die Eingabe und Ausgabe von Daten.
1
Eine Assembly kann aus einer einzelnen kompilierten Datei, aber auch aus mehreren Dateien bestehen. Die einzelnen Dateien werden als Module bezeichnet. Ein Modul enthält kompilierten Zwischencode (CIL-Code), optional Ressourcen (z. B. Bitmaps und Icons) und Metadaten in Form einer .dll- oder .exe-Datei.
2
3 Abbildung 1.10: Schematische Darstellung einer Assembly mit einem Modul
4
5 Abbildung 1.11: Schematische Darstellung einer Assembly mit zwei Modulen
6
7
8
9
10
Metadaten beschreiben die im Modul enthaltenen Typen (also z. B. welche Klassen enthalten sind und wie die Eigenschaften, Methoden und Ereignisse der Klassen deklariert sind). Ein Modul der Assembly verwaltet normalerweise das so genannte Manifest (das aber auch in einer separaten Datei gespeichert sein kann). Das Manifest verwaltet die Abhängigkeiten der Assembly (inkl. der Version der verknüpften Ressourcen), verweist auf die enthaltenen Module, speichert den Namen des Autors, definiert eine Versionsnummer und beschreibt, welche (System-)Zugriffsrechte zur Ausführung der Assembly notwendig sind.
11
Damit beschreibt eine Assembly sich vollständig selbst. Es sind also keine weiteren Informationen notwendig, wie es z. B. im veralteten COM-Modell in Form von Regis-
79
Einführung
try-Eintragungen notwendig war. Deshalb kann eine Assembly einfach auf einen Computer kopiert und dort verwendet werden, was die Verteilung von .NET-Anwendungen im Vergleich zum COM-Modell erheblich vereinfacht.
TIPP
Das Manifest einer Assembly können Sie über das .NET Framework-Tool ildasm.exe auslesen, das Sie im Ordner C:\Programme\Microsoft SDKs\Windows\V6.0A\Bin finden oder über den Visual Studio Command Prompt aufrufen können.
1.8.4
Assemblys mit und ohne starkem Namen
Assemblys können einen einfachen (schwachen) oder einen starken Namen besitzen. Ein starker Name besitzt einige Vorteile, die ich in diesem Abschnitt kurz anspreche. Zunächst will ich aber klären, was ein einfacher Name ist und welche Erweiterungen ein starker Name besitzt. Ein einfacher Name besteht lediglich aus dem Basisnamen der Assembly (dem Dateinamen ohne Endung) und der Kultur, die für die Assembly angegeben ist. Kulturen und den Umgang damit beschreibe ich in Kapitel 15. Für das Verständnis von Assembly-Namen ist wichtig, dass Assemblys nur für bestimmte Kulturen entwickelt werden können oder für alle Kulturen. Diese Technik wird bei Ressourcen-Assemblys genutzt, um Ressourcen für verschiedene Kulturen zur Verfügung zu stellen. Eine Assembly mit einem starken Namen wurde zusätzlich mit einem Schlüssel-Paar signiert, das aus einem privaten und einem öffentlichen Schlüssel besteht. Diese Schlüssel werden dazu in einer Datei verwaltet, die Sie über das Tool sn.exe oder direkt über Visual Studio erzeugen können. Wie das geht und was Signieren bedeutet, zeigt Kapitel 22. Ein starker Name besteht ebenfalls aus dem Basisnamen und der Kultur der Assembly, beinhaltet aber zusätzlich die Version und den öffentlichen Schlüssel der Signatur. Die Vorteile bzw. Notwendigkeiten eines starken Namens zeigt die folgende Auflistung: ■
■ ■ ■
80
Assemblys mit einem starken Namen sind immer auch signiert. Der öffentliche Schlüssel der Signatur kann ausgelesen und mit dem Hersteller in Verbindung gebracht werden. Das bei der Signierung verwendete asymmetrische Verschlüsselungsverfahren (siehe Kapitel 23) stellt zudem sicher, dass Assemblys mit einem (von einem Angreifer) veränderten Inhalt nicht von der CLR ausgeführt werden. Nur Assemblys mit einem starken Namen können im GAC installiert werden (siehe nächsten Abschnitt). Nur Assemblys mit einem starken Namen können über ClickOnce (siehe Kapitel 16) über das Internet oder über ein Intranet verteilt werden. Werden Assemblys mit einem starken Namen referenziert, sucht die CLR nach einer Assembly mit genau diesem Namen (sofern die Assembly-Lokalisierung nicht über die Konfiguration der Anwendung oder der Maschinen umdefiniert ist). Da auch die Version der Assembly im Namen vorkommt, wird also eine ganz bestimmte Version verwendet. Das vermeidet Versionsprobleme. Außerdem ist über den öffentlichen Schlüssel des Herstellers sichergestellt, dass nicht eine Assembly geladen wird, die von einem anderen »Hersteller« (z. B. einem Hacker) stammt.
Typen, Namensräume, Assemblys und Module
■
Wenn Sie eine Anwendung oder eine Assembly signieren wollen, um diese gegen Missbrauch zu schützen, müssen alle referenzierten Assemblys ebenfalls signiert sein, also einen starken Namen besitzen. Einer Firma, die signierte Anwendungen herausgeben will, bleibt also gar nichts anderes übrig, als ihre Klassenbibliotheks-Assemblys mit einem starken Namen zu versehen.
Gegen starke Namen spricht eigentlich nichts, außer, dass Visual Studio bei der Referenzierung von Assemblys mit starken Namen manchmal Probleme macht (wie ich in Kapitel 2 im Abschnitt »Der Projektmappen-Explorer« erläutere).
1.8.5
1
Der Global Assembly Cache (GAC) 2
Assemblys können einfach in dem Ordner gespeichert werden, in dem die Anwendung abgelegt ist, die diese verwendet. Für nicht zum .NET Framework gehörende Assemblys sollte das die Regel sein.
3
Jedes System, auf dem das .NET Framework installiert ist, besitzt aber auch einen so genannten Global Assembly Cache (GAC). Der GAC verwaltet globale Assemblys, die in der Regel von mehreren Anwendungen verwendet werden. Die Assemblys des .NET Framework sind im GAC registriert.
4
Sie können den Inhalt des GAC erforschen und bearbeiten, indem Sie den ASSEMBLYEintrag im Windows-Ordner öffnen (Abbildung 1.12). Abbildung 1.12: Der GAC-Viewer zur Bearbeitung des GAC, der über den Eintrag Assembly im Windows-Ordner erreichbar ist
5
6
7
8
9
10
11 Da der GAC nur Assemblys mit starkem Namen zulässt, sehen Sie neben dem Namen und der Kultur der Assembly auch die Version und ein Token des öffentlichen Schlüssels des Herstellers. Das Token ist dabei nicht der wirkliche Schlüssel, sondern lediglich eine Kurzform davon.
81
Einführung
Über den Assembly-Ordner öffnen Sie aber nicht den physischen Ordner, sondern Sie starten den GAC-Viewer, eine kleine Shell-Erweiterung (shfusion.dll), die den Inhalt des GAC in einer lesbaren Form anzeigt. Der GAC besteht in Wirklichkeit aus einer Vielzahl von Unterordnern im Assembly-Ordner. Sie können diese Ordner einsehen, wenn Sie den Assembly-Ordner in einer DOS-Eingabeaufforderung öffnen.
TIPP
Sie können allerdings (mit der gebotenen Vorsicht) auch einen kleinen Trick anwenden, um den GAC im Explorer in seiner wirklichen Struktur zu sehen. Der AssemblyOrdner enthält nämlich die versteckte Systemdatei Desktop.ini. In dieser Datei ist die Shell-Erweiterung shfusion.dll als Erweiterung für den Assembly-Ordner (über ihre GUID) angegeben. Um die Berücksichtigung dieser Datei auszuschalten, können Sie die Registry bearbeiten. Öffnen Sie dazu den Registry-Editor, indem Sie im Ausführen- bzw. Start-Feld im Startmenü regedit eingeben und die (¢_)-Taste betätigen. Wechseln Sie in den Schlüssel HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\FUSION und legen Sie dort einen neuen DWORD- bzw. DWORD-32-Wert mit dem Namen DisableCacheViewer an. Setzen Sie den Wert auf 1 um den GAC-Viewer abzuschalten. Wenn Sie den Assembly-Ordner danach im Windows-Explorer öffnen, sehen Sie die wirkliche Struktur (Abbildung 1.13). Vergessen Sie nicht, die Ansicht wieder zurückzusetzen, indem Sie DisableCacheViewer auf 0 setzen.
Abbildung 1.13: Inhalt des Assembly-Ordners im Explorer mit abgeschaltetem GAC-Viewer
Der GAC hat mehrere Vorteile gegenüber dem Speichern von Assemblys im Anwendungsordner. Ein Vorteil für Programmierer ist, dass Assemblys, die im GAC verwaltet werden, mit wenig Aufwand referenziert werden können (wie Sie in Kapitel 2 noch sehen werden). Ein anderer Vorteil ist die integrierte Versionsverwaltung: Der GAC ist in der Lage mehrere Versionen einer Assembly zu verwalten, wie Sie in Abbildung 1.12 z. B. für die System.AddIn-Assembly sehen, die auf meinem System in den Versionen 2.0.0.0 und 3.5.0.0 vorhanden ist. Versionsverwaltung wird ab Seite 84 behandelt. Eine erhöhte Performance, die sich daraus ergibt, dass die CLR bei der Auflösung von Referenzen auf Assemblys mit starkem Namen zunächst in den GAC schaut, ist ein weiterer Vorteil.
82
Typen, Namensräume, Assemblys und Module
Auch wenn Sie jetzt denken, der GAC ist der ideale Speicherort für Assemblys: Assemblys externer Hersteller oder Assemblys, die Sie selbst entwickelt haben, im GAC zu installieren ist nicht unbedingt von Vorteil. Die Installation einer Assembly im GAC erhöht den Installationsaufwand einer Anwendung enorm. Bei der Referenzierung einer Assembly, die im GAC verwaltet wird, geht Visual Studio beim Kompilieren eines Projekts davon aus, dass diese Assembly immer im GAC zu finden ist. Die Assembly wird deswegen nicht in den Anwendungsordner kopiert. Wenn Sie die Anwendung auf einem anderen System installieren und dabei die im GAC installierten speziellen Assemblys vergessen, wird die Anwendung auf dem anderen System nicht ausgeführt. Die Firmen, für die ich arbeite, setzen den GAC deswegen nicht für ihre eigenen Assemblys ein und nehmen den kleinen Performancenachteil des Ladens aus dem Anwendungsordner in Kauf.
1.8.6
INFO
1 2
Referenzierung von Assemblys
3
Wenn eine Assembly eine andere Assembly referenziert, wird der Name der referenzierten Assembly im Manifest der referenzierenden Assembly gespeichert. Der Name der zu referenzierenden Assembly wird beim Kompilieren einer Assembly aus dem Dateinamen und Informationen aus dem Manifest zusammengesetzt.
4
Wird bei der Ausführung des Programms ein Typ verwendet, der in einer referenzierten Assembly gespeichert ist, lädt die CLR diese Assembly in den Arbeitsspeicher (falls diese noch nicht geladen ist). Dazu muss sie die Assembly aber erst einmal finden.
5
Die CLR sucht deswegen nach einem bestimmten Schema in vorgegebenen Ordnern nach einer Assembly, deren Name dem referenzierten Namen entspricht. Dabei werden natürlich alle Informationen des Namens berücksichtigt. Bei einfachen Namen sind das der Basisname der referenzierten Assembly und die Kultur. Bei starken Namen kommen der öffentliche Schlüssel des Herstellers und die Version hinzu.
6
7
Das Such-Schema, bei dem auch die Anwendungskonfiguration und die globale .NET-Konfiguration berücksichtigt werden und bei dem Assembly-Referenzen auch umgebogen werden können, ist recht komplex und soll hier (und in diesem Buch) nicht weiter behandelt werden.
8
Vereinfacht gesagt, beginnt die CLR normalerweise die Suche im GAC (sofern eine Assembly mit einem starken Namen referenziert wird). Findet sie keine passende Assembly, sucht sie in Ordnern, die ggf. in der Anwendungs- oder der Maschinenkonfiguration als Assembly-Ordner angegeben sind. Findet sie auch dort keine passende Assembly, wird schließlich im Anwendungsordner gesucht. Erweiterte Informationen zur Assembly-Ladestrategie finden Sie auf der Webseite www.thescarms.com/dotNet/assembly.aspx oder in der Dokumentation des .NET Framework unter DOKUMENTATION ZU VISUAL STUDIO / .NET FRAMEWORK-PROGRAMMIERUNG IN VISUAL STUDIO / GRUNDLEGENDE .NET FRAMEWORK-ENTWICKLUNG / BEREITSTELLEN VON ANWENDUNGEN / SO SUCHT COMMON LANGUAGE RUNTIME NACH ASSEMBLYS.
9
10 REF
11
Beim Laden wird gleich noch überprüft, ob die Assembly eventuell nachträglich (nach dem Kompilieren) geändert wurde. Diese Überprüfung ist deswegen möglich, da die Signatur der Assembly ein verschlüsselter Hashcode ist, der den Inhalt der Assembly identifiziert. Wurde der Inhalt (z. B. durch einen Hacker) geändert, ist der
83
Einführung
Hashcode ein anderer und die Signatur kann nicht mehr nachvollzogen werden. Die CLR lehnt die Ausführung der Assembly dann mit einer Ausnahme ab.
1.8.7
Versionsverwaltung
Das Manifest einer Assembly enthält also u. a. die Versionsnummer (in der Form Major.Minor.Build.Revision). der Assembly selbst. Für alle referenzierten Assemblys werden zumindest der Basisname, die Kultur (falls die Assembly nicht für die neutrale Kultur kompiliert wurde, die für alle Kulturen gilt) und die Versionsnummer im Manifest verwaltet. Wird nun eine Assembly mit einem einfachen Namen referenziert, spielt die Version für die CLR keine Rolle. Die Assembly wird in den in der Konfiguration ggf. angegebenen Ordnern und im Anwendungsordner gesucht (im GAC wird nicht gesucht, da nur Assemblys mit starkem Namen im GAC verwaltet werden). Wird eine Assembly mit dem passenden Namen und der passenden Kultur gefunden, wird diese geladen, unabhängig davon, ob es sich um dieselbe, eine neuere oder eine ältere Version handelt. Problematisch wird dies, wenn die Assembly inkompatibel zu der Assembly ist, mit der die referenzierende Assembly kompiliert wurde. Das ist z. B. dann der Fall, wenn die Signatur5 einer Methode geändert wurde. In diesem Fall kann die CLR Aufrufe geänderter Methoden oder Eigenschaften nicht mehr auflösen und generiert eine Ausnahme. Aber genau dieses Problem (die so genannte DLL-Hölle) soll .NET ja vermeiden. Das Problem tritt eigentlich in der Praxis auch nicht auf, da alle Assemblys, die nicht zum .NET Framework gehören, normalerweise mit der Anwendung im Anwendungsordner gespeichert werden. Bei einem Update der Anwendung sollten auch immer alle zwischenzeitlich geänderten Assemblys mitgeliefert werden (was z. B. bei einem Web-Update u. U. nicht immer der Fall ist). Da also jede Anwendung ihren eigenen Satz an privaten Assembly verwaltet, sollten Versionsprobleme erst gar nicht auftreten. Wenn allerdings eine Assembly mit einem starken Namen referenziert wird, spielt die Version der Assembly bei der Auflösung von Referenzen sehr wohl eine Rolle, da der starke Name einer Assembly eben neben dem Basisnamen, der Kultur und dem öffentlichen Schlüssel der Signatur auch die Version enthält. Findet die CLR bei ihrer Suche keine Assembly mit der benötigten Version, generiert sie eine Ausnahme. Der GAC, der alle globalen Assemblys eines Systems verwaltet, bietet dazu eine automatische Versionsverwaltung. Er ist in der Lage, eine Assembly mit demselben Basisnamen, derselben Kultur und demselben öffentlichen Schlüssel in mehreren Versionen zu verwalten. Möglich ist dies, da der GAC die verschiedenen Assemblys in separaten Unterordnern speichert. Referenziert eine Anwendung also eine Assembly aus dem GAC und der GAC wird danach um ältere oder neuere Versionen der Assembly erweitert, läuft die Anwendung fehlerfrei und mit derselben Version weiter, mit der sie entwickelt wurde. Für den Fall, dass eine im GAC installierte Assembly in einer neueren Version wesentliche Verbesserungen erfahren hat, kann die Referenzierung älterer Versionen übrigens über die Konfigurationsdatei der Anwendung oder der Maschine oder über eine Publisher-Policy-Assembly auf die neue Version umgebogen werden. Dieses Thema sprengt aber den Rahmen dieses Buchs.
5
84
Die Signatur einer Methode setzt sich zusammen aus den Datentypen, der Reihenfolge und der Anzahl der Argumente und aus dem Rückgabewert.
Der .NET-Reflector: Ein wichtiges Tool zur Erforschung von Assemblys
1.8.8
Verwaltung der Zugriffsrechte
Im Manifest einer Assembly ist eine Liste aller Zugriffsrechte gespeichert, die von der Assembly gefordert werden. Daneben existiert eine Liste von Rechten, die erwünscht sind, und eine Liste solcher Rechte, die explizit abgelehnt werden. Eine Assembly, die das Senden, Empfangen, Editieren und Anzeigen von E-Mails ermöglicht, benötigt z. B. die Rechte, über das Netzwerk auf den Ports 110 (POP3) und 25 (SMTP) zu kommunizieren, sie erwünscht das Recht, JavaScript-Programme auszuführen, um die volle Funktionalität von E-Mails im HTML-Format zu ermöglichen, und lehnt Rechte ab, auf den Datenträger zu schreiben oder das lokale Adressbuch zu lesen, um E-Mail-Viren keinen Spielraum zu lassen6.
1 2
Die CLR überprüft bei der Ausführung von Programmcode einer Assembly, ob diese die notwendigen Rechte für die auszuführenden Aktionen besitzt, und generiert im negativen Fall eine Ausnahme. Die Rechte einer Assembly werden zum einen über die so genannte Codezugriffssicherheit (Code Access Security, CAS) definiert. In der Konfiguration des .NET Framework (erreichbar über die Systemsteuerung) kann ein Administrator für Gruppen von Assemblys oder auch für einzelne Assemblys genau festlegen, welche Rechte diese besitzen. In der Standardeinstellung besitzen Assemblys, die auf dem lokalen Rechner gespeichert sind, alle Rechte. Assemblys, deren Speicherort im Intranet oder Internet liegt, sind hingegen in den Rechten mehr oder weniger stark eingeschränkt. Eine Assembly, die Sie über eine Internetadresse geladen haben, darf z. B. nicht auf lokale Dateien zugreifen.
3
4
5
Zum anderen werden die Rechte einer Assembly natürlich auch über die Rechte des Windows-Benutzerkontos definiert, in dessen Kontext die Anwendung ausgeführt wird. Dazu und zur Codezugriffssicherheit finden Sie mehr in Kapitel 23.
1.9
6
Der .NET-Reflector: Ein wichtiges Tool zur Erforschung von Assemblys
7
Lutz Roeder stellt auf der Seite www.aisto.com/roeder/dotnet den .NET Reflector zur Verfügung, über den Sie Assemblys sehr schön erforschen können. Ein in vielen Fällen sehr hilfreiches Feature dieses Tools ist, dass es Methoden und Eigenschaften sogar wieder in Quellcode zurückführen kann. So können Sie sich anschauen, wie eine Assembly von innen aussieht (was allerdings nur funktioniert, wenn die Assembly nicht speziell geschützt ist, was aber nur sehr selten der Fall ist). Der Reflector lädt nach seinem ersten Start die wichtigsten Standard-Assemblys. Über den Menübefehl FILE / OPEN können Sie beliebige Assemblys nachladen, über FILE / OPEN CACHE können Sie Assemblys aus dem GAC nachladen.
8
9
Eine weitere Möglichkeit sind Add-Ins, die Sie in den .NET Reflector integrieren können. Diese finden Sie auf der Seite www.codeplex.com/reflectoraddins. Ein sehr hilfreiches Add-In ist der File Disassembler (www.denisbauer.com/NETTools/ FileDisassembler.aspx), über den Sie für einen einzelnen Typen oder sogar für eine komplette Assembly C#-Quellcodedateien erzeugen können. Add-Ins integrieren Sie über das Menü VIEW/ADD-INS in den Reflector.
10
11
Abbildung 1.14 zeigt den Reflector mit dem disassemblierten Quellcode der SendMethode der SmtpClient-Klasse. 6
O.K., dieses Beispiel habe ich dem Buch »C# Essentials« entnommen, aber es ist eben ein gutes Beispiel.
85
Einführung
Abbildung 1.14: Der .NET Reflector von Lutz Roeder
86
Inhalt
2
Einführung in die Arbeit mit Visual Studio 2008 1
2
Visual Studio ist die Entwicklungsumgebung (IDE, Integrated Development Environment) von Microsoft für C++, C# , J# und Visual Basic. Diese Entwicklungsumgebung bietet, verglichen mit anderen, die meisten Features zur Entwicklung von Anwendungen und Anwendungs-Komponenten.
3
Auch wenn Sie mit den Express-Editionen arbeiten, setzen Sie in Wirklichkeit Visual Studio ein. Die Oberfläche der Express-Editionen wurde von Microsoft lediglich vereinfacht und bietet nicht alle Features des großen Bruders (oder ist Visual Studio eher die große Schwester? Hmmm…).
4
Obwohl es einige freie Entwicklungsumgebungen für .NET gibt (allen voran Sharp Develop), sollten Sie Visual Studio den Vorzug geben. Diese Entwicklungsumgebung ist sehr ausgereift und mächtig, aber trotzdem einfach anzuwenden. Außerdem sind die Express-Editionen, mit denen Sie die meisten Anwendungen ohne Probleme erstellen können, kostenfrei.
5
Ein Nachteil von Visual Studio ist, dass diese IDE nicht für andere Betriebssysteme zur Verfügung steht, für die eine Implementierung des .NET Framework (z. B. in Form des Mono-Projekts) verfügbar ist. Man kann eben nicht alles haben …
6
Dieses Kapitel behandelt also die grundsätzliche Arbeit mit Visual Studio. Es führt allerdings auch in die Grundlagen der Entwicklung von Windows- und Konsolenanwendungen ein. Leser, die sich bereits in einer älteren Version von Visual Studio auskennen, können die ersten Abschnitte dieses Kapitels überspringen und den Abschnitt »Neues in Visual Studio 2008 und in den neuen Express-Editionen« ab Seite 137 lesen, in dem ich die Neuigkeiten von Visual Studio 2008 und den Express-Editionen vorstelle. Ich nenne in diesem Kapitel viele Tastenkombinationen, deren Anwendung bei der täglichen Arbeit mit Visual Studio essentiell sind. Sie können sich diese wahrscheinlich nicht alle direkt merken. Deswegen habe ich alle im Kapitel verwendeten und ein paar zusätzlich wichtige in die Datei Tastenkombinationen.pdf gepackt, die Sie auf der Buch-DVD finden. Die Tastenkombinationen finden Sie außerdem in der Referenz des Buchs. Das macht das Leben einfacher, auch für mich ☺.
7
8
9 DISC
10
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■
11
Die verschiedenen Projekttypen Die Bedeutung von Projekten und Projektmappen Übersicht über die wichtigen Fenster der IDE Grundlagen zum Erstellen von Windows- und Konsolenanwendungen Die Optionen der Entwicklungsumgebung Die Eigenschaften von Projekten Grundlagen zur Suche nach Fehlern
87
Index
■
Einführung in die Arbeit mit Visual Studio 2008
■ ■
Wichtige Tastenkombinationen Neues in Visual Studio 2008 und in den Express-Editionen
2.1
Die (wichtigen) C#-Projekttypen
Mit C# können Sie alle möglichen Arten von .NET-Anwendungen und -Komponenten erzeugen. Tabelle 2.1 zeigt die verschiedenen Möglichkeiten, die in Visual Studio über einen entsprechenden Projekttyp abgebildet sind. Tabelle 2.1: Die verschiedenen Anwendungen bzw. Komponenten, die mit C# erzeugt werden können
Anwendung
Beschreibung
In einer ExpressEdition verfügbar?
Konsolenanwendungen
Eine Konsolenanwendung läuft unter Windows direkt In Visual C# 2008 Express an der Konsole (der »Eingabeaufforderung« oder dem möglich. »DOS-Prompt«). Ein- und Ausgaben erfolgen bei einer solchen Anwendung in Textform an der Konsole. Konsolenanwendungen werden in diesem Kapitel grundlegend behandelt.
WPF-Anwendungen
Eine WPF-Anwendung (Windows Presentation Foun- In Visual C# 2008 Express dation-Anwendung) nutzt die Klassen der Windows möglich. Presentation Foundation zur Erstellung einer Benutzeroberfläche. Dazu stehen Klassen für Fenster, Seiten und Steuerelemente zur Verfügung. Über Ereignisse der Fenster und Steuerelemente können Sie auf Benutzereingaben (oder andere Ereignisse) reagieren. WPF erlaubt die Erstellung von Windows-Anwendungen und von Anwendungen, die in einem Browser ausgeführt werden. WPF-Anwendungen können zudem relativ problemlos mit Effekten, Animationen, 3-D-Grafiken und anderem ausgestattet werden. Ein weiteres Feature von WPF ist, dass Design und Programmierung einer Anwendung getrennt sind und deswegen von unterschiedlichen Personen bearbeitet werden können. WPF-Anwendungen werden ab Kapitel 12 behandelt.
88
Windows.FormsAnwendungen
Windows.Forms ist die »alte« (Vor-WPF-)Art, Windows-Anwendungen zu erstellen. Windows.FormsAnwendungen arbeiten wie WPF-Anwendungen mit Fenstern (die hier »Formulare« heißen) und mit Steuerelementen. Windows.Forms bietet aber keine direkte Integration von Effekten, Animationen, von 3D etc., und keine Trennung des Designs von der Programmierung. Windows.Forms wird in diesem Kapitel grundlegend behandelt. In Kapitel 12 finden Sie eine Gegenüberstellung von WPF und Windows.Forms.
In Visual C# 2008 Express möglich.
Klassenbibliotheken
Ein Klassenbibliothek-Projekt erzeugt eine Assembly, In Visual C# 2008 Express die eine oder mehrere Klassen, Strukturen und andere möglich. Typen enthält. Diese Typen können Sie später in anderen Anwendungen verwenden, indem Sie die Assembly referenzieren. Informationen zu Klassenbibliotheken finden Sie in Kapitel 5.
Die (wichtigen) C#-Projekttypen
Anwendung
Beschreibung
In einer ExpressEdition verfügbar?
WPF-Steuerelementbibliotheken
Ein WPF-Steuerelementbibliothek-Projekt erzeugt eine Assembly, die selbst entwickelte Steuerelemente enthält, die Sie in WPF-Anwendungen verwenden können. Eine WPF- Steuerelementbibliothek ist übrigens nichts anderes als eine Klassenbibliothek, in deren Projekt Visual Studio bereits die für WPF notwendigen Referenzen eingefügt hat.
In Visual C# 2008 Express indirekt über ein Klassenbibliothek-Projekt möglich, das von Hand mit den notwendigen Referenzen ausgestattet wird.
Ein Windows-Steuerelementbibliothek-Projekt erzeugt ähnlich einem WPF-Steuerelementbibliothek-Projekt eine Assembly, die selbst entwickelte Steuerelemente enthält. Diese Steuerelemente setzen Sie dann aber in einer Windows.Forms-Anwendung ein.
In Visual C# 2008 Express indirekt über ein Klassenbibliothek-Projekt möglich, das von Hand mit den notwendigen Referenzen ausgestattet wird.
Das .NET Compact Framework bietet gegenüber dem .NET Framework einen eingeschränkten Funktionsumfang, der allerdings auch auf mobilen Geräten wie Pocket PCs und MDAs verwendet werden kann. Zur Entwicklung von Anwendungen, die mit dem .NET Compact Framework arbeiten, können Sie in Visual Studio mit einem Emulator arbeiten.
Nicht möglich
Windows.FormsSteuerelementbibliotheken
Windows-Anwendungen für mobile (oder: intelligente) Geräte
ASP.NET-Webanwendungen
Tabelle 2.1: Die verschiedenen Anwendungen bzw. Komponenten, die mit C# erzeugt werden können (Forts.)
1
2 3
4
5
Eine ASP.NET-Webanwendung enthält Webformulare In Visual Web Developer mit speziellen Steuerelementen, die prinzipiell behan- 2008 Express möglich. delt werden, wie bei Windows-Anwendungen (mit Ereignissen). Im Unterschied zu einer WindowsAnwendung wird eine Webanwendung aber von einem Webserver ausgeführt und erzeugt HTML-Code für die Benutzerschnittstelle.
6
7
Webanwendungen behandelt dieses Buch nicht. Web-Steuerelementbibliotheken
WPF-Browser-Anwendungen
Ein solches Projekt erzeugt eine Assembly, die selbst entwickelte Steuerelemente enthält, die Sie in Webanwendungen verwenden können.
Da WPF-Anwendungen auch im Browser ausgeführt werden können, bietet Visual Studio dazu auch eine Projektvorlage. Der wesentliche Unterschied zu einer WPF-Windows-Anwendung ist, dass WPF-Browseranwendungen nicht mit Fenster arbeiten, sondern mit Seiten (Pages).
In Visual C# 2008 Express indirekt über ein Klassenbibliothek-Projekt möglich, das von Hand mit den notwendigen Referenzen und Klassen ausgestattet wird.
8
In Visual C# 2008 Express möglich.
10
9
11
89
Einführung in die Arbeit mit Visual Studio 2008
Tabelle 2.1: Die verschiedenen Anwendungen bzw. Komponenten, die mit C# erzeugt werden können (Forts.)
Anwendung
Beschreibung
In einer ExpressEdition verfügbar?
ASP.NET-Webdienste
Ein Webdienst ist eine Komponente, die auf einem In Visual Web Developer Webserver ausgeführt wird und deren Methoden über 2008 Express über eine das Internet aufgerufen werden können. Webdienste neue Website möglich. liefern Daten (bzw. Dienstleistungen) über das Internet und können von beliebigen (Webdienst-fähigen) Anwendungen verwendet werden. Webdienst-Projekte sind eigentlich ASP.NET-Webanwendungs-Projekte, die lediglich keine Webseiten enthalten, sondern eben Webdienste (Webdienste können Sie auch in einer ASP.NET-Webanwendung mit Webseiten kombinieren).
90
WCF-Dienste
Ein WCF-Dienst-Projekt erlaubt die Entwicklung von Nicht möglich Diensten, die in einer Anwendung ausgeführt oder im IIS gehostet werden. Ein WCF-Dienst ist so etwas Ähnliches wie ein Webdienst. WCF ist aber die modernere Variante, die mehr Features erlaubt und wesentlich konfigurierbarer ist.
Workflow-Anwendungen und -Bibliotheken
Workflow-Anwendungen sind Anwendungen, die mit der Workflow Foundation (WF) arbeiten. Die Workflow Foundation erlaubt u. a. die (auch grafische) Erstellung von Workflows (Arbeitsabläufen mit einzelnen Aktionen). Über verschiedene Visual-Studio-Vorlagen können Sie Workflow-Anwendungen und -Bibliotheken erzeugen. Sie können einen Workflow aber natürlich auch später in ein anderes Projekt integrieren.
In Visual C# 2008 Express nur sehr indirekt über die direkte Verwendung der Workflow-Typen möglich (ohne DesignerUnterstützung).
Mobile Webanwendungen
Mobile Webanwendungen sind Webanwendungen für mobile Plattformen, die sich automatisch an die limitierte Größe von Pocket-PCs, PDAs etc. anpassen. Mobile Webanwendungen behandelt dieses Buch nicht.
Nicht möglich
Windows-Dienste
Ein Windows-Dienst läuft unter Windows im Hintergrund. Im Prinzip ist ein Windows-Dienst ein Programm ohne Oberfläche, das automatisch ausgeführt wird und das bestimmte Dienste anbietet, die von anderen Anwendungen verwendet werden können. Der SQL Server und der Internet Information Server laufen z. B. als Windows-Dienst.
In Visual C# 2008 Express indirekt über ein Windows-AnwendungProjekt möglich, dem Dienst-Klassen (von ServiceBase abgeleitet) von Hand hinzugefügt werden.
Gespeicherte Prozeduren, Funktionen und benutzerdefinierte Datentypen für den SQL Server 2005 (SQL-ServerProjekte)
Der SQL Server 2005 erlaubt die Integration von gespeicherten Prozeduren, Funktionen und benutzerdefinierten Datentypen, die in einer .NET-Assembly definiert sind. Wenn Sie Datenbanklogik in den SQL Server auslagern wollen, bietet die .NET-Integration wesentlich mehr Möglichkeiten als die alternative Variante, Prozeduren, Funktionen und Datentypen in der SQL-Syntax des SQL Servers (TSQL) zu schreiben. Die .NET-Integration im SQL Server wird in diesem Buch nicht behandelt.
In Visual C# 2008 Express indirekt über ein Klassenbibliothek-Projekt möglich, das auf die SQL-Server-Assemblys verweist.
Projekte und Projektmappen
2.2
Projekte und Projektmappen
Visual Studio verwaltet alle Dateien und verschiedene Einstellungen, die zu einem C#-Projekt gehören, in einer XML-Datei mit der Endung .csproj (CSharp-Project). Über diese Datei können Sie ein C#-Projekt öffnen.
Projektdateien verwalten Projekte
In einer Datei mit der Endung csproj.user verwaltet Visual Studio weitere Einstellungen, die nicht das Projekt selbst betreffen, sondern die Entwicklungsumgebung. Wenn Sie z. B. in den Projekteigenschaften in Visual Studio einen speziellen Pfad angeben, in dem Visual Studio beim Kompilieren eines Projektes nach referenzierten Assemblys suchen soll, wird diese Einstellung in der .user-Datei verwaltet. Diese Datei können Sie löschen, ohne das Projekt selbst zu verändern, verlieren dann aber u. U. wichtige Einstellungen. Visual Studio erlaubt zudem, dass Sie mehrere Projekte über eine Projektmappe zusammenfassen. Projektmappen sind XML-Dateien, die Verweise auf die enthaltenen Projekte beinhalten. In vielen Fällen besteht eine Projektmappe allerdings lediglich aus einem Projekt.
1
2 Projektmappen verwalten zusammengehörige Projekte
Eine Projektmappe mit mehreren Projekten ist z. B. dann sinnvoll, wenn Sie eine Anwendung entwickeln, die mit mehreren Klassenbibliotheken arbeitet, die Sie gleichzeitig mit der Anwendung programmieren oder weiterentwickeln. In diesem Fall können Sie die Anwendung und die Klassenbibliotheken gleichzeitig entwickeln und vor allen Dingen auch testen und nach Fehlern suchen.
4
5
Eine Projektmappendatei besitzt die Endung .sln, was für Solution (englisch für »Lösung«) steht. Wenn Sie diese Datei unter Windows ausführen, öffnet Visual Studio alle darin enthaltenen Projekte. Visual Studio verwaltet zu einer Projektmappe eine zusätzliche, versteckte Datei mit der Endung .suo. Diese Datei speichert u. a., welche Fenster bei der Projektmappe geöffnet sind. Wenn Sie die Datei löschen (was problemlos möglich ist), werden beim nächsten Öffnen der Projektmappe die zuletzt geöffneten Fenster nicht automatisch geöffnet. In der .suo-Datei werden aber scheinbar noch andere Einstellungen verwaltet, denn diese Datei führt manchmal, bei stark frequentierten Projektmappen, zu Problemen. Diese Probleme können ganz unterschiedlicher Natur sein. Es kann z. B. sein, dass der Compiler einen Fehler meldet, der aber gar keine Ursache zu haben scheint. Ein anderes Problem, das ich schon mal hatte, war, dass das Einfügen neuer Zeilen in einer großen Klasse enorm viel Zeit in Anspruch nahm. Wenn Sie solche eigenartigen Probleme haben, schließen Sie die Projektmappe und löschen Sie die .suo-Datei (denken Sie daran, dass diese versteckt ist und dass Sie den Windows-Explorer zunächst so einstellen müssen, dass dieser versteckte Dateien anzeigt). In vielen Fällen sind die Probleme damit beseitigt.
2.3
3
6
HALT
7
8
9
10
Der Start von Visual Studio und das Erzeugen bzw. Öffnen von Projekten
11
Visual Studio startet mit einer Startseite, die die zuletzt geöffneten Projekte anzeigt und Ihnen die Möglichkeit gibt, neue Projekte zu erzeugen oder vorhandene zu öffnen.
91
Einführung in die Arbeit mit Visual Studio 2008
Abbildung 2.1: Die Startseite von Visual Studio 2008
Über den Verweis ERSTELLEN: PROJEKT oder über den entsprechenden Eintrag im DATEI-Menü öffnen Sie den Dialog zur Erzeugung eines neuen Projekts. Hier können Sie für alle installierten .NET-Sprachen die verschiedenen Projekttypen erzeugen. Die Auswahl der Projekttypen ist in den Express-Editionen allerdings stark eingeschränkt. Abbildung 2.2: Der Visual-Studio2008-Dialog zur Erzeugung eines neuen Projekts
Die Projekttypen, die unter dem Eintrag VISUAL C# angegeben sind, sind im Moment die für Sie interessantesten. Um gleich ein wenig Praxis ins Buch zu bringen, können Sie nun ein Projekt für eine Windows.Forms-Anwendung erzeugen.
92
Der Start von Visual Studio und das Erzeugen bzw. Öffnen von Projekten
Abbildung 2.3: Der Visual-C#2008-ExpressEdition-Dialog zur Erzeugung eines neuen Projekts
1
2 3
4 Ich setze hier bewusst nicht WPF ein, weil die Designer und Fenster einer WPF-Anwendung (zurzeit noch) etwas schwieriger zu handhaben sind als die einer Windows.FormsAnwendung. Das Grundprinzip von Windows.Forms- und WPF-Anwendungen ist sowieso gleich: Sie »designen« Fenster bzw. Formulare, indem Sie Steuerelemente darauf platzieren und diese über deren Eigenschaften an Ihre Vorstellungen anpassen. Dann lassen Sie Visual Studio für Ereignisse, die Sie in Ihrem Programm auswerten wollen, Ereignisbehandlungsmethoden erzeugen. Schließlich füllen Sie diese Methoden mit Programmcode, kompilieren das Programm und testen. Aber mit Windows.Forms ist das Ganze (zurzeit noch) ein wenig einfacher und (besonders für Anfänger) potenziell fehlerfreier.
INFO
5
6
7
Wählen Sie den Eintrag WINDOWS.FORMS-ANWENDUNG um ein Projekt zu erzeugen, das eine (Windows.Forms-)Windows-Oberfläche besitzt. Im sich nun öffnenden Dialog können Sie zumindest den Namen des neuen Projekts eingeben (bei Visual C# 2008 Express). In Visual Studio 2008 geben Sie hier gleich noch den Speicherort an.
8
Ein nachträgliches Umbenennen der Projektdateien ist schwierig, achten Sie also auf einen korrekten Namen. Geben Sie für das erste Beispiel den Namen »Hello World« ein (Ja. Ich weiß. Das ist mindestens das Millionste Hello-World-Programm. Aber ohne ein solches Programm steht Ihre C#-Karriere vielleicht unter einem schlechten Stern. Und dafür möchte ich nicht verantwortlich sein ☺).
9
10
Bevor Sie den Dialog in Visual Studio 2008 bestätigen, sollten Sie den Speicherort des Projekts einstellen und die Option PROJEKTMAPPENVERZEICHNIS ERSTELLEN ggf. ausschalten. In Visual C# 2008 Express geben Sie den Speicherort später (beim Speichern) an.
11
Die Entwicklungsumgebung erzeugt beim Erstellen neuer Projekte in dem unter SPEICHERORT angegebenen Ordner einen Unterordner. Wenn die Option PROJEKTMAPPENVERZEICHNIS ERSTELLEN eingeschaltet ist, wird dieser Ordner mit dem Namen benannt, den Sie in dem Feld PROJEKTMAPPENNAME angegeben haben. In diesem Ordner wird die Projektmappendatei angelegt. Außerdem legt Visual Studio dort einen weiteren Unterordner an, der den Namen des Projekts erhält. In diesem Ordner werden die Projekt-
93
Einführung in die Arbeit mit Visual Studio 2008
dateien gespeichert. Sinn macht diese Option, wenn Sie mehrere zu einer Projektmappe gehörende Projekte in einem (Projektmappen-)Ordner verwalten wollen. Für einzelne Projekte oder Projektmappen, die aus Projekten zusammengestellt werden, die in verschiedenen Ordnern auf dem System gespeichert sind, macht diese Option keinen Sinn. Schalten Sie die Option aus, erstellt Visual Studio nur einen einzigen Unterordner mit den Namen des Projekts und legt alle Dateien dort an. Wählen Sie also einen Speicherort, in dem Sie Ihre Projekte verwalten wollen. Schalten Sie die Option PROJEKTMAPPENVERZEICHNIS ERSTELLEN ab, da unsere Hello-WorldProjektmappe nur ein Projekt beinhalten wird.
EXKURS
HALT
Das nachträgliche Verschieben von ganzen Projekten ist übrigens einfach, dazu verschieben Sie einfach den Projektordner über den Windows-Explorer (oder ähnliche Tools ☺). Wenn die Projektmappendatei nicht in dem verschobenen Ordner liegt, müssen Sie diese ggf. (über einen Editor) anpassen. In vielen Fällen reicht aber auch einfach ein Doppelklick auf der Projektdatei um das Projekt zu öffnen. Visual Studio erzeugt eine evtl. fehlende Projektmappendatei einfach neu.
Nachdem Sie das Projekt erstellt haben, sollten Sie dieses in Visual C# 2008 Express über die Tastenkombination (STRG) + (ª) + (S) unbedingt speichern. Visual C# 2008 Express fordert Sie bei ungespeicherten Projekten auf, den Speicherort anzugeben und die Projektmappen-Ordner-Option auszuwählen. Wenn Sie das Speichern vergessen, können Sie das Projekt trotzdem kompilieren und testen. Ungespeicherte Projekte werden dann aber nur temporär (im Ordner C:\Dokumente und Einstellungen\\Lokale Einstellungen\Anwendungsdaten\Temporary Projects) gespeichert. Wenn die Entwicklungsumgebung zwischendurch abstürzt (was ja schon einmal vorgekommen sein soll), ist Ihre Arbeit erst zwar nicht verloren, Sie müssen aber die temporären Dateien suchen und von Hand in Ihren Projektordner kopieren. Nachdem Sie ein neues Windows.Forms-Anwendung-Projekt erzeugt haben, sieht Visual Studio 2008 etwa aus wie in Abbildung 2.4.
Abbildung 2.4: Visual Studio 2008 mit einem neuen Windows.FormsAnwendungProjekt
94
Einstellung der Entwicklungsumgebung
2.4
Einstellung der Entwicklungsumgebung
Visual Studio 2008 und die Express-Editionen können in vielen Bereichen angepasst werden. So können Sie z. B. Menü- oder Toolbox-Befehle hinzufügen oder nicht benötigte entfernen. Den Befehl dazu finden Sie im ANSICHT-Menü unter SYMBOLLEISTEN / ANPASSEN. Visual Studio 2008 bietet zudem einige vordefinierte Einstellungen, die für unterschiedliche Aufgaben vorgesehen sind. Die Einstellung VISUAL BASIC ENTWICKLUNGEINSTELLUNGEN ist z. B. für Visual-Basic-6-Entwickler gedacht, die ihre alte IDE gut kennen und sich nicht umgewöhnen wollen.
1
2
Sie können die Einstellungen der Entwicklungsumgebung über den Befehl EINSTELLUNGEN IMPORTIEREN UND EXPORTIEREN im EXTRAS-Menü auf eine der vordefinierten Einstellungen zurücksetzen bzw. umsetzen. Bei den Express-Editionen können Sie allerdings nur auf die einzig vorhandene Einstellung für die jeweilige Umgebung zurücksetzen.
3
Das Zurücksetzen hilft auch dann, wenn Ihre IDE so durcheinander geraten ist, dass Menü- oder Toolbox-Befehle verloren gegangen sind (oder wenn die nachträgliche Installation des SQL Servers die Einstellung auf BUSINESS INTELLIGENCE-EINSTELLUNGEN gesetzt hat und Sie sich nicht mehr zurechtfinden, was auch schon vorgekommen ist …).
4
5 Bevor Sie den Zurücksetzen-Befehl ausführen, sollten Sie beachten, dass nachträgliche Änderungen an der Entwicklungsumgebung, die z. B. Add-Ins vorgenommen haben, und alle geänderten Optionen der IDE (wie z. B. eine geänderte Tabulatorgröße) damit verloren gehen und ggf. von Hand wiederhergestellt werden müssen.
HALT
6
Im sich öffnenden Dialog wählen Sie die Option ALLE EINSTELLUNGEN ZURÜCKSETZEN. Nachdem Sie den WEITER-Schalter betätigt haben, können Sie im nächsten Schritt Ihre aktuellen Einstellungen speichern, was ich auch empfehlen würde. Im darauffolgenden Schritt setzen Sie die Einstellungen der Entwicklungsumgebung zurück. Bei Visual Studio 2008 können Sie hier eine der verfügbaren Einstellungen wählen. Die allgemeine Entwicklungseinstellung ist in meinen Augen die beste, weil sie alle Optionen beinhaltet. Anderen Einstellungen fehlen das eine oder andere Feature in den Menüs und Symbolleisten.
2.5
7
8
9
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel
Visual Studio 2008 und die Express-Editionen besitzen eine Vielzahl an Fenstern, die in der Voreinstellung entweder aneinander andocken oder registerförmig angeordnet sind (Fenster können auch frei auf dem Bildschirm angeordnet werden, aber das wird in der Voreinstellung nicht genutzt). Fenster, die zurzeit nicht sichtbar sind, können Sie über das ANSICHT- oder das DEBUGGEN-Menü sichtbar schalten. Dieser Abschnitt beschreibt die grundsätzliche Arbeit mit Fenstern und dann die für die tägliche Arbeit wichtigen Fenster am Hello-World-Beispiel, das im Abschnitt »Der Start von Visual Studio und das Erzeugen bzw. Öffnen von Projekten« ab Seite 91 begonnen wurde.
10
11
95
Einführung in die Arbeit mit Visual Studio 2008
2.5.1
Positionierung der Fenster
Sie können die Position der einzelnen Fenster selbst bestimmen, indem Sie diese mit der Maus an eine andere Position bewegen. Visual Studio unterstützt Sie bei der Positionierung der Fenster über Positionierungshilfen, die beim Verschieben eingeblendet werden. Wenn Sie mit mehreren Bildschirmen arbeiten, können Sie Fenster auch auf einen anderen Bildschirm ziehen.
TIPP
Wenn Sie die Fenster (wie ich in meinen ersten Versuchen) einmal komplett durcheinander platziert haben, können Sie das Fenster-Layout über den Befehl FENSTER/FENSTERLAYOUT zurücksetzen zurücksetzen. In schwierigen Fällen oder wenn Sie andere Teile der Entwicklungsumgebung (z. B. Menüeinträge oder Symbolleisten-Schalter) vermissen, hilft ein Zurücksetzen der Entwicklungsumgebung auf eine der vordefinierten Einstellungen, wie ich es im Abschnitt »Einstellung der Entwicklungsumgebung« ab Seite 95 erläutert habe. Die andockenden Fenster der Entwicklungsumgebung besitzen einen Pin in der Titelleiste. Wenn Sie auf diesen Pin klicken, schalten Sie die Fenster vom normalen in den versteckten Modus. Im versteckten Modus werden Fenster automatisch auf ein Icon verkleinert, das am Rand des Arbeitsbereichs angezeigt wird. Das ToolboxFenster befindet sich per Voreinstellung in diesem Modus. Wenn Sie die Maus auf das Icon bewegen, wird das Fenster automatisch vergrößert. Wenn Sie einen kleinen Bildschirm besitzen, können Sie durch dieses automatische Verstecken der Fenster erreichen, dass Ihr Arbeitsbereich ausreichend groß ist. Im Menü FENSTER finden Sie einen Eintrag ALLE AUTOMATISCH AUSBLENDEN zum automatischen Verstecken aller Fenster.
2.5.2
Der Projektmappen-Explorer
Nachdem Sie ein neues Projekt erzeugt haben, sehen Sie die Dateien des Projekts im Projektmappen-Explorer (oben rechts). Dieser verwaltet die Verweise Ihres Projekts (zu Assemblys aus dem GAC oder aus einem Systemordner) und bildet die Dateien ab, die zum Projekt gehören. Abbildung 2.5: Der ProjektmappenExplorer mit aufgeklapptem Verweise-Ordner
96
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel
Über den Projektmappen-Explorer erhalten Sie per Doppelklick schnellen Zugriff auf die Dateien eines Projekts, können diese entfernen und umbenennen und neue hinzufügen. Die bei einer Windows.Forms-Anwendung erzeugten Ordner und Dateien erläutert die folgende Auflistung: ■
■
■
■
■
■
Properties\AssemblyInfo.cs: In dieser Datei können Sie allgemeine Informationen zur später erzeugten Assembly für Ihr Projekt unterbringen. Dazu gehören z. B. der Titel der Assembly, eine Beschreibung, das Copyright und die Versionsnummer. Die hier eingegebenen Daten werden beim Kompilieren in die Metadaten der Assembly übernommen. Properties\Resources.resx: Diese Datei ist eine Ressourcen-Datei, die von Visual Studio zur Speicherung von Ressourcen verwendet wird, die Sie über die integrierte Ressourcenverwaltung hinzufügen. Näheres zu Ressourcen finden Sie in Kapitel 15. Properties\Settings.settings: Bei dieser Datei handelt es sich um eine Datei, in der Visual Studio Einstellungen verwaltet, die Sie für Ihre Anwendung über die in Visual Studio integrierte Einstellungsverwaltung hinzugefügt haben. Näheres dazu finden Sie ebenfalls in Kapitel 15. Verweise: Dieser Ordner, der nicht physikalisch vorhanden ist, zeigt alle Assembly-Referenzen des Projekts an und ermöglicht das Löschen und Hinzufügen von Referenzen. Form1.cs: Diese Datei enthält die Klasse eines (noch leeren) Formulars. Windows-Anwendungen besitzen normalerweise ein Formular, mit dem die Anwendung startet. Das von Visual Studio eingefügte Formular ist bereits für diesen Zweck vorgesehen. Program.cs: Diese Quellcodedatei enthält den Programmcode, mit dem die Anwendung startet.
1
2 3
4
5
6
7
Umbenennen von Dateien Dateien können Sie über den Projektmappen-Explorer so umbenennen, wie es in Windows üblich ist. Das können Sie gleich einmal an der Formular-Datei ausprobieren, deren Name nicht besonders aussagekräftig ist: 1.
2. 3.
8
STEPS
Klicken Sie dazu auf die Datei Form1.cs im Projektmappen-Explorer und betätigen Sie die (F2)-Taste, damit Visual Studio den Eintrag in den BearbeitungsModus umschaltet. Benennen Sie die Datei um (z. B. in StartForm.cs) und betätigen Sie die (¢_)Taste um Ihre Eingabe zu bestätigen. Visual Studio fragt Sie danach in einem Dialog, ob Sie auch die Klasse umbenennen und den Rest des Projekts an diesen neuen Namen anpassen wollen. Da die Dateinamen von Klassen zur besseren Übersicht natürlich mit dem Klassennamen übereinstimmen sollten, sollten Sie diesen Dialog mit JA bestätigen. Visual Studio nutzt seine Refactoring-Features um die Umbenennung so vorzunehmen, dass Ihr Projekt danach genauso funktioniert wie zuvor.
9
10
11
97
Einführung in die Arbeit mit Visual Studio 2008
INFO
Seien Sie nicht verwirrt, dass bei der Umbenennung des Formulars dessen (unsinniger) Titel nicht geändert wird. Der Titel eines Formulars wird in der Eigenschaft Text festgelegt, und die haben Sie (noch) nicht geändert.
Verweise verwalten Visual Studio hat für das Projekt bereits Verweise auf die wichtigsten Assemblys des .NET Framework eingestellt. Wenn Sie andere Assemblys verwenden wollen, müssen Sie Verweise auf diese hinzufügen. Dazu können Sie den Befehl VERWEIS HINZUFÜGEN im Kontextmenü des Projekteintrags oder des VERWEISE-Ordners verwenden. Sie können hier auf Assemblys verweisen, die im GAC installiert sind oder auf Assemblys aus einem beliebigen Ordner. Außerdem ermöglicht der VERWEISE-Dialog auch das Hinzufügen von Verweisen auf Komponenten des veralteten COM-Modells (die aber in wenigen Fällen immer noch benötigt werden) und von Verweisen auf Klassenbibliothek-Projekte, die in dieselbe Projektmappe eingebunden sind. Unser erstes Beispielprojekt benötigt jedoch keine weiteren Verweise.
HALT
Wenn Sie Verweise auf Assemblys mit einem starken Namen hinzufügen, sollten Sie auf die Option SPEZIFISCHE VERSION achten, die Sie im Eigenschaftenfenster einstellen können. Ist diese Option true, referenziert Visual Studio die Assembly nur, wenn diese der Version der ersten Referenzierung entspricht. Wird die Assembly im Verlauf der Entwicklung einer Anwendung gegen eine neue Version ausgetauscht, behauptet Visual Studio, die referenzierte Assembly nicht mehr zu finden. In der Praxis sollte Spezifische Version deshalb prinzipiell auf false stehen. Es kommt aber in wenigen Fällen vor, dass Visual Studio diese Option beim Referenzieren von Assemblys mit einem starken Namen auf true einstellt. Setzen Sie Spezifische Version dann auf false.
2.5.3
Die Klassenansicht
Die Klassenansicht, die Sie über den Befehl KLASSENANSICHT im ANSICHT-Menü öffnen können, stellt alle Namensräume und Klassen eines Projekts übersichtlich dar. Wenn Sie den Eintrag einer Klasse öffnen, sehen Sie alle Elemente dieser Klasse. Über einen Doppelklick auf einem Element gelangen Sie zum entsprechenden Quellcode. Die Klassenansicht ist bei größeren Projekten bzw. Klassen mit vielen Feldern, Eigenschaften und Methoden bei der Suche nach einem Klassenmember sehr hilfreich.
2.5.4
Der Code-/Designer-Bereich
In der Mitte der Entwicklungsumgebung finden Sie den Code-/Designer-Bereich. Hier öffnet Visual Studio so genannte Editoren und Designer, in denen Sie eine Datei bearbeiten können. Dazu klicken Sie im Projektmappen-Explorer in der Regel einfach doppelt auf diese Datei. Für Windows.Forms-Formulare, WPF-Objekte, Webanwendungs-Formulare und selbst entwickelte Steuerelemente (die in der Laufzeit eine Oberfläche besitzen) stellt Visual Studio neben dem Code-Editor auch einen Oberflächen-Designer zur Verfügung. Dieser öffnet sich automatisch, wenn Sie die entsprechende Datei im Projektmappen-Explorer doppelklicken. Auf einen solchen Designer ziehen Sie üblicherweise Steuerelemente und Komponenten aus der Toolbox, bearbeiten deren Position und Größe mit der Maus und stellen deren Eigenschaften im Eigenschaftenfenster ein.
98
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel
2.5.5
Die Toolbox
In der Toolbox, die Sie am linken Rand von Visual Studio finden, sind die im .NET Framework enthaltenen Steuerelemente und Komponenten, die zum jeweiligen Projekt-Typ passen. Die Toolbox kann allerdings auch um eigene Steuerelemente und Komponenten oder um solche von externen Herstellern erweitert werden.
1 Der Unterschied zwischen Steuerelementen und Komponenten ist, dass Steuerelemente eine Oberfläche besitzen, die auch in der Laufzeit der Anwendung angezeigt wird. Komponenten sind im Prinzip nur einfache Objekte, die in der Laufzeit keine Oberfläche besitzen. Komponenten werden allerdings in dem Designer in Visual Studio angezeigt, auf den sie gezogen wurden. Die Vorteile von Komponenten im Vergleich zu Objekten, die im Programm instanziert werden, sind, dass Komponenten nicht explizit instanziert werden müssen, dass deren Eigenschaften über das Eigenschaftenfenster von Visual Studio eingestellt werden können und dass für deren Ereignisse über Visual Studio Ereignisbehandlungsmethoden erzeugt werden können.
EXKURS
2 3
4
Das Toolbox-Fenster ist per Voreinstellung nicht angeheftet, weswegen es auf eine Icon-Größe verkleinert wird, wenn es nicht benutzt wird. Wenn Sie die Maus auf dieses Icon bewegen, klappt das Toolbox-Fenster auf. Denken Sie daran, dass Sie das Fenster über den Pin in der Titelleiste auch anheften können, sodass dieses nicht mehr automatisch verkleinert wird.
5 Abbildung 2.6: Visual Studio mit aufgeklappter Toolbox
6
7
8
9
10
Die Toolbox zeigt immer nur die Steuerelemente und Komponenten an, die zum jeweils aktuellen Designer passen. Ist ein Windows.Forms-Designer aktuell, werden die Windows.Forms-Steuerelemente und -Komponenten angezeigt. Ist ein WPF-Objekt aktuell, werden die für WPF verfügbaren Steuerelemente und Komponenten angezeigt. Ähnliches gilt für andere Designer, die ebenfalls die Gestaltung einer Oberfläche erlauben. Ist allerdings kein Designer, sondern ein Quellcode-Editor aktuell, ist die Toolbox leer.
11 TIPP
99
Einführung in die Arbeit mit Visual Studio 2008
Wenn ein Windows.Forms-Fenster-Designer aktuell ist, sind einige Register vorhanden, die nicht hier erläutert werden sollen. Das Register ALLGEMEINE STEUERELEMENTE enthält die meistgebrauchten und ist für Sie im Moment das wichtigste.
2.5.6 Über den FormularDesigner bearbeiten Sie Formulare
Der Formular-Designer
Wie bereits gesagt, öffnen Sie den Formular-Designer für ein Windows.Forms-Formular, indem Sie im Projektmappen-Explorer auf den Eintrag des Formulars doppelklicken. Der Formular-Designer ermöglicht die Bearbeitung eines Formulars (oder in einer WPF-Anwendung eines WPF-Objekts) über die Maus. Sie können die Größe des Formulars ändern, Steuerelemente und Komponenten auf das Formular ziehen und die Eigenschaften des Formulars und der Steuerelemente anpassen. Wenn Sie ein Steuerelement oder eine Komponente verwenden wollen, ziehen Sie dieses einfach mit der Maus aus der Toolbox auf den Designer. Die Position und Größe eines Steuerelements können Sie ebenfalls über die Maus ändern. Zur Änderung der Position ziehen Sie das Steuerelement an die neue Position. Zur Änderung der Größe klicken Sie das Steuerelement an, um dieses zu aktivieren. Sie sehen am Rand des Steuerelements kleine, quadratische »Anfasser«. Wenn Sie diese oder die gestrichelte Linie zwischen den Anfassern anklicken und ziehen, ändern Sie die Größe.
TIPP
Die Position und Größe können Sie auch über das Eigenschaftenfenster einstellen. Eine andere Möglichkeit ist, die Position und Größe von Steuerelementen an andere Steuerelemente anzupassen. Visual Studio hilft dabei, indem beim Ziehen Hilfslinien angezeigt werden, die an anderen Steuerelementen ausgerichtet sind. In Windows. Forms-Projekten (nicht in WPF-Projekten) haben Sie zudem noch die Möglichkeit, Steuerelemente über das FORMAT-Menü oder die Layout-Symbolleiste aneinander anzupassen. Die Layout-Symbolleiste wird per Voreinstellung automatisch angezeigt, wenn ein Formular-Designer aktiv ist. Falls dies nicht der Fall ist, können Sie diese Symbolleiste über den Eintrag SYMBOLLEISTEN im ANSICHT-Menü aktivieren. Zur Übung können Sie nun ein Button-Steuerelement (einen Schalter) auf den Designer des Formulars ziehen und dessen Größe und Position anpassen:
STEPS
1. 2. 3. 4. 5.
Abbildung 2.7: Das BeispielFormular in der ersten Rohform
100
Öffnen bzw. aktivieren Sie dazu zunächst den Formular-Designer des Start-Formulars. Öffnen Sie die Toolbox, klicken Sie den BUTTON-Eintrag an und ziehen Sie den Button auf das Formular. Klicken Sie den Schalter auf dem Formular an und ziehen Sie ihn an etwa die Position, die Sie in Abbildung 2.7 sehen. Klicken Sie den Schalter auf dem Formular an und stellen Sie die Größe des Schalters über die rechteckigen »Anfasser« ein. Klicken Sie auf die Titelleiste des Formulars um dieses zu aktivieren und stellen Sie die Größe des Formulars so ein, dass dieses Abbildung 2.7 entspricht.
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel
2.5.7
Das Eigenschaftenfenster
Im Eigenschaftenfenster, das Sie über das ANSICHT-Menü öffnen können und das Sie unter dem Projektmappen-Explorer finden, stellen Sie die Eigenschaften von Formularen, Steuerelementen, Komponenten und anderen Oberflächenelementen ein.
Das Eigenschaftenfenster zeigt die Eigenschaften des aktiven Objekts an
Dazu aktivieren Sie zunächst das Objekt, dessen Eigenschaften Sie einstellen wollen. Klicken Sie dieses dazu einfach an. Wollen Sie die Eigenschaften eines Formulars einstellen, klicken Sie auf dessen Titelzeile oder auf einen freien Bereich zwischen den enthaltenen Steuerelementen. Alternativ können Sie das Formular und dessen Steuerelemente und Komponenten auch in der Objektliste des Eigenschaftenfensters suchen und dort aktivieren.
1
2
Das Eigenschaftenfenster von Windows.Forms- und Webanwendungen ermöglicht eine kategorische und eine alphabetische Ansicht (das Eigenschaftenfenster von WPF-Anwendungen erlaubt leider keine alphabetische Ansicht, besitzt dafür aber einen hilfreichen Suchfilter). Sie können die Ansicht über die Schalter in der Symbolleiste des Eigenschaftenfensters umschalten. Ich bevorzuge die alphabetische Ansicht, weil ich in der Regel weiß, welche Eigenschaft ich ändern will, und diese bei einer alphabetischen Sortierung schneller finde. Das Eigenschaftenfenster zeigt nicht nur Eigenschaften, sondern auch die Ereignisse von Objekten an. Wenn das Symbol mit dem Pfeil aktiv ist, werden Ereignisse angezeigt. Sie können in diesem Fall auf die Eigenschaftenseite umschalten, indem Sie auf das Symbol links daneben klicken.
3
4
5
INFO
Steuerelemente, Komponenten, Formulare und Fenster besitzen eine große Anzahl an Eigenschaften. Ich beschreibe die wichtigsten in Kapitel 13 bei der Behandlung von WPF-Windows-Anwendungen. Für den Moment sind nur zwei Eigenschaften wichtig: Name und Text.
6
7
Die Eigenschaft Name verwaltet bei allen Oberflächenobjekten von Windows-Anwendungen den Namen des Objekts. Text verwaltet die Beschriftung (falls eine solche vorhanden ist) oder den textuellen Inhalt von Windows.Forms-Steuerelementen und Formular-Titelleisten.
8
Stellen Sie zur Übung die Eigenschaft Name des Schalters auf btnHello ein. Ich verwende dabei übrigens eine Namenskonvention, bei der btn für Button steht. Diese Konvention und die Gründe dafür erläutere ich im Anhang. Die Eigenschaft Name finden Sie im Eigenschaftenfenster immer ganz oben. Es ist extrem wichtig, dass Sie alle Steuerelemente, Komponenten und das Fenster bzw. Formular benennen, bevor Sie beginnen zu programmieren. ☺ Eine spätere Änderung des Namens führt in größeren Projekten zu viel Arbeit, weil der verwendete Name auch im Quellcode angepasst werden muss. Außerdem wird der Quellcode beim nachträglichen Umbenennen inkonsistent. Der Name von über Visual Studio erzeugten Ereignisbehandlungsmethoden enthält nämlich automatisch den Namen des Objekts, für das das Ereignis gilt. Wenn Sie eine Ereignisbehandlungsmethode erzeugen, bevor Sie ein Steuerelement bzw. das Formular umbenennen, besitzt diese einen unzusammenhängenden Namen.
9
10
HALT
11
Stellen Sie dann noch die Eigenschaft Text des Schalters auf »Klick mich« (oder ähnlich) und des Formulars auf »Hello World« ein.
101
Einführung in die Arbeit mit Visual Studio 2008
Abbildung 2.8: Einstellung der Eigenschaft Text eines ButtonSteuerelements im Eigenschaftenfenster von Visual Studio
Abbildung 2.9: Das Beispiel-Formular mit geänderten Beschriftungen
Verankerung: Automatische Ausrichtung bei einer Größenänderung des Formulars Windows.Forms- und WPF-Steuerelemente sind auf einem Formular bzw. Fenster verankert, standardmäßig an der oberen, linken Ecke. Sie können die Verankerung jedoch auch ändern. Die Verankerung können Sie sich vorstellen wie Pinne, mit denen das Steuerelement an das Formular geheftet ist. Die Standard-Verankerung bewirkt, dass Steuerelemente zwar auch verschoben werden, wenn das Formular verschoben wird, aber nicht verschoben oder in der Größe geändert werden, wenn das Formular in der Größe verändert wird. Sie finden die Verankerung für Windows.Forms-Steuerelemente in der Eigenschaft Anchor. Wenn Sie auf das Eingabefeld dieser Eigenschaft klicken, öffnet sich ein kleines Fenster, in dem Sie die Verankerung komfortabel einstellen können (Abbildung 2.10). Mit der Verankerung können Sie z. B. dafür sorgen, dass ein Steuerelement sich an die Breite des Fensters anpasst, indem Sie die Verankerungen links, oben und rechts setzen. Setzen Sie alle Verankerungen, wird das Steuerelement an die Breite und an die Höhe angepasst. Setzen Sie die Verankerungen unten und rechts, bleibt das Steuerelement immer am unteren rechten Rand des Fensters kleben. Sie können dieses Feature bereits in der Entwicklungsumgebung ausprobieren, indem Sie die Verankerung setzen und dann das Formular in der Größe verändern. Ändern Sie zur Übung die Verankerung des Schalters so, dass dieser oben angeheftet bleibt, aber seine Breite der Fensterbreite anpasst.
102
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel
Abbildung 2.10: Die Eigenschaft Anchor im Eigenschaftenfenster mit der Default-Verankerung (oben, links)
1
2 3
2.5.8
Der Code-Editor
Der Code-Editor erlaubt die Bearbeitungen des Quellcodes einer Klasse (oder eines anderen Typen). Den Code-Editor einer Klasse können Sie auf verschiedene Weise öffnen.
4
Für Klassen, die kein Oberflächenelement (Formular, Fenster, Webseite, eigenes Steuerelement etc.) beschreiben, können Sie einfach auf den Eintrag im Projektmappen-Explorer doppelklicken. In unserem Beispielprojekt ist das der Fall für die Klasse Program.
5
Den Code-Editor von Klassen, die Oberflächenelemente beschreiben, können Sie nicht über einen Doppelklick öffnen. Dieser öffnet schließlich den Designer.
6
Wird gerade der Designer angezeigt, können Sie über (F7) zu der zugehörigen CodeDatei wechseln. Umgekehrt funktioniert dieses Feature auch: In einem Code-Editor einer Klasse, die ein Oberflächenelement beschreibt, führt (F7) dazu, dass der zugehörige Designer angezeigt wird.
7 TIPP
8
Eine weitere Möglichkeit ist, das Element (z. B. das Startformular) im ProjektmappenExplorer zu aktivieren und entweder den Befehl CODE ANZEIGEN in dessen Kontextmenü oder den Code-Anzeigen-Schalter (der, der so aussieht wie ein beschriebenes Blatt Papier) in der Symbolleiste des Projektmappen-Explorers zu betätigen.
9
Und schließlich besitzen Sie noch eine weitere Möglichkeit, nämlich einen Doppelklick auf dem Formular-Designer. Damit gelangen Sie aber zu der Ereignisbehandlungsmethode für das Standard-Ereignis des Objekts, auf das Sie geklickt haben. Wenn Sie z. B. auf dem Formular doppelklicken (was in meinen Seminaren immer wieder passiert ☺), erzeugen Sie im Quellcode eine Methode mit dem Namen _Load. Diese Methode ist für Initialisierungen vorgesehen, die beim Laden des Formulars ausgeführt werden müssen. Wenn Sie einmal auf diese Weise Ereignisbehandlungsmethoden erzeugt haben, die Sie eigentlich nicht benötigen, können Sie diese einfach löschen. Löschen Sie dazu die vier Zeilen von private bis zur schließenden geschweiften Klammer.
10
11
TIPP
103
Einführung in die Arbeit mit Visual Studio 2008
Wenn Sie Ihr Projekt danach kompilieren ((ª) + (STRG) + (B)), wird in der Datei mit der Endung Designer.cs der Fehler gemeldet, dass diese Methode nicht mehr vorhanden ist. Klicken Sie doppelt auf den Fehler in der Fehlerliste und löschen Sie einfach die entsprechende Zeile in dieser Datei ((STRG) + (L)). Der Code-Editor bietet eine Menge Hilfestellungen beim Schreiben von Quellcode. Eine sofort ersichtliche ist die farbliche Hervorhebung von Syntax-Elementen im Quellcode. Eine weitere Hilfestellung zur besseren Übersicht des Quellcodes ist, dass Sie einzelne Bereiche über das Plus- bzw. Minus-Symbol auf der linken Seite auf- und zuklappen können. Dieses Feature hilft mir bei großen Quellcodedateien, besonders in Zusammenhang mit Regionen, die ich in Kapitel 4 erläutere, bei der Übersicht über den Quellcode am meisten. Abbildung 2.11: Der Code-Editor von Visual Studio mit zugeklappten Bereichen
Ein weiteres hilfreiches Feature des Code-Editors ist, dass er den Quellcode automatisch einrückt. Eine Anweisung, die Sie an einer beliebigen Position unterbringen, rückt der Editor meist an die korrekte Position, wenn Sie die Zeile komplettieren und mit dem Eingabecursor verlassen. Falls das einmal nicht automatisch funktioniert, oder wenn Sie einen Quellcode neu formatieren wollen, können Sie über (Strg)+ (K), (Strg) + (F) den aktuell markierten Text auch explizit einrücken. Über (Strg)+ (K), (Strg) + (D) können Sie den gesamten Quellcode einer Datei formatieren. Hilfreich ist auch, dass der Editor Syntaxfehler mit einer geschlängelten Linie unterlegt. Wenn Sie die Maus auf diese Linie bewegen, zeigt ein kleines Fenster eine Beschreibung des Fehlers an. Der Code-Editor besitzt daneben noch viele weitere hilfreiche Features. Eines davon – IntelliSense – stelle ich im nächsten Abschnitt vor. Ein paar weitere wichtige finden Sie in der folgenden Auflistung: ■ ■ ■
104
Mit (STRG)+(Z) können Sie (wie in Windows üblich) die letzten Änderungen rückgängig machen. Mit (STRG)+(-) können Sie zu der Position im Quellcode gehen, die Sie zuvor bearbeitet haben. Das funktioniert auch mehrfach! Mit (STRG)+(ª)+(-) können Sie zur nächsten Position im Quellcode gehen, die Sie bearbeitet haben, nachdem Sie mit (STRG)+(-) zu einer vorherigen gesprungen sind.
Die wichtigen Fenster der IDE, erläutert an einem einfachen Beispiel
■
Über den Befehl ENTHALTENEN ORDNER ÖFFNEN im Kontextmenü des Reiters eines Code-Fensters können Sie den Ordner im Windows Explorer öffnen, der die jeweilige Code-Datei enthält. Ähnliches gilt für den Befehl ORDNER IM WINDOWS-EXPLORER ÖFFNEN im Kontextmenü des Projekt-Eintrags im Projektmappen-Explorer.
Die anderen Features, für die in diesem Kapitel kein Platz bleibt, finden Sie im Menü BEARBEITEN.
2.5.9
1
IntelliSense
Der Code-Editor zeigt beim Schreiben von Quellcode in vielen Fällen eine automatische Hilfe an, die als IntelliSense bezeichnet wird. Wenn Sie z. B. einen Namensraum- oder Klassennamen gefolgt von einem Punkt schreiben, zeigt der Editor in einer Liste alle darin enthaltenen Elemente an. Wenn Sie nach dem Punkt weiterschreiben, markiert IntelliSense den passenden Eintrag in der Liste.
IntelliSense hilft beim Programmieren
Abbildung 2.12: IntelliSense zeigt die Elemente des System-Namensraums an
2 3
4
5
6 Wenn Sie dieses Element übernehmen wollen, müssen Sie den Namen nicht selbst vervollständigen. Sie können einfach die (¢)-Taste betätigen um das selektierte Element zu übernehmen. Zum Selektieren können Sie die Cursortasten verwenden.
7
Sie können aber auch eine andere Technik anwenden, die in der Praxis effizienter ist: Schreiben Sie einfach so weit, bis die Auswahl im IntelliSense-Fenster diejenige ist, die Sie benötigen. Alternativ verwenden Sie die Cursortasten zur Auswahl. Dann schreiben Sie einfach den Text weiter, der dem Namen folgt (also z. B. einen Punkt oder ein Gleichheitszeichen oder eine Klammer). IntelliSense übernimmt den kompletten Text des markierten Eintrags automatisch.
8
9
Wenn Sie das einmal ausprobieren wollen, klicken Sie im Formulardesigner doppelt auf den Schalter, sodass dieser den Code-Editor öffnet und eine Methode btnHello_Click erzeugt (die ausgeführt wird, wenn der Schalter betätigt wird). Schreiben Sie Ihre ersten Anweisungen innerhalb dieser Methode, also innerhalb der geschweiften Klammern. Im ersten Beispiel will ich die Meldung »Hello World« in einer MessageBox ausgeben. Dazu müssen Sie die Show-Methode der MessageBoxKlasse aufrufen.
10
11
Schreiben Sie z. B. »Mes«, wird IntelliSense die Message-Klasse vorschlagen. Da Sie aber die MessageBox-Klasse verwenden wollen, betätigen Sie die (¼)-Taste um diese zu aktivieren. Sie können auch alternativ so lange weiterschreiben, bis die Eingabe dazu führt, dass die MessageBox-Klasse markiert wird. Da Sie nun die Show-Methode der MessageBox-Klasse aufrufen wollen, schreiben Sie einfach ».S« (der Name von aufzurufenden Methoden oder von Eigenschaften, die Sie auswerten oder beschreiben wollen, wird durch einen Punkt vom Klassen- oder Objektnamen getrennt). Da Intelli-
105
Einführung in die Arbeit mit Visual Studio 2008
Sense nun die Show-Methode vorschlägt, können Sie direkt das weiterschreiben, was einem Methodennamen folgt, nämlich eine öffnende Klammer. IntelliSense sollte Ihre Eingabe so vervollständigt haben, wie in Abbildung 2.13. Abbildung 2.13: IntelliSense zeigt die Signaturen einer Methode an
Jetzt sehen Sie auch gleich ein weiteres IntelliSense-Feature, nämlich die Anzeige der Signaturen einer Methode. Wenn Sie den Namen einer Methode schreiben, gefolgt von einer Klammer, zeigt IntelliSense die verschiedenen Möglichkeiten an, diese Methode aufzurufen. Mit den Cursortasten können Sie die verschiedenen Varianten durchgehen. Die Show-Methode der MessageBox-Klasse besitzt z. B. zwölf verschiedene Aufrufvarianten. Sie können diese Methode u. a. mit einer Zeichenkette am ersten Argument aufrufen. Die anderen Varianten sollen hier noch nicht besprochen werden. Sie finden eine Beschreibung der MessageBox-Klasse in Kapitel 8. Vervollständigen Sie Ihre erste Anweisung zum folgenden Quellcode: private void btnHello_Click(object sender, EventArgs e) { MessageBox.Show("Hello World"); }
TIPP
Sie können IntelliSense auch über die Tastenkombination (Strg) + (____) explizit aufrufen. Dies macht immer dann Sinn, wenn das IntelliSense-Fenster nicht automatisch geöffnet wird, oder wenn Sie das Fenster (über die (ESC)-Taste) geschlossen haben und wieder öffnen wollen. Die Syntaxhilfe eines Methodenaufrufs erhalten Sie über die Tastenkombination (ª) + (Strg) + (____).
NEU
Ein weiteres Feature von IntelliSense ist, dass die Fenster transparent geschaltet werden können. Betätigen Sie dazu die (STRG)-Taste, während ein IntelliSense-Fenster geöffnet ist. Das nun transparente Fenster ermöglicht den Blick auf den darunter liegenden Quellcode. Dadurch können Sie mit IntelliSense weiterarbeiten, wenn Sie Informationen aus dem Quellcode benötigen, den das IntelliSense-Fenster verdeckt.
2.5.10 Testen der Beispielanwendung Sie können Ihr Hello-World-Programm testen, indem Sie die (F5)-Taste betätigen. Visual Studio kompiliert das Projekt und startet die Anwendung im Debug-Modus (der das Debuggen erlaubt). Wenn der Programmcode Fehler enthält, zeigt Visual Studio einen Dialog an, der Sie darauf aufmerksam macht. Falls Sie diesen Dialog angezeigt bekommen, schauen Sie in Abschnitt »Kompilieren und Ausführen einer Projektmappe« ab Seite 108 nach, wo ich das Kompilieren und Ausführen von Projekten intensiver behandle.
2.6
Weitere wichtige Fenster der IDE
Die Entwicklungsumgebung enthält noch einige weitere Fenster, die nicht unbedingt bei der Programmierung benötigt werden. Die wichtigsten davon stelle ich im Folgenden vor.
106
Weitere wichtige Fenster der IDE
2.6.1
Die dynamische Hilfe
Das Fenster für die dynamische Hilfe können Sie über den Eintrag DYNAMISCHE HILFE im HILFE-Menü öffnen. Es wird per Voreinstellung unter dem Projektmappen-Explorer geöffnet. Die dynamische Hilfe zeigt Hilfethemen zum aktuellen Kontext an und kann deswegen hilfreich sein. Setzen Sie den Eingabecursor im Quellcode in ein Schlüsselwort oder aktivieren Sie ein Objekt auf einem Designer, zeigt die dynamische Hilfe Verweise zu (in der Regel) passenden Hilfethemen an. Probieren Sie es einfach aus.
1
2.6.2
2
Die Aufgabenliste
Die Aufgabenliste, die Sie über das Menü ANSICHT / ANDERE FENSTER / AUFGABENLISTE öffnen können, ist ein hervorragendes Werkzeug zur Erinnerung an Dinge, die noch zu erledigen sind. Dieser Liste können Sie eigene Aufgaben hinzufügen, indem Sie auf den Eintrag KLICKEN SIE HIER UM EINE NEUE AUFGABE HINZUZUFÜGEN klicken. Benutzer-Aufgaben werden mit dem Projekt gespeichert, stehen also immer dann zur Verfügung, wenn Sie das Projekt öffnen.
3
4
Ein anderes sehr hilfreiches Feature ist, dass die Aufgabenliste automatisch alle Kommentare im Quellcode, die mit »TODO«, »HACK«, »UNDONE« oder mit »UnresolvedMergeConflict« beginnen, als Aufgabe anzeigt. Dazu legen Sie im Quellcode einen C#-Kommentar an, den Sie mit zwei Schrägstrichen beginnen und der eines der Aufgaben-Kommentar-Schlüsselwörter enthält, also z. B:
5
// TODO: Hier fehlt noch die Programmierung
Abbildung 2.14: Die Aufgabenliste von Visual Studio mit einer Kommentar-Aufgabe
6
7
8 Die Aufgabenliste zeigt diese Kommentare dann an, wenn die Datei, die die Kommentare enthält, geöffnet ist und wenn Sie den entsprechenden Eintrag aus der Liste im oberen Bereich des Aufgabenlisten-Fensters auswählen. Ein Doppelklick auf einer solchen Aufgabe markiert den entsprechenden Kommentar im Quellcode. Aufgabenkommentare werden leider nur dann angezeigt, wenn die Datei, in der die Kommentare angebracht sind, geöffnet ist. Ich halte dieses Verhalten für nicht besonders hilfreich. In größeren Projekten ist es durchaus üblich, dass Teile der Programmierung noch nicht fertig gestellt werden oder noch verbessert werden könnten. Wenn Sie diese Stellen nur mit Aufgabenkommentaren versehen, besteht die Gefahr, dass Sie diese nicht mehr finden. Sie können dann natürlich alle Dateien des Projekts öffnen um die Aufgabenkommentare in der Aufgabenliste anzuzeigen. Oder Sie können im Projekt nach dem Text (»TODO«, »HACK« etc.) suchen. Das ist in größeren Projekten aber zu aufwändig. Ich verwende deswegen einen kleinen Trick: Ich versehe Aufgabenkommentare zusätzlich über die Tastenkombination (STRG) + (K) + (H) mit einer Verknüpfung. Verknüpfungen werden ebenfalls in der Aufgabeliste angezeigt,
9
10 HALT
11
107
Einführung in die Arbeit mit Visual Studio 2008
wenn Sie den Verknüpfungen-Eintrag im Aufgabenliste-Filter auswählen. Verknüpfungen haben aber den Vorteil, dass sie auch dann angezeigt werden, wenn die Datei geschlossen ist, in der die Verknüpfung angelegt ist. Aufgabenkommentare werden in Kapitel 3 behandelt. REF
2.6.3
Der Objektbrowser
Der Objektbrowser hilft dabei, die in einem Namensraum bzw. die in einer Assembly enthaltenen Typen und deren Member zu erforschen. Sie öffnen den Objektbrowser über das Menü ANSICHT / ANDERE FENSTER / OBJEKTBROWSER. Abbildung 2.15: Der Objektbrowser zeigt die Deklaration der BinarySearchMethode der ArrayKlasse im SystemNamensraum an
Der Objektbrowser zeigt den Inhalt aller Assemblys an, die im aktuellen Projekt referenziert werden. Beachten Sie, dass Sie viele Elemente im Eintrag mscorlib (das ist die Kernbibliothek von .NET) finden. Mit dem Objektbrowser können Sie also den Inhalt der Assemblys des .NET Framework, aber auch den Inhalt externer Assemblys erforschen. Dabei hilft die kurze Erläuterung zum aktiven Element, die im unteren Bereich des Objektbrowsers in der Regel angezeigt wird.
2.7
Kompilieren und Ausführen einer Projektmappe
Visual Studio ermöglicht das einfache Kompilieren und das Ausführen einer Projektmappe. Beim Kompilieren erzeugt Visual Studio (bzw. der C#-Compiler des .NET Framework, der von Visual Studio aufgerufen wird) für jedes Projekt, das in der Projektmappe enthalten ist, eine Assembly. Beim Ausführen wird die Projektmappe zunächst kompiliert. Danach wird das Projekt der Projektmappe gestartet, das als Startprojekt angegeben ist.
108
Kompilieren und Ausführen einer Projektmappe
2.7.1
Kompilieren einer Projektmappe
Die Projekte einer Projektmappe können Sie in Visual Studio über die Tastenkombination (ª) + (STRG) + (B) kompilieren. In Visual C# 2008 Express können Sie dazu auch die (F6)-Taste verwenden, alternativ dazu den Befehl PROJEKTMAPPE ERSTELLEN im ERSTELLEN Menü. Wenn Sie eine Projektmappe kompilieren oder zum Testen starten, speichert Visual Studio 2008 immer (und zuverlässig) alle geänderten Dateien. Die Express-Editionen machen das prinzipiell auch. Das Problem ist hier nur, dass diese die Dateien so lange in einem temporären Ordner speichern, bis Sie die Projektmappe explizit speichern, wie ich bereits angemerkt habe. Deswegen sollten Sie Projektmappen in den Express-Editionen nach dem Erstellen zunächst einmal von Hand speichern ((ª) + (STRG) + (S)).
1 INFO
2
Beim Kompilieren erstellt Visual Studio (bzw. der C#-Compiler des .NET Framework) lediglich die Assembly der in der Projektmappe enthaltenen Projekte, führt diese aber nicht aus. Das Kompilieren erfolgt dabei so, dass nur die seit dem letzten Kompilieren geänderten Projekte neu kompiliert werden.
3
4 In einigen Fällen merkt Visual Studio allerdings nicht, dass das Projekt geändert wurde. Das ist z. B. unter Umständen der Fall, wenn Sie lediglich eine Ressource ändern, die in ein Projekt eingebunden ist. In diesem Fall können Sie auch das komplette Neuerstellen erzwingen. Dies können Sie über den Befehl PROJEKTMAPPE NEU ERSTELLEN im ERSTELLEN-Menü oder über den Befehl NEU ERSTELLEN im Kontextmenü des Projektmappen-Eintrags im Projektmappen-Explorer erreichen.
HALT
5
6
Wenn beim Kompilieren Fehler gefunden werden, werden diese in die Fehlerliste eingetragen, die Sie im unteren Bereich der IDE finden. Über einen Doppelklick auf einem Eintrag gelangen Sie zu der entsprechenden Stelle im Quellcode. Abbildung 2.16: Visual Studio 2008 zeigt Fehler in der Fehlerliste an
7
8
9
10
11
109
Einführung in die Arbeit mit Visual Studio 2008
2.7.2
Projektmappen ausführen
Wenn Sie Ihr Programm testen wollen, können Sie dieses mit (F5) oder dem Menübefehl STARTEN im DEBUGGEN-Menü erstellen und ausführen. Das Debuggen wird grundsätzlich in diesem Kapitel und ausführlich in Kapitel 9 behandelt. Für den Moment reicht es aus, dass Sie eine Anwendung mit (F5) starten können. Enthält das Programm Fehler, zeigt Visual Studio dies in einem Dialog an (Abbildung 2.17). Abbildung 2.17: Visual Studio meldet beim Ausführen einer Projektmappe, dass diese Fehler enthält
HALT
Sie sollten diesen Dialog immer mit dem Nein-Schalter schließen. Wenn Sie mit Ja schließen, startet Visual Studio eine ggf. zuvor erfolgreich kompilierte (und damit ältere) Version Ihrer Anwendung. Da Sie aber natürlich Ihre aktuelle Version testen wollen, haben Sie gar nichts von diesem »Feature«. Ich empfehle deswegen, dass Sie die Option DIESES DIALOGFELD NICHT MEHR ANZEIGEN wählen und danach den NeinSchalter betätigen. Dann zeigt Visual Studio diesen Dialog nicht mehr an, wenn beim Kompilieren Fehler gemeldet werden, und startet auch nicht eine ggf. vorhandene ältere Version des kompilierten Programms. Diese Einstellung können Sie auch in den Optionen der IDE anpassen (falls Sie sich beim Klicken vertan haben). Wählen Sie dazu den Befehl OPTIONEN im EXTRAS-Menü. In den Optionen finden Sie unter PROJEKTE UND PROJEKTMAPPEN / ERSTELLEN UND AUSFÜHREN die Option BEIM AUSFÜHREN, BEI BUILD- UND BEREITSTELLUNGSFEHLERN, in der Sie das Verhalten bei Fehlern anpassen können.
Ausführen ohne Debuggen Sie können Ihre Projektmappen auch so ausführen, dass der Debugger nicht eingeschaltet wird. Dazu betätigen Sie die Tastenkombination (STRG) + (F5) oder den Befehl STARTEN OHNE DEBUGGEN im DEBUGGEN-Menü. Sinn macht dieses Feature z. B. dann, wenn Sie in Ihrer Anwendung viele Haltepunkte haben, diese aber testen wollen, ohne dass der Debugger an den Haltepunkten anhält. Eine andere Anwendung ist ein Test der Anwendung auf das Verhalten beim Eintreten von unbehandelten Ausnahmen. Bei Konsolenanwendungen führt das Starten ohne Debuggen auch dazu, dass die Konsole am Ende des Programms eine Aufforderung anzeigt, eine Taste zu betätigen, und so geöffnet bleibt. Denken Sie aber daran, dass der Debugger in diesem Fall nicht gestartet wird und Sie deswegen nicht nach Fehler suchen können.
110
Entwicklung einer einfachen Windows-Anwendung
Projektmappenkonfigurationen in Visual Studio 2008 Beim Erstellen der Assembly verwendet Visual Studio 2008 (nicht die Express-Editionen) die aktuelle Projektmappenkonfiguration, die Sie in der Projektmappenkonfigurationsliste der oberen Symbolleiste einstellen können. Per Voreinstellung ist hier die Konfiguration DEBUG eingestellt. In dieser Konfiguration werden (wiederum per Voreinstellung) Debuginformationen für die erzeugte Assembly generiert und der Code wird nicht optimiert. In einer solchen Assembly können Sie (auch ohne Quellcode) mit dem Debugger nach Fehlern suchen.
1
Die andere voreingestellte Konfiguration RELEASE kompiliert die Assembly ohne Debuggerinformationen (und damit ohne Ballast) und mit einer Code-Optimierung.
2
Sie können diese Konfigurationen in Visual Studio 2008 in den Projekteigenschaften einsehen und anpassen (wie ich es im Abschnitt »Projekteigenschaften« ab Seite 127 beschreibe). Wenn Sie mit der Konfiguration DEBUG kompilieren, wird die Assembly per Voreinstellung im Unterordner bin\debug erzeugt. Mit der Konfiguration RELEASE erzeugt Visual Studio die Assembly im Ordner bin\release.
3
Automatische Projektmappenkonfiguration in den Express-Editionen
4
Die Express-Editionen besitzen keine einstellbare Projektmappenkonfiguration. Beim Kompilieren und Ausführen einer Projektmappe gehen diese Editionen so vor, dass beim expliziten Kompilieren ((SHIFT) + (STRG) + (B)) die Release-Version (im einstellbaren Release-Ordner) und beim Ausführen ((F5)) die Debug-Version (im Ordner bin\debug) erzeugt werden.
2.8
5
Entwicklung einer einfachen Windows-Anwendung
6
In den vorhergehenden Abschnitten haben Sie bereits einiges über die Erstellung von (Windows.Forms-)Windows-Anwendungen erfahren. Dieser Abschnitt vertieft Ihr Wissen ein wenig, sodass Sie in der Lage sind, einfache Windows-Anwendungen zu entwickeln. Dabei ist mir klar, dass Sie den Quellcode, den ich hier verwende, noch nicht unbedingt verstehen (es sei denn, Sie kennen C# oder eine ähnliche Programmiersprache bereits). Ich will mit diesem Abschnitt aber erreichen, dass Sie in den nächsten Kapiteln, die die Grundlagen von C# behandeln, bereits mit eigenen kleinen Testanwendungen arbeiten können. Das Erstellen von WPF-Anwendungen wird ab Kapitel 12 vertieft behandelt.
7
8
9
Als Beispiel soll eine Anwendung erstellt werden, in die der Anwender einen Bruttobetrag und einen Steuer-Prozentwert eintragen kann und die den Nettobetrag berechnet (Abbildung 2.18).
10 Abbildung 2.18: Ein kleiner Nettorechner
111
11
Einführung in die Arbeit mit Visual Studio 2008
Diese kleine Anwendung enthält bereits sehr viele Elemente der Anwendungsprogrammierung unter Windows und kann als Beispiel für eine benutzerfreundliche Programmierung dienen ☺.
2.8.1
Das Projekt
Im ersten Schritt erstellen Sie das Projekt. STEPS
1. 2. 3.
4.
Erstellen Sie zunächst ein neues Windows.Forms-Anwendung-Projekt und nennen Sie dieses z. B. NetCalculator. Nennen Sie das Formular Form1.cs z. B. in StartForm.cs um. Bestätigen Sie die Frage, ob Visual Studio auch die Klasse umbenennen soll, mit JA. Speichern Sie die Projektmappe in Visual C# 2008 Express mit (STRG)+ (ª) + (S) um sicherzustellen, dass Ihre Änderungen in dem Ordner gespeichert werden, die Sie dafür vorsehen. Kompilieren Sie die Projektmappe, indem Sie die Tastenkombination (ª) + (STRG) + (B) betätigen, um zu überprüfen, ob bis hierhin alles in Ordnung ist.
2.8.2
STEPS
Im nächsten Schritt designen Sie das Formular des Nettorechners. Ich beschreibe hier einen praxisorientierten Ansatz, der das Kopieren von Steuerelementen über die Zwischenablage beinhaltet. 1. 2. 3. 4. 5. 6. 7. 8. 9.
112
Das Formular
Fügen Sie dem Formular aus der Toolbox ein Label und eine TextBox hinzu. Platzieren Sie die TextBox rechts neben dem Label. Markieren Sie beide Steuerelemente, idem Sie mit der Maus einen Rahmen ziehen, der das Label und die TextBox beinhaltet. Kopieren Sie die markierten Steuerelemente über (STRG) + (C) in die Zwischenablage. Betätigen Sie (STRG) + (V) um die kopierten Steuerelemente einzufügen. Verschieben Sie die eingefügten Steuerelemente mit der Maus an eine Position unterhalb der ersten. Betätigen Sie noch einmal (STRG) + (V) um die kopierten Steuerelemente noch einmal einzufügen und verschieben Sie diese Steuerelemente ebenfalls. Fügen Sie dem Formular aus der Toolbox zwei Button-Steuerelemente hinzu. Platzieren Sie die Button-Steuerelemente an den rechten unteren Rand des Formulars. Stellen Sie die Eigenschaft Name aller Steuerelemente ein. Ich habe dazu die folgenden Namen verwendet: ■ Erstes Label: lblGross ■ Zweites Label: lblTax ■ Drittes Label lblNet ■ Erste TextBox: txtNet ■ Zweite TextBox: txtTax ■ Dritte TextBox: txtGross ■ Erster Button: btnCalculate ■ Zweiter Button: btnClose
Entwicklung einer einfachen Windows-Anwendung
10. Stellen Sie die Eigenschaft Text des Formulars, der Label- und der ButtonSteuerelemente entsprechend Abbildung 2.18 ein. 11. Setzen Sie die Eigenschaft ReadOnly der letzten TextBox (txtNet) auf true um zu verhindern, dass der Benutzer den Text ändern kann.
2.8.3
Die Programmierung in einer ersten Version
Die Programmierung des Nettorechners umfasst zum einen die Reaktion auf eine Betätigung des RECHNEN-Schalters und zum anderen die Reaktion auf die Betätigung des SCHLIESSEN-Schalters.
1
Den entsprechenden Programmcode entwickeln Sie in je einer Ereignisbehandlungsmethode, die mit dem Click-Ereignis des jeweiligen Schalters verknüpft ist. Um diese Methoden zunächst einmal zu erzeugen, klicken Sie im Formular-Designer jeweils doppelt auf die Schalter.
2 3
Die Position einer Ereignismethode innerhalb einer Klasse ist für den Compiler vollkommen unbedeutend. Ob die Methode für den Rechnen-Schalter unter oder über der Methode für den Schließen-Schalter steht, spielt absolut keine Rolle. Wichtig ist nur, dass die C#-Syntax eingehalten wird, z. B., dass Methoden nur innerhalb von Klassen programmiert werden können und dass sie selbst keine Methoden enthalten können (ich schreibe das hier nur, weil ich das alles in meinen Seminaren schon gesehen habe, angehende Entwickler können sehr kreativ sein ☺). Sie können Methoden innerhalb der Klasse problemlos an einen anderen Platz verschieben. Wie Sie die Elemente einer Klasse anordnen, ist aber ganz allein Ihre Sache. In Kapitel 4 behandle ich das Organisieren von Klassenelementen mit Hilfe von Regionen.
INFO
4
5
6 Die Programmierung des SCHLIESSEN-Schalters ist einfach: Sie rufen dazu lediglich die Close-Methode des Formulars auf. Das Formular erreichen Sie über die spezielle Referenz this. Der zum Schließen notwendige Programmcode sieht also folgendermaßen aus:
7
private void btnClose_Click(object sender, EventArgs e) { this.Close(); }
8
Die Programmierung des Click-Ereignisses des RECHNEN-Schalters ist da schon komplizierter. Ich erläutere die Programmierung hier Schritt für Schritt.
9 Wenn Sie über wenig oder keine Programmiererfahrung verfügen, werden Sie den hier beschriebenen Programmcode unter Umständen noch nicht verstehen. Ich möchte aber hier, wo es darum geht, die grundsätzliche Arbeit mit Visual Studio zu erläutern, nicht bereits zu tief auf die Grundlagen eingehen. Die Grundlagen der C#-Programmierung werden ab dem nächsten Kapitel vertieft behandelt.
INFO
10
Für eine Nettoberechnung benötigen Sie natürlich die Eingaben. Sie erreichen diese über die Eigenschaft Text der Textboxen. Um mit den in Textform vorliegenden Eingaben rechnen zu können, müssen Sie diese in numerische Daten konvertieren. C# ist eine typsichere Sprache und lässt mathematische Berechnungen mit Zeichenketten nicht zu. Zur Konvertierung können Sie eine der Methoden der Convert-Klasse verwenden. Die ToDouble-Methode konvertiert z. B. in einen double-Wert (einen Zahlwert mit Nachkommastellen). Ich verwende zur besseren Übersicht Variablen, denen ich die konvertierten Eingaben zuweise. Die Anweisungen, die mit zwei
11
113
Einführung in die Arbeit mit Visual Studio 2008
Schrägstrichen beginnen, sind übrigens Kommentare, die der Erläuterung des Quellcodes dienen und die vom Compiler nicht ausgewertet werden. private void btnCalculate_Click(object sender, EventArgs e) { // Deklaration von Variablen für die Berechnung double gross, tax, net; // Die Eingaben in double-Werte konvertieren // und in die dafür vorgesehenen Variablen schreiben gross = Convert.ToDouble(this.txtGross.Text); tax = Convert.ToDouble(this.txtTax.Text);
Die Berechnung erfolgt dann mit Hilfe der Variablen: // Den Nettowert berechnen net = gross * 100 / (100 + tax);
Bei der Ausgabe müssen Sie wieder konvertieren, dieses Mal in eine Zeichenkette, da die Eigenschaft Text diesen Datentyp besitzt. Zu dieser Konvertierung können Sie einfach die ToString-Methode verwenden, die alle Typen besitzen: // Den berechneten Wert in die Netto-TextBox schreiben this.txtNet.Text = net.ToString(); }
Prinzipiell ist die Berechnung damit fertig. Ihr Programm funktioniert aber nur dann korrekt, wenn der Anwender numerische Daten eingibt. Probieren Sie einmal aus, was passiert, wenn Sie nichtnumerische Daten eingeben und dann versuchen zu rechnen: Beim Versuch, die nichtnumerische Eingabe in einen double-Wert zu konvertieren, tritt eine Ausnahme auf, die im Debugger angezeigt wird. Das Debuggen behandle ich grundlegend ab Seite 122 und vertieft in Kapitel 9. Fürs Erste reicht aus, dass Sie ein im Debugger angehaltenes Programm mit (ª) + (F5) beenden können.
STEPS
Wir werden die Programmierung im weiteren Verlauf verbessern. Für das später behandelte Debugging sollten Sie allerdings die erste Version behalten. Wenn Sie das wollen, gehen Sie folgendermaßen vor, um eine Kopie des Projekts zu erhalten: 1. 2. 3.
4. 5. 6.
Beenden Sie Visual Studio. Wechseln Sie im Windows-Explorer in den Ordner, der Ihre Projekte enthält. Kopieren Sie den Ordner, der das Nettoberechnungs-Projekt enthält, über (STRG) + (C) in die Zwischenablage und fügen Sie den kopierten Ordner über (STRG) + (V) gleich wieder ein. Nennen Sie den ursprünglichen Ordner so um, dass Sie erkennen, dass es die erste Version ist (hängen Sie z. B. » – Version 1« an den Namen). Nennen Sie den neuen Ordner so um, dass Sie erkennen, dass es die zweite Version ist (hängen Sie z. B. » – Version 2« an den Namen). Wechseln Sie in den neuen Ordner und öffnen Sie die Solution-Datei (die mit der Endung .sln) über einen Doppelklick. Damit haben Sie Ihre gesamte Projektmappe kopiert und arbeiten nun in der zweiten Version.
2.8.4
Die Programmierung in einer fehlerfreien und benutzerfreundlichen Version
Für ein korrekt funktionierendes Programm müssen Sie vor der Berechnung zunächst überprüfen, ob die Eingaben vorhanden und numerisch sind. Diese Über-
114
Entwicklung einer einfachen Windows-Anwendung
prüfung ist eine wichtige Grundlage der Windows-Programmierung: In den meisten Fällen müssen Sie erst überprüfen, ob Eingaben den Anforderungen entsprechen, bevor Sie mit diesen Eingaben arbeiten. Die Überprüfung auf numerische Eingaben können Sie über die TryParse-Methode vornehmen, die jeder der Standardtypen von .NET besitzt. TryParse ist etwas schwierig zu verstehen, da diese Methode schon fortgeschrittene Konzepte einsetzt. Wesentlich ist, dass Sie dieser Methode am ersten Argument den zu prüfenden Wert übergeben und am zweiten eine Variable, in die die Methode den konvertierten Wert schreibt, sofern dieser konvertiert werden kann. Außerdem gibt TryParse true zurück, wenn der übergebene Wert konvertiert werden konnte, und false, wenn der Wert nicht konvertiert werden konnte. Das mache ich mir in einer geschachtelten Abfrage zunutze, die zuerst die Brutto- und dann die Steuer-Eingabe überprüft:
1
2
Listing 2.1: Berechnung des Nettobetrags mit Überprüfung der Eingaben
3
private void btnCalculate_Click(object sender, EventArgs e) { // Deklaration von Variablen für die Berechnung double gross, tax, net;
4
// Die Eingaben überprüfen und gleichzeitig in double-Werte // konvertieren und in die dafür vorgesehenen Variablen schreiben if (double.TryParse(this.txtGross.Text, out gross)) { if (double.TryParse(this.txtTax.Text, out tax)) { // Beide Eingaben sind in Ordnung, // also kann gerechnet werden
5
6
// Den Nettowert berechnen net = gross * 100 / (100 + tax); // Den berechneten Wert in die Netto-TextBox schreiben this.txtNet.Text = net.ToString();
7
} } }
Vollständig ist der Programmcode aber noch nicht. Was fehlt, ist eine Meldung an den Anwender für den Fall, dass die Eingaben ungültig sind. Dazu verwende ich die MessageBox-Klasse, die Sie ja bereits kennen gelernt haben. Die Programmierung erfolgt in einem zusätzlichen else-Block, der unter den jeweiligen if-Block angebracht wird. Um dafür zu sorgen, dass der Anwender bei einer Fehleingabe nicht aus Versehen das Ergebnis einer eventuellen vorhergehenden erfolgreichen Berechnung verwendet, lösche ich zusätzlich den Text der Ergebnis-TextBox:
8
Listing 2.2: Komplette Berechnung des Nettobetrags mit Überprüfung der Eingaben und Fehlermeldung
10
private void btnCalculate_Click(object sender, EventArgs e) { // Deklaration von Variablen für die Berechnung double gross, tax, net;
11
9
// Die Eingaben überprüfen und gleichzeitig in double-Werte // konvertieren und in die dafür vorgesehenen Variablen schreiben if (double.TryParse(this.txtGross.Text, out gross)) { if (double.TryParse(this.txtTax.Text, out tax)) { // Beide Eingaben sind in Ordnung, // also kann gerechnet werden
115
Einführung in die Arbeit mit Visual Studio 2008
// Den Nettowert berechnen net = gross * 100 / (100 + tax); // Den berechneten Wert in die Netto-TextBox schreiben this.txtNet.Text = net.ToString(); } else { // Die Steuer-Eingabe ist nicht OK this.txtGross.Text = null; MessageBox.Show("Der Steuerwert ist ungültig"); } } else { // Die Bruttoeingabe ist nicht OK this.txtGross.Text = null; MessageBox.Show("Der Bruttobetrag ist ungültig"); } }
Fertig ist Ihre zweite C#-Anwendung ☺.
EXKURS
Das Programm könnte noch benutzerfreundlicher sein. Idealerweise sollten die TextBoxen gar keine ungültigen Werte zulassen. Sie können das programmieren, aber dies erfordert bereits fortgeschrittenes Wissen. Außerdem sollten Sie in der Praxis alle Überprüfungen am Anfang der Methode vornehmen und die Fehler z. B. in einer string-Variablen sammeln. Wenn Eingabefehler aufgetreten sind, sollte die Berechnung natürlich nicht ausgeführt, aber alle Fehler in einer Meldung ausgegeben werden. In unserem Programm ist das leider nicht der Fall. Wenn der Anwender für den Brutto- und den Steuerwert ungültige Daten eingibt, erhält er zwei Meldungen. Eingabe-Validierung wird intensiver in den Kapiteln 12, 13 und 15 behandelt.
2.8.5
Verwenden von anderen Ereignissen am Beispiel eines Längenumrechners
Das Programm zur Nettoberechnung arbeitet mit dem Click-Ereignis der ButtonSteuerelemente zum Rechnen und Schließen. Den allermeisten Programmen reicht eine Auswertung dieses Ereignisses der enthaltenen Schalter aus. Aber nicht nur Schalter besitzen Ereignisse. Alle Steuerelemente besitzen mehr oder weniger viele Ereignisse, die Sie für Ihre Programme auswerten können. Die wichtigsten Ereignisse werden zwar in den Kapiteln 12, 13 und 15 behandelt. Sie sollen hier aber erfahren, wie Sie prinzipiell mit anderen Ereignissen arbeiten. Abbildung 2.19: Der Längenumrechner
Zur Demonstration dient ein Längenumrechner, der eine Längenangabe von Zentimeter nach Inch (auch: Zoll) und umgekehrt umrechnet. Die Anwendung soll immer
116
Entwicklung einer einfachen Windows-Anwendung
dann, wenn der Anwender in der Zentimeter-TextBox die (¢_)-Taste betätigt, den Inch-Wert berechnen und immer dann, wenn der Anwender in der Inch-TextBox die (¢_)-Taste betätigt, den Zentimeter-Wert.
Grundlegende Arbeiten 1.
2.
3.
4.
Im ersten Schritt erstellen Sie wieder das Projekt. Erstellen Sie dazu ein neues Windows.Forms-Anwendung-Projekt und nennen Sie dieses z. B. LengthCalculator. Nennen Sie das Startformular um und speichern Sie Ihr neues Projekt. Legen Sie dann die benötigten Steuerelemente auf dem Formular an. Ich denke, Sie wissen schon, welche Sie verwenden müssen ☺. Ändern Sie die Beschriftungen des Formulars und der Label entsprechend Abbildung 2.19. Benennen Sie die Steuerelemente mit sinnvollen Namen. Ich habe die folgenden verwendet: ■ Erstes Label: lblCm ■ Zweites Label: lblInch ■ Erste TextBox: txtCm ■ Zweite TextBox: txtInch ■ Button: btnClose Erzeugen Sie eine Ereignisbehandlungsmethode für den SCHLIESSEN-Schalter und programmieren Sie den bereits bekannten Code zum Schließen des Formulars (this.Close();).
1 STEPS
2 3
4
5
Programmierung der Berechnung Die Berechnung soll dann ausgeführt werden, wenn der Anwender in einem der TextBox-Steuerelemente die (¢_)-Taste betätigt. Zur Auswertung von Tastenbetätigungen stellt die TextBox das KeyPress-Ereignis zur Verfügung. Sie müssen für beide TextBox-Steuerelemente eine separate Ereignisbehandlungsmethode für dieses Ereignis erzeugen, um die Betätigung der (¢_)-Taste auswerten zu können.
6
7
Dieses Ereignis ist allerdings nicht das Standard-Ereignis einer TextBox, weswegen Sie die notwendige Ereignisbehandlungsmethode nicht über einen Doppelklick erzeugen können. Sie finden das Ereignis aber im Eigenschaftenfenster. 1. 2. 3.
4.
Klicken Sie zur Erzeugung einer Ereignisbehandlungsmethode zunächst auf die erste TextBox, um diese zu aktivieren. Wechseln Sie zum Eigenschaftenfenster und betätigen Sie den Toolbox-Schalter mit dem Pfeil, um die Ereignisliste anzuzeigen. Suchen Sie in der Ereignisliste nach dem KeyPress-Ereignis. Achten Sie darauf, dass es sich um das richtige Ereignis handelt, und nicht z. B. um KeyDown. Klicken Sie doppelt in das Eingabefeld. Visual Studio erzeugt dann eine Ereignisbehandlungsmethode. Wiederholen Sie die Schritte für die zweite TextBox.
8
9
STEPS
10
11
In den Ereignisbehandlungsmethoden müssen Sie nun zunächst auswerten, ob die (¢_)-Taste betätigt wurde. Das Ereignis liefert dazu das Argument e mit, dessen Eigenschaft KeyChar Informationen über die betätigte Taste beinhaltet. Der hier zurückgegebene Wert ist ein Zeichen, das Sie mit '\r' (Escape-Zeichen für (¢)) vergleichen können. Die Grundlage der Methoden sieht dann folgendermaßen aus:
117
Einführung in die Arbeit mit Visual Studio 2008
private void txtCm_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '\r') { } } private void txtInch_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '\r') { } }
Die Programmierung ist relativ einfach. Die verwendeten Techniken entsprechen denen, die Sie bereits beim Nettoberechnungs-Programm verwendet haben. Beachten Sie die Kommentare, die einiges erläutern: Listing 2.3: Die komplette Berechnung des Längenumrechners /* Rechnet die eingegebenen cm in Inch um, wenn die Return-Taste betätigt wurde */ private void txtCm_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '\r') { // Variable für die Eingabe und das Ergebnis double cm, inch; // Die Eingabe überprüfen und gleichzeitig in // einen double-Wert konvertieren if (double.TryParse(this.txtCm.Text, out cm)) { // Das Ergebnis berechnen und ausgeben inch = cm / 2.54; this.txtInch.Text = inch.ToString(); } else { // Die Eingabe kann nicht konvertiert werden: // Ergebnis-TextBox leeren und Meldung ausgeben this.txtInch.Text = null; MessageBox.Show("Die eingegebenen cm sind nicht gültig"); } } } /* Rechnet die eingegebenen Inch in cm um, wenn die Return-Taste betätigt wurde */ private void txtInch_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '\r') { // Variable für die Eingabe und das Ergebnis double inch, cm; // Die Eingabe überprüfen und gleichzeitig in // einen double-Wert konvertieren if (double.TryParse(this.txtInch.Text, out inch)) { // Das Ergebnis berechnen und ausgeben cm = inch * 2.54; this.txtCm.Text = cm.ToString();
118
Entwicklung einer Konsolenanwendung
} else { // Die Eingabe kann nicht konvertiert werden: // Ergebnis-TextBox leeren und Meldung ausgeben this.txtCm.Text = null; MessageBox.Show("Die eingegebenen Inch sind nicht gültig"); } }
1
}
2.9
Entwicklung einer Konsolenanwendung
2
Konsolenanwendungen werden in der Praxis eigentlich nur noch selten eingesetzt. Für Lernzwecke sind Konsolenanwendungen jedoch häufig sehr gut geeignet, weil sich die Programmierung auf das Wesentliche konzentriert. Wenn Sie z. B. die Features einer Programmiersprache ausprobieren wollen, ist eine Windows-Anwendung in der Regel zu aufwändig.
3
Konsolenanwendungen sind einfach, da sie keine eigene Oberfläche besitzen. Ausgaben werden als Zeichenkette an der Konsole ausgegeben, Eingaben werden ebenfalls als Zeichenkette eingelesen.
4
Abbildung 2.20 zeigt das Nettoberechnungsprogramm als Konsolenanwendung. Abbildung 2.20: Das Nettoberechnungsprogramm als Konsolenanwendung unter Windows Vista
Eine Konsolenanwendung erstellen Sie über ein Konsolenanwendung-Projekt. Konsolenanwendungs-Projekte besitzen per Voreinstellung eine Start-Klasse mit Namen Program. Diese enthält (wie auch die Start-Klasse bei Windows.Forms-Anwendungen) eine Main-Methode. Diese Methode wird beim Start der Anwendung aufgerufen.
6
7
8
In der Main-Methode programmieren Sie ganz klassisch. Sie können Eingaben entgegennehmen, mit den eingegebenen Daten arbeiten und Ergebnisse ausgeben (was ja als EVA-Prinzip bezeichnet wird – Eingabe, Verarbeitung, Ausgabe). Nachdem die letzte Anweisung der Main-Methode ausgeführt wurde, ist das Programm automatisch beendet.
2.9.1
5
9
Programmierung an der Konsole
Zur Arbeit mit der Konsole verwenden Sie die Console-Klasse. Diese Klasse erlaubt eine vielfältige Arbeit mit der Konsole, so können Sie z. B. ausgegebene Texte farblich hervorheben. Ich beschreibe hier allerdings nur die wichtigsten Methoden dieser Klasse.
10
Ausgaben können Sie über die (statischen) Methoden Write und WriteLine vornehmen. Beiden können Sie verschiedene Datentypen übergeben, unter anderem natürlich auch Zeichenketten. Der Unterschied zwischen Write und WriteLine ist, dass Write nur die übergebenen Daten schreibt, und WriteLine zusätzlich noch einen Zeilenvorschub anhängt.
11
119
Einführung in die Arbeit mit Visual Studio 2008
Eingaben können Sie über die ReadLine-Methode einlesen. Wenn Sie ReadLine aufrufen, wartet die Konsole, bis der Anwender die (¢_)-Taste betätigt. Den Text, den er bis dahin eingegeben hat, gibt die ReadLine-Methode dann zurück. Das reicht im Prinzip aus, um Konsolenanwendungen schreiben zu können. Wenn Sie weitere Features benötigen, lesen Sie die bitte in der Hilfe der Console-Klasse nach. Ich setze in mein Beispiel allerdings noch ein weiteres Feature ein, nämlich die Möglichkeit, den Titel der Konsole zu beeinflussen. Dazu setzen Sie einfach die TitleEigenschaft. Die Programmierung an der Konsole ist eine andere als eine Programmierung in einer Windows-Anwendung, besonders was die Eingabe-Validierung betrifft. Schließlich wird der Anwender ja aufgefordert, eine Eingabe zu tätigen. Ist die Eingabe ungültig, sollte das Programm diesen Umstand melden, und er muss die Eingabe wiederholen. Um dies zu erreichen, benötigen Sie für jede Eingabe eine Schleife, die so lange läuft, bis die Eingabe in Ordnung ist. Am Beispiel des Nettorechners können Sie sehen, was ich damit meine. Listing 2.4: Der Nettorechner als Konsolenanwendung static void Main(string[] args) { // Den Titel der Konsole setzen Console.Title = "Nettoberechnung"; // Variablen für die eingegebenen Werte und das Ergebnis double gross, tax, net; // Variable, die angibt, ob die Eingabe in Ordnung war bool inputIsOk; // Hier wird eine Schleife begonnen, in der der Anwender // aufgefordert wird, den Bruttobetrag einzugeben do { // Den Anwender auffordern, den Bruttobetrag einzugeben Console.Write("Geben Sie den Bruttobetrag ein: "); // Auf Return warten und die Eingabe einlesen string input = Console.ReadLine(); // Überprüfen, ob die Eingabe in einen double-Wert // konvertiert werden kann inputIsOk = double.TryParse(input, out gross); // Auswerten des Ergebnisses von TryParse if (inputIsOk == false) { // Dem Anwender mitteilen, dass die Eingabe nicht // in Ordnung war Console.WriteLine("Ihre Eingabe war ungültig."); } } while (inputIsOk == false);
// Diese zweite Schleife ist nahezu identisch zu der ersten, // mit dem Unterschied, dass hier der Steuerwert eingegeben wird do {
120
Entwicklung einer Konsolenanwendung
// Den Anwender auffordern, den Steuerwert einzugeben Console.Write("Geben Sie den Steuerwert ein: "); // Auf Return warten und die Eingabe einlesen string input = Console.ReadLine(); // Überprüfen, ob die Eingabe in einen double-Wert // konvertiert werden kann inputIsOk = double.TryParse(input, out tax);
1
// Auswerten des Ergebnisses von TryParse if (inputIsOk == false) { // Dem Anwender mitteilen, dass die Eingabe nicht // in Ordnung war Console.WriteLine("Ihre Eingabe war ungültig."); } } while (inputIsOk == false);
2 3
// Das Ergebnis berechnen net = gross * 100 / (100 + tax);
4
// Das Ergebnis ausgeben Console.WriteLine("Nettobetrag: " + net); }
Mir ist übrigens bewusst, dass ein C#-Anfänger mit diesem Quellcode Probleme hat. Schließlich werden hier schon Sprachkonstrukte verwendet, die erst in den nächsten Kapiteln behandelt werden. Aber ich wollte eine Konsolenanwendung zeigen, deren grundsätzliche Aufgabe bereits bekannt ist und die einen praxisorientierten Aufbau besitzt. Vielleicht hilft die folgende Erläuterung des Quellcodes:
5
6
Das Beispiel arbeitet mit zwei Schleifen, die jeweils mit do begonnen werden. In der ersten Schleife wird der Anwender aufgefordert, den Bruttobetrag einzugeben. Die zweite Schleife ist nahezu identisch zu der ersten, der Unterschied ist, dass in dieser Schleife der Steuerwert eingegeben wird.
7
Innerhalb der Schleifen wird der Anwender zunächst über die Write-Methode aufgefordert, seine Eingabe zu tätigen. Ich verwende deswegen Write (und nicht WriteLine), damit der Eingabecursor an der Konsole direkt hinter dem ausgegebenen Text steht.
8
Danach liest das Programm über die ReadLine-Methode die Eingabe des Anwenders in die Variable input, die als string deklariert ist und deswegen Zeichenketten speichern kann.
9
Über die bereits bekannte TryParse-Methode des double-Typs wird überprüft, ob die Eingabe in einen double-Wert konvertiert werden kann. Das Ergebnis dieser Überprüfung wird in die Variable inputIsOk geschrieben, die als bool deklariert ist und deswegen die booleschen Werte true und false speichern kann.
10
Im nächsten Schritt wird überprüft, ob die inputIsOk-Variable den Wert false speichert, also ob die Überprüfung der Eingabe einen ungültigen Wert ergab. Ist das der Fall, wird eine entsprechende Meldung ausgegeben.
11
Die Überprüfung der Variablen inputIsOk im Schleifenfuß bewirkt, dass die Schleife so lange ausgeführt wird, wie die Eingabe nicht in Ordnung ist. Erst wenn inputIsOk true speichert, wird die Schleife beendet.
121
Einführung in die Arbeit mit Visual Studio 2008
Unterhalb der Schleifen wird dann nur noch der Nettowert berechnet und schließlich ausgegeben. Abbildung 2.21 zeigt das Programm mit einigen fehlerhaften Eingaben. Abbildung 2.21: Das Nettoberechnungsprogramm mit einigen fehlerhaften Eingaben
2.9.2
Testen von Konsolenanwendungen
Konsolenanwendungen können Sie in Visual Studio prinzipiell wie WindowsAnwendungen testen, indem Sie (F5) betätigen. In den meisten Konsolenanwendungen führt das aber zu dem Problem, dass die Anwendung nach der letzten Anweisung beendet wird. Da es sich dabei meist um die Ausgabe des Ergebnisses handelt, sehen Sie dieses nur sehr kurz, bevor Visual Studio das Debuggen der Anwendung beendet. Sie können dieses Problem dadurch lösen, dass Sie Konsolenanwendungen mit (STRG) + (F5) starten. Damit starten Sie die Anwendung zwar ohne Debugger-Unterstützung (weswegen der Debugger dann bei Ausnahmen und an Haltepunkten nicht anhält, siehe bei »Grundlagen zum Debuggen« ab Seite 122), dafür zeigt die Konsole am Ende die Aufforderung an, eine beliebige Taste zu betätigen, um das Programm zu beenden. Eine andere Möglichkeit, die ich häufig nutze, ist, dass Sie eine ähnliche Aufforderung selbst in die Konsolenanwendung integrieren. Dazu fügen Sie die folgenden Anweisungen am Ende der Main-Methode ein: Console.WriteLine("Betätigen Sie die Return-Taste " + "um das Programm zu beenden"); Console.ReadLine();
Der Aufruf von ReadLine führt dazu, dass das Programm wartet, bis der Anwender die (¢_)-Taste betätigt.
2.10
Grundlagen zum Debuggen
Debuggen bedeutet, ein Programm von Fehlern zu befreien. Das Debuggen ist ein wichtiger Teil der C#-Programmierung, der in Kapitel 9 ausführlich behandelt wird. Sie sollten jedoch bereits bei Ihren ersten Anwendungen in der Lage sein, Fehler zu beseitigen. Deswegen schreibe ich hier kurz einige Grundlagen des Debuggens.
122
Grundlagen zum Debuggen
2.10.1
Kompilierfehler beseitigen
Wenn Sie mit Visual Studio eine Projektmappe kompilieren und diese enthält Kompilierfehler, werden die Fehler in der Fehlerliste angezeigt. Um die Fehlerstelle zu lokalisieren, können Sie einfach doppelt auf den entsprechenden Eintrag in der Fehlerliste klicken. Visual Studio öffnet den Code-Editor für die entsprechende Datei und selektiert die Zeile, in der der Fehler aufgetreten ist.
1
Starten Sie eine Projektmappe zum Testen mit (F5), wird diese vor dem Starten ebenfalls kompiliert. Treten dabei Fehler auf, meldet Visual Studio diesen Umstand per Voreinstellung in einem Dialog, wie ich es bereits im Abschnitt »Kompilieren und Ausführen einer Projektmappe« (Seite 108) beschrieben habe. Sie sollten diesen Dialog nie mit JA bestätigen, da Visual Studio ansonsten das Programm startet, das beim letzten Erstellen fehlerfrei erstellt werden konnte.
2
Ansonsten ist das Beseitigen von Kompilierfehlern Ihre Sache. Korrigieren Sie einfach den Quellcode ☺.
2.10.2
3
Ausnahmen debuggen 4
Ausnahmen treten in einer .NET-Anwendung immer dann auf, wenn eine Methode einer Klasse nicht korrekt ausgeführt werden kann. In der Nettorechner-Beispielanwendung aus diesem Kapitel war dies in der ersten Version (der die Überprüfung der Eingabe fehlte) der Fall, wenn eine der Eingaben nicht in eine Zahl konvertiert werden konnte.
5
Tritt eine unbehandelte Ausnahme ein, hält Visual Studio das Programm an und zeigt Informationen zur aufgetretenen Ausnahme an. Abbildung 2.22 zeigt Visual Studio beim Testen der ersten Version des Nettorechners, nachdem ungültige Werte eingegeben und der RECHNEN-Schalter betätigt wurden.
6
Abbildung 2.22: Visual Studio zeigt eine Ausnahme an
7
8
9
10
11
Der Link DETAILS ANZEIGEN ist bei solchen Ausnahmen in vielen Fällen hilfreich, da Sie damit mehr Informationen über die Ausnahme erhalten. Für einfache Ausnahmen enthalten die Details aber meist nicht mehr Informationen, als Visual Studio sowieso bereits anzeigt.
123
Einführung in die Arbeit mit Visual Studio 2008
Wenn Sie das Ausnahme-Fenster schließen (über (ESC) oder das kleine Kreuz rechts oben), bleibt Ihr Programm angehalten. Wenn Sie versuchen, das Programm weiter auszuführen (mit (F5)), wird die Ausnahme wieder gemeldet, da Sie die Ursache ja noch nicht beseitigt haben. Visual Studio ermöglich aber, in einem angehaltenen Programm Änderungen vorzunehmen. Dieses Feature, das als Bearbeiten und Fortfahren (Edit and Continue) bezeichnet wird, ist sehr hilfreich beim Testen eines Programms, das bei der Ausführung Ausnahmen erzeugt. Schließen Sie das Ausnahme-Fenster und ändern Sie den Quellcode so ab, dass die Ausnahme nicht mehr auftreten kann. In unserem Fall wäre eine Änderung entsprechend der zweiten Version des Nettorechners angebracht. Der Quellcode muss dabei natürlich fehlerfrei sein. Beim Ändern des Quellcodes haben Sie u. U. die aktuelle Zeile, die Visual Studio gelb hinterlegt hat, gelöscht oder verschoben. In der Regel wollen Sie Ihre neuen Änderungen aber gleich ausprobieren. Dazu können Sie die aktuelle Anweisung, an der der Debugger das Programm weiter ausführt, sehr einfach neu definieren. Setzen Sie dafür den Eingabecursor in die als Nächstes auszuführende Zeile und betätigen Sie (ª) + (STRG) + (F10), oder klicken Sie mit der rechten Maustaste auf die Zeile, die Sie als aktuelle Zeile setzen wollen, und wählen Sie den Befehl NÄCHSTE ANWEISUNG FESTLEGEN im Kontextmenü. Ist Ihr neuer Quellcode fehlerfrei und kann deswegen kompiliert werden, setzt Visual Studio die aktuelle Anweisung auf die Zeile, die Sie als nächste auszuführende Zeile festgelegt haben (Abbildung 2.23). Abbildung 2.23: Das fehlerhafte Programm wurde geändert und die aktuelle Anweisung an den Anfang des neuen Programms gesetzt
124
Grundlagen zum Debuggen
Wenn Sie Ihr Programm dann weiter ausführen wollen, können Sie einfach die (F5)Taste betätigen. Sie können aber auch, wie im folgenden Abschnitt beschrieben, das Programm mit (F10) zeilenweise ausführen um Ihre Änderungen zu testen.
2.10.3 Debuggen logischer Fehler Logische Fehler sind Fehler, die nicht zu einer Ausnahme, aber zu einem falschen Verhalten der Anwendung führen. Ein logischer Fehler des Nettorechners kann z. B. sein, wenn dieser den Nettobetrag nicht korrekt berechnet.
1
Logische Fehler finden Sie (hoffentlich ☺), indem Sie an der Stelle im Programmcode, an der Sie den Fehler vermuten, einen Haltepunkt setzen. Klicken Sie dazu einfach auf den linken grauen Rand an der Zeile, an der Sie anhalten wollen. Visual Studio markiert die Zeile rot.
2 Abbildung 2.24: Ein im Quelltext gesetzter Haltepunkt
3
4
5
6
7
Wenn Sie das Programm dann ausführen und es erreicht die Stelle, an der der Haltepunkt gesetzt ist, hält Visual Studio das Programm an und Sie können debuggen.
8
Ein wichtiger Teil des Debuggens ist, den aktuellen Inhalt von Variablen oder Eigenschaften auszulesen. Dazu können Sie einfach die Maus auf die Variable bzw. Eigenschaft setzen. Visual Studio zeigt den Inhalt in einem kleinen gelben Fenster an. Beachten Sie, dass bei Zuweisungen der Inhalt der Variable, an die zugewiesen wird, erst nach der Ausführung der Anweisung überschrieben wurde.
9
10
Andere Möglichkeiten sind das Auto-, das Lokal- und das SchnellüberwachungsFenster. Diese Fenster erläutere ich in Kapitel 9. Ein an einem Haltepunkt angehaltenes Programm können Sie genauso ändern wie eines, das an einer Ausnahme angehalten wurde, wie ich es im vorigen Abschnitt bereits beschrieben habe. Außerdem können Sie das Programm schrittweise über die (F10)-Taste ausführen um dessen Verlauf zu überprüfen. Mit (F5) führen Sie das Programm ab der aktuellen Anweisung weiter aus. Über die Tastenkombination (ª) + (F5) können Sie die Ausführung beenden.
11
125
Einführung in die Arbeit mit Visual Studio 2008
Die Tastenkombinationen finden Sie noch einmal in der Datei Tastenkombinationen.pdf auf der Buch-DVD und im Referenzteil dieses Buchs. DISC
2.11
Grundlagen zum Verteilen einer Anwendung
Das »richtige« Verteilen einer Anwendung wird eigentlich erst in Kapitel 16 behandelt. Sie haben aber auch die Möglichkeit, die kompilierten Dateien einfach auf einen anderen Rechner zu kopieren. Dazu wird die Anwendung normalerweise zunächst in der Release-Konfiguration kompiliert, damit die Debug-Informationen nicht erzeugt werden und das Programm vom Compiler optimiert wird. In Visual Studio 2008 stellen Sie die Konfiguration auf Release um, indem Sie diese Konfiguration aus der Konfigurationsliste in der oberen Symbolleiste einstellen. In den Express-Editionen müssen Sie nichts voreinstellen, da diese sowieso beim Kompilieren immer die Release-Version erzeugen. Kompilieren Sie die Projektmappe dann über (ª) + (STRG) + (B)). Vergessen Sie nicht, die aktuelle Konfiguration wieder auf DEBUG zurückzusetzen. Die kompilierten Dateien finden Sie nun im Projektordner im Unterordner bin\Release (sofern Sie diesen nicht in den Projekteigenschaften angepasst haben). Wenn Sie alle hier enthaltenen Dateien auf einen Rechner kopieren, der das .NET Framework in der benötigten Version installiert hat, kann das Programm auf dem anderen Rechner ausgeführt werden.
2.12
Optionen der Entwicklungsumgebung
Die Optionen der Entwicklungsumgebung erreichen Sie über das Menü EXTRAS / OPTIONEN. Visual Studio 2008 bietet eine Vielzahl an Optionen. Die Optionen der Express-Editionen sind allerdings eingeschränkt. Wichtige Optionen finden Sie im Eintrag TEXT-EDITOR. Hier können Sie u. a. die Tabulatorgröße für einzelne oder auch für alle Sprachen definieren. Abbildung 2.25: Die Optionen von Visual Studio 2008 mit der wichtigen Tabstopps-Option für C#
126
Projekteigenschaften
In den Express-Editionen müssen Sie die Option ALLE EINSTELLUNGEN ANZEIGEN einschalten, damit die Tabstopps- und andere wichtige Einstellungen verfügbar sind. INFO
Interessant ist auch die Option SPEICHERORT DER VISUAL STUDIO-PROJEKTE, die Sie im Ordner ALLGEMEIN unter dem Eintrag PROJEKTE UND PROJEKTMAPPEN finden. Hier können Sie den Ordner einstellen, den Visual Studio für neue Projekte als Standardordner anbietet.
1
Die Voreinstellungen der anderen Optionen haben sich in meiner Praxis als gut erwiesen, weswegen ich diese hier nicht weiter aufführe.
2.13
2
Projekteigenschaften
Jedes Projekt besitzt eigene Eigenschaften, die Sie über den Befehl -EIGENSCHAFTEN im PROJEKT-Menü erreichen. Alternativ können Sie die Eigenschaften auch über das Kontextmenü des Projekt-Eintrags im ProjektmappenExplorer öffnen.
3
Abbildung 2.26: Die Projekteigenschaften eines Visual-StudioProjekts
4
5
6
7
8
9
Im Register ANWENDUNG können Sie den Namen der zu kompilierenden Assembly angeben (ohne Dateiendung). Der Name kann auch Sonderzeichen und Leerzeichen enthalten. Neben dem Standard-Namensraum können Sie noch den Ausgabetyp des Projekts ändern. So können Sie z. B. eine Windows-Anwendung zu einer Konsolenanwendung machen (deren Programm Sie dann aber noch entsprechend anpassen müssen). Wichtig ist auch noch die Einstellung SYMBOL, die Sie mit einem Icon belegen können, das Windows dann für die Anwendung anzeigt, wenn Sie diese irgendwo im System ablegen oder referenzieren.
10
11
Das Register ERSTELLEN enthält Informationen zum Kompilieren. In Visual Studio 2008 (nicht in den Express-Editionen) können Sie in diesem Register oben die Konfiguration auswählen, deren Einstellungen Sie ändern wollen. Per Voreinstellung
127
Einführung in die Arbeit mit Visual Studio 2008
stehen Ihnen die Konfigurationen Debug und Release zur Verfügung. Über den Konfigurations-Manager können Sie auch weitere Konfigurationen hinzufügen (sofern Sie dieses überhaupt benötigen). Die einzig wirklich wichtige Einstellung in diesem Register ist der AUSGABEPFAD. In der Praxis wird dieser häufig für die Release-Konfiguration auf einen Ordner gelegt, in dem alle Releases von Anwendungen verwaltet werden (sofern zur Release-Erstellung kein externes Build-Tool verwendet wird, was in Firmen auf jeden Fall zu empfehlen ist). Die weiteren Register sind im Moment nicht wichtig und werden in diesem Buch teilweise dann beschrieben, wenn ein Feature die Änderung der Projekteigenschaften erfordert.
2.14
Weitere Features von Visual Studio
Visual Studio besitzt noch eine Vielzahl weiterer Features, die ich in diesem Buch leider nur teilweise ansprechen kann. Hier sind die in meinen Augen wichtigsten. Beachten Sie auch hier, dass Sie die genannten Tastenkombinationen in der Datei Tastenkombinationen.pdf auf der Buch-DVD und im Referenzteil dieses Buchs finden. DISC
2.14.1
Lesezeichen
Lesezeichen haben eine ähnliche Funktion wie die, die Sie in Büchern verwenden: Sie markieren damit Stellen im Programmcode, zu denen Sie dann relativ einfach springen können. Über die Tastenkombination (STRG) + (K), (STRG) + (K) (also: (STRG) betätigen und festhalten, (K) betätigen und loslassen und noch einmal (K) betätigen und loslassen, dann (STRG) loslassen) setzen Sie ein Lesezeichen bzw. löschen ein bereits gesetztes. Mit (STRG) + (K), (STRG) + (N) (»N« steht für »Next«) springen Sie zum nächsten Lesezeichen, (STRG) + (K), (STRG) + (P) (»P« steht für »Previous«) führt Sie zum vorherigen Lesezeichen. Lesezeichen werden dabei projektübergreifend verwaltet. In Visual Studio 2008 (nicht in den Express-Editionen) können Sie über (STRG) + (K), (STRG) + (W) das Lesezeichen-Fenster anzeigen, über das Sie die Lesezeichen verwalten und zu den Lesezeichen springen können. Das Lesezeichen-Fenster ermöglicht zudem, Lesezeichen in Lesezeichen-Ordnern zu organisieren. Neue Ordner können Sie im Lesezeichenfenster oder über (STRG) + (K), (STRG) + (F) erzeugen. Sinn macht das bei zusammengehörigen Lesezeichen, wenn Sie zunächst über das Lesezeichen-Fenster zu einem Lesezeichen und dann mit (STRG) + (ª) + (K), (STRG) + (ª) + (N) zum nächsten und mit (STRG) + (ª) + (K), (STRG) + (ª) + (P) zum vorherigen Lesezeichen springen, das im selben Ordner angelegt ist wie das aktuelle Lesezeichen.
2.14.2
Quellcode-Verknüpfungen
Quellcode-Verknüpfungen haben eine ähnliche Bedeutung wie Lesezeichen. Sie verweisen auf Stellen im Quellcode. Eine Quellcode-Verknüpfung können Sie über die Tastenkombination (STRG) + (K), (STRG) + (H) anlegen bzw. löschen. QuellcodeVerknüpfungen werden in der Aufgabenliste angezeigt, wenn Sie den Filter auf VERKNÜPFUNGEN einstellen.
128
Weitere Features von Visual Studio
Sie können den angezeigten Text in der Aufgabenliste ändern, indem Sie zweimal kurz hintereinander auf den zu ändernden Eintrag klicken. TIPP
2.14.3
Codeausschnitte
Codeausschnitte (Code-Snippets) sind Code-Schnipsel für häufig verwendete Programmcodes, die Sie sehr einfach in einen Programmcode übernehmen können. Codeausschnitte werden im IntelliSense-Fenster angezeigt, das sich automatisch öffnet, wenn Sie eine Anweisung beginnen. Schreiben Sie z. B. »mbox«, wird der Codeausschnitt mbox im IntelliSense-Fenster markiert. Dieser Codeausschnitt fügt den Aufruf der Show-Methode der MessageBox-Klasse in den Code ein.
2
Zum Einfügen eines Codeausschnitts müssen Sie zweimal hintereinander die (ÿ)Taste betätigen. Wenn Sie, wie im IntelliSense-Fenster ansonsten üblich, die (¢)Taste betätigen, fügen Sie nur den Namen des Codeausschnitts ein.
3
1
Eine Übersicht über die vorhandenen Codeausschnitte erhalten Sie über den Codeausschnitt-Manager, über den Sie Codeausschnitte verwalten können. Den Codeausschnitt-Manager können Sie über das (Extras)-Menü öffnen.
4 Abbildung 2.27: Der CodeausschnittManager
5
6
7
8
9
Achten Sie darauf, dass Sie in der oberen Liste VISUAL C# als Programmiersprache auswählen. In der Codeausschnitt-Liste werden die Codeausschnitte in Ordnern verwaltet, die eine unterschiedliche Bedeutung besitzen. Die Ordner korrespondieren mit Ordner im Dateisystem. Der jeweilige Pfad wird unter SPEICHERORT angegeben. Codeausschnitte sind spezielle XML-Dateien mit der Endung .cr, die in diesen Ordnern gespeichert sind.
10
11
Sie können die vorhandenen Codeausschnitte auch selbst erweitern. Leider stellt Visual Studio dazu keinen eigenen Editor zur Verfügung. Für eigene Codeausschnitte gehen Sie vielleicht so vor, dass Sie vorhandene in einen eigenen Ordner kopieren und entsprechend anpassen. Diesen Ordner können Sie dann über den HINZUFÜGEN-Schalter (nicht über IMPORTIEREN!) dem Codeausschnitt-Manager hinzufügen.
129
Einführung in die Arbeit mit Visual Studio 2008
DISC
Auf der Buch-DVD finden Sie den Ordner Kompendium-Code-Snippets, in dem ich einige selbst entwickelte, hilfreiche Codeausschnitte platziert habe. Unter anderem finden Sie dort Snippets für das Einfügen einer Standard-MessageBox (mit Informations-, Warnungs-, Frage- und Fehler-Icon) und einen Codeausschnitt zum Einfügen von Console.ReadLine(). Probieren Sie diese Codeausschnitte einfach aus.
2.14.4 Codeausschnitte in der Toolbox Auch die Toolbox kann Codeausschnitte verwalten. Der einfachste Weg zum Hinzufügen eines Codeausschnitts ist, diesen im Code-Editor zu markieren und auf die Toolbox zu ziehen. Sie können dazu das Register ALLGEMEIN verwenden oder ein neues Register in der Toolbox erzeugen (über deren Kontextmenü). Ein anderer Weg zum Hinzufügen von Codeausschnitten ist das Kopieren in die Zwischenablage und Einfügen über das Kontextmenü der Toolbox. Die hinzugefügten Codeausschnitte können Sie über das Toolbox-Kontextmenü umbenennen und natürlich auch wieder löschen. Wenn Sie einen Codeausschnitt verwenden wollen, klicken Sie einfach doppelt auf den entsprechenden Eintrag, ziehen Sie diesen in ein Code-Fenster oder kopieren ihn über die Zwischenablage.
2.14.5 Refactoring Als Refactoring wird ein Prozess bezeichnet, bei dem die Struktur eines Programms verbessert wird, um dessen Lesbarkeit, Wartbarkeit und Erweiterbarkeit zu verbessern. Dabei werden häufig Bezeichner umbenannt, redundanter Quellcode in Methoden ausgelagert, zu große Methoden in mehrere kleine aufgeteilt, fehlerhafter Quellcode korrigiert und unsinniger Quellcode entfernt. Der englische Begriff »Refactoring« ist übrigens schwer zu übersetzen. Die häufig verwendeten deutschen Begriffe »Refaktorisierung« und »Refaktorierung« sind eher unzutreffend. Visual Studio verwendet den Begriff »Umgestaltung«, aber der ist in meine Augen zu allgemein. Visual Studio unterstützt das Refactoring in vielen Bereichen. Da im Buch nicht allzu viel Platz bleibt, beschreibe ich die Refactoring-Features nur in Kurzform. Sie finden die meisten Refactoring-Befehle im UMGESTALTEN-Menü und im UMGESTALTEN-Untermenü des Kontextmenüs des Code-Editors.
TIPP
Ein Tipp zum Merken der Tastenkombinationen: Die Tastenkombinationen zum Refactoring beginnen in der Regel mit (STRG) + (R), wobei das »R« für »Refactoring« steht (außer beim Umschließen von Anweisungen mit Block-Anweisungen). Danach folgen wieder (STRG) und eine Taste, mit deren Buchstabe der englische Begriff für das Refactoring-Feature beginnt. »R« steht z. B. für »Rename«, »M« für »Method Extraction«. ■
130
Umbenennen von Bezeichnern: Wenn Sie einen Bezeichner umbenennen, erscheint auf der rechten Seite des Bezeichners ein Smart-Tag. Sie können diesen über (ª) + (ALT) + (F10) oder über die Maus öffnen. Im Menü des Smart-Tag finden Sie die Befehle UMBENENNEN und UMBENENNEN MIT VORSCHAU. Diese Befehle bewirken ein Umbenennen aller Stellen im Quellcode, die genau dieses Element verwenden, das Sie umbenannt haben. Alternativ können Sie das Umbenennen auch gleich über Refactoring ausführen, indem Sie (STRG) +(R), (STRG) + (R) (»R« für »Rename«) betätigen, wenn der Eingabecursor auf dem Bezeichner liegt.
Weitere Features von Visual Studio
■
■
■
■
■
■
■
■
Extrahieren von Methoden: Wenn Sie eine oder mehrere Anweisungen selektieren, können Sie diese mit (STRG) +(R), (STRG) + (M) (»M« für »Method Extraction«) in eine Methode extrahieren. Visual Studio erzeugt eine neue Methode (deren Namen Sie angeben), platziert die selektierten Zeilen in diese und ersetzt die Selektion durch einen Aufruf der Methode. Dabei werden für alle innerhalb der selektierten Anweisungen verwendeten Variablen, die außerhalb der Selektion deklariert sind, Argumente definiert. Umschließen von Anweisungen mit Block-Anweisungen: Selektierte Anweisungen können Sie über (STRG) +(K), (STRG) + (S) (»S« für »Surround« = »Umschließen«) mit einer Block-Anweisung wie z. B. einem try-Block umschließen. Diese Tastenkombination öffnet ein Codeausschnitt-Fenster mit Codeausschnitten, die Block-Anweisungen enthalten. Wählen Sie einfach einen Codeausschnitt aus und betätigen Sie die (¢)-Taste. Heraufstufen von lokalen Variablen zu Parametern: Variablen, die innerhalb einer Methode deklariert sind, und die bei der Deklaration mit einem Wert initialisiert werden, können Sie über (STRG) +(R), (STRG) + (P) (»P« für »Promote« = »Befördern«) zu einem Parameter der Methode heraufstufen. Visual Studio passt automatisch alle Aufrufe der Methode an, indem der Initialisierungswert der Variablen an das neue Argument übergeben wird. Parameter der aktuellen Methode umsortieren: Über (STRG) +(R), (STRG) + (O) (»O« für »Order«) öffnen Sie einen Dialog, der Ihnen ermöglicht, die Parameter der aktuellen Methode umzusortieren. Visual Studio passt beim Umsortieren natürlich wieder alle Aufrufe der Methode an. Parameter der aktuellen Methode löschen: Wenn Sie innerhalb einer Methode (STRG) +(R), (STRG) + (V) betätigen, erscheint ein Dialog, über den Sie die Parameter der Methode löschen können. Beim Entfernen von Parametern passt Visual Studio zwar die Aufrufe der Methode an, die Stellen, an denen die Parameter innerhalb der Methode verwendet werden, werden aber nicht geändert. »V« steht wahrscheinlich für »remoVe«. Felder zu Eigenschaften heraufstufen: Wenn Sie auf einem Feld einer Klasse die Tastenkombination (STRG) +(R), (STRG) + (E) (»E« für »Encapsulate Field«) betätigen, können Sie über den nachfolgenden Dialog das Feld zu einer Eigenschaft umbauen. Extrahieren einer Schnittstelle aus einer Klasse: Wenn Sie innerhalb einer Klasse (STRG) +(R), (STRG) + (I) betätigen, erscheint ein Dialog, in dem Sie die öffentlichen Methoden und Eigenschaften der Klasse (nicht die öffentlichen Felder) auswählen können, um daraus eine Schnittstelle zu extrahieren. Wenn Sie den Dialog mit OK bestätigen, erzeugt Visual Studio eine neue Datei mit der Schnittstelle und erweitert die Klasse so, dass diese Schnittstelle implementiert wird. usings organisieren: Im Kontextmenü des Code-Editors finden Sie den Eintrag USINGS ORGANISIEREN, über dessen Befehle Sie ungenutzte using-Einträge löschen und die using-Einträge sortieren können.
1
2 3
4
5
6
7
8
9
10
11
2.14.6 Wichtige weitere Features des Code-Editors Der Code-Editor besitzt noch einige weitere Features, von denen ich hier die wichtigsten aufliste: ■
Anzeige der Anweisungen, die ein Element verwenden: Gerade beim Refactoring ist es häufig notwendig, herauszufinden, wo ein bestimmtes Element (eine Klasse, ein anderer Typ, eine Methode, eine Eigenschaft, eine Variable
131
Einführung in die Arbeit mit Visual Studio 2008
■
■
etc.) in der aktuellen Projektmappe verwendet wird. Dazu bietet Visual Studio eine Referenzliste, die Sie mit (ª) + (F12) oder über das Kontextmenü des CodeEditors öffnen können. Wechsel zur Deklaration eines Elements: Wenn Sie im Code-Editor zur Deklaration einer Klasse, eines anderen Typen, einer Methode, Eigenschaft, Variable oder eines anderen Klassenelements wechseln wollen, setzen Sie den Eingabecursor auf das Element und betätigen Sie (F12). usings verwalten: Über die Befehle im Eintrag USINGS ORGANISIEREN des Kontextmenüs des Code-Editors können Sie die unbenutzten using-Direktiven der aktuellen Datei löschen und die using-Direktiven sortieren.
2.14.7
Makros und Add-Ins
Über den Menüpunkt EXTRAS / MAKROS können Sie Makros erzeugen und ausführen. Sehr interessant ist die Möglichkeit, Makros aufzuzeichnen ((ª) + (STRG) + (R)). Der Makrorecorder zeichnet alle Schritte auf, die Sie über die Tastatur ausführen, und bedingt auch solche, die Sie über die Maus ausführen. Das aufgezeichnete Makro wird als »temporäres Makro« gespeichert, das Sie dann über den Makro-Explorer ((Alt) + (F8)) umbenennen und einem Makromodul zuordnen können. Makros sind (leider) Visual-Basic-Projekte, die über die Klasse EnvDTE.DTE auf die Entwicklungsumgebung und die darin geöffneten Projekte zugreifen. Makros können Sie also auch selbst programmieren. Die Entwicklungsumgebung kann damit sehr flexibel erweitert werden. Add-Ins sind Makros ähnlich, nur dass diese in einer externen Assembly gespeichert sind, deswegen auch in anderen .NET-Sprachen entwickelt und verteilt werden können, ohne den Quellcode mitliefern zu müssen. Im Internet finden Sie eine Vielzahl an Add-Ins für Visual Studio, die Sie installieren und über den Add-In-Manager (EXTRAS / ADD-IN-MANAGER) aktivieren können. Wichtige Add-Ins finden Sie auf der Seite www.microsoft.com/germany/msdn/library/visualtools/WichtigeAddInsFuerVisual StudioNETUnd2005.mspx.
2.14.8 Server-Explorer Den Server-Explorer, der nur in Visual Studio 2008 verfügbar ist, können Sie über das ANSICHT-Menü öffnen. Sie finden ihn auf der linken Seite von Visual Studio 2008 über der Toolbox. Der Server-Explorer zeigt alle Server-Dienste an, die auf einem Server (bzw. auf dem lokalen Rechner laufen) und ermöglicht, diese in einfacher Form zu administrieren. Sie können nicht nur den lokalen Rechner, sondern auch entfernte Server im ServerExplorer anzeigen. Über den Ordner DATENVERBINDUNGEN können Sie eine Verbindung zu einer Datenbank erzeugen (die aber nur für den Server-Explorer gilt). Über diese Verbindung können Sie die Datenbank dann erforschen und (je nach Datenbank) eingeschränkt auch administrieren (z. B. bei Access die Daten einer Tabelle ändern oder beim SQLServer auch Tabellen hinzufügen und deren Struktur ändern). Mit dem Server-Explorer besitzen Sie also eine Möglichkeit, die im Rahmen einer Programmentwicklung notwendigen Administrier-Arbeiten direkt über Visual Studio auszuführen und Informationen zu externen Diensten und Datenbanken zu erhalten.
132
Weitere Features von Visual Studio
2.14.9 Editoren für XML, HTML, CSS, Ressourcen, Bitmaps, Cursor und Icons Visual Studio enthält spezielle Editoren für XML-, HTML-, CSS-, Ressourcen-, Bitmap-, Cursor- und Icon-Dateien. Über den Menüpunkt PROJEKT / NEUES ELEMENT HINZUFÜGEN können Sie eine dieser Dateien dem Projekt hinzufügen. Ein Doppelklick auf der Datei im Projektmappen-Explorer öffnet den entsprechenden Editor.
1
2.14.10 Suchen mit Ergebnisliste Wenn Sie den Suchdialog mit (STRG) + (ª) + (F) öffnen, können Sie im Wesentlichen genauso suchen wie im normalen Dialog ((STRG) + (F)). Im Unterschied zu diesem werden die Suchergebnisse aber in einem Rutsch in einer Ergebnisliste angezeigt. So können Sie die einzelnen Fundstellen sehr komfortabel durchgehen, wenn Sie nach Texten suchen, die häufiger vorkommen.
2 3
2.14.11 Inkrementelle Suche Über (STRG) + (I) starten Sie die inkrementelle Suche. Jede Eingabe führt dann zu einer Weitersuche nach einem passenden Begriff in der aktuellen Datei. Mit (STRG) + (I) oder (F3) können Sie zwischenzeitlich den nächsten passenden Begriff suchen.
4
2.14.12 Flexible Suche mit regulären Ausdrücken und Platzhaltern
5
Der Such- und der Ersetzen-Dialog ((Strg) + (F); (Strg) + (H)) unterstützen Platzhalter (Wildcards) und reguläre Ausdrücke für das flexible Suchen und Ersetzen. Wählen Sie den entsprechenden Eintrag der Liste MIT im Dialog.
6
Platzhalter sind relativ einfach: Der Stern (*) steht für beliebig viele beliebige Zeichen, das Fragezeichen steht für genau ein beliebiges Zeichen.
7
Reguläre Ausdrücke hingegen sind sehr mächtig, weil sie die Suche nach allen erdenklichen Teilen von Texten erlauben. Als Beispiel soll eine kleine Praxisanwendung dienen: Wenn Sie im Ersetzen-Dialog die Suchoption MIT / REGULÄRE AUSDRÜCKE wählen, im Suchfeld den Ausdruck » *\n{ *\n}+« eingeben (achten Sie dabei auf die Leerzeichen) und im Ersetzen-Feld »\n\n«, dann ersetzen Sie alle mehrfachen Leerzeilen durch eine. Sehr praktisch, wenn ein Quellcode mit vielen Leerzeilen in eine bessere Form gebracht werden muss …
8
9
Die in .NET verwendeten regulären Ausdrücke werden in Kapitel 8 behandelt. Die Syntax der beim Suchen verwendeten regulären Ausdrücke ist aber leider etwas anders als die in den .NET-Klassen verwendete (die der allgemein verwendeten Syntax entspricht). Diesen dummen Umstand habe ich Microsoft gemeldet. Leider sieht sich die Visual-Studio-Entwicklungsabteilung nicht in der Lage, die .NET-Syntax der regulären Ausdrücke auch in Visual Studio zu ermöglichen. Schade.
10
11
Die Hilfe zu den regulären Ausdrücken in Visual Studio erreichen Sie, indem Sie im Suchen-/Ersetzen-Dialog (F1) betätigen. Suchen Sie in der erscheinenden Hilfeseite nach dem Link REGULÄRE AUSDRÜCKE. Über diesen Link öffnen Sie die Hilfeseite für die regulären Ausdrücke beim Suchen und Ersetzen.
133
Einführung in die Arbeit mit Visual Studio 2008
Eine kleine Referenz zu den regulären Ausdrücken beim Suchen finden Sie auch in der Datei RegEx-VS-Suchausdrücke.pdf auf der Buch-DVD und im Referenzteil dieses Buchs. DISC
2.14.13 Der Zwischenablagering Visual Studio besitzt einen so genannten Zwischenablagering (Clipboard Ring). Bei jedem Einfügen eines Textes in die Zwischenablage (über ein Kopieren oder Ausschneiden) wird dieser dem Zwischenablagering hinzugefügt. Überschreitet der Zwischenablagering seine maximale Größe, werden die ältesten Einträge entfernt. Wenn Sie nun über (STRG) + (ª) + (V) (statt mit (STRG) + (V)) einfügen, können Sie durch weiteres Betätigen dieser Tastenkombination die einzelnen Einträge im Zwischenablagering durchgehen. Dieses Feature erleichtert die tägliche Arbeit, bei der recht häufig Texte über die Zwischenablage kopiert oder verschoben werden. Über den Zwischenablagering kommen Sie auch an Texte heran, die Sie zuvor kopiert hatten und die durch ein erneutes Kopieren oder Ausschneiden in der normalen Zwischenablage überschrieben wurden (was mir andauernd passiert …). Außerdem können Sie den Zwischenablagering auch bewusst einsetzen um verschiedene Texte an mehreren Stellen einzufügen.
2.14.14 Eigene Vorlagen für Projekte und ProjektElemente Visual Studio installiert bereits eine Vielzahl an Vorlagen für Projekte und ProjektElemente. Sie finden die Projektvorlagen im Visual-Studio-Ordner im Unterordner Common7\IDE\ProjectTemplates\CSharp und die Projektelement-Vorlagen im Unterordner Common7\IDE\ItemTemplates\CSharp. Sie können jedoch auch eigene Vorlagen verwenden. Diese Vorlagen werden in den Ordnern verwaltet, die in den Visual-Studio-Optionen in der Option PROJEKTE UND PROJEKTMAPPEN / ALLGEMEIN / SPEICHERORT VON VISUAL STUDIO-BENUTZERVORLAGEN und SPEICHERORT VON VISUAL STUDIO-BENUTZERELEMENTVORLAGEN angegeben ist. Per Voreinstellung sind das die Ordner Visual Studio 2008\Templates\ProjectTemplates und Visual Studio 2008\Templates\ItemTemplates im Eigene-Dateien- bzw. Dokumente-Ordner. Der einfachste Weg zur Erstellung einer neuen Vorlage ist, ein entsprechendes Projekt oder Projektelement zu erzeugen und im DATEI-Menü den Befehl VORLAGE EXPORTIEREN zu wählen. Über den erscheinenden Dialog können Sie ein Projekt oder eines der Projektelemente der aktuellen Projektmappe als Vorlage exportieren (was die Vorlage in den Ordner Visual Studio 2008\My Exported Templates im EigeneDateien- bzw. Dokumente-Ordner exportiert) und die Vorlage auch gleich wieder »importieren« (was die Vorlage in den entsprechenden Vorlagen-Ordner im EigeneDateien- bzw. Dokumente-Ordner kopiert). Sie können aber natürlich auch eine der vorhandenen Vorlagen in den eigenen Vorlagen-Ordner kopieren und dort ändern. Vorlagen sind dabei im ZIP-Format archiviert und werden auch nur dann eingelesen, wenn sie im korrekten Format vorliegen.
HALT
134
Wenn Sie exportierte oder vorhandene Vorlagen entpacken, ändern und wieder packen, erkennt Visual Studio die Vorlagen nicht in allen Fällen. Beim Packen mit Winrar wurden in meinem Fall Vorlagen z. B. nicht als solche erkannt. Mit Windows (über den Explorer) gepackte Vorlagen-Ordner wurden allerdings von Visual Studio erkannt.
Kompilieren ohne Visual Studio
In den Vorlagen können Sie mit Platzhaltern arbeiten, die von Visual Studio durch entsprechende Texte ersetzt werden. Die Standardvorlagen setzen z. B. die Platzhalter $projectname$ und $safeprojectname$ ein. Tabelle 2.2 zeigt die vordefinierten Platzhalter. Platzhalter
Bedeutung
$clrversion$
Die Version der CLR
$guid1$ … $guid10$
Erzeugt eine neue GUID. Sie können bis zu 10 GUIDs verwenden.
$itemname$
Der vom Benutzer im Dialog NEUES ELEMENT HINZUFÜGEN angegebene Name.
$machinename$
Der aktuelle Computername
$projectname$
Der vom Benutzer eingegebene Name für ein neues Projekt
Tabelle 2.2: Die Platzhalter für Vorlagen
1
2 3
$registeredorganization$ Der in der Registry verwaltete Wert für die Firma des Benutzers $rootnamespace$
Der Stammnamensraum des aktuellen Projekts
$safeitemname$
Der Name eines neuen Elements als gültiger C#-Bezeichner
$safeprojectname$
Der Projektname als gültiger C#-Bezeichner (für die NamensraumDeklaration)
$time$
Das aktuelle Datum inkl. Uhrzeit
$userdomain$
Die Domäne des aktuellen Benutzers
$username$
Der Name des aktuellen Benutzers
$year$
Das aktuelle Jahr
2.15
4
5
6
7
Kompilieren ohne Visual Studio 8
C#-Programme können Sie auch ohne Visual Studio erzeugen und kompilieren. Im einfachsten Fall schreiben Sie die Anwendung in einem einfachen Editor und kompilieren diese über den Kommandozeilencompiler.
2.15.1
9
Der Kommandozeilencompiler
Der Kommandozeilencompiler ist unter dem Namen csc.exe im Ordner C:\Windows\Microsoft.NET\Framework\vx.x gespeichert, wobei x.x für die aktuelle Versionsnummer des .NET Framework steht.
10
Um beim Aufruf von csc.exe nicht immer den Ordner mit angeben zu müssen, sollten Sie diesen über den speziellen Visual Studio 2008 Command Prompt ausführen, bei dem die Pfade zu den wichtigen .NET-Ordnern temporär der Systemvariablen PATH angehängt sind. Diesen finden Sie im Startmenü im Ordner MICROSOFT VISUAL STUDIO 2008 / VISUAL STUDIO TOOLS.
11
Wenn Sie nur die Express-Editionen installiert haben, fehlt diese Möglichkeit. In diesem Fall können Sie die Pfade zu den wichtigen .NET-Ordnern in den Windows-Pfad aufnehmen. Unter Windows XP und Vista finden Sie den dazu notwendigen Dialog in
135
Einführung in die Arbeit mit Visual Studio 2008
der Systemsteuerung unter dem Eintrag SYSTEM (unter Vista in der klassischen Ansicht der Systemsteuerung den Eintrag SYSTEM wählen, dann auf ERWEITERTE SYSTEMEINSTELLUNGEN). Klicken Sie dort auf das Register ERWEITERT und dann auf UMGEBUNGSVARIABLEN. Suchen Sie in der unteren Liste die Variable PATH und klicken auf BEARBEITEN. Fügen Sie dem Pfad die folgenden Pfade hinzu (die Sie anpassen müssen, wenn das .NET Framework bei Ihnen nicht in den Default-Ordnern installiert ist): ■ ■ ■
C:\Windows\Microsoft.NET\Framework\v3.5 C:\Windows\Microsoft.NET\Framework\v2.0.50727 C:\Programme\Microsoft SDKs\Windows\V6.0A\Bin
Nun können Sie den Kommandozeilen-Compiler (und andere .NET-Tools) in jedem Ordner des Systems direkt aufrufen. Eine Quellcodedatei kompilieren Sie einfach über csc [Optionen] [Dateiname(n)]
Die Optionen des C#-Compilers sind sehr vielfältig. Sie können diese abfragen, indem Sie csc /? eingeben. Die wichtigsten zeigt Tabelle 2.3. Tabelle 2.3: Die wichtigsten Optionen des Befehlszeilencompilers csc.exe
Option
Bedeutung
/out:Dateiname
In dieser Option können Sie den Dateinamen der zu erzeugenden Assembly angeben. Falls Sie diese Option nicht verwenden, erhält die Assembly den Namen der ersten Quellcodedatei mit einer zum Typ der Assembly passenden Endung.
/recurse:Platzhalter
Wenn Sie alle Quellcodedateien eines Ordners in die Assembly kompilieren wollen, können Sie diese einfach über diese Option einbinden. /recurse:*.cs kompiliert z. B. alle Dateien mit der Endung .cs.
/target: {exe | winexe | module | library}
Standardmäßig erzeugt csc eine Konsolen- oder Windows-Anwendung (je nach Quellcode). Wenn Sie eine Klassenbibliothek oder ein Modul erzeugen wollen, müssen Sie die target-Option mit dem entsprechenden Argument angeben. Diese Option muss vor dem Dateinamen angegeben werden. Eine Klassenbibliothek erzeugen Sie z. B. so: csc /target:library Quellcodedatei.cs.
/reference: Dateiliste oder /r:Dateiliste
Wenn Sie im Quellcode Assemblys (Klassenbibliotheken, Steuerelementbibliotheken etc.) verwenden, die nicht zum Standard gehören (also nicht automatisch referenziert werden) und die nicht im Ordner der erzeugten Anwendung gespeichert sind, müssen Sie diese über die reference-Option einbinden.
/lib: Dateiliste
Mit dieser Option können Sie zusätzliche Verzeichnisse angeben, in denen der Kommandozeilencompiler nach Assemblys sucht, auf die Sie verwiesen haben.
2.15.2
SharpDevelop
SharpDevelop ist ein recht guter Open Source-Editor für C# und VB.NET. Er ist zwar lange nicht so mächtig wie Visual Studio, bietet aber einige hilfreiche Features für das Schreiben des Quellcodes. Sie finden diesen recht intuitiv bedienbaren Editor bei www.icsharpcode.net/opensource/sd/. Dort können Sie auch den C#-Quellcode (!) von SharpDevelop downloaden. Die Installation des Editors ist (.NET-mäßig) ein-
136
Neues in Visual Studio 2008 und in den neuen Express-Editionen
fach: Kopieren Sie die Dateien einfach in einen beliebigen Ordner. Rufen Sie danach die Datei SharpDevelop.exe auf, um den Editor zu starten. Den Rest müssen Sie dann selbst herausfinden. Eine Beschreibung von SharpDevelop passt nicht in den Rahmen dieses Buchs.
2.16
Neues in Visual Studio 2008 und in den neuen Express-Editionen
1
Visual Studio 2008 und die 2008er Express-Editionen bringen als Weiterentwicklung von Visual Studio 2005 bzw. der 2005er Express-Editionen natürlich auch einiges Neues mit. In diesem Kapitel wurden diese neuen Features allerdings größtenteils nicht verwendet. Das hole ich hiermit auf. Dabei beschreibe ich allerdings nicht alle Änderungen (wie z. B. wenn Menüeinträge in ein anderes Menü verschoben wurden), sondern nur die wichtigsten. Dass Visual Studio alle neuen Sprachmerkmale von C# 3.0 unterstützt, sehe ich als selbstverständlich und beschreibe dies hier nicht mehr weiter. Nähere Informationen zu den Änderungen finden Sie in der Visual-Studio-2008-Hilfe unter Entwicklungstools und Sprachen / Visual Studio / Einführung in Visual Studio / Erste Schritte mit Visual Studio / Neues in Visual Studio 2008. In der Visual-C#-2008Express-Hilfe (und äquivalent auch in der Hilfe für Visual Web Developer 2008 Express) finden Sie die Neuigkeiten unter Visual C# Express / Einführung in Visual C# Express / Erste Schritte mit Visual C# Express / Neues in Visual C# Express 2008.
2.16.1
2 3
4 REF
5
Neue Projekttypen
6
Visual Studio unterstützt nun die folgenden neuen Projekttypen:
7
NEU
–
WPF-Anwendungen: WPF-Anwendungen sind Windows-Anwendungen, die mit den Klassen der Windows Presentation Foundation arbeiten. WPF-Anwendungen werden ab Kapitel 12 behandelt.
–
WPF-Browseranwendungen: WPF-Browseranwendungen arbeiten auch mit WPF, werden aber im Browser ausgeführt. WPF-Browseranwendungen werden in diesem Buch nicht behandelt.
8
–
WPF-Benutzersteuerelement-Bibliotheken: In Visual Studio 2008 (nicht direkt in Visual C# 2008 Express) können Sie nun auch Bibliotheken erzeugen, die WPF-Benutzersteuerelemente enthalten.
9
–
Webanwendungs-Projekte (nur Visual Studio 2008): In Visual Studio 2008 können Sie nun neben den Standard-Webanwendungs-Projekten auch solche erzeugen, die auf einer Projektdatei basieren. Diese Projekte verhalten sich eher wie Windows-Anwendungs-Projekte: Sie integrieren nur die in der Projektdatei beschriebenen Dateien in das Projekt, können mit unterschiedlichen Projektmappenkonfigurationen kompiliert werden und werden in eine einzige Assembly kompiliert. Webanwendungs-Projekte entsprechen damit im Wesentlichen den Webanwendungs-Projekten von Visual Studio 2003. Der Unterschied dazu ist aber, dass die neue Variante auch auf dem ASP.NET-Entwicklungs-Webserver ausgeführt werden kann und dass auf dem IIS die Frontpage-Servererweiterungen nicht benötigt werden.
10
11
137
Einführung in die Arbeit mit Visual Studio 2008
–
ASP.NET.Ajax-Projekte: In Visual Studio 2008 (nicht Visual Web Developer 2008 Express) können Sie nun ASP.NET-Ajax-Server-Steuerelement-Projekte und ASP.NET-Ajax-Server-Control-Extender-Projekte erzeugen.
–
WCF-Projekte: Zur Unterstützung von WCF bietet Visual Studio 2008 verschiedene Projektvorlagen. In den Express-Editionen können Sie allerdings nur in Visual Web Developer 2008 Express WCF-Dienste erzeugen, die in einer Webanwendung gehostet werden.
–
Workflow-Projekte: Da auch die Workflow Foundation (WF) unterstützt wird, bietet Visual Studio 2008 verschiedene WF-Projektvorlagen. Den Express-Editionen fehlen diese Vorlagen.
–
Testprojekte: Visual Studio 2008 unterstützt ab der Professional-Version nun auch Unit-Tests, die Sie als Testprojekt in eine Projektmappe integrieren können. Unit-Tests werden in Kapitel 9 behandelt.
–
Berichts-Projekte: Visual Studio 2008 (nicht die Express-Editionen) enthält nun zwei neue Projektvorlagen zur Erstellung von Bericht-Anwendungen. Diese Anwendungen nutzen einen neuen, Visual-Studio-eigenen Bericht-Designer und einen neuen Bericht-Generator. Sie sollen wahrscheinlich Crystal Reports ersetzen, bieten aber bisher nicht die Funktionalität dieses bekannten Bericht-Tools.
2.16.2 Neue Features für die Arbeit mit Daten Für die Arbeit mit Daten wurde Visual Studio um die folgenden Features erweitert: NEU
–
Unterstützung von LINQ: LINQ (Language Integrated Query) ist ein neues Feature des .NET Framework, über das unterschiedliche Daten (wie Auflistungen, XML-Dokumente oder Datenbanken) mit einer SQL-ähnlichen Abfrageund Bearbeitungssyntax direkt im Code verarbeitet werden können.
–
Unterstützung des SQL Servers Compact 3.5: Der SQL Server Compact 3.5 ist eine Minimalversion des SQL Servers, der in Form von Assemblys vorliegt und als »kleine« Standalone-Datenbank einer Anwendung mitgeliefert werden kann, ohne zusätzlich Installationen zu erfordern. Visual Studio unterstützt den SQL Server Compact 3.5, indem einem Projekt entsprechende Datenbanken hinzugefügt werden können. Außerdem unterstützt Visual Studio die Administrierung solcher Datenbanken über den Datenbank-Explorer.
–
O/R-Designer: Visual Studio enthält nun Designer zum Erzeugen von Klassen, die auf Datenbanktabellen gemappt werden. Solche Klassen werden zum einen mit LINQ to SQL verwendet und zum anderen mit dem ADO.NET Entity Framework.
2.16.3 Neues im Code-Editor Der Code-Editor wurde um die folgenden Features erweitert: NEU
138
–
Transparente IntelliSense-Fenster: IntelliSense-Fenster besitzen nun die Möglichkeit, transparent dargestellt zu werden, sodass Sie den darunterliegenden Code erkennen können. Um IntelliSense-Fenster transparent zu schalten, betätigen Sie bei geöffnetem Fenster die (STRG)-Taste.
Neues in Visual Studio 2008 und in den neuen Express-Editionen
–
IntelliSense für Jscript und ASP.NET Ajax: Der Code-Editor bietet nun auch IntelliSense für Jscript (bzw. JavaScript), das in ASPX-Seiten eingebunden ist, und für ASP.NET-Ajax-Elemente.
–
using-Deklarationen organisieren: Über den neuen Kontextmenü-Ordner USINGS ORGANISIEREN können Sie die using-Deklarationen der aktuellen Datei sortieren und nicht verwendete using-Deklarationen entfernen.
1
2.16.4 Sonstige Neuerungen –
Multitargeting: Visual Studio 2008 ist in der Lage, auch Anwendungen für die älteren Versionen des .NET Framework zu kompilieren. In den Projekteigenschaften können Sie dazu die Ziel-Plattform auf eine der mit .NET 3.5 mitgelieferten einstellen (2.0, 3.0 oder 3.5). So können Sie in Visual Studio 2008 auch Anwendungen entwickeln, die auf Systemen ausgeführt werden, die lediglich das .NET Framework 2.0 installiert haben.
–
Unit Tests: Ab der Professional Edition können Sie in Visual Studio 2008 nun Unit Tests verwenden. Unit Tests werden in Kapitel 9 behandelt.
–
Erweiterung von ClickOnce: Visual Studio unterstützt nun die Weitergabe von WPF-Browseranwendungen über ClickOnce. ClickOnce wurde außerdem um die Möglichkeit erweitert, dass das Anwendungsmanifest neu signiert werden kann, z. B. um Internet-Service-Providern die Möglichkeit zu geben, ihre eigene Signatur zu vergeben. Daneben können nun auch VSTO-Projekte per ClickOnce verteilt werden. Schließlich wird noch die Erzeugung von Manifesten unter Vista mit eingeschaltetem User Account Control (UAC) unterstützt.
–
Integration von WCF-Diensten: Visual Studio bietet nun Support zur Integration von WCF-Diensten in ein Projekt.
–
XHTML- und JavaScript-Fehler als Warnungen: In Visual Studio 2005 wurden Fehler in XHTML-Dokumenten und JavaScript-Programmen immer als Fehler angezeigt, wenn die betroffene Datei geöffnet war. Dies führte bei Dokumenten, die nicht dem XHTML-Standard entsprechen (sondern z. B: dem HTML-4-Standard), dazu, dass die Fehlerliste im Fall von zusätzlichen Kompilierfehlern sehr viele Fehler enthielt und die zunächst wichtigen Kompilierfehler nur schwer zu finden waren. In Visual Studio 2008 werden XHTML- und JavaScript-Fehler nun per Voreinstellung als Warnungen angezeigt.
2 NEU
3
4
5
6
7
8
9
10
11
139
Inhalt
3
Die Sprache C# 1
Nachdem Sie in Kapitel 2 erfahren haben, wie Sie einfache C#-Programme mit Visual Studio entwickeln und kompilieren, beschreibe ich in diesem Kapitel die Grundlagen der Sprache C#. Ich zeige, wie Sie Anweisungen schreiben, mit Typen umgehen und mit Schleifen und Verzweigungen arbeiten. Um die Beispiele möglichst einfach zu halten, verwende ich dazu einfache Konsolenanwendungen.
2
3
Methoden werden übrigens noch nicht in diesem Kapitel, sondern erst bei der objektorientierten Programmierung in Kapitel 4 beschrieben.
4
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
Grundlagen von Windows- und Konsolenanwendungen Grundlagen zu (Daten)Typen Wert- und Referenztypen Nullables: Werttypen ohne Wert Grundlagen generischer Typen Die Standardtypen Über- und Unterläufe und deren Behandlung Typumwandlungen und Konvertierungen Variablen und Konstanten Ausdrücke und Operatoren Verzweigungen Schleifen Präprozessor-Direktiven und bedingte Kompilierung
5
6
7
8
Die C#-3.0-Neuigkeiten sind: ■ ■
9
Objektinitialisierer (Seite 155) Implizit typisierte lokale Variablen (Seite 198)
Neu im .NET Framework 3.5: ■
10
Die Struktur DateTimeOffset (Seite 181)
3.1
Die Grundlage einer C#-(Windows-)Anwendung
11
141
Index
Konsolen- und Windows.Forms-Windows-Anwendungen bestehen mindestens aus einer Klasse, die in einer Textdatei mit der Endung .cs gespeichert ist. Je nach Art der Anwendung unterscheidet sich der Aufbau dieser Klasse. WPF- und Webanwendungen sind prinzipiell anders aufgebaut. WPF-Anwendungen beschreibe ich ab Kapitel 12.
Die Sprache C#
3.1.1
Konsolenanwendungen
Eine Konsolenanwendung ist die einfachste Anwendung in C#. Eine solche Anwendung besteht mindestens aus einer Datei, in der eine Klasse deklariert ist. Anwendungen benötigen immer einen Einsprungpunkt (der Punkt der Anwendung, mit der diese startet). Solch einen Einsprungpunkt können Sie in einer Konsolenoder Windows.Forms-Anwendung definieren, indem Sie einer Klasse eine Methode Main hinzufügen, die einen festgelegten Aufbau besitzen muss: Listing 3.1: Eine einfache Konsolenanwendung using System; namespace Kompendium.Samples.Applications { class Program { static void Main(string[] args) { Console.WriteLine("Hello World"); } } }
Die erste Anweisung (using System;) bindet den Inhalt des System-Namensraums so ein, dass bei der Verwendung der darin enthaltenen Typen und Namensräume »System.« nicht immer wieder mit angegeben werden muss. namespace erzeugt einen Namensraum
Die namespace-Anweisung erzeugt einen neuen Namensraum. Alle in den geschweiften Klammern enthaltenen Typen gehören diesem Namensraum an. Diese Anweisung ist nicht unbedingt notwendig. Das Namensraum-Konzept ist aber sehr sinnvoll, weswegen Sie Ihre eigenen Anwendungen auch in Namensräumen organisieren sollten. Das Beispiel erzeugt einen übergeordneten Namensraum Kompendium, einen untergeordneten Namensraum Samples und einen weiter untergeordneten Namensraum Applications.
class erzeugt eine Klasse
Die class-Anweisung erzeugt eine neue Klasse (die für die Start-Klasse üblicherweise Program heißt). Diese Klasse enthält die Main-Methode. Das Schlüsselwort static bewirkt, dass diese Methode statisch ist, d. h. ohne Instanz der Klasse aufgerufen werden kann. Main muss statisch sein, da die CLR bei der Ausführung von Windows.Formsund Konsolenanwendungen keine Instanz der Startklasse der Anwendung erzeugt. Das in den Klammern angegebene string[] args deklariert einen Parameter der Main-Methode. An diesem Parameter werden eventuell beim Aufruf der Anwendung angegebene Befehlszeilenargumente übergeben. Befehlszeilenargumente werden in Kapitel 8 behandelt. Das args-Argument muss nicht unbedingt angegeben sein. Die Main-Methode kann also auch so aussehen: static void Main() { Console.WriteLine("Hello World") }
Wie Sie bereits in Kapitel 2 erfahren haben, können Sie in einer Konsolenanwendung über die Write- oder die WriteLine-Methode der Console-Klasse einen Text an der Konsole ausgeben. Das Beispiel nutzt die WriteLine-Methode um den Text »Hello World« auszugeben. Der Unterschied zwischen WriteLine und Write ist, dass WriteLine hinter dem Text noch einen Zeilenvorschub ausgibt.
142
Die Grundlage einer C#-(Windows-)Anwendung
Zusätzlich können Sie über die ReadLine-Methode auf Eingaben warten und diese auswerten. ReadLine wartet, bis der Anwender die (¢)-Taste betätigt hat, und gibt das, was der Anwender bis dahin eingegeben hat, zurück. Kapitel 2 enthält dazu ein Beispiel.
3.1.2
Windows.Forms-Windows-Anwendungen
Windows.Forms-Windows-Anwendungen sind grundlegend ähnlich aufgebaut wie Konsolenanwendungen. Der Unterschied ist, dass eine solche Anwendung (normalerweise) zumindest aus zwei Quellcodedateien besteht. Eine Datei verwaltet die Klasse, die die Main-Methode enthält, mit der das Programm startet, die andere verwaltet das Formular, das als erstes Formular geöffnet wird, wenn die Anwendung startet.
1
2
Der Aufbau einer Formular-Klasse ist relativ komplex. Die Start-Klasse, die üblicherweise Program heißt, sieht aber fast so aus wie die Start-Klasse einer Konsolenanwendung. Im Unterschied zu einer solchen Anwendung wird hier lediglich eine Instanz des Start-Formulars erzeugt und der (statischen) Run-Methode der Application-Klasse übergeben. Die Anweisungen, die davor stehen, sind nicht unbedingt notwendig, schalten aber erweiterte Features wie die XP- oder Vista-übliche Darstellung der Steuerelemente ein:
3 4
Listing 3.2: Aufbau der Start-Klasse bei Windows.Forms-Anwendungen using using using using
5
System; System.Collections.Generic; System.Linq; System.Windows.Forms;
6
namespace Kompendium.Samples.Applications: { static class Program { /* Der Haupteinstiegspunkt für die Anwendung. */ [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new StartForm()); } } }
7
8
9
Im Unterschied zu einer Konsolenanwendung sind oben in der Quellcodedatei per Voreinstellung mehr Namensräume eingebunden. Diese sind nicht alle für die StartKlasse wirklich notwendig. Es handelt sich dabei aber um die Standard-Namensräume, die Visual Studio in jede neue Klasse einfügt.
10
Die Main-Methode ist prinzipiell dieselbe Methode, die auch in einer Konsolenanwendung verwendet wird. Visual Studio hat das args-Argument weggelassen, weil Windows-Anwendungen nur in den seltensten Fällen Befehlszeileargumente übergeben werden. Sie können dieses Argument aber auch von Hand in die Parameterliste der Main-Methode einfügen wenn die Anwendung Befehlszeilenargumente unterstützen soll.
11
Ein anderer Unterschied ist, dass die Methode mit dem STAThread-Attribut markiert ist. Attribute, die in Kapitel 6 vertieft behandelt werden, ermöglichen die Angabe von Metadaten für Typen und deren Elemente. Diese speziellen Zusatzdaten werden vom
143
Die Sprache C#
Compiler, von der CLR oder von Programmen ausgewertet und können die unterschiedlichsten Bedeutungen besitzen. Das STAThread-Attribut ist enorm wichtig (wie ich im nächsten Abschnitt noch näher ausführe), löschen Sie dieses also niemals. Der letzte Unterschied ist, dass die Main-Methode speziell kommentiert ist. Diese Kommentare beginnen mit drei Schrägstrichen. Dabei handelt es sich um Dokumentationskommentare, aus denen später eine Programmdokumentation erzeugt werden kann. Programmdokumentationen werden in Kapitel 8 behandelt.
Das STAThread-Attribut Das STAThreadAttribut ist wichtig für COM
HALT
Das STAThread-Attribut hat eine Bedeutung für das seit langer Zeit in Windows eingesetzte COM-Modell. Es führt dazu, dass die Anwendung in einem »Single Threaded Apartment« (STA) ausgeführt wird. Eine Erläuterung dieses Begriffs möchte ich Ihnen ersparen, schließlich lesen Sie gerade ein Buch über .NET und nicht über das sehr komplexe COM-Modell (das glücklicherweise fast tot ist). Da aber viele der vom Betriebssystem zur Verfügung gestellten Features, wie z. B. der Datei-Öffnen-Dialog, COM-Komponenten sind, kommen Sie mit dem COM-Modell zumindest indirekt in Kontakt. Die Kommunikation zwischen Windows.Forms und COM ist nur korrekt möglich, wenn die Anwendung in einem Single Threaded Apartment ausgeführt wird. Geben Sie das STAThread-Attribut nicht an, läuft die Anwendung in einem freien Thread. Das kann dann in einigen Fällen zu Kommunikations-Problemen zwischen COM und der Anwendung führen. Der Datei-Öffnen-Dialog zeigt z. B. für einige Ordner keinen Inhalt mehr an, wenn das STAThread-Attribut nicht angegeben ist. Glücklicherweise warnt Visual Studio beim Ausführen des Programms im Debugger bei der Verwendung von COM-Objekten, wenn dieses Attribut fehlt. Da diese Warnung in der kompilierten Datei aber nicht ausgegeben wird, sollten Sie immer darauf achten, dass das STAThread-Attribut angegeben ist. Eigentlich sollte das auch für Konsolenanwendungen gelten, zumindest wenn Sie darin auf die Windows.Forms-Assembly verweisen und Klassen dieses Namensraums verwenden.
3.2 Assemblys müssen referenziert werden
Assemblys und Namensräume
Wenn Sie die in einer Assembly enthaltenen Typen in Ihren Programmen verwenden wollen, muss das jeweilige Projekt diese Assembly referenzieren. Die einzige Assembly, die nicht referenziert werden muss, weil das der Compiler automatisch macht, ist mscorlib.dll. Diese Assembly enthält die grundlegenden Typen des .NET Framework in verschiedenen Namensräumen. In Visual Studio verwenden Sie zur Referenzierung von Assemblys den ReferenzenEintrag im Projektmappen-Explorer, so wie ich es bereits in Kapitel 2 gezeigt habe. Wenn Sie den Kommandozeilencompiler verwenden, geben Sie die zu referenzierenden Assemblys im reference-Argument an.
Typen können voll qualifiziert verwendet werden
Wenn Sie die in einem Namensraum enthaltenen Typen in Ihrem Programm verwenden wollen, können Sie diese unter Angabe des Namensraums voll qualifiziert angeben. Wollen Sie beispielsweise über die Show-Methode der MessageBox-Klasse eine Meldung ausgeben, können Sie die entsprechende Anweisung so schreiben: System.Windows.Forms.MessageBox.Show("Hello World");
144
Bezeichner und Schlüsselwörter
Sie können den Namensraum aber auch über eine using-Direktive, die ganz oben im Quellcode stehen muss, quasi importieren: using System.Windows.Forms;
using erleichtert die Verwendung von Typen
Dann können Sie alle darin enthaltenen Typen ohne Angabe des Namensraums verwenden: MessageBox.Show("Hello World");
1
Falls der Compiler eine Kollision mit einem Typnamen eines anderen Namensraums meldet, können Sie diesen Typen trotz importiertem Namensraum auch voll qualifiziert (also mit Namensraum) angeben, um den Konflikt zu lösen.
2
Mit der using-Direktive können Sie für einen Namensraum oder eine Klasse auch noch einen Alias einrichten: using mb = System.Windows.Forms.MessageBox;
3
Den Namensraum bzw. die Klasse können Sie dann über den Alias referenzieren: mb.Show("Hello World");
Ich rate Ihnen allerdings von dieser Technik ab, weil es schwer zu erkennen ist, welche Klasse bzw. welcher Namensraum tatsächlich verwendet wird. Mit der Hilfe von IntelliSense und Codeausschnitten sollte es auch kein Problem sein, lange Typnamen ohne Alias zu verwenden.
4
5
Der globale Namensraum Klassen, Strukturen und andere Typen müssen nicht unbedingt einem Namensraum zugeordnet sein. Wenn Sie einen Typen ohne Namensraum deklarieren, wird dieser dem so genannten globalen Namensraum zugeordnet. Der globale Namensraum ist nichts Besonderes, er enthält lediglich alle weiteren Namensräume und eben Typen, die keinem Namensraum zugeordnet sind. Da im .NET Framework alle Typen einem speziellen Namensraum angehören, sollten Sie darauf verzichten, Typen ohne Namensraum zu deklarieren.
3.3
6
7
Bezeichner und Schlüsselwörter
In C# müssen die Bezeichner für Variablen, Klassen, Strukturen, Felder, Eigenschaften, Methoden und andere Elemente, die Sie bei Programmierung deklarieren, den Standardregeln entsprechen: Bezeichner dürfen nur Buchstaben, Zahlen und den Unterstrich enthalten und müssen mit einem Buchstaben oder dem Unterstrich beginnen.
Bezeichner dürfen Buchstaben, Zahlen und den Unterstrich enthalten
8
9
Die folgenden Variablendeklarationen sind demnach gültig: Listing 3.3: Variablendeklarationen mit gültigen Bezeichnern
10
int number; int number1; int _number;
11
Die folgenden Deklarationen sind ungültig: Listing 3.4: Variablendeklarationen mit ungültigen Bezeichnern int number 1; // Leerzeichen nicht erlaubt int number@1; // Sonderzeichen nicht erlaubt int 1Number; // Zahl am Anfang nicht erlaubt
145
Die Sprache C#
Der Compiler beschwert sich außerdem, wenn Sie für einen Bezeichner eines der reservierten C#-Schlüsselwörter verwenden (Tabelle 3.1). Tabelle 3.1: Die reservierten C#-Schlüsselwörter
@ erlaubt auch Schlüsselwörter als Bezeichner
abstract
do
In
protected
true
as
double
Int
public
try
base
else
interface
readonly
typeof
bool
enum
internal
ref
uint
break
event
Is
return
ulong
byte
explicit
Lock
sbyte
unchecked
case
extern
Long
sealed
unsafe
catch
false
namespace
short
ushort
char
finally
New
sizeof
using
checked
fixed
Null
stackalloc
virtual
class
float
Object
static
void
const
for
operator
string
volatile
continue
foreach
Out
struct
while
decimal
goto
override
switch
default
if
Params
this
delegate
implicit
private
throw
Sie können ein Schlüsselwort aber trotzdem als Bezeichner einsetzen, indem Sie diesem ein @ voranstellen: Listing 3.5: Deklaration einer Variablen mit Namen eines Schlüsselworts int @in;
Sinn macht das aber nur in den seltensten Fällen. C# enthält zudem einige kontextuelle Schlüsselwörter, deren Bedeutung vom Kontext abhängt (Tabelle 3.2). Diese können Sie jederzeit auch als Bezeichner einsetzen (auch wenn das ebenfalls in der Regel keinen Sinn macht ...). Tabelle 3.2: Die kontextuellen C#-Schlüsselwörter
146
add
from
join
Remove
where
ascending
get
let
Select
yield
by
global
on
Set
descending
group
orderby
Value
equals
into
partial
Var
Anweisungen
3.4
Anweisungen
Nun geht es (endlich) los mit der eigentlichen Sprache. Dieser Abschnitt behandelt dazu zunächst elementare Anweisungen, Kommentare und den Aufruf von Methoden. Strukturanweisungen, über die Sie Ihre Programme strukturieren können, werden erst im Abschnitt »Verzweigungen und Schleifen« ab Seite 216 behandelt, nach den dafür notwendigen Grundlagen.
3.4.1
1
Elementare Anweisungen
Die Mutter aller Programme, die elementare Anweisung, entspricht in C# syntaktisch der Anweisung von C++ oder Java. Eine C#-Anweisung wird immer mit einem Semikolon abgeschlossen:
2
Console.WriteLine("Hello World");
3
Daraus folgt, dass Sie Anweisungen einfach in die nächste Zeile umbrechen können: Console.WriteLine( "Hello World");
4
Das Umbrechen eines Zeichenketten-Literals ist allerdings nicht so einfach. Die folgende Anweisung: Console.WriteLine("Hello World");
5
ergibt direkt mehrere Syntaxfehler. U. a. meldet der Compiler (bzw. Visual Studio) den Fehler, dass eine schließende Klammer erwartet wird.
6
Zeichenketten müssen prinzipiell immer in der aktuellen Zeile abgeschlossen und in einer neuen Zeile wieder begonnen werden (außer, Sie verwenden so genannte wortwörtliche Zeichenketten, die ich im Abschnitt »Zeichen und Zeichenketten« auf Seite 182 beschreibe). Wollen Sie eine Zeichenkette umbrechen (was in der Praxis sehr häufig vorkommt), schließen Sie diese ab und verbinden sie mit der Teilzeichenkette in der nächsten Zeile über den Plus-Operator.
7
Die folgende Anweisung ist korrekt:
8
Console.WriteLine("Hello " + "World");
Elementare Anweisungen sind entweder Zuweisungen von arithmetischen oder logischen Ausdrücken an Variablen:
9
i = 1 + 1;
oder Aufrufe von Methoden:
10
Console.WriteLine("Hello World");
Arithmetische und logische Ausdrücke arbeiten mit Operatoren, die in diesem Kapitel später (ab Seite 201) behandelt werden. Das Beispiel verwendet die Operatoren = (Zuweisung) und + (Addition). Den Aufruf von Methoden behandelt dieses Kapitel ab Seite 152.
11
147
Die Sprache C#
C# unterscheidet Groß- und Kleinschreibung
Bei allen Anweisungen müssen Sie beachten, dass C# Groß- und Kleinschreibung unterscheidet. Jedes Element einer Anweisung muss genauso geschrieben werden, wie es ursprünglich deklariert wurde. Sie können z. B. nicht console.writeline("Hello World");
schreiben.
3.4.2
Anweisungsblöcke
Anweisungen müssen häufig zusammengefasst werden. Eine Methode enthält z. B. einen Block von Anweisungen: private static void SayHello() { string name; Console.Write("Ihr Name: "); name = Console.ReadLine(); Console.WriteLine("Hallo " + name + ", wie geht's?"); }
Blöcke werden in C#, wie in C++ und in Java, mit geschweiften Klammern umschlossen. Wie in anderen Sprachen auch, kann ein Block überall dort eingesetzt werden, wo eine einzelne Anweisung erwartet wird. Die if-Verzweigung kann z. B. mit einzelnen Anweisungen arbeiten. Die folgende Anweisung überprüft, ob heute Sonntag ist, und gibt in diesem Fall einen entsprechenden Text an der Konsole aus: if (System.DateTime.Now.DayOfWeek == 0) Console.WriteLine("Heute ist Sonntag.");
Wenn für einen Fall, dass heute Sonntag ist, nun aber mehrere Anweisungen ausgeführt werden sollen, müssen Sie einen Block einsetzen: if (System.DateTime.Now.DayOfWeek == 0) { Console.WriteLine("Heute ist Sonntag."); Console.WriteLine("Heute wird nicht gearbeitet."); Console.WriteLine("Heute gehen wir snowboarden."); }
INFO
Falls Sie die letzte Anweisung nicht verstehen, weil Sie irgendwo gelesen haben, dass ich am Niederrhein wohne (zurzeit allerdings in meinem anderen Wohnsitz in Dublin): Auch am Niederrhein kann man snowboarden. In Neuss. In der Jever Skihalle. Sogar im Sommer ☺. Würden Sie die Blockklammern weglassen, if (System.DateTime.Now.DayOfWeek == 0) Console.WriteLine("Heute ist Sonntag."); Console.WriteLine("Heute wird nicht gearbeitet."); Console.WriteLine("Heute gehen wir snowboarden.");
würde der Compiler nur die erste Anweisung der if-Verzweigung zuordnen und die zweite und dritte immer ausführen, auch dann, wenn kein Sonntag ist. Ich fände das zwar recht nett (jeden Tag snowboarden ...), korrekt ist die Programmierung aber nicht.
148
Anweisungen
Die Blockklammern teilen dem Compiler also mit, dass die darin enthaltenen Anweisungen zusammengehören. Verwenden Sie grundsätzlich für alle Strukturanweisungen (wie if und while) für die enthaltenen Anweisungen Blockklammern, auch wenn nur eine Anweisung enthalten ist. Damit erhöhen Sie die Lesbarkeit, Wartbarkeit und Erweiterbarkeit Ihrer Programme und sorgen wahrscheinlich gleich auch noch dafür, dass diese fehlerfreier sind.
TIPP
1
Schreiben Sie also besser:
2
if (System.DateTime.Now.DayOfWeek == 0) { Console.WriteLine("Heute ist Sonntag."); }
3
an Stelle von: if (System.DateTime.Now.DayOfWeek == 0) Console.WriteLine("Heute ist Sonntag.");
Ich setze diese Konvention in diesem Buch (hoffentlich ☺) konsequent ein.
3.4.3
4
Sichere und unsichere Anweisungen 5
Normale Anweisungen sind in C# sicher. Die CLR überprüft sichere Anweisungen daraufhin, ob diese u. U. Operationen ausführen, die Speicherbereiche beeinflussen würden, die nicht zum aktuellen Kontext gehören. Die CLR erlaubt deswegen z. B. keine Zeiger, weil diese es einem Programm erlauben, beliebig in den Speicher zu schreiben und daraus zu lesen.
6
Wenn Sie unsichere Features wie Zeiger verwenden wollen (was nur in absoluten Ausnahmefällen sinnvoll ist), können dazu unsichere Anweisungsblöcke einsetzen. Diese Blöcke leiten Sie mit dem Schlüsselwort unsafe ein:
7
unsafe { ... }
8
Alternativ können Sie ganze Methoden als unsicher kennzeichnen: unsafe void UnsafeDemo() { ... }
9
In einem unsicheren Block können Sie alle Anweisungen unterbringen, auch solche, die die CLR als unsicher erkennen würde. Unsichere Blöcke benötigen Sie immer dann, wenn Sie direkt auf den Speicher zugreifen wollen.
10
Normalerweise müssen Sie dies nur tun, wenn Sie extrem schnelle Programme schreiben wollen. Mit Zeigern können Sie eben direkt mit dem Speicher arbeiten. Zeiger verursachen aber auch massive Probleme, da Sie damit versehentlich Speicherbereiche überschreiben oder auslesen können, die Sie gar nicht reserviert haben. Da dieses Thema wohl eher die C++-Programmierer interessiert, die systemnahe Programme wie z. B. Treiber entwickeln, behandle ich unsichere Programmierung in diesem Buch nicht weiter.
11
149
Die Sprache C#
3.4.4 Über Kommentare wird Quellcode kommentiert
Kommentare
Kommentare gehören auch zu den Anweisungen, allerdings werden Kommentare vom Compiler nicht berücksichtigt. Sie dienen lediglich der Kommentierung des Quellcodes. C# kennt vier Arten von Kommentaren: Einfache Kommentare, mehrzeilige einfache Kommentare, Aufgabenkommentare und Dokumentationskommentare. Unabhängig von der Art des Kommentars kompiliert der Compiler diese nicht mit in die Assembly.
TIPP
Kommentare im Quellcode sind in den meisten Programmen der Schlüssel zum Verständnis eines Programms. Gute Programme enthalten etwa so viele Kommentarzeilen wie Programmzeilen. Kommentieren Sie also immer ausführlich.
Einfache Kommentare Einfache Kommentare enthalten lediglich einen beschreibenden Text, der das Verständnis eines Programmteils erleichtert. Einfache Kommentare können Sie an das Ende einer Zeile anfügen, indem Sie diese mit zwei Schrägstrichen einleiten: Console.WriteLine("Hello World"); /* Das ist ein einfacher Kommentar
Sie können diese aber natürlich auch in separaten Zeilen unterbringen (was ich bevorzuge): // Das ist ein einfacher Kommentar Console.WriteLine("Hello World");
Mehrzeilige einfache Kommentare werden mit /* eingeleitet und mit */ beendet: Console.WriteLine("Hello World"); /* Das ist ein mehrzeiliger Kommentar */
Visual Studio beginnt die zweite Zeile eines mehrzeiligen Kommentars automatisch mit einem Stern: /* Das ist ein mehrzeiliger Kommentar, * der mit Visual Studio * erzeugt wurde. */
Die Sterne vor den einzelnen Zeilen dienen lediglich der optischen Darstellung und haben keine weitere Bedeutung.
Aufgabenkommentare Aufgabenkommentare werden in der Aufgabenliste angezeigt
Wie ich bereits in Kapitel 2 beschrieben habe, können Sie den Text eines Kommentars automatisch in die Aufgabenliste übernehmen, wenn Sie den Kommentar mit einem Aufgabentoken beginnen: static void Main() { // TODO: Den Rest programmieren :-) }
Visual Studio fügt neuen Projekten per Voreinstellung die Aufgabentoken HACK, TODO, UNDONE und UnresolvedMergeConflict hinzu. Ich nutze in meinen Programmen lediglich TODO für Stellen, an denen noch gearbeitet werden muss, und HACK für Workarounds um Probleme, für die ich keine zufrieden stellende Lösung gefunden habe. Sie sollten diese speziellen Aufgabenkommentare immer dann nutzen, wenn irgendwo im Quellcode noch irgendetwas zu tun ist. Da diese Kommentare immer dann automatisch in der Aufgabenliste erscheinen, wenn die entsprechende Datei
150
Anweisungen
geöffnet ist, und Sie diese Liste auch gut filtern können (etwa nur die Aufgabenkommentare anzeigen lassen), haben Sie eine gute Übersicht über die noch zu erledigenden Aufgaben. Sie sollten allerdings beachten, dass Visual Studio nur die Aufgabenkommentare der aktuell geöffneten Dateien anzeigt. Aufgabenkommentare von nicht geöffneten Dateien werden (unverständlicherweise) nicht in der Aufgabenliste angezeigt. Eine Lösung dieses Problems ist die zusätzliche Definition einer Verknüpfung mit (STRG) + (K) + (H) auf einem Aufgabenkommentar. Verknüpfungen werden in der Aufgabenliste auch dann angezeigt, wenn die betroffene Datei geschlossen ist. Zur Anzeige von Verknüpfungen müssen Sie allerdings den Filter der Aufgabenliste entsprechend einstellen.
HALT
1
2
Dokumentationskommentare Zur Dokumentation eines Programms gehören nicht nur einfache Kommentare, die den Quelltext verständlicher machen. Ein wichtiger Teil der Dokumentation ist der, der die Klassen eines Programms und deren Methoden, Eigenschaften und andere Elemente für andere Programmierer beschreibt. Die .NET Framework-Dokumentation ist ein gutes Beispiel dafür.
Aus Dokumentationskommentaren kann eine externe Dokumentation erzeugt werden
4
Diese Art der Dokumentation ist besonders wichtig für Klassen, die in Klassenbibliotheken veröffentlicht und somit auch von anderen Programmierern verwendet werden. Sind die Klassen verständlich und ausführlich dokumentiert, müssen Programmierer, die Ihre Klassenbibliotheken anwenden, sich nicht an Sie wenden, um zu erfahren, wie Ihre Klassen verwendet werden. Und Sie haben dann wieder etwas mehr Zeit zum Windsurfen, Snowboarden oder für Ihre Kinder.
5
6
Eine gute Dokumentation zu erstellen erfordert einiges an Arbeit. Die meiste Arbeit haben Sie beim Schreiben der Dokumentationskommentare. Das Erstellen einer Dokumentation selbst ist dann relativ einfach, allerdings leider nicht (mehr1) mit Visual Studio möglich.
7
Die Basis für diese Dokumentation ist eine XML-Datei, die vom Compiler erzeugt werden kann. In Visual Studio geben Sie dazu einfach den Namen der Datei in den Projekteigenschaften an. Diese XML-Datei ist für Menschen wenig hilfreich, enthält aber alle Informationen, die zum Erstellen einer auch für Menschen lesbaren Dokumentation notwendig sind. Über ein externes Tool (z. B. Doxygen) können Sie aus der XML-Datei dann eine ansprechend formatierte Dokumentation erzeugen. Wie das geht, zeige ich in Kapitel 8.
8
9
Dokumentationskommentare werden mit drei Schrägstrichen eingeleitet. Mit diesen Kommentaren können Sie alle Typen (Klassen, Strukturen, Aufzählungen etc.) und deren Elemente (Methoden, Felder, Eigenschaften, Ereignisse etc.) dokumentieren. Innerhalb der Kommentare können Sie einfachen Text und XML-Elemente unterbringen. Über XML-Elemente können Sie eine eigene XML-Struktur aufbauen (was aber in der Praxis nur sehr selten verwendet wird). Sie können eigene XML-Elemente einfügen (die Sie dann aber auch selbst in der XML-Datei auswerten müssen) oder einige der vordefinierten (die von den Tools zur Erzeugung einer HTML-Dokumentation verwendet werden). Das Element summary wird z. B. verwendet, um einen Typen oder ein Element zusammenfassend zu beschreiben:
1
3
10
11
In Visual Studio 2003 war das Erstellen einer HTML-Dokumentation noch möglich.
151
Die Sprache C#
/// /// Der Einstiegspunkt der Anwendung /// [STAThread] static void Main() ...
Zur Erstellung der XML-Datei geben Sie deren Dateinamen in den Projekteigenschaften im Register ERSTELLEN an. Wählen Sie dazu die Option XML-DOKUMENTATIONSDATEI und ändern Sie ggf. den vorgeschlagenen Dateipfad. Beachten Sie, dass Sie diese Einstellung für jede Konfiguration (Debug, Release) einzeln vornehmen können bzw. müssen. Wenn Sie das Projekt erstellen oder mit der (F5)-Taste starten, wird die XML-Dokumentationsdatei automatisch erzeugt. In Kapitel 8 erfahren Sie dann mehr über Dokumentationskommentare. Außerdem stelle ich in diesem Kapitel ein Tool vor, über das Sie ansprechende Dokumentationen erzeugen können.
3.4.5
Der Aufruf von Methoden
Methoden enthalten, wie Sie ja wahrscheinlich bereits wissen, vorgefertigte Programme, die Sie über den Namen der Methode aufrufen können. Der Aufruf von Methoden ist ein wichtiger Teil beim Schreiben von Anweisungen.
Klassen- und Instanzmethoden Das Verständnis des Unterschieds zwischen Klassen- und Instanzmethoden ist ein wichtiger Schlüssel zum Verständnis des Aufrufs von Methoden. Dummerweise erfordert dies schon ein wenig OOP-Kenntnisse. Dieses Buch behandelt die OOP ab Kapitel 4. Ich will aber bereits hier versuchen, den Unterschied zwischen Klassenund Instanzmethoden zu erläutern. Wenn Sie den Unterschied bereits kennen, können Sie diesen Abschnitt (natürlich) auslassen. Eine Klasse ist ein Bauplan für Objekte
Wie ich in der Einführung bereits beschrieben habe, enthalten Klassen in der reinen OOP die Beschreibung von Objekten, die später, im Programm aus diesen Klassen erzeugt werden. Eine Klasse ist damit so etwas wie ein Bauplan für Objekte. Im Wesentlichen beschreibt eine Klasse, welche Methoden, Eigenschaften und Ereignisse die Objekte besitzen, die später aus der Klasse erzeugt werden. Wenn Sie einem Formular einer Windows-Anwendung z. B. TextBox-Steuerelemente hinzufügen, führt das in Wirklichkeit dazu, dass in der Laufzeit der Anwendung Instanzen der Klasse TextBox erzeugt und dem Formular hinzugefügt werden. Die Klasse TextBox beschreibt, welche Eigenschaften, Methoden und Ereignisse diese TextBox-Objekte besitzen. Objekte (Instanzen von Klassen) können Sie aber auch in Ihren Programmen erzeugen, was in diesem Buch noch sehr ausführlich genutzt wird.
Instanzmethoden gehören zu Instanzen
Instanzmethoden sind nun alle Methoden, die ein Objekt besitzt. Diese Methoden können nur über das Objekt (eine Instanz) aufgerufen werden. TextBox-Instanzen besitzen z. B. die Methode Focus, die bewirkt, dass der Eingabecursor in die TextBox gestellt wird.
Klassenmethoden gehören zu Klassen
Klassenmethoden (die auch als statische Methoden bezeichnet werden) haben eine andere Bedeutung. Sie gehören nicht zu den Objekten, die aus einer Klasse erzeugt werden, sondern zu der Klasse. Das ist vielleicht ein wenig schwer zu verstehen, aber im Wesentlichen geht es bei Klassenmethoden darum, dass diese aufgerufen werden können, ohne dass Instanzen der Klasse erzeugt werden müssen. Ein gutes Beispiel für solche Methoden sind die der Math-Klasse, die mathematische Berech-
152
Anweisungen
nung erlauben. Es wäre ziemlich unsinnig, wenn Sie zur Berechnung des Minimums von zwei Werten zunächst eine Instanz der Math-Klasse erzeugen müssten, um dann die Min-Methode des Math-Objekts aufrufen zu können. Für solch allgemeine Aufgaben wie mathematische Berechnungen sind Klassenmethoden, die eben direkt über die Klasse aufgerufen werden können, wesentlich besser geeignet. Ich hoffe, ich habe den Unterschied damit einigermaßen gut erläutert ☺.
Grundlagen zum Aufruf von Methoden
1
Methoden werden immer über deren Namen aufgerufen, gefolgt von Klammern, in denen Sie der Methode in vielen Fällen Argumente übergeben, die die Ausführung steuern. Handelt es sich um eine Klassenmethode, geben Sie den Namen der Klasse als Präfix an. Den Präfix trennen Sie durch einen Punkt vom Methodennamen. Das folgende Beispiel ruft die Klassenmethode WriteLine der Console-Klasse auf:
2
Console.WriteLine("Hello World");
3
Handelt es sich um eine Instanzmethode, geben Sie an Stelle der Klasse die Variable an, die die Instanz der Klasse (das Objekt) verwaltet. Das folgende Beispiel erzeugt zwei Instanzen der StreamWriter-Klasse (die zum Schreiben von Texten in Dateien verwendet wird) und ruft deren WriteLine-Methode auf.
4
Listing 3.6: Aufrufen von Instanzmethoden am Beispiel von StreamWriter-Instanzen StreamWriter sw1 = new StreamWriter("C:\\Demo1.txt"); sw1.WriteLine("Hello World"); StreamWriter sw2 = new StreamWriter("C:\\Demo2.txt"); sw1.WriteLine("Und hier auch: Hello World");
5
Das Erzeugen von Instanzen soll an dieser Stelle noch nicht behandelt werden. Das Beispiel benötigt jedoch eine Instanz, da eine Instanzmethode aufgerufen wird. Die WriteLine-Methode, die hier verwendet wird, ist in der Klasse StreamWriter definiert, aber eben nicht als Klassenmethode (wie die gleichnamige Methode in der Console-Klasse), sondern als Instanzmethode. Deswegen kann sie nur über eine Instanz der Klasse (ein Objekt) aufgerufen werden. Falls Sie sich wundern, dass die Console- und die StreamWriter-Klasse eine gleich aussehende WriteLine-Methode besitzen: Das Schreiben von Daten basiert in beiden Klassen auf dem Stream-Konzept, und dieses stellt eben eine WriteLine-Methode zur Verfügung, die beide Klassen auf identische Weise nutzen. Die Console-Klasse schreibt die Daten aber an die Konsole, die StreamWriter-Klasse schreibt die Daten in eine Datei (bzw. in einen beliebigen Stream).
6
7
8
EXKURS
9
An diesen Beispielen können Sie auch gut den Unterschied zwischen Klassen- und Instanzmethoden erkennen: In einer Konsolenanwendung existiert die Konsole nur genau ein Mal. Deswegen wäre es unsinnig, wenn zunächst Instanzen der ConsoleKlasse erzeugt werden müssten, um mit der Konsole zu arbeiten. Dateien können hingegen (in mehreren StreamWriter-Instanzen) beliebig viele geöffnet werden. Es ist also absolut notwendig, dass die Methoden der StreamWriter-Klasse sich auf die Datei beziehen, die das jeweilige StreamWriter-Objekt verwaltet. Deswegen müssen diese Methoden Instanzmethoden sein.
10
11
Sie geben also normalerweise beim Aufruf von Klassenmethoden den Namen der Klasse und beim Aufruf von Instanzmethoden den Namen der das Objekt referenzierenden Variable als Präfix an. Eine Ausnahme von dieser Regel ist, wenn Sie Methoden aufrufen, die in der Klasse deklariert sind, in der der Aufruf erfolgt. Dann können Sie
153
Die Sprache C#
den Klassen- bzw. Instanznamen weglassen. Sie können (und sollten) in diesem Fall bei Klassenmethoden aber auch den Klassennamen und bei Instanzmethoden eine Referenz auf die Instanz der Klasse angeben. Eine Referenz auf das Objekt, das später aus einer Klasse erzeugt wird, erhalten Sie innerhalb der Klasse über das Schlüsselwort this. Darüber, und über Klassenmethoden, erfahren Sie noch mehr in Kapitel 4.
Methoden und Namensräume Wie Sie ja bereits wissen, sind Klassen in Namensräumen organisiert. Wenn Sie den Namensraum nicht über die using-Direktive »importiert« haben, müssen Sie dessen kompletten Namen beim Aufruf einer Methode angeben. Die Console-Klasse gehört z. B. zum Namensraum System. Den vollständigen Aufruf der WriteLine-Methode zeigt der folgende Quellcode: System.Console.WriteLine("Hello World");
Binden Sie den System-Namensraum über using ein, müssen Sie nur noch die Klasse beim Aufruf angeben: Console.WriteLine("Hello World");
Argumente übergeben Die Deklaration der Methode legt fest, wie viele Argumente welchen Typs übergeben werden müssen. In Visual Studio zeigt IntelliSense beim Schreiben einer Anweisung die Deklaration(en) der Methode und damit die zu übergebenden Argumente an (Abbildung 3.1). Abbildung 3.1: IntelliSense beim Schreiben von Anweisungen mit Methoden
Viele Methoden liegen in mehreren, so genannten »überladenen« Varianten vor. Das Überladen von Methoden erläutere ich noch näher für eigene Methoden in Kapitel 4, fürs Erste reicht es aus, dass Sie wissen, dass eine Methode auch in mehreren Varianten vorkommen kann. Die WriteLine-Methode kommt z. B. (zurzeit) in 19 Varianten vor. Die einzelnen Varianten können Sie im IntelliSense-Fenster über die Cursortasten oder einen Klick auf die Pfeile anzeigen lassen. Die Variante 11 erwartet z. B. nur einen einfachen String (eine Zeichenkette) als Argument (Abbildung 3.2). Abbildung 3.2: Die Variante 11 der WriteLine-Methode
Gibt die Methode einen Wert zurück, können Sie diesen Wert in Zuweisungen, in arithmetischen oder logischen Ausdrücken oder als Argument einer anderen Methode verwenden. Stellen Sie sich einfach vor, dass der Compiler Methoden immer zuerst aufruft, bevor er weitere Teile der Anweisung auswertet und das Ergebnis der Methode dann als Wert in der Anweisung weiterverarbeitet. So sind einfache Zuweisungen möglich: sinY = Math.Sin(y);
oder Ausdrücke: result = Math.Min(x1, x2) / Math.Max(y1, y2);
154
(Daten)Typen
oder geschachtelte Aufrufe: result = Math.Min(y1, Math.Max(y2, y3));
Ein wichtiger Grundsatz beim Aufruf von Methoden ist: Überall da, wo ein bestimmter Datentyp erwartet wird, können Sie neben Literalen, Konstanten und Variablen immer auch eine Methode einsetzen, die diesen Datentyp zurückgibt.
3.5
TIPP
1
(Daten)Typen
Typen kommen in einem Programm sehr häufig vor. Ein Ausdruck wie 1 + 1.5 beinhaltet z. B. Operanden, die einen bestimmten Typ besitzen. Im Beispiel sind das die »Typen« Ganzzahl (eigentlich: int) und Dezimalzahl (eigentlich: double). Und auch das Ergebnis eines Ausdrucks besitzt einen Typ (im Beispiel: double).
2
Daneben besitzen Variablen, Konstanten und Literale (einfache Wertangaben) einen Typ, genau wie Parameter und Rückgabewerte von Methoden.
3
C# unterstützt die üblichen Typen wie z. B. int (Integer) und double. Die in C# normalerweise verwendeten Typ-Schlüsselwörter sind allerdings nur Aliasnamen für Typen, die im .NET Framework definiert sind. int steht z. B. für System.Int32. Prinzipiell können Sie immer die .NET-Typen verwenden, z. B. um eine Variable zu deklarieren:
4
System.Int32 i;
5
Verwenden Sie aber lieber die C#-Aliasnamen, damit Ihr Quellcode besser lesbar wird.
3.5.1
Typsicherheit und der Typ Object
C# ist eine typsichere Sprache. Wenn irgendwo ein bestimmter Typ erwartet wird, können Sie nur einen Typ einsetzen, der zum erwarteten passt. Die Zuweisung eines String-Literals an eine int-Variable ist z. B. nicht möglich:
6 C# ist typsicher
7
string s = "10"; int i = s; // Fehler
Das Beispiel resultiert im Compilerfehler »Eine implizite Konvertierung vom Typ "string" in "int" ist nicht möglich«. C# sichert damit ab, dass Sie nicht versehentlich einen falschen Typ verwenden. Wollen Sie trotzdem einen anderen Typ einsetzen, als der Compiler erwartet, müssen Sie diesen explizit konvertieren. Wie das geht, zeige ich in Abschnitt »Konvertierungen« ab Seite 188. Deswegen folgt hier nur ein kleines Beispiel:
8
9
int i = Convert.ToInt32(s);
Ist der zugewiesene Typ kompatibel und kann er die zugewiesenen Daten ohne Verlust aufnehmen, wird er normalerweise automatisch (implizit) konvertiert. Die Zuweisung eines byte-Typs an einen int-Typ ist z. B. problemlos möglich, weil byte immer in einen int-Typ hineinpasst:
10
11
byte b = 10; int i = b;
Alles ist ein Objekt In C# sind alle Typen (auch eigene!) von der Basisklasse Object abgeleitet. Object stellt den .NET-Typen einige Methoden zur Verfügung, die ich im Abschnitt »Der Typ Object« (Seite 185) erläutere.
Alle Typen sind von Object abgeleitet
155
Die Sprache C#
Object stellt einige Methoden zur Verfügung
In den von Object abgeleiteten Typen werden diese Methoden normalerweise (d. h. nicht unbedingt immer) mit einer neuen Implementierung überschrieben. Die ToString-Methode gibt z. B. bei einem int-Datentyp die gespeicherte Zahl als Zeichenkette zurück: int i = 10; Console.WriteLine(i.ToString());
Bei einem double-Typ wird die Zeichenkette unter Berücksichtigung einer Ländereinstellung zurückgegeben. ToString können Sie ohne Argument aufrufen, dann wird die Systemeinstellung verwendet: double x = 1.234; Console.WriteLine(x.ToString());
Auf deutschen Systemen kommt dabei der String »1,234« heraus. Alternativ können Sie diese Methode bei einem double-Wert auch mit einem Format-String aufrufen: Console.WriteLine(x.ToString("0.00"));
Die Zahl wird nun auf zwei Stellen hinter dem Komma formatiert ausgegeben. Den Umgang mit Zeichenketten und deren Formatierung beschreibe ich in Kapitel 8. Die Verwaltung aller Typen als Objekt ist sogar so konsequent, dass Sie auch für einfache Konstanten die Methoden des zugrunde liegenden Typs aufrufen können: string s = 10.ToString();
Die verschiedenen Typen des .NET Framework bieten meist noch zusätzliche Methoden und Eigenschaften. Der Typ String besitzt z. B. eine Vielzahl an Methoden zur Arbeit mit Zeichenketten. Ein Beispiel dafür ist die Replace-Methode, über die Sie einen Teilstring durch einen anderen String ersetzen können. Einige der weiteren Methoden der einzelnen Typen sind statisch. Diese (Klassen-) Methoden können Sie (wie Sie ja bereits wissen) aufrufen, ohne eine Instanz des Datentyps zu besitzen. Die Format-Methode der String-Klasse ist z. B. eine solche statische Methode: Console.WriteLine(String.Format("{0:0.00}", 1.234));
Ich beschreibe die wichtigsten dieser Methoden in Kapitel 8.
3.5.2
Wert- und Referenztypen
C# unterscheidet bei den Typen grundsätzlich Wert- und Referenztypen. Zu den Werttypen gehören alle Standardtypen (außer string), Strukturen und Aufzählungen. Alle anderen Typen (um genau zu sein, außer Methoden und Delegaten, die auch Typen sind) sind Klassen und damit Referenztypen. Ein wesentlicher Unterschied zwischen Wert- und Referenztypen ist, dass die Daten von Werttypen im Stack gespeichert werden, die Daten von Referenztypen werden auf dem Heap gespeichert.
EXKURS
156
Der Stack ist ein spezieller Speicherbereich, den der Compiler für jede aufgerufene Methode neu reserviert. Alle lokalen Daten einer Methode werden, sofern es sich um Werttypen handelt, auf dem Stack abgelegt. Der Stack wird auch intern verwendet, um Argumente an eine Methode zu übergeben. Der aufrufende Programmteil legt die Argumente, die an die Methode übergeben werden sollen, auf dem Stack ab, die Methode liest diese Argumente dann aus dem Stack aus.
(Daten)Typen
Der Heap ist ein anderer Speicherbereich, der allerdings global für das gesamte Programm gilt und so lange besteht, wie das Programm läuft. Auf dem Heap werden üblicherweise programmglobale Daten, aber eben auch Referenztypen abgelegt. Wenn Sie selbst Typen entwickeln, können Sie nur bei den strukturierten Typen (nicht bei Aufzählungen, die per Definition Werttypen sind) entscheiden, ob Sie einen Wert- oder einen Referenztypen implementieren: Entweder Sie programmieren eine Struktur (also einen Werttypen) oder eine Klasse (also einen Referenztypen). Wie das geht, zeige ich in Kapitel 4. Ich benötige für diesen Abschnitt allerdings Beispiele, deswegen zeige ich kurz, wie Sie eine einfache Struktur und eine einfache Klasse deklarieren. Die Typen im Beispiel verwalten Personendaten:
Strukturen sind Werttypen, Klassen sind Referenztypen
1
2
Listing 3.7: Struktur (Werttyp) und Klasse (Referenztyp), die Personendaten speichern public struct PersonStruct { public string FirstName; public string LastName; }
3 4
public class PersonClass { public string FirstName; public string LastName; }
5
Wenn der Typ einer Variablen (oder eines Feldes oder einer Eigenschaft) ein Werttyp ist, speichert die Variable den Wert direkt (auf dem Stack). Ist der Typ einer Variablen ein Referenztyp, speichert die Variable nicht den Wert, sondern lediglich die Adresse des Speicherbereichs, der den Wert verwaltet. Die Adresse (der Wert der Variablen) wird auf dem Stack verwaltet, der eigentliche Wert auf dem Heap.
6
Wenn Sie z. B. auf den oben deklarierten Beispieltypen in einer Methode je zwei Objekte erzeugen, sieht der Speicher schematisch dargestellt aus wie in Abbildung 3.3.
7 Abbildung 3.3: Schematische Darstellung des Stack und des Heap nach der Erzeugung von je zwei PersonStruct- und PersonClassObjekten
8
9
10
11
157
Die Sprache C#
Unterschiede zwischen Wert- und Referenztypen Werttypen werden schneller gelesen und geschrieben
EXKURS
Wert- und Referenztypen verhalten sich bei Zuweisungen unterschiedlich
Referenztypen erlauben beliebig viele Referenzen
Da Werttypen direkt im Stack gespeichert sind, kann ein Programm den Wert dieser Typen sehr performant lesen und schreiben. Um auf einen Referenztypen zuzugreifen, benötigt ein Programm hingegen zwei Schritte: Es muss zunächst die Adresse der Daten aus dem Stack auslesen und kann erst dann mit Hilfe dieser Adresse auf die Daten im Heap zugreifen. Referenztypen werden also prinzipiell etwas langsamer bearbeitet als Werttypen. Ich denke aber nicht, dass dieser Unterschied zwischen Wert- und Referenztypen in der Praxis (bei den heutigen schnellen Rechnern) eine große Rolle spielt. In einem Test (den Sie auch in den Beispielen auf der Buch-DVD finden) habe ich ermittelt, dass auf meinem Rechner das Erzeugen von 1.000.000 einfachen Objekten ca. 0,0026 Sekunden benötigte, wenn es sich um Werttypen handelte. Bei Referenztypen benötigte das Erzeugen ca. 0,052 Sekunden. Werttypen wurden in diesem Test also etwa 20-mal schneller erzeugt als Referenztypen. Das Schreiben einer Eigenschaft dieser Typen benötigte ca. 0,0019 Sekunden bei Wert- und ca. 0,0089 Sekunden bei Referenztypen (jeweils natürlich auch wieder bei 1.000.000 Instanzen). Werttypen waren auch hier schneller, allerdings nur etwa 4,7-mal. Beim Lesen war das Verhältnis etwas ausgeglichener (0,0017 Sekunden zu 0,0042 Sekunden, was dem Faktor 2,4 entspricht). Der wesentliche Unterschied zwischen Wert- und Referenztypen basiert auf ihrem Verhalten bei Zuweisungen und bei der Übergabe an Methoden: Wenn Sie einen Werttypen auf einen anderen Werttypen zuweisen oder einen Werttypen an ein Argument einer Methode übergeben, erzeugt der Compiler immer eine Kopie der gespeicherten Daten (des Objekts). Die Übergabe von Werttypen, die große Datenmengen speichern, an Methoden, kann somit recht viel Zeit in Anspruch nehmen. Bei Referenztypen wird bei Zuweisungen oder beim Übergeben an Methoden allerdings keine Kopie des Objekts erzeugt. Die Referenz, die den Referenztypen referenziert (die Variable), speichert schließlich nur die Adresse des Objekts und nicht das Objekt selbst. Wenn Sie eine Variable, die einen Referenztypen verwaltet, auf eine andere Variable oder an ein Argument einer Methode zuweisen, wird nur die Adresse des Objekts kopiert, nicht das Objekt selbst. Die Übergabe von Referenztypen an Methoden ist deswegen wesentlich performanter und speicherschonender als die Übergabe von Werttypen. Referenztypen sind somit zur Übergabe an Methoden besser geeignet als Werttypen. Ein weiteren Unterschied ist, dass auf Referenztypen mehrere beliebige Referenzen zeigen können. Das ergibt sich schon daraus, dass bei Zuweisungen oder bei der Übergabe eines Referenztyps an eine Methode nur die Referenzen kopiert werden. Dieses Feature wird bei der objektorientierten Programmierung sehr häufig verwendet. Mit Werttypen ist das prinzipiell nicht möglich, weil der Compiler immer die Werte kopiert2. Wenn Sie mit Typen arbeiten, sollten Sie das Verhalten von Wert- und Referenztypen bei Zuweisungen und bei der Übergabe an Methoden immer im Auge haben.
INFO
2
158
Eine Ausnahme ist die Übergabe von Argumenten an eine Methode mit der Übergabeart »By Reference« (siehe Kapitel 4).
(Daten)Typen
Ein letzter Unterschied ist schließlich, dass Werttypen von der Klasse ValueType abgeleitet sind und Referenztypen nicht. ValueType überschreibt die von Object geerbte Methode Equals, die das Objekt, auf dem sie aufgerufen wird, mit dem übergebenen Objekt auf Gleichheit überprüft. In der originalen Object-Variante überprüft diese Methode lediglich, ob die Referenzen der beiden Objekte gleich sind. In der ValueType-Variante vergleicht diese Methode alle Felder auf Gleichheit (wozu Reflection verwendet wird, weswegen diese Methode unter Umständen relativ langsam ist).
Werttypen sind von ValueType abgeleitet
1
Eine in einigen besonderen Situationen hilfreiche Technik ist, dass Sie bei einem Objekt über eine Abfrage auf ValueType ermitteln können, ob es sich um einen Werttypen handelt (was u. U. wichtig ist, wenn Sie mit einer Variablen vom Typ object arbeiten):
2
Listing 3.8: Abfrage darauf, ob ein Objekt ein Wert- oder Referenztyp ist object o = 10; if (o is ValueType) { Console.WriteLine("o } else { Console.WriteLine("o } o = new PersonClass(); if (o is ValueType) { Console.WriteLine("o } else { Console.WriteLine("o }
3 ist ein Werttyp");
4 ist ein Referenztyp");
5 ist jetzt ein Werttyp");
6
ist jetzt ein Referenztyp");
Was mich anfangs immer wunderte, war die Tatsache, dass Werttypen genau wie Referenztypen von Object abgeleitet sind. Object ist aber eine Klasse und damit ein Referenztyp. Wird normalerweise von einem Referenztyp abgeleitet, resultiert immer auch ein Referenztyp. .NET lässt auch gar nicht zu, dass ein Werttyp (eine Struktur) von einem Referenztyp (einer Klasse) abgeleitet wird, da Strukturen nicht abgeleitet werden können.
7 EXKURS
8
Die Lösung dieses Problems habe ich nicht recherchieren können und deshalb selbst hergeleitet ☺:
9
Werttypen sind in Wirklichkeit auch Instanzen von Klassen. Eine Variable, deren Typ ein Werttyp ist, ist in Wirklichkeit auch eine Referenz auf ein Objekt. Werttypen werden aber vom Compiler und von der CLR anders behandelt als Referenztypen. Werttypen werden im Stack angelegt, Referenztypen im Heap. Bei Zuweisungen werden Werttypen kopiert, Referenztypen allerdings nicht. Wie aber erkennen der Compiler und die CLR, dass es sich bei einem Objekt um einen Werttypen handelt?
10
11
Die Lösung ist einfach: Werttypen sind, wie ich ja bereits beschrieben habe, nicht direkt von Object abgeleitet, sondern von der Klasse ValueType. Der Compiler und die CLR erkennen einen Werttypen daran, dass dieser mehr oder weniger direkt von ValueType abgeleitet ist. Der Compiler legt die Daten eines Objekts, das (mehr oder weniger direkt) von ValueType abgeleitet ist, im Stack an (statt im Heap wie bei Refe-
159
Die Sprache C#
renztypen). Die CLR erzeugt bei der Weitergabe einer Referenz auf ein Werttyp-Objekt implizit eine neue Instanz, kopiert die Daten des Objekts in diese und schreibt die neue Referenz in die Variable, das Feld oder Argument, auf das zugewiesen wurde.
Erzeugen von Werttypen Werttypen werden implizit erzeugt
Werttypen werden implizit erzeugt, wenn Sie Variablen (Felder etc.) der entsprechenden Wertypen deklarieren. Das folgende Beispiel deklariert eine int-Variable und eine Instanz der PersonStruct-Struktur: int i; PersonStruct personStructInstance1;
Der Compiler reserviert an der Stelle der Deklaration einen Speicherbereich im Stack, der für die Standardtypen (nicht für Strukturen!) allerdings noch uninitialisiert ist. Beim Kompilieren von Programmen, in denen uninitialisierte Variablen gelesen werden, meldet der Compiler den Fehler »Verwendung der nicht zugewiesenen lokalen Variablen x« bzw. »Verwendung des möglicherweise nicht zugewiesenen Feldes y«. Die Initialisierung können Sie bei den Standardtypen direkt bei der Deklaration vornehmen: int i = 0;
Strukturen werden automatisch mit Leerwerten initialisiert
Strukturen werden (wie Klassen) vom Compiler bei der Erzeugung automatisch mit Leerwerten initialisiert. Näheres dazu erfahren Sie im Abschnitt »Initialisierung von Objekten bei der Erzeugung« (Seite 161). Strukturen können sich bei der Erzeugung (in ihrem Konstruktor) aber auch selbst initialisieren, was allerdings in der Regel nur dann der Fall ist, wenn bei der Erzeugung Initialisierungswerte übergeben werden. Sie können die Felder und Eigenschaften einer Struktur aber natürlich auch selbst initialisieren. Eine Möglichkeit dazu ist das Schreiben in die Eigenschaften des Objekts: personStructInstance1.FirstName = "Zaphod"; personStructInstance1.LastName = "Beeblebrox";
Die andere (und bessere) Möglichkeit über Objektinitialisierer zu initialisieren zeigt der Abschnitt »Initialisierung von Objekten bei der Erzeugung«. Der Compiler sichert damit ab, dass Werttypen immer einen definierten Wert speichern. Schließlich können Werttypen wie Referenztypen auch explizit erzeugt werden: int i = new int();
Dieses Feature wird genutzt, wenn der Konstruktor des Werttypen Argumente besitzt und damit eine Initialisierung bei der Erzeugung erlaubt.
Erzeugen von Referenztypen Referenztypen müssen explizit erzeugt werden
Wenn Sie eine Variable (Feld etc.) eines Referenztyps deklarieren, verweist dieser noch nicht auf eine Instanz des Typs: PersonClass personClassInstance1;
Wie bei den Standardtypen (aber anders als bei Strukturen) meldet der Compiler einen Fehler, wenn Sie uninitialisierte Referenztypen im Programm verwenden. Sie können der verwendeten Variablen aber den Wert null zuweisen. Dieser Wert steht dafür, dass eine Referenz auf keine Instanz zeigt: PersonClass personClassInstance2 = null;
160
(Daten)Typen
Beim Versuch, mit einer Referenz, die null »speichert«, zu arbeiten, generiert jedoch die CLR eine Ausnahme (eine NullRefererenceException). Sie können allerdings abfragen, ob ein Referenztyp auf eine Instanz zeigt, indem Sie mit null vergleichen: Listing 3.9: Abfrage darauf, ob eine Referenz null speichert if (personClassInstance2 != null) { Console.WriteLine(personClassInstance2.FirstName); } else { Console.WriteLine("Die Referenz personClassInstance2 " + "referenziert kein Objekt"); }
1
2
Möglicherweise werden Sie sich fragen, was das Ganze soll. In der Praxis kommt es aber sehr häufig vor, dass Referenztypen nicht auf eine Instanz zeigen. Gut, wenn Sie dann damit umgehen können ☺. Wenn Sie Referenztypen verwenden wollen, müssen Sie diese über das Schlüsselwort new erzeugen: personClassInstance1 = new PersonClass(); personClassInstance2 = new PersonClass();
3 Referenztypen müssen über new erzeugt werden
Initialisierung von Objekten bei der Erzeugung
4
5
Klassen werden (wie Strukturen) vom Compiler bei der Erzeugung automatisch mit Leerwerten initialisiert. Numerische Felder werden auf 0 gesetzt, boolesche Felder auf false, Datumsfelder erhalten das minimale Datum (1.1.0001 00:00:00), Strings. Referenztypen und Nullables (Seite 167) erhalten den Wert null. Ist der Typ des Feldes eine Struktur, wird diese entsprechend initialisiert.
Klassen werden wie Strukturen mit Leerwerten initialisiert
Wenn Sie Instanzen von Typen (Wert- oder Referenztypen) explizit erzeugen, können (oder müssen) Sie in den Klammern in vielen Fällen Argumente übergeben, die das Objekt direkt bei der Erzeugung initialisieren. Welche Werte übergeben werden können, legen die Konstruktoren dieser Typen fest. Ein Konstruktor ist eine spezielle Methode, die bei der Erzeugung eines Objekts automatisch aufgerufen wird. Konstruktoren werden in Kapitel 4 behandelt.
Konstruktoren erlauben die explizite Initialisierung
6
7
8
Die DateTime-Struktur besitzt z. B. (u. a.) einen Konstruktor, dem das Jahr, der Monat und der Tag des gewünschten Datums übergeben werden können:
9
DateTime date = new DateTime(2010, 12, 31);
Die meisten Typen besitzen mehrere Konstruktoren. IntelliSense unterstützt Sie bei der Auswahl des Konstruktors, der Ihren Ansprüchen gerecht wird.
10 In C# 3.0 können Sie Objekte bei der Erzeugung allerdings auch über Objektinitialisierer initialisieren. Dazu geben Sie statt den runden Klammern geschweifte an. In den Klammern können Sie nun die einzelnen öffentlichen (und nicht schreibgeschützten) Felder und Eigenschaften der Struktur bzw. Klasse angeben und diese mit passenden Werten versehen. Bei der PersonClass-Klasse sieht das z. B. so aus:
NEU
11
161
Die Sprache C#
Listing 3.10: Initialisieren mit Objektinitialisierern PersonClass personClassInstance1 = new PersonClass { FirstName = "Tricia", LastName = "McMillan" }; PersonClass personClassInstance2 = new PersonClass { FirstName = "Ford", LastName = "Prefect" };
Objekte können auch über Objektinitialisierer initialisiert werden
Die einzelnen Initialisierungen trennen Sie mit Kommata. Da Sie die Namen der Felder bzw. Eigenschaften angeben, spielt die Reihenfolge keine Rolle. Außerdem müssen Sie nicht alle Felder bzw. Eigenschaften initialisieren, sondern nur die, die Sie initialisieren wollen. Sie sollten aber beachten, dass Felder und Eigenschaften auch schreibgeschützt sein können. Solche Felder und Eigenschaften können Sie dann natürlich nicht initialisieren. Wenn der verwendete Typ keinen parameterlosen Konstruktor besitzt, müssen Sie wie beim normalen Erzeugen hinter der Typangabe Klammern angeben und in den Klammern die Parameter übergeben, die einer der Konstruktoren des Typs erwartet. Objektinitialisierer können Sie trotzdem verwenden. Nur machen diese dann in der Regel keinen Sinn, weil zumindest einer der Konstruktoren Ihre InitialisierungsBedürfnisse abdecken sollte.
EXKURS
Falls Sie interessiert, was dahintersteckt: Wenn Sie Objektinitialisierer verwenden, erzeugt der C#-Compiler daraus CIL-Code, der das Objekt ganz normal instanziert, gefolgt von separaten Initialisierungen der in der Initialisierungsliste verwendeten Eigenschaften und Feldern. Das Erzeugen der ersten Instanz der PersonClass-Klasse sieht im erzeugten CIL-Code prinzipiell so aus: PersonClass personClassInstance1 = new PersonClass(); personClassInstance1.FirstName = "Tricia"; personClassInstance1.LastName = "McMillan";
Falls Sie weiterhin interessiert, wie ich das herausgefunden habe: Dazu habe ich die erzeugte Assembly zunächst über den Reflector von Lutz Roeder (www.aisto.com/ roeder/dotnet) wieder in Quellcode zurückgewandelt, wobei ich allerdings in den Optionen eine Optimierung nach C# 2.0 eingestellt hatte, damit die neuen Features nicht berücksichtigt werden. Über den Microsoft CIL-Code-Disassembler (ildasm.exe) habe ich daraufhin überprüft, ob der vom Reflector erzeugte C#-Code dem CIL-Code entspricht.
Zuweisungen an Wert- und Referenztypen Den Standardtypen und Aufzählungen (deren Basis ein Integer-Typ ist) können Sie passende Werte direkt zuweisen, weil diese nur einen Wert speichern: int i; i = 10; // Zuweisung des Integer-Literals 10 an die Integer-Variable
Strukturen und Klassen sind hingegen strukturierte Typen, die in der Regel mehrere Werte verwalten. Es ist zwar prinzipiell möglich, dass eine Struktur oder Klasse auch die Zuweisung eines einfachen Werts erlaubt, in diesem Fall müssen aber spezielle (Konvertierungs-)Operatoren für die Klasse überschrieben worden sein. Dieses Thema wird in Kapitel 5 behandelt.
162
(Daten)Typen
Normalerweise müssen Sie bei Instanzen von Strukturen und Klassen deren Felder bzw. Eigenschaften beschreiben, um diese mit (neuen) Werten zu versorgen. PersonStruct personStructInstance1 personStructInstance1.FirstName = "Zaphod"; personStructInstance1.LastName = "Beeblebrox";
Sie können einer Variablen (bzw. einem Feld oder einer Eigenschaft) aber nicht nur Literale, sondern auch andere Variablen zuweisen. Dabei verhalten sich Wert- und Referenztypen aber grundlegend anders. Wenn Sie einen Werttypen einem anderen zuweisen, werden die Werte kopiert: int i, j; i = 10; j = i;
1 Das Zuweisen von Werttypen kopiert die Werte
Am Ende dieses Beispiels besitzt j den Wert 10, ist aber immer noch ein eigener Speicherbereich. Wenn Sie j danach verändern, wird i nicht davon beeinflusst:
2
3
j = 11; // i ist immer noch 10
Das funktioniert natürlich auch mit Struktur-Instanzen:
4
PersonStruct personStructInstance3; personStructInstance3.FirstName = "Marvin"; personStructInstance3.LastName = "The Robot"; PersonStruct personStructInstance4; personStructInstance4 = personStructInstance3;
5
Das Objekt personStructInstance4 speichert in diesem Fall die gleichen Werte wie das Objekt personStructInstance3. Es handelt sich aber um zwei verschiedene Objekte. Das können Sie sehen, wenn Sie eines der Objekte verändern und deren Daten ausgeben:
6
personStructInstance4.FirstName = "Prostetnik"; personStructInstance4.LastName = "Vogon Jeltz"; Console.WriteLine("Person-Struktur-Instanz 3: " + personStructInstance3.FirstName + " " + personStructInstance3.LastName); Console.WriteLine("Person-Struktur-Instanz 4: " + personStructInstance4.FirstName + " " + personStructInstance4.LastName);
7
8
Das Beispiel gibt die folgenden Texte an der Konsole aus: Person-Struktur-Instanz 3: Marvin The Robot; Person-Struktur-Instanz 4: Prostetnik Vogon Jeltz
Referenztypen verhalten sich beim Aufeinander-Zuweisen aber anders. Wenn Sie einen Referenztypen einem anderen Referenztypen zuweisen, werden nicht die Werte, sondern die Referenzen kopiert.
Das Zuweisen von Referenztypen kopiert die Referenz
Das folgende Beispiel erzeugt zunächst eine Instanz der PersonClass-Klasse und weist diese dann einer weiteren Variablen zu: PersonClass personClassInstance3 = new PersonClass(); personClassInstance3.FirstName = "Marvin"; personClassInstance3.LastName = "The Robot"; PersonClass personClassInstance4; personClassInstance4 = personClassInstance3; Console.WriteLine("Person-Klassen-Referenz 3: " + personClassInstance3.FirstName + " " + personClassInstance3.LastName); Console.WriteLine("Person-Klassen-Referenz 4: " + personClassInstance4.FirstName + " " + personClassInstance4.LastName);
9
10
11
163
Die Sprache C#
Das Beispiel gibt den folgenden Text an der Konsole aus: Person-Klassen-Referenz 3: Marvin The Robot Person-Klassen-Referenz 4: Marvin The Robot
Dann wird das Objekt, das über personClassInstance4 referenziert wird, geändert: personClassInstance4.FirstName = "Prostetnik"; personClassInstance4.LastName = "Vogon Jeltz"; Console.WriteLine("Person-Klassen-Referenz 3: " + personClassInstance3.FirstName + " " + personClassInstance3.LastName); Console.WriteLine("Person-Klassen-Referenz 4: " + personClassInstance4.FirstName + " " + personClassInstance4.LastName);
Da beide Referenzen dasselbe Objekt referenzieren, wird an der Konsole nun Folgendes ausgegeben: Person-Klassen-Referenz 3: Prostetnik Vogon Jeltz Person-Klassen-Referenz 4: Prostetnik Vogon Jeltz
INFO
Das Verhalten bei Zuweisungen sollten Sie immer im Auge behalten, wenn Sie mit Wert- und Referenztypen arbeiten. Dies gilt besonders auch deswegen, da es ebenso die Übergabe von Typen an Methoden betrifft: Wird einer Methode ein Werttyp übergeben, erzeugt der Compiler eine Kopie des Objekts. Ändert die Methode die Daten des (kopierten) Objekts, betreffen diese Änderungen nicht das Objekt, das übergeben wurde. Wird hingegen ein Referenztyp übergeben, übergibt der Compiler lediglich eine Referenz auf das Objekt. Änderungen, die die Methode vornimmt, betreffen dann das eine Objekt und sind natürlich nach außen sichtbar.
3.5.3
Standardwerte der verschiedenen Typen
Der C#-Compiler initialisiert Felder von Klassen oder Strukturen immer mit einem Standardwert. Dabei handelt es sich um einen Leerwert: Numerische Felder erhalten den Wert 0, boolesche Felder werden auf false gesetzt, Datumsfelder werden mit dem minimalen Datum (1.1.0001 00:00:00) initialisiert, char-Instanzen werden auf das 0Zeichen ('\0') gesetzt, Strings. Referenztypen und Nullables (Seite 167) erhalten den Wert null. Ist der Typ des Feldes selbst wieder eine Struktur, wird diese entsprechend initialisiert. Zum Initialisieren von Variablen können Sie default verwenden
Variablen werden allerdings nicht implizit initialisiert. Variablen müssen Sie immer initialisieren, bevor Sie diese verwenden können. Die Verwendung einer uninitialisierten Variablen lässt der Compiler nicht zu. Zum Initialisieren verwenden Sie normalerweise einen zur Variablen passenden Wert. Sie können aber auch das defaultSchlüsselwort verwenden um eine Variable (oder ein Feld) mit dem Standardwert des Typs zu initialisieren. Diesem Schlüsselwort übergeben Sie den Typ in Klammern: int i = default(int);
3.5.4 Der Typ der verwalteten Daten wird bei generischen Typen erst bei der Verwendung festgelegt
Generische Typen
Das .NET Framework enthält eine große Anzahl an so genannten generischen Typen. Generische Typen sind zunächst einmal Typen, die andere Objekte verwalten oder verwenden. Anders als bei »normalen« Typen ist der Typ der verwalteten bzw. verwendeten Objekte aber nicht festgelegt. Erst bei der Verwendung eines generischen Typs in einem Programm legt der Programmierer den Typ der verwalteten Daten fest, indem er diesen in spitzen Klammern angibt. Ein sehr gutes Beispiel für den sinnvollen Einsatz generischer Typen ist eine Auflistung. Eine Auflistung verwaltet eine Liste von Objekten. So könnten Sie z. B. Instan-
164
(Daten)Typen
zen einer eigenen Person-Klasse in einem Programm aus den Daten einer XML-Datei ermitteln und in einer Auflistung verwalten um diese weiterverarbeiten zu können. Würden keine generischen Typen existieren, könnten Sie dazu eine der alten (.NET1.0-)Auflistungen verwenden, die Referenzen vom Typ object verwalten. Da eine Referenz vom Typ object alle möglichen Objekte referenzieren kann, wäre auch die Verwaltung von Address-Objekten möglich.
1
Der massive Nachteil dieser alten Auflistungen ist aber, dass Sie beim Lesen zum einen die referenzierten Objekte über eine Typumwandlung in den erwarteten Typ (in unserem Beispiel in Person-Instanzen) umwandeln müssen. Zum anderen können Sie beim Lesen nie sicher sein, dass wirklich nur Instanzen der erwarteten Klasse in der Auflistung gespeichert sind (glauben Sie mir: In komplexen Projekten passieren so einige Unfälle ☺).
2
Eine andere Möglichkeit wäre die eigene Implementierung einer typsicheren Auflistung, die nur Person-Objekte aufnehmen kann. Der Nachteil hier wäre, dass dies eine Menge Arbeit macht.
3
Glücklicherweise bietet .NET ab der Version 2.0 eine Vielzahl an generischen Auflistungen (die in Kapitel 7 noch genauer besprochen werden). Die List-Klasse aus dem Namensraum System.Collections.Generic ist z. B. eine Standard-Auflistung, die das Durchgehen über einen Integer-Index erlaubt. Bei der Deklaration und der Erzeugung einer List-Instanz müssen Sie (wie bei allen generischen Typen) den Typ, den die Auflistung verwalten soll, in spitzen Klammern angeben. Eine typsichere Auflistung von Person-Objekten erhalten Sie also z. B. so:
4
5
List personList = new List();
6
Dieser Auflistung können Sie nun nur Person-Instanzen hinzufügen und beim Lesen erhalten Sie immer auch Person-Referenzen zurück: Person person = new Person(); person.FirstName = "Zaphod"; person.LastName = "Beeblebrox"; ... personList.Add(person); ... for (int i = 0; i < personList.Count; i++) { Console.WriteLine(personList[i].FirstName + " " + personList[i].LastName); }
7
8
9
Wenn Sie generische Typen verwenden, zeigt IntelliSense zunächst über die spitzen Klammern am Namen des Typs an, dass es sich um einen generischen Typ handelt (Abbildung 3.4). Abbildung 3.4: IntelliSense zeigt den Namen der List-Klasse an
165
10
11
Die Sprache C#
Wenn Sie den Namen eines generischen Typs und eine öffnende spitze Klammer zur Angabe des oder der verwalteten Typen schreiben, zeigt IntelliSense die erwarteten Typen an und deren Beschreibung (Abbildung 3.5). Abbildung 3.5: IntelliSense zeigt Informationen zu Typparametern an
Generische Typen können beliebig viele Typparameter besitzen. Mehrere Typparameter werden durch Kommata voneinander getrennt. Ein Beispiel dafür ist die Dictionary-Klasse (Abbildung 3.6) Abbildung 3.6: IntelliSense für die Dictionary-Klasse
EXKURS
Ein Dictionary ist eine so genannte assoziative Auflistung. In einer solchen werden die aufgelisteten Objekte mit einem Schlüssel assoziiert. Über den Schlüssel können Sie später auf die Objekte zugreifen. Ein Beispiel für den sinnvollen Einsatz einer Dictionary-Auflistung wäre das Speichern von Kundendaten, wobei die Kundennummer als Schlüssel verwendet wird. In Kapitel 7 erfahren Sie mehr über diese Klasse. Dictionary erwartet zwei Typparameter: TKey und TValue. TKey ist der Typ des Schlüssels, TValue der Typ der verwalteten Objekte. Der Name der Typparameter ist übrigens beim eigenen Programmieren generischer Typen frei vergebbar. Die .NETKlassen benennen Typparameter üblicherweise mit einem einfachen »T« (für »Type«), wenn die Klasse nur einen Typparameter besitzt, und mit einem Namen, der mit »T« beginnt, wenn die Klasse mehrere Typparameter besitzt. Wenn Sie eine Dictionary-Instanz erzeugen, müssen Sie also zwei Typparameter angeben. Für das Kunden-Beispiel mit einer String-Kundennummer wäre das: Dictionary customerList = new Dictionary();
Einige generische Typen schränken die für die Typparameter verwendbaren Typen auch ein. So kann es z. B. sein, dass ein Typ eine bestimmte Schnittstelle implementieren muss. Der Compiler beschwert sich in diesem Fall, wenn Sie einen Typen einsetzen, der den Einschränkungen nicht entspricht. Über dieses (etwas komplexere) Thema erfahren Sie mehr in Kapitel 6, bei der Programmierung eigener generischer Typen. Generische Typen machen das Programmierer-Leben damit in vielen Fällen um einiges leichter. Kapitel 7 setzt sich noch näher mit den generischen Auflistungen auseinander, Kapitel 6 behandelt die Programmierung eigener generischer Typen.
166
(Daten)Typen
3.5.5
Nullables: Werttypen mit der Möglichkeit nichts (null) zu speichern
Normalen Werttypen müssen Sie immer einen zum Typ passenden Wert zuweisen, was in manchen Fällen ein Nachteil ist. Wenn Sie z. B. in einer Instanz der DateTimeStruktur ein Datum verwalten, aber ermöglichen wollen, dass auch gespeichert werden kann, dass kein Datum angegeben ist, haben Sie ein Problem. Dieses Problem können Sie allerdings mit Nullable Types (Nullables) lösen. Nullables sind spezielle Werttypen, denen Sie neben dem zum Typ passenden Wert auch null zuweisen können. Nullables basieren auf der speziellen generischen Struktur System.Nullable, die die Zuweisung, den Vergleich und arithmetische Operationen mit Standardtypen ermöglicht.
1 Nullables sind Werttypen, die null speichern können
Das folgende Beispiel deklariert einen Nullable-Integer, eine Nullable-DateTime- und eine Nullable-PersonStruct-Instanz (aus dem Abschnitt »Wert- und Referenztypen« ab Seite 155):
2
3
Nullable i1; Nullable date1; Nullable person1;
4
C# ermöglicht eine verkürzte Schreibweise, indem Sie dem Typen ein Fragezeichen anhängen:
5
int? i2 = null; DateTime? date2 = null; PersonStruct? person2;
Diese spezielle Syntax wird vom C#-Compiler einfach in die eigentliche Deklaration mit der Nullable-Struktur umgesetzt.
6
Mit Nullables, die auf Standardtypen basieren, können Sie nun prinzipiell so arbeiten wie mit normalen Typen. Der Compiler unterstützt Sie dabei, da er bei allen Typen eine Initialisierung erzwingt. Sie können also gar keine uninitialisierten Nullables verwenden.
7
i1 = 10; i2 = i1 * 10; Console.WriteLine("i2: " + i2);
8
date1 = DateTime.Now; Console.WriteLine(date1);
9
Sie können aber jetzt auch null zuweisen: i1 = null; date1 = null; person1 = null;
10
Sie können (und sollten) vor der Verwendung von Nullable-Objekten nachfragen, ob diese null speichern. Dazu können Sie einfach mit null vergleichen: if (date1 != null) { Console.WriteLine("Datum: " + date1); } else { Console.WriteLine("Kein Datum angegeben"); }
11
167
Die Sprache C#
Alternativ dazu können Sie die HasValue-Eigenschaft der Nullable-Struktur abfragen: if (date1.HasValue) { Console.WriteLine("Datum: " + date1); } else { Console.WriteLine("Kein Datum angegeben"); }
Sie können alle Werttypen (alle Standardtypen, Aufzählungen und Strukturen) mit der Nullable-Struktur einsetzen (auch eigene). Der Zugriff auf die Daten des Objekts kann für Standardtypen und Aufzählungen über den Namen der entsprechenden Variablen erfolgen. Dabei sollten Sie jedoch beachten, dass Ausdrücke, die Nullables enthalten, immer einen Nullable zurückgeben. Darauf gehe ich noch ein.
Nullables und Strukturen und die Value-Eigenschaft Nullable-Strukturen können (in der Regel) nicht so einfach verwendet werden wie Standardtypen und Aufzählungen. Das liegt daran, dass diese Felder und/oder Eigenschaften besitzen. Ein Zugriff wie der folgende: Console.WriteLine(person1.FirstName);
ist nicht möglich. Das erkennen Sie schon daran, dass IntelliSense die Felder und Eigenschaften der Struktur gar nicht anbietet. Das ist auch korrekt so, denn die Variable, mit der Sie arbeiten, ist nicht vom Typ des eigentlich gespeicherten Objekts, sondern vom Typ der Nullable-Struktur. Und die besitzt nur einige spezielle Eigenschaften. Value speichert den eigentlichen Wert
Über die Eigenschaft Value der Nullable-Struktur erhalten Sie allerdings Zugriff auf das eigentlich gespeicherte Objekt. Damit können Sie die Daten des Objekts lesen: Console.WriteLine(person1.Value.FirstName);
Speichert die Nullable-Variable, mit der Sie arbeiten, allerdings null, resultiert in der Laufzeit eine Ausnahme vom Typ InvalidOperationException mit der Meldung »Das Objekt mit Nullwert muss einen Wert haben«. Sie müssen also vor dem Zugriff immer abfragen, ob die Nullable-Variable einen Wert besitzt: Listing 3.11: Abfrage darauf, ob ein Nullable einen Wert besitzt if (person1.HasValue) // oder if (person1 != null) { Console.WriteLine(person1.Value.FirstName); } else { Console.WriteLine("person1 ist null"); }
Der schreibende Zugriff ist nur möglich, wenn es sich um einen Standardtypen oder eine Aufzählung handelt oder wenn eine Struktur Konvertierungsoperatoren besitzt, die einfache Werte in Instanzen der Struktur umwandeln. Beim Versuch, in eine Eigenschaft oder ein Feld einer Struktur zu schreiben, meldet der Compiler den Fehler, dass der Rückgabewert von Value nicht modifiziert werden kann, weil es keine Variable ist: person1.Value.FirstName = "Zaphod"; // Compilerfehler »Der Rückgabewert // "System.Nullable.Value" kann nicht geändert // werden, da er keine Variable ist«
168
(Daten)Typen
Prinzipiell würde aber die folgende Anweisung funktionieren, wenn die PersonStruct-Struktur einen Konvertierungsoperator zur Verfügung stellen würde, der Strings in PersonStruct-Instanzen konvertiert: person1.Value = "Zaphod Beeblebrox";
Ansonsten können Sie eine Nullable-Strukturvariable nur mit einer neuen Instanz der Struktur beschreiben. Da eine nachträgliche Änderung der Daten nicht mehr möglich ist, müssen Sie das Objekt bei der Erzeugung initialisieren. Dazu können Sie einen ggf. vorhandenen Konstruktor verwenden oder einen Objektinitialisierer:
1
person1 = new PersonStruct { FirstName = "Zaphod", LastName = "Beebebrox" };
2
Nullables in Ausdrücken Sie können Nullables ohne Probleme in Ausdrücken verwenden, sofern die verwendeten Basistypen die verwendeten Operatoren anbieten. Die Standardtypen überschreiben z. B. alle arithmetischen Operatoren. Deswegen können Sie mit diesen Typen rechnen, auch dann, wenn es sich um Nullables handelt:
3
Listing 3.12: Rechnen mit Nullables
4
int? i3 = 1; int? j3 = 2; double? result1 = (i3 + j3) * 0.5; Console.WriteLine(result1); // 1,5
Das Ergebnis eines Ausdrucks, der Nullables enthält, ist immer auch ein Nullable. Deswegen weist das Beispiel das Ergebnis einer Nullable-double-Variablen zu. Die Zuweisung an eine normale Variable würde der Compiler nicht zulassen.
5 NullableAusdrücke ergeben Nullable-Werte
6
Der Grund dafür ist, dass ein Ausdruck mit Nullable-Werten auch null ergeben kann. Das ist dann der Fall, wenn mindestens einer der Operanden null speichert: int? i4 = null; int? j4 = 2; double? result2 = (i4 + j4) * 0.5; Console.WriteLine(result2); // result2 ist null
7
Speichert einer der Operanden null, ist das Ergebnis eines Ausdrucks mit Nullables immer ebenfalls null.
8 HALT
Wollen Sie allerdings mit den Basistypen rechnen oder das Ergebnis einem solchen zuweisen, müssen Sie die Nullables in normale Werttypen konvertieren. Das erledigen Sie idealerweise, indem Sie vor der Berechnung abfragen, ob die Operanden nicht null speichern. In der Berechnung können Sie dann über die Value-Eigenschaft auf die Werte zugreifen:
9
10
Listing 3.13: Abfragen der HasValue-Eigenschaft um zu ermitteln, ob ein Nullable einen Wert verwaltet int? i3 = null; int? j3 = 2; if (i3.HasValue && j3.HasValue) { double result3 = (i3.Value + j3.Value) * 0.5; Console.WriteLine(result3); }
11
169
Die Sprache C#
else { Console.WriteLine("Mindestens einer der Operanden ist null"); }
3.5.6
Freigeben von Objekten
Irgendwann einmal muss jedes Objekt (leider) sterben. Ok, ich glaube an Wiedergeburt, aber das gehört hier wohl nicht hin … Spätestens dann, wenn ein Programm beendet ist, werden alle Speicherbereiche freigegeben, die das Programm reserviert hat. Darum müssen Sie sich also nicht kümmern. Während der Laufzeit einer Anwendung werden aber immer wieder Instanzen erzeugt, die später nicht mehr benötigt werden. Würden diese nicht freigegeben werden, würden .NET-Programme immer mehr Speicher benötigen und ggf. (mit einer OutOfMemoryException) abstürzen. Wie lange ein Objekt benötigt wird, hängt davon ab, wie lange es referenziert wird. Wenn Sie in einer Methode Objekte erzeugen und nur dort (über lokale Variablen) referenzieren, werden diese freigegeben, sobald die Methode beendet ist. Handelt es sich um Werttypen, werden diese sofort aus dem Speicher »entfernt«, weil das Programm den Teil des Stacks, den die Methode reserviert hat, wieder freigibt. Referenztypen bleiben allerdings noch ein wenig im Heap. Wird ein Objekt allerdings nicht nur in einer Methode referenziert, sondern global (über eine statische Eigenschaft einer Klasse oder über ein Objekt, das selbst global verwaltet wird), wird das Objekt erst dann freigegeben, wenn es nicht mehr referenziert wird. bzw. wenn das Objekt, das das andere Objekt beinhaltet, freigegeben wird. Der GC räumt Objekte auf
Das Aufräumen von Objekten auf dem Heap, die nicht mehr benötigt werden, übernimmt der Garbage Collector. Dieser geht immer dann, wenn eine Anwendung nicht besonders beschäftigt ist, durch den Heap, sucht »verwaiste« Objekte und gibt deren Speicher frei3. Die Freigabe geschieht allerdings nach einem komplexen Muster, das sicherstellt, dass die Performance der Anwendung nicht leidet. Für Sie bedeutet dies, dass Sie nichts weiter tun müssen, als die Referenzen auf ein Objekt freizugeben bzw. sich darauf zu verlassen, dass diese automatisch freigegeben werden, wenn die Objektvariable ihren Gültigkeitsbereich verlässt. Wenn Sie Objekte selbst freigeben wollen, setzen Sie die Objektreferenz auf null: p = null;
Dieses explizite »Freigeben« wird allerdings nur in sehr seltenen Fällen sinnvoll sein. Wenn Sie z. B. in einer aufwändigen Methode Objekte einsetzen und nach deren Verwendung Arbeitsspeicher sparen wollten, während die Methode noch weiter ausgeführt wird, könnten Sie auf die Idee kommen, die Objektreferenzen von Hand freizugeben. Dummerweise werden diese aber erst aus dem Arbeitsspeicher entfernt, wenn der Garbage Collector dafür Zeit hat. Und das ist mit Sicherheit nicht mitten in der Ausführung einer Methode.
3
170
Der Garbage Collector wird dazu in einem Thread ausgeführt, der parallel zum Hauptthread der Anwendung ausgeführt wird.
(Daten)Typen
Der Garbace Collector macht zwar, wie Sie sehen, seine Arbeit. Das Problem ist aber, dass die Freigabe von Objekten zu einem unbestimmten Zeitpunkt geschieht. Deswegen ist zum Freigeben von externen Ressourcen, die Objekte verwenden, unter .NET ein Modell vorgesehen, das auf der IDisposable-Schnittstelle beruht. Typen, die diesem Modell entsprechen, bieten eine Methode an, die Dispose heißt. Diese Methode sollte – wenn vorhanden – immer aufgerufen werden, wenn ein Objekt nicht mehr benötigt wird. In Kapitel 4 gehe ich im Abschnitt »Konstruktoren, Finalisierer, Dispose und using« noch näher darauf ein.
3.5.7
TIPP
1
Der Typ String als Ausnahme
Der Typ string, der Zeichenketten verwaltet, ist eigentlich auch ein Referenztyp. Der Compiler erlaubt für diesen Typ aber auch eine Erzeugung ohne new: string s1 = "Das ist ein String";
2 String-Instanzen können auch ohne new erzeugt werden
Alternativ können Sie Strings auch mit new erzeugen. Dem Konstruktor können Sie dazu verschiedene Argumente übergeben. U. a. können Sie den String mit einer bestimmten Anzahl von Zeichen initialisieren:
3 4
string s2 = new string('*', 1024);
Bei Zuweisungen eines Strings auf einen anderen verhält sich ein String ebenfalls etwas anders als ein normaler Referenztyp: Wenn Sie zwei Stringvariablen oder Eigenschaften einander zuweisen, kopiert der Compiler zunächst die Referenz.
5
s1 = "Zaphod"; s2 = s1;
Nach der Ausführung dieser Anweisungen referenziert s2 dieselbe Zeichenkette wie s1. So weit stimmen Strings noch mit normalen Referenztypen überein. Da die Zuweisung eines String-Literals aber immer zur Erzeugung eines neuen StringObjekts führt, führt die »Änderung« eines Strings nicht dazu, dass auch Strings, die über andere Variablen referenziert werden, geändert werden:
6
7
s2 = "Ford";
Nun referenziert s2 einen anderen String als s1. Eine »Änderung« des Strings in s2 bewirkt keine Änderung des Strings in s1. Strings sind in Wirklichkeit unveränderbar (immutable). Wenn Sie einen String neu beschreiben, führt das immer dazu, dass eine neue Instanz erzeugt und die alte freigegeben wird. Die String-Klasse sorgt damit dafür, dass Strings ähnlich wie Werttypen behandelt werden können, obwohl es sich um Referenztypen handelt.
3.5.8
8 Strings sind unveränderbar
9
Übersicht über die Standardtypen 10
Jede Programmiersprache besitzt Standardtypen zur Speicherung von numerischen Werten, Zeichenketten, Datumswerten und anderen häufig benötigten Daten. Auch C# besitzt solche Typen. Allerdings handelt es sich dabei nicht um C#-eigene Typen, sondern um Typen, die die CLR zur Verfügung stellt und die deswegen in allen .NETSprachen zur Verfügung stehen. Diese Typen gehören zum Namensraum System und heißen z. B. Int32. In C# können Sie für diese Typen aber auch Aliasnamen verwenden. System.Int32 entspricht z. B. dem Alias int.
11
Tabelle 3.3 beschreibt die Standardtypen, inklusive des für API-Aufrufe wichtigen Typs IntPtr, für den es keinen Aliasnamen gibt.
171
Die Sprache C#
Beachten Sie, dass die vorzeichenlosen Integer-Typen nicht CTS-kompatibel sind. Wenn Sie Komponenten entwickeln, die von anderen .NET-Programmiersprachen verwendet werden sollen, sollten Sie diese Typen nur für interne, private Zwecke verwenden. Tabelle 3.3: Die C#-Standardtypen
C#-Datentyp
CLR-Datentyp
Größe
Wertebereich
sbyte
SByte
8 Bit
-128 bis 127
byte
Byte
8 Bit
0 bis 255
char
Char
16 Bit
ein beliebiges Unicode-Zeichen (Unicode wird in Kapitel 1 beschrieben)
short
Int16
16 Bit
-32.768 bis 32.767
ushort
UInt16
16 Bit
0 bis 65.535
int
Int32
32 Bit
-2.147.483.648 bis 2.147.483.647
uint
UInt32
32 Bit
0 bis 4.294.967.295
long
Int64
64 Bit
-9.223.372.036.854.775.808 bis 9.223.372.036.854.775.807
ulong
UInt64
64 Bit
0 bis 18.446.744.073.709.551.615
float
Single
32 Bit
±1,5 × 10-45 bis ±3,4 × 1038 mit 7 bis 8 Ziffern Genauigkeit
double
Double
64 Bit
±5,0 × 10-324 bis ±1,7 × 10308 mit 15 bis 16 Ziffern Genauigkeit
decimal
Decimal
128 Bit
1,0 × 10-28 bis 7,9 × 1028 mit maximal 28-29 Dezimalstellen
bool
Boolean
8 Bit
true oder false
string
String
variabel beliebige Unicode-Zeichenketten
IntPtr
32 Bit
3.5.9
mit int kompatibler Zeiger-Typ, der als Argument von Windows-API-Funktionen verwendet wird
Instanzmethoden der Standardtypen
Die Standardtypen besitzen wie alle Typen die Instanzmethoden, die sie von Object geerbt haben (siehe Seite 155). Der ToString-Methode können Sie bei numerischen Typen allerdings auch einen Format-String oder einen Format-Provider übergeben, um die gespeichert Zahl formatiert auszugeben. Formatierungen werden in Kapitel 8 behandelt. Ich zeige hier nur kurz, wie Sie z. B. einen double-Wert auf zwei Stellen hinter dem Komma formatieren können: double d = 1.2345; Console.WriteLine(d.ToString("0.00"));
Neben den geerbten Methoden besitzen die Standardtypen noch die in Tabelle 3.4 dargestellten zusätzlichen Instanzmethoden.
172
(Daten)Typen
Methode
Beschreibung
int CompareTo( Typ value)
vergleicht ein Objekt mit einem anderen. Ist die Rückgabe kleiner 0, ist das Objekt kleiner als das andere. Wird 0 zurückgegeben, sind beide Objekte gleich. Bei einer Rückgabe größer 0 ist das Objekt größer als das andere.
Tabelle 3.4: Die zusätzlichen Methoden der Standardtypen
TypeCode ermittelt den Typ des Objekts als Wert der Aufzählung TypeCode. TypeCode enthält GetTypeCode() Konstanten, die so benannt sind wie die Typen. TypeCode.Byte steht z. B. für einen Byte-Typ.
1
Die CompareTo-Methode wird hauptsächlich implizit verwendet, wenn ein Typ in einer Auflistung gespeichert ist, die sortierbar ist. Explizit brauchen Sie diese Methode eigentlich nie aufrufen, da Sie für Vergleiche Vergleichsoperatoren verwenden können.
2
3
Der Typ String besitzt noch eine Vielzahl weiterer Methoden. Ich beschreibe die wichtigsten davon separat in Kapitel 8.
3.5.10 Klassenmethoden und -eigenschaften der Standardtypen
4
Alle Standardtypen besitzen neben den Instanzmethoden auch noch Klassenmethoden und -eigenschaften, die Sie ohne Instanz der Klasse verwenden können. Die MaxValue-Eigenschaft der numerischen Typen gibt z. B. den größten speicherbaren Wert zurück:
5
Console.WriteLine("double kann maximal " + double.MaxValue + " speichern.");
6
Über die Parse-Methode können Sie einen String in den entsprechenden Typ umwandeln:
7
d = double.Parse("1.234");
Tabelle 3.5 zeigt die wichtigsten dieser Eigenschaften und Methoden. Eigenschaft/Methode
Beschreibung
MinValue
liefert den kleinsten speicherbaren Wert.
MaxValue
liefert den größten speicherbaren Wert.
Typ Parse(string s [...])
Über diese Methode können Sie einen String in den Typ umwandeln, auf dem Sie die Methode anwenden. Parse erlaubt bei numerischen Typen zusätzlich die Angabe der zu verwendenden Kultur. Kulturen werden in Kapitel 15 behandelt.
bool TryParse( string s, NumberStyles style, IFormatProvider provider, out double result)
Tabelle 3.5: Die wichtigsten Klasseneigenschaften und -methoden der Standardtypen
8
9
10
Über diese komplexe Methode können Sie überprüfen, ob eine Umwandlung eines Strings in einen speziellen Typ möglich ist.
11
Die TryParse-Methode will ich hier nicht näher beschreiben, weil Sie dazu wissen müssen, wie Sie mit Schnittstellen umgehen (IFormatProvider) und was KulturInformationen sind. Schnittstellen werden in Kapitel 5 behandelt, Kultur-Informa-
173
Die Sprache C#
tionen (Globalisierung) in Kapitel 15. Das folgende Beispiel überprüft einen String darauf, ob der mit den aktuellen Ländereinstellungen in einen double-Wert konvertierbar ist: Listing 3.14: Überprüfung eines Strings darauf, ob dieser in einen double-Wert konvertiert werden kann string input = "abc"; double result; if (double.TryParse(input, out result) == false) { Console.WriteLine(input + " kann nicht in einen " + "double-Wert umgewandelt werden."); }
3.5.11 Integer-Typen speichern Ganzzahlen
Integer-Typen
C# unterscheidet einige Typen für die Speicherung von Integer-Werten (Werte ohne Dezimalanteil): sbyte, byte, short, ushort, int, uint, long und ulong. Bei der Auswahl dieser Typen müssen Sie eigentlich nur beachten, dass der Datentyp groß genug ist für Ihre Anwendung und ob Sie ein Vorzeichen benötigen. Machen Sie sich aber nicht zu viel Gedanken: Wenn Sie für einzelne Daten einen zu großen Datentyp verwenden, macht das dem Programm nichts aus. Der Arbeitsspeicher ist heute ja wohl immer groß genug. Verwenden Sie lieber einen größeren Typ, damit Sie später bei der Speicherung von Daten keine Probleme haben. Wenn Sie z. B. für die Speicherung einer Kundennummer ushort verwenden, und die Nummer wird größer als 65.535, gibt es Probleme (siehe unter »Über- und Unterläufe und spezielle Werte« auf Seite 177). Achten Sie aber auf die Größe, wenn Sie sehr viele Daten speichern (z. B. in einem Array). Die gängigen Literale für Integer-Werte sind dezimale Zahlwerte: int number1 = 1234;
Alternativ können Sie auch die hexadezimale Schreibweise verwenden. Stellen Sie dazu 0x vor den Wert: byte number2 = 0xff; // = 255
Wenn Sie eine Zahl ohne Dezimalstelle schreiben, erkennt der Compiler diese als int, uint, long oder ulong, je nachdem, ob der Wert in den Typ passt: ulong number3 = 10; // die Zahl 10 ist vom Typ int ulong number4 = 4294967295; // die Zahl 4294967295 ist vom Typ uint
Ausprobieren können Sie dies, indem Sie den Typ eines Literals ermitteln: Console.WriteLine("Das Literal 4294967295 besitzt " + "den Typ '" + 4294967295.GetType().Name + "'");
Sie können ein Integer-Literal aber auch einer kleineren oder größeren Variablen zuweisen: byte number5 = 10; // Zuweisung des int-Werts 10 auf byte long number6 = 10; // Zuweisung des int-Werts 10 auf long
Wenn Sie versuchen, einem Datentyp ein zu großes oder nicht konvertierbares Literal zuzuweisen, meldet der Compiler einen Fehler: byte number7; number7 = 1024; // // number7 = 10.5; // //
Compilerfehler "Konstantenwert '1024' kann nicht nach 'byte' konvertiert werden" Compilerfehler "Implizite Konvertierung des Typs 'double' zu 'byte' nicht möglich"
Konvertierungen werden ab Seite 188 behandelt.
174
(Daten)Typen
Sie können einem Literal einen Suffix anhängen um damit den Datentyp zu bestimmen. Groß- und Kleinschreibung spielt dabei keine Rolle. Suffix
Datentyp
l
long
u
uint oder ulong, je nach Größe des Werts
ul
ulong
3.5.12
Tabelle 3.6: Die Suffixe für Integer-Literale
1
2
Fließkommatypen und der Typ decimal
Fließkommatypen (float und double) und der Typ decimal speichern Dezimalzahlen mit einer festgelegten maximalen Anzahl an Stellen (unabhängig vom Komma). Der Typ float besitzt eine Genauigkeit von sieben Stellen. Dies bedeutet, dass maximal sieben Stellen genau gespeichert werden (die restlichen werden ungenau gespeichert). Handelt es sich um eine Dezimalzahl und steht eine 0 vor dem Komma, wird die Angabe der Genauigkeit allerdings um diese Null erhöht, beträgt dann also acht Stellen.
Fließkommatypen besitzen eine eingeschränkte Genauigkeit
3 4
double besitzt eine Genauigkeit von 15 bis 16 Stellen (16 wieder dann, wenn eine Null vor dem Komma steht). decimal besitzt eine Genauigkeit von 29 Stellen. Die Genauigkeit und die Probleme, die daraus entstehen, behandle ich gleich noch.
5
Als Literal für einen Fließkommatypen können Sie eine Zahl in der englischen Schreibweise verwenden. Der Compiler wertet Zahlen mit Dezimalstellen grundsätzlich als double aus:
6
double number1 = 1.234;
Wenn Sie einer float-Variablen ein Literal zuweisen wollen, das einen Nachkommaanteil besitzt, müssen Sie dieses explizit als float kennzeichnen. Hängen Sie dazu das Suffix f an:
7
float number2 = 1.234f;
Integer-Werte können Sie allerdings ohne Konvertierung zuweisen:
8
float number3 = 1;
Dasselbe gilt, wenn Sie einem decimal-Typ ein Literal zuweisen wollen. Verwenden Sie zur Konvertierung den Suffix m (von »Money«):
9
// Decimal erfordert eine Konvertierung bei Dezimal-Literalen ... decimal number4 = 1.234m; // ... aber nicht bei Integer-Literalen decimal number5 = 1234;
10
Für Dezimalzahlen können Sie wie bei Ganzzahlen die wissenschaftliche Schreibweise verwenden:
11
double number6 = 5E-2; // 0.05
5E-2 steht im Beispiel für: 5 * 10-2. Für die Festlegung des Typs können Sie eines der in Tabelle 3.7 dargestellten Suffixe verwenden.
175
Die Sprache C#
Tabelle 3.7: Suffixe für die Festlegung des Datentyps eines FließkommaLiterals
Suffix
Datentyp
d
Double
f
Float
m
decimal
Die (Un)Genauigkeit der Fließkommatypen und von decimal Ein float-Wert besitzt eine Genauigkeit von sieben bis acht Stellen. Unabhängig davon, an welcher Stelle das Komma steht, sind die Ziffern ab der Position 9 (mit 0 vor dem Komma) bzw. 8 also ungenau. Die Zahl 1234567890 wird ebenso ungenau gespeichert wie die Zahl 1,234567890. Bei der ersten Zahl resultiert 1234567936, bei der zweiten 1,2345678806304932. Zum Beweis der Ungenauigkeit speichert das folgende Beispiel zunächst einen möglichst genauen Wert für PI in einer float-Variablen. Dieser Wert wird dann an der Konsole ausgegeben. Schließlich wird der Inhalt der float-Variablen in eine doubleVariable geschrieben und noch einmal ausgegeben: Listing 3.15: Beweis der Ungenauigkeit von float float pi1 = 3.1415926535897932384626433832795F; Console.WriteLine(pi1); // 3,141593 double pi2 = pi1; Console.WriteLine(pi2); // 3,1415927410125732
Wie Sie an dem Beispiel erkennen, speichert die float-Variable den ungenauen Wert 3,1415927410125732. Wenn Sie Fließkommawerte ausgeben, gibt die implizit aufgerufene ToString-Methode den Wert allerdings auf die signifikanten Stellen gerundet aus. Deswegen erscheint an der Konsole der Wert 3,141593. Das Beispiel beweist dann aber, dass die float-Variable tatsächlich einen ungenauen Wert speichert, nämlich in etwa den, der in die double-Variable geschrieben und von dort aus mit einer größeren Genauigkeit ausgegeben wird. Als letzten Beweis der Ungenauigkeit schreibt das Beispiel den PI-Wert in eine double-Variable, gibt diesen Wert aus und multipliziert schließlich den float-Wert mit 2 um zu zeigen, dass tatsächlich der angegebene Wert gespeichert ist: Listing 3.16: Letzter Beweis der Ungenauigkeit von float double pi3 = 3.1415926535897932384626433832795; Console.WriteLine(pi3); // 3,1415926535897931 double test = pi1 * 2; Console.WriteLine(test); // 6,2831854820251465
Der beim Multiplizieren resultierende Wert 6,2831854820251465 entspricht ziemlich genau dem Zweifachen des Werts 3,1415927410125732 (und nicht des Werts 3,141593, den ToString der float-Variablen suggeriert), was endgültig beweist, dass das Rechnen mit Fließkommazahlen bei Zahlen mit mehr als sieben bzw. 15 Ziffern ungenau ist.
176
(Daten)Typen
Ungenauigkeiten treten aber häufig schon mit Zahlen auf, die eigentlich noch in den speicherbaren Bereich fallen: float price1 = 4.99F; int quantity = 17; float total1 = price1 * quantity; Console.WriteLine("Gesamtpreis: " + total1); // 84,829996109008789
Obwohl 4,99 * 17 eigentlich 84,83 ergibt, resultiert der Wert 84,829996109008789. Erst, wenn Sie double zum Speichern verwenden, ist das Ergebnis genau:
1
double price2 = 4.99; double total2 = price2 * quantity; Console.WriteLine("Gesamtpreis: " + total2); // 84,83
Die Ungenauigkeit von Fließkommatypen basiert auf der (komplexen) Technik, mit der sie gespeichert werden. Diese Technik wird im IEEE-Standard 754 beschrieben (de.wikipedia.org/wiki/IEEE_754, 754r.ucbtest.org/standards/754.pdf). float ist wegen seiner Genauigkeit von lediglich sieben Stellen normalerweise nicht besonders zur Speicherung von Zahlen geeignet. Verwenden Sie lieber double, denn dieser Datentyp besitzt eine Genauigkeit von 15 bis 16 Stellen. Um eine noch höhere Genauigkeit zu erreichen können Sie auch mit dem Festkommatyp decimal arbeiten, der eine Genauigkeit von 29 Stellen besitzt.
decimal ist ein spezieller Datentyp mit einer festgelegten Anzahl von Ziffern (29). Damit können Sie sich also recht schnell ausrechnen, wie viele Dezimalstellen dieser Datentyp speichern kann, wenn eine bestimmte Anzahl Ziffern vor dem Komma verwaltet wird. Diesen Datentyp können Sie verwenden, wenn Sie hochgenaue Berechnungen ausführen wollen:
2
REF
3 4
HALT
5 decimal besitzt immer 29 Stellen
6
decimal pi4 = 3.14159265358979323846264338327950288419m; Console.WriteLine(pi4); // 3,1415926535897932384626433833
Verwenden Sie decimal immer für Währungsberechnungen, da die für diese Berechnungen erforderliche Genauigkeit von vier Stellen hinter dem Komma mit decimal auch bei recht großen Zahlen gegeben ist.
7
INFO
8
3.5.13 Über- und Unterläufe und spezielle Werte Typen, die Zahlen speichern, können nur einen bestimmten Bereich verwalten. Wenn dieser Wert bei einer Zuweisung oder bei einer Berechnung unter- oder überschritten wird, resultieren bei Integer-Typen ein Unter- bzw. Überlauf und bei Fließkommatypen der spezielle Wert +∞ bzw. -∞. decimal hingegen generiert eine Ausnahme vom Typ OverflowException. Das Verhalten des Programms in einem solchen Fall wird davon beeinflusst, ob es sich um einen Integer-, einen Fließkommatyp oder um decimal handelt, und bei Integer-Typen zusätzlich von einer Option, die der Compiler auswertet.
9
10
11
Über- und Unterläufe bei Integer-Typen Wenn Sie einem Integer-Typen einen zu großen Wert zuweisen, resultiert daraus ein Überlauf:
177
Die Sprache C#
Listing 3.17: Beispiel für einen Überlauf byte number1 = 255; number1 += 2; Console.WriteLine(number1); // Ergebnis: 1
Der Wert in der Variablen number1 ist durch die Addition mit 2 übergelaufen und beträgt danach 1. Dieser Wert resultiert daraus, dass der maximale byte-Wert (255) überschritten wurde. Bei diesem Wert sind alle Bits gesetzt. Die Addition von 2 führt dazu, dass bei der Addition von 1 zunächst alle Bits zurückgesetzt werden. Das folgende Beispiel erläutert diesen Umstand. Bei der Addition von 1 resultiert theoretisch der Dualwert 100000000: 111111112 + 000000012 = 1000000002 Da byte aber nur acht Bit verwaltet, wird das linke Bit verworfen. Das Resultat ist also 00000000. Die zusätzliche Addition von 1 führt dann dazu, dass die Variable den Wert 1 verwaltet. Ein Unterlauf wird erzeugt, wenn Sie einen zu kleinen Wert zuweisen: Listing 3.18: Beispiel für einen Unterlauf byte number2 = 0; number2 -= 2; Console.WriteLine(number2); // Ergebnis: 254
HALT
Der Compiler kann so eingestellt werden, dass er bei Über- und Unterläufen eine Ausnahme generiert
Wenn Sie nicht darauf achten, Ihre Typen ausreichend groß zu dimensionieren, kann ein Über- oder Unterlauf zu enormen logischen Fehlern führen, nach denen Sie u. U. tagelang (oder nächtelang) suchen. Sie können den Compiler aber auch so einstellen, dass dieser bei einem Über- oder Unterlauf von Integer-Werten eine Ausnahme generiert. In Visual Studio stellen Sie dazu die Option AUF ARITHMETISCHEN ÜBER-/UNTERLAUF PRÜFEN ein. Sie finden diese Option in den Projekteigenschaften im ERSTELLEN-Register in dem Dialog, den Sie über den ERWEITERT-Schalter öffnen können. Achten Sie darauf, dass Sie die richtige Konfiguration einstellen (Debug oder Release). Sinnvoll ist die Überprüfung auf jeden Fall für die Debug-Konfiguration. Wenn Sie Ihr Programm in der Entwicklungsphase mit dieser Konfiguration fleißig testen (lassen), werden Über- und Unterläufe auf jeden Fall gemeldet, sodass Sie darauf reagieren und Fehler beseitigen können. Im Release sollten Sie die Überprüfung u. U. ebenfalls einschalten. Dabei sollten Sie allerdings bedenken, dass die Überprüfung auf Unter- und Überlauf etwas Rechenzeit kostet (was aber bei den heutigen Rechnern kein Problem darstellen sollte, vor allen Dingen, wenn Sie nicht sehr intensiv mit Integer-Werten rechnen). Denken Sie daran, dass die Suche nach logischen Fehlern, die durch Über- oder Unterläufe verursacht werden, sehr aufwändig werden kann (sofern diese Fehler überhaupt erkannt werden).
HALT
178
Ich setze in meiner Praxis immer ausreichend große (und manchmal auch zu große) Integer-Typen ein. Damit minimiere ich die Gefahr von Über- und Unterläufen. byteWerte verwende ich nur in Sonderfällen als Methodenparameter. Meine Anwendungen verwalten Integer-Werte in der Regel in int- oder long-Variablen. Gehen auch Sie besser den sicheren Weg. Moderne Systeme haben kein Problem mit den paar Byte, die Ihre Programme in diesem Fall zusätzlich benötigen.
(Daten)Typen
Alternativ können Sie auch den checked-Block verwenden, um einzelne Anweisungen bei ansonsten abgeschalteter Prüfung auf einen Über- oder Unterlauf zu überprüfen:
Ein checked-Block überprüft explizit auf Unter- und Überläufe
checked { number1 += 2; }
1
Alle im checked-Block enthaltenen Anweisungen werden nun überprüft. Tritt ein Über- oder Unterlauf ein, wird eine Ausnahme generiert. Diese Ausnahme können Sie natürlich abfangen (wie ich in Kapitel 8 beschreibe), aber alleine die Tatsache, dass eine Ausnahme generiert wird, hilft bei der Erkennung logischer Fehler.
2
Wenn Sie bei eingeschalteter Prüfung einzelne Anweisungen explizit nicht überprüfen wollen, verwenden Sie den unchecked-Block: unchecked { number1 += 2; }
3
Alternativ können Sie für einzelne Ausdrücke auch den checked- oder uncheckedOperator verwenden:
4
number1 = unchecked((byte)(number1 + 2));
Die Typumwandlung (byte) konvertiert in diesem Beispiel das int-Ergebnis des Ausdrucks in den Datentyp byte.
5
Keine Über- und Unterläufe bei Fließkommatypen Bei Fließkommatypen treten Über- und Unterläufe nicht auf. Wenn Sie einem Fließkommatypen eine größere oder kleinere Zahl zuweisen, als dieser speichern kann, resultieren die speziellen Werte +∞bzw. –∞, die ich im nächsten Abschnitt beschreibe. Allerdings hängt das davon ab, ob Sie mit dem Wert überhaupt in den Bereich der signifikanten Stellen gelangen. Im folgenden Listing wird die gespeicherte Zahl z. B. bei der Addition mit 2 nur im Bereich der nicht signifikanten Stellen geändert:
Fließkommatypen erzeugen keine Unter- und Überläufe
7
Listing 3.19: Demonstration des Verhaltens von Fließkommatypen bei der Zuweisung von zu großen Werten
8
float number4 = float.MaxValue; Console.WriteLine(((double)number4).ToString("N")); // 340.282.346.638.529.000.000.000.000.000.000.000.000,00
9
number4 += 2; // Die gespeicherte Zahl wurde nicht verändert, da die Addition // nicht den Bereich der signifikanten Stellen betraf Console.WriteLine(((double)number4).ToString("N")); // 340.282.346.638.529.000.000.000.000.000.000.000.000,00
10
// Eine Multiplikation mit 2 betrifft aber auch // den signifikanten Bereich und führt zu +∞ number4 = number4 * 2; Console.WriteLine(number4); // Ausgabe: +unendlich
Das im Listing demonstrierte Verhalten sollten Sie bei Fließkommatypen, gemeinsam mit der eingeschränkten Genauigkeit, immer im Auge behalten. Mein Tipp hier ist noch einmal: Verwenden Sie statt float lieber double und bei größeren Zahlen bzw. wenn Sie mehr Nachkommastellen benötigen decimal.
6
11
HALT
179
Die Sprache C#
+0, -0, +∞, -∞, NaN Fließkommatypen können spezielle Werte speichern
Über- und Unterläufe treten bei Fließkommatypen also nicht auf. Dafür können die Typen float und double (nicht decimal!) spezielle Werte speichern. Wenn Sie z. B. einen double-Typ, der eine positive Zahl speichert, durch 0 teilen, resultiert der Wert +∞(positiv unendlich): Listing 3.20: Beispiel für einen Ausdruck, der +∞ergibt double number5 = 10; double number6 = 0; double number7 = number5 / number6; Console.WriteLine(number7); // Ausgabe: +unendlich
Teilen durch Null führt zu dem Wert »Unendlich« oder NaN
Teilen Sie einen negativen Wert durch 0, resultiert der Wert -∞ (negativ unendlich). Wenn Sie 0 durch 0 teilen, ergibt das den Wert NaN (Not a Number). In einigen speziellen Fällen resultiert auch der Wert -0 oder +0. Die Regeln dazu sind ziemlich kompliziert und im Standard für Fließkommaoperationen beschrieben (IEEE 754). Sie finden diesen Standard unter der Adresse 754r.ucbtest.org/standards/754.pdf. Wikipedia erläutert den Standard vielleicht etwas »verstehbarer«: de.wikipedia.org/wiki/ IEEE_754. In der C#-1.2-Sprachspezifikation finden Sie im Abschnitt 7.7.2 bei der Beschreibung des Divisions-Operators eine Tabelle, die die einzelnen Möglichkeiten auflistet. Die C#-1.2-Sprachspezifikation finden Sie im Visual-Studio-Ordner im Unterordner VC#\Specifications\1033. Nur für den Fall, dass Sie sich über die Versionsnummer wundern: Der Divisionsoperator ist bereits in C# 1.0 enthalten. Die Sprachspezifikations-Dokumente der Versionen 2.0 und 3.0 behandeln lediglich die Neuerungen. Über einige statische Methoden des double- und des float-Typen können Sie herausfinden, ob ein Objekt einen dieser Werte speichert: Listing 3.21: Verwendung der statischen Methoden des double-Typs zur Ermittlung der besonderen Fließkommawerte if (double.IsInfinity(number6)) { Console.WriteLine("number6 ist unendlich."); } if (double.IsNegativeInfinity(number6)) { Console.WriteLine("number6 ist negativ unendlich."); } if (double.IsPositiveInfinity(number6)) { Console.WriteLine("number6 ist positiv unendlich."); } if (double.IsNaN(number6)) { Console.WriteLine("number6 ist NaN."); }
INFO
180
Die Bedeutung dieser Werte ist für die Praxis wohl eher gering. Wenn Sie das Teilen durch 0 vermeiden, treten die Unendlich-Werte lediglich dann auf, wenn Sie einem Fließkommatyp eine zu kleine oder zu große Zahl zuweisen.
(Daten)Typen
decimal Der Typ decimal verhält sich anders als Integer- und Fließkommatypen. Er generiert bei Über- oder Unterläufen grundsätzlich eine Ausnahme vom Typ OverflowException. Listing 3.22: decimal generiert bei Über- oder Unterläufen eine OverflowException
1
decimal number9 = decimal.MaxValue; number9 += 2; // OverflowException number9 -= 2; // OverflowException
Diese Ausnahme können Sie auf verschiedene Weise abfangen, wie ich in Kapitel 8 zeige.
2
3.5.14 Datumswerte
3
Datumswerte werden in einer Instanz der Struktur DateTime oder DateTimeOffset gespeichert: DateTime date1; DateTimeOffset date2
DateTimeOffset ist die neuere Struktur. Diese behebt einige Probleme, die mit DateTime entstehen. Beide Strukturen speichern ein Datum mit Zeitangabe und sind prinzipiell identisch. Der Unterschied ist allerdings, dass DateTimeOffset immer den aktuellen Offset zur UTC-Zeit verwaltet, und DateTime diesen Offset nur dann verwaltet, wenn es sich um ein Datum vom Typ DateTimeKind.Local oder DateTimeKind.Utc handelt. Da es bei DateTime sehr schnell passiert, dass ein unspezifiziertes Datum erzeugt wird (vom Typ DateTimeKind.Unspecified), ist dieser Typ nicht zur Speicherung oder Übertragung von Daten geeignet, wenn verschiedene Zeitzonen im Spiel sind. Auf dieses Problem gehe ich in Kapitel 8 noch näher ein. In diesem Abschnitt verwende ich den neuen Typ, der diese Probleme nicht aufweist.
Ich würde Ihnen empfehlen, grundsätzlich die neue DateTimeOffset-Struktur zu verwenden und auf DateTime zu verzichten. Damit vermeiden Sie Probleme, die entstehen können, wenn Daten mit Datumswerten zwischen Systemen ausgetauscht werden, die verschiedenen Zeitzonen angehören. Leider besitzt DateTimeOffset aber nicht alle Möglichkeiten, die DateTime bietet, wie Sie im Verlauf dieses Abschnitts noch sehen.
4
5
NEU
6
7
8
HALT
9
Dem Konstruktor der DateTimeOffset-Struktur können Sie verschiedene Werte übergeben, mit denen Sie das Datum genau spezifizieren können:
10
DateTimeOffset date = new DateTimeOffset( 2099, 12, 31, 12, 0, 0, TimeSpan.Zero); // 31.12.2099 12:00 UTC-Zeit
Das letzte Argument im Beispiel ist ein TimeSpan-Wert, der eine Zeitspanne definiert. Diese Zeitspanne gibt den Offset zur UTC-Zeit an. Im Beispiel habe ich eine 0-Zeitspanne gewählt, weil ich ein UTC-Datum angeben wollte.
11
Das Datum 31.12.2099 habe ich übrigens gewählt, weil ich hoffe, dass sich dieses Buch dann immer noch verkauft. Dann muss ich das Datum in den einzelnen Auflagen nicht immer wieder ändern ☺.
181
Die Sprache C#
Alternativ können Sie auch eine der statischen Eigenschaften der DateTimeOffsetStruktur verwenden, um das aktuelle oder ein bestimmtes Datum einzustellen: // Aktuelles Datum einstellen date = DateTimeOffset.Now; // Datum aus einem String ermitteln date = DateTimeOffset.Parse("31.12.2099");
Now liefert das aktuelle Datum
Now liefert dabei das bis auf die Millisekunde genaue aktuelle Datum. Parse ermöglicht das Parsen eines Strings mit einer Datumsangabe.
ToString formatiert ein Datum
Um ein Datum auszugeben, können Sie die ToString-Methode verwenden. Dieser können Sie einen Formatierungs-String übergeben. Das Beispiel zeigt, wie Sie das Datum in verschiedenen Varianten ausgeben: Listing 3.23: Formatierte Ausgabe eines Datums Console.WriteLine(date.ToString("g")); Console.WriteLine(date.ToString("d")); Console.WriteLine(date.ToString("T")); Console.WriteLine(date.ToString("t"));
// // // //
Langes Datum inkl. Zeit Kurzes Datum Lange Zeit (12:00:00) Kurze Zeit (12:00)
DateTime besitzt zudem u. a. die Methoden ToLongDateString, ToShortDateString, ToLongTimeString und ToShortTimeString, die DateTimeOffset (leider) fehlen. In Kapitel 6 erfahren Sie im Abschnitt zu den Erweiterungsmethoden, wie Sie diese Methoden in DateTimeOffset einblenden können.
TIPP
Beachten Sie bitte, dass die Formatierungen des Datums sich auf die Datums- und Zeitformat-Einstellungen im aktuellen System beziehen. Es kann z. B. sein, dass das lange Datum auf einem Windows-System in einer anderen Form ausgegeben wird, weil das Format (über die Systemsteuerung) geändert wurde. Den Umgang mit Datumswerten beschreibe ich in Kapitel 8.
REF
3.5.15 Zeichen und Zeichenketten Zeichen werden in char verwaltet, Strings in string
Zur Speicherung einzelner Zeichen verwenden Sie den Typ char, für Zeichenketten den Typ string. char ist ein Werttyp, string ein Referenztyp (der aber ähnlich einem Werttyp ausgewertet wird, siehe in »Der Typ String als Ausnahme« auf Seite 171). Zeichen und Zeichenketten werden im Speicher in der Unicode-Codierung UTF-16 verwaltet. Unicode wurde bereits in Kapitel 1 beschrieben. Als Literal für Zeichen verwenden Sie ein in einfache Anführungsstriche eingeschlossenes Zeichen: char c1 = 'A';
Alternativ können Sie auch den Unicode-Wert des Zeichens angeben (wenn Sie diesen kennen). C# bietet dazu die Escape-Sequenzen \x und \u, denen Sie den hexadezimalen Wert des Zeichens anhängen. \x und \u unterscheiden sich scheinbar nur dadurch, dass \u einen vierstelligen (hexadezimalen) Unicode-Wert erwartet, \x aber auch Unicode-Hexadezimalwerte mit weniger als vier Ziffern zulässt.
182
(Daten)Typen
char char char char
c2 c3 c4 c5
= = = =
'\u20AC'; '\u0041'; '\x20AC'; '\x41';
// // // //
0x20AC 0x0041 0x20AC 0x41 =
= '_' = 'A' = '_' 'A'
Wenn Sie den ISO-8859-1-Code eines Zeichens kennen, setzen Sie diesen im ersten Byte ein: Der ISO-8859-1-Zeichensatz ist im ersten Byte des Unicode-Zeichensatzes abgebildet.
1
string-Literale schließen Sie in Anführungszeichen ein: string s1 = "Das ist ein String";
Innerhalb einer Zeichenkette können Sie auch die Unicode- oder Hexadezimal-Darstellung der Zeichen verwenden:
2
string s2 = "\x0041\x0042\x0043"; // "ABC"
Escape-Sequenzen char- und string-Typen können so genannte Escape-Sequenzen speichern. Eine Escape-Sequenz wird immer mit einem Backslash eingeleitet. Einige Escape-Sequenzen sorgen dafür, dass Zeichen mit einer besonderen Bedeutung im String als normale Zeichen verwendet werden können. Das Anführungszeichen können Sie innerhalb eines String-Literals z. B. nur über die Escape-Sequenz \" verwenden, da der Compiler dieses Zeichen ansonsten als Stringbegrenzer auswertet:
3 Escape-Sequenzen ermöglichen besondere Zeichen
4
5
Console.WriteLine("Sein Name war \"Old Trashbarg\"");
Andere Escape-Sequenzen stehen für spezifische Zeichen, wie z. B. einen Zeilenumbruch:
6
Console.WriteLine("Das ist Zeile 1\r\nDas ist Zeile 2");
Tabelle 3.8 listet die Escape-Sequenzen von C# auf. Escape-Sequenz
Bedeutung
\'
Apostroph
\"
Anführungszeichen
\\
Backslash
\0
Null-Zeichen
\a
Warnung (alert). Erzeugt einen Piepston.
\b
Backspace
\f
Seitenvorschub (Form Feed)
\n
Zeilenumbruch (New Line)
\r
Wagenrücklauf (Carriage Return)
\t
Horizontaler Tabulator
\v
Vertikaler Tabulator
Tabelle 3.8: Die C#-EscapeSequenzen
7
8
9
10
11
183
Die Sprache C#
TIPP
Zeilenumbrüche sollten über Environment.NewLine erzeugt werden
In Zeichenketten werden häufig Zeilenumbrüche benötigt, z. B. weil mehrere Zeilen in einem Label ausgegeben werden sollen. Sie könnten dazu die Escape-Sequenzen \r und \n verwenden. Dummerweise wird ein Zeilenumbruch je nach Betriebssystem aber unterschiedlich interpretiert. Auf Windows-Systemen ist das ein Carriage Return (Wagenrücklauf, '\r') gefolgt von einem Line Feed (Zeilenvorschub, '\n'). Die UNIXund Linux-Entwickler haben die Sinnlosigkeit dieses von der Schreibmaschine übernommenen Verhaltens erkannt und nur das Line-Feed-Zeichen als Zeilenumbruch verwendet. Da .NET-Anwendungen prinzipiell (u. U. mit Einschränkungen) auch auf anderen Systemen ausgeführt werden können, sollten Sie statt den Escape-Sequenzen für Zeilenumbrüche immer die statische Eigenschaft NewLine der Environment-Klasse verwenden. Auf Windows-Systemen gibt diese Eigenschaft den String "\r\n" zurück. Auf UNIX- oder Linux-Systemen wird allerdings der String "\n" zurückgegeben. Console.WriteLine( "Das ist Zeile 1" + Environment.NewLine + "Das ist Zeile 2");
Und nur für den Fall, dass Sie sich fragen, wie .NET-Anwendungen auf UNIX oder Linux laufen können: Das Mono-Projekt (www.mono-project.com) implementiert das .NET Framework für diese (und andere) Betriebssysteme.
Wortwörtliche Stringliterale Wortwörtliche Stringliterale werten keine Escape-Sequenzen aus
In normalen Stringliteralen werden vorhandene Escape-Sequenzen entsprechend ihrer Bedeutung ausgewertet. In einigen Fällen kann das zum Problem werden. Wenn Sie z. B. einen Dateipfad angeben, wollen Sie die im Pfad enthaltenen Backslash-Zeichen nicht als Escape-Zeichen verwenden. Häufig beschwert sich schon der Compiler: string filename1 = "c:\files\books\C#\C#.doc";
In diesem Fall meldet der Compiler eine »Nicht erkannte Escape-Folge«. Enthält die Zeichenfolge zufällig nur gültige Escape-Sequenzen, ist ein Programmfehler vordefiniert: string filename2 = "c:\files\new\release.doc"; Console.WriteLine(filename2);
Abbildung 3.7: Ein Dateiname, der aufgrund der Escape-Sequenzen fehlerhaft ausgewertet wird
Sie können Escape-Sequenzen zwar über doppelte Backslashs deaktivieren: string filename3 = "c:\\files\\new\\release.doc";
Einfacher und besser ist aber, stattdessen »wortwörtliche« Stringliterale (Verbatim String Literals) zu verwenden. Diese Literale, die keine Escape-Sequenzen auswerten, leiten Sie über ein @ ein: string filename4 = @"c:\files\new\release.doc";
184
(Daten)Typen
Wortwörtliche Stringliterale können sogar in mehreren Zeilen umbrochen werden, ohne dass Sie diese in jeder Zeile abschließen und mit + addieren müssen: string s3 = @"Das ist ein wortwörtliches Stringliteral, das einfach umbrochen wurde";
Dabei müssen Sie allerdings beachten, dass nun alle Zeilenvorschübe und Leerzeichen mit in den String übernommen werden. Der String hat also genau dasselbe Format, wie es im Quellcode angegeben ist.
1
Der Typ char 2
Der Typ char wird in einem 16-Bit-Speicherbereich verwaltet (da es sich ja um Unicode-Zeichen in der UTF-16-Codierung handelt). Eigentlich ist zu diesem Typen nichts Besonderes zu sagen, außer, dass er mit Integer-Werten kompatibel ist. Ein char-Wert kann also in einen Integer-Wert umgewandelt oder konvertiert werden und umgekehrt. Das Konvertieren wird ab Seite 188 behandelt. Deshalb folgt hier nur ein kleines Beispiel:
3
Listing 3.24: Konvertieren von char-Werten in int und umgekehrt
4
char c4 = 'A'; int integerValue = (int)c4; Console.WriteLine(integerValue); // 65 integerValue = 97; c4 = (char)integerValue; Console.WriteLine(c4); // a
5
Strings Strings werden automatisch dynamisch verwaltet. Die Größe des Strings ist lediglich durch den verfügbaren Speicher begrenzt. Wenn Sie einen String neu zuweisen oder verändern, erzeugt das Programm immer eine neue Instanz des Strings:
Strings werden dynamisch verwaltet
6
7
string s4 = "Das ist ein "; s4 = s4 + "Test"; // erzeugt eine neue Instanz
Wenn Sie viel mit einem String arbeiten, ist das nicht sehr effizient. In diesem Fall können Sie alternativ eine Instanz der StringBuilder-Klasse verwenden, die das Umkopieren vermeidet (siehe Kapitel 8).
8
In Stringausdrücken ruft der Compiler implizit die ToString-Methode anderer Typen auf, wenn dies nicht bereits im Programmcode der Fall ist. Deshalb können Sie beispielsweise einen int-Typen mit einem String ohne Konvertierung addieren:
9
int i = 42; string s5 = i + " ist die Antwort auf die Frage aller Fragen";
Die String-Klasse besitzt eine große Anzahl an Methoden zur Arbeit mit Zeichenketten. In Kapitel 8 beschreibe ich, wie Sie diese nutzen.
10
3.5.16 Der Typ Object
11
Der Typ Object, der in C# mit dem Alias object verwendet wird, ist ein besonderer Typ. Object ist zunächst der Basistyp aller anderen .NET-Typen. Mit anderen Worten sind alle .NET-Typen mehr oder weniger direkt von Object abgeleitet. .NET-Typen besitzen also grundsätzlich immer die Elemente, die in Object enthalten sind.
185
Die Sprache C#
Object stellt allen Typen öffentliche Methoden zur Verfügung
Tabelle 3.9: Die öffentlichen Instanzmethoden des Basistyps Object
HALT
Object besitzt vier öffentliche Methoden, zwei geschützte und eine statische. Die öffentlichen Methoden werden an die anderen .NET-Typen (auch an selbst entwickelte) so vererbt, dass diese auch mit Instanzen dieser Typen aufgerufen werden können. Die ToString-Methode liefert z. B. in der Regel einen String zurück, der den Inhalt des Objekts darstellt. Tabelle 3.9 beschreibt die öffentlichen Object-Methoden. Methode
Beschreibung
bool Equals( object obj)
vergleicht die Daten eines Objekts mit den Daten eines anderen Objekts auf Gleichheit (d. h., dass die in den Objekten gespeicherten Werte gleich sind).
int GetHashCode()
ermittelt einen Hashcode für das Objekt. Ein Hashcode ist ein aus den Daten des Objekts ermittelter Code, der das Objekt in verkürzter Form identifiziert.
Type GetType()
ermittelt den Typ des Objekts. Gibt ein Objekt der Klasse Type zurück. Aus diesem Objekt können Sie sehr viele Informationen auslesen. Die Eigenschaft Name liefert z. B. den Namen des Typs, die Eigenschaft Namespace gibt den Bezeichner des Namensraums zurück, in dem der Typ verwaltet wird.
string ToString()
Diese Methode gibt die Daten des Objekts als Zeichenkette zurück.
Wenden Sie diese Methoden auf einem Objekt an, sollten Sie beachten, dass nur GetType garantiert in allen Fällen korrekt funktioniert. Alle anderen Methoden müssen in den entsprechenden Typen mit einer neuen Implementierung überschrieben sein, damit diese einen korrekten Wert zurückliefern. In den Typen des .NET Framework ist dies zwar meistens, aber nicht immer der Fall. Die ToString-Methode liefert in der originalen Implementierung der Object-Klasse z. B. nur den Namen der Klasse zurück. In vielen Typen des .NET Framework (und in vielen externen Typen) wird diese Methode (leider) nicht überschrieben und liefert auch dort lediglich den Namen der Klasse zurück. Object stellt abgeleiteten Typen zudem zwei geschützte Methoden zur Verfügung. Geschützte Methoden können nicht von außen, über eine Instanz des Typs, aufgerufen werden. Wenn Sie aber eine eigene Struktur oder Klasse entwickeln, können Sie diese innerhalb der Methoden der Struktur bzw. Klasse für eigene Zwecke aufrufen.
Tabelle 3.10: Die geschützten Instanzmethoden des Basistyps object
Methode
Beschreibung
object erzeugt einen Klon des Objekts, indem eine neue Instanz erzeugt wird und die MemberwiseClone() Werte aller Felder kopiert werden. Bei Feldern, deren Typ ein Referenztyp ist, wird die Referenz kopiert. Es wird keine Kopie des referenzierten Objekts erzeugt. Diese Einschränkung ist auch der Grund dafür, dass diese Methode nicht öffentlich ist. In einem abgeleiteten Typ können Sie MemberwiseClone verwenden, um einen Klon zu erzeugen, müssen aber gegebenenfalls Referenztyp-Felder separat behandeln. void Finalize()
Finalize wird vom Garbage Collector aufgerufen, wenn ein Objekt zerstört wird. Über ein Überschreiben dieser Methode können Sie darauf reagieren, was aber in der Praxis eigentlich niemals notwendig sein sollte.
Neben den Instanzmethoden besitzt die Object-Klasse noch die statische Methode ReferenceEquals, der zwei Object-Referenzen übergeben werden und die über-
186
(Daten)Typen
prüft, ob diese dasselbe Objekt referenzieren. Diese Methode kann für Referenztypen interessant sein, deren Vergleichsoperator (==) so überschrieben wurde, dass dieser nicht mehr die Referenzen vergleicht, sondern den Inhalt der Objekte. Object kann zudem als Typ eingesetzt werden. Einer Object-Instanz können Sie jeden anderen Typen zuweisen:
Object kann als Typ verwendet werden
object o1; o1 = 10; o1 = "Hallo";
1
Sinnvoll ist diese Verwendung aber nicht, da Sie damit die sehr wichtige Typsicherheit unterlaufen.
2
HALT
Einige der .NET-Methoden besitzen allerdings Argumente vom Typ object. Das ist – neben der Verwendung im veralteten COM-Modell – auch der Haupt-Anwendungsbereich dieses Typs. object-Argumenten können Sie beliebige Werte übergeben. Die WriteLine-Methode der Console-Klasse, die in einer Variante ein object-Argument erwartet, ist ein Beispiel dafür. Der object-Variante der WriteLine-Methode können Sie ein beliebiges Objekt übergeben. Methoden wie WriteLine, die den übergebenen Wert als String auswerten, nutzen dabei die ToString-Methode des übergebenen Objekts, das dieses von object geerbt hat. Sie selbst sollten object nicht verwenden. Wenn Sie an einem Methodenargument unterschiedliche Typen übergeben wollen, ist das Prinzip des Polymorphismus4 dafür viel besser geeignet. Polymorphismus wird in Kapitel 5 behandelt.
Einige Methoden besitzen ObjectArgumente
3 4
5 HALT
6 Wenn Sie eine object-Referenz in einem Ausdruck verwenden, müssen Sie diesen in der Regel in einen Typ konvertieren, der zum Ausdruck passt: object o2 = 10; int i1 = (int)o2 * 10;
Object-Referenzen müssen in Ausdrücken konvertiert werden
7
In Stringausdrücken ist das allerdings nicht notwendig, da der Compiler in diesem automatisch die ToString-Methode des object-Typen aufruft:
8
object o3 = "42"; string s1 = o3 + " ist die Antwort auf die Frage aller Fragen";
Über die GetType-Methode können Sie den Typ ermitteln, der über die object-Referenz verwaltet wird: Console.WriteLine(o3.GetType().Name); // String
GetType ermittelt den eigentlichen Typ
Beachten Sie, dass GetType ein Objekt der Klasse Type zurückgibt, über das Sie noch wesentlich mehr Informationen über den Typ erhalten können, als nur dessen Namen. Die Eigenschaft IsArray ist z. B. true, wenn der Typ ein Array ist. Verwenden Sie den Typ object idealerweise nur in Ausnahmefällen. Die Typsicherheit von C# geht mit diesem Typ verloren. Wenn Sie object-Variablen verwenden, wissen Sie in größeren Programmen nie genau, welchen Typ diese tatsächlich speichern. Logische Programmfehler und Ausnahmefehler sind damit vorprogrammiert.
4
9
10
11 HALT
OK, dass eine Object-Referenz alle anderen Typen verwalten kann, ist bereits Polymorphismus
187
Die Sprache C#
Boxing und Unboxing Boxing erlaubt, dass Object auch Werttypen verwalten kann
Der Datentyp object ist ein Referenztyp. Theoretisch könnte er deswegen auch nur Referenztypen verwalten. Über das so genannte Boxing und Unboxing ermöglicht die CLR es aber, dass object auch Werttypen verwalten kann. Wenn ein Werttyp auf einen object-Typen zugewiesen wird, wird implizit Boxing verwendet: int i2 = 10; object o4 = i2; // i2 wird geboxt
Da object ein Referenztyp ist, werden die Werte, die eine object-Referenz verwaltet, folglich auf dem Heap gespeichert. Wenn Sie einen Werttypen auf eine objectVariable zuweisen oder an ein object-Argument übergeben, erzeugt der Compiler eine »Box« auf dem Heap und kopiert den Wert des Werttypen in diese. Die Box ist ein Referenztyp und kann deshalb über eine object-Variable referenziert werden. Unboxing bedeutet dann lediglich den umgekehrten Vorgang, nämlich dass der Wert eines Werttypen aus der Box ausgelesen wird. Das geschieht aber nicht implizit, weil C# eine typsichere Sprache ist. Sie müssen die object-Referenz explizit in den Werttyp umwandeln: object o5 = 10; int i3 = (int)o5; // der Wert von o5 wird in den Werttyp »entboxt«
3.5.17
Konvertierungen
Wie Sie ja bereits wissen, ist C# eine typsichere Sprache. Sie können einem Objekt nur einen vom Typ her passenden Wert zuweisen, dessen Maximal- bzw. Minimalwert ohne Kürzungen in das Objekt passt. Einer int-Variablen können Sie z. B. ohne Probleme einen byte-Wert zuweisen: byte b1 = 255; int i1 = b1;
Dasselbe gilt, wenn Sie einem Nullable-Objekt einen passenden Werttypen zuweisen: Nullable i2 = b1; i2 = i1;
Der Typ wird in diesem Fall implizit konvertiert. Wenn der Grundtyp aber nicht passt oder der Wert nicht in den zugewiesenen Typ passen würde, erzeugt der Compiler einen Fehler: string s1 = "255"; int i3 = s1; // Fehler »Eine implizite Konvertierung vom Typ "string" in // "int" ist nicht möglich« float f1 = 1.234f; i3 = f1;
// Fehler »Der Typ "float" kann nicht implizit in "int" // konvertiert werden ...«
i3 = i2;
// Fehler »Der Typ "int?" kann nicht implizit in "int" // konvertiert werden ...«
Konvertierungen, die nicht implizit vorgenommen werden, können Sie bei Literalen über die Datentypzeichen explizit vornehmen. Das double-Literal 1.234 habe ich im Beispiel über das Zeichen f zu einem float-Typ umgewandelt. Andere Typen müs-
188
(Daten)Typen
sen Sie über eine Typumwandlung (englisch: »Typecast« oder nur »Cast«) oder mit Hilfe der Methoden der Convert-Klasse explizit konvertieren.
Implizite und explizite Konvertierungen (Typumwandlungen) Jeder Typ kann spezielle Operatoren für implizite und explizite Konvertierungen zur Verfügung stellen. Solch ein Operator ist so etwas wie eine Methode, die allerdings automatisch aufgerufen wird, wenn der Typ konvertiert werden muss. Wenn Sie eine eigene Klasse oder Struktur erzeugen, können Sie beliebig viele Konvertierungs-Operatoren implementieren und damit die implizite oder explizite Konvertierung von verschiedenen Typen in Ihren Typ erlauben. Kapitel 5 zeigt, wie das programmiert wird. Implizite Konvertierungen werden, wie der Name schon sagt, implizit vorgenommen. Eine Voraussetzung dafür ist allerdings, dass der Typ, dem ein Ausdruck zugewiesen wird, einen Konvertierungsoperator für den Typ des Ausdrucks besitzt. Ist ein solcher vorhanden (was bei den Standardtypen für die möglichen impliziten Konvertierungen der Fall ist), müssen Sie nichts weiter programmieren.
1
2 Implizite Konvertierungen werden automatisch vorgenommen
Ein int-Typ lässt z. B. (u. a.) die implizite Konvertierung eines byte- und eines short-Typs zu:
3 4
int i4 = 0; byte b2 = 10; i4 = b2; // Implizite Konvertierung
Wenn ein Typ spezielle Operatoren besitzt, die die explizite Konvertierung eines anderen Typs erlauben, können Sie diesen anderen Typ über eine Typumwandlung explizit konvertieren. Dazu setzen Sie den neuen Typ in Klammern vor den zu konvertierenden Ausdruck. Die Typen int und float erlauben z. B. die explizite Konvertierung eines double-Werts.
Typumwandlungen erlauben eine explizite Konvertierung
5
6
Listing 3.25: Explizite Typumwandlung
7
double d1 = 1.2345678901234567890; // Explizite Konvertierung. Die Dezimalstellen gehen verloren. int i5 = (int)d1;
8
// Explizite Konvertierung. Die Genauigkeit wird eingeschränkt. float f2 = (float)d1;
Wie bereits gesagt, kann es beim expliziten Konvertieren vorkommen, dass Informationen verloren gehen. Die int-Variable des Beispiels speichert nach der Ausführung z. B. den Wert 1, die float-Variable den Wert 1,23456788. Ein wichtiger Merksatz ist, dass bei impliziten Konvertierungen normalerweise5 nichts verloren geht (bzw. »gehen sollte«, schließlich ist es dem Programmierer eines Typs überlassen, wie er implizite Konvertierungsoperatoren implementiert). Bei expliziten Konvertierungen kann es aber sein, dass Informationen verloren gehen. Für einige Fälle ist auch interessant, dass Sie eine vorhandene implizite Konvertierung auch explizit einsetzen können, um den Typ eines Ausdrucks festzulegen. Wenn Sie z. B. eine Integer-Variable durch eine andere teilen, erhalten Sie normalerweise eine Ganzzahldivision (siehe Seite 203). Wollen Sie aber eine normale Division ausführen, müssen Sie eine der Variablen nach float oder double umwandeln: 5
9
10 HALT
Implizite Konvertierungen können auch explizit verwendet werden
Die Konvertierung eines long-Werts in einen double-Wert führt allerdings dann zu ungenauen Werten, wenn es sich um große long-Werte handelt.
189
11
Die Sprache C#
Listing 3.26: Explizite Verwendung einer impliziten Konvertierung int i6 = 1; int i7 = 3; double d2 = i6 / (double)i7;
Konvertieren von Nullables Wenn Sie einem Werttypen ein Nullable-Objekt zuweisen wollen, müssen Sie diesen ebenfalls explizit konvertieren. Dabei müssen Sie beachten, dass eine Ausnahme generiert wird, wenn das Nullable-Objekt null speichert. Vor der Typumwandlung sollten Sie also auf null abfragen: Listing 3.27: Explizite Typumwandlung bei Nullables int? i8 = null; int i9; if (i8 != null) { i9 = (int)i8; }
Änderung des Typs eines Ausdrucks Wenn Sie den Typ von Ausdrücken ändern wollen, müssen Sie diese klammern. Der Typumwandlungsoperator gilt immer nur für den Operanden rechts von ihm: float f3 = (float)(1.234567890 * 1.234567890);
Konvertierungen über die Convert-Klasse Typen, die nicht denselben Grundtyp besitzen, können nicht einfach über eine Typumwandlung konvertiert werden. Ein String kann z. B. nicht in einen int-Wert umgewandelt werden: Listing 3.28: Explizite Typumwandlungen von Typen, die nicht denselben Grundtyp besitzen, sind nicht möglich string s2 = "10"; int i10 = (int)s2; // Fehler »Eine Konvertierung vom Typ "string" in // "int" ist nicht möglich«
Convert erlaubt die Konvertierung unterschiedlicher Typen
Zur Konvertierung von Typen mit unterschiedlichen Grundtypen können Sie die Klassenmethoden der Convert-Klasse verwenden. Der Grund dafür liegt darin, dass Konvertierungen zwischen grundverschiedenen Typen auch fehlschlagen können. Die Methoden der Convert-Klasse erzeugen in diesem Fall eine Ausnahme. Einen String konvertieren Sie z. B. über die ToInt32-Methode in einen Integer-Wert: Listing 3.29: Verwendung der Convert-Klasse zum Konvertieren grundverschiedener Typen string s3 = "10"; int i11 = Convert.ToInt32(s3);
Schlägt die Konvertierung fehl, erzeugt die Methode eine Ausnahme vom Typ FormatException: string s4 = "10a"; int i12 = Convert.ToInt32(s4); // Ausnahme
Diese Ausnahme können Sie abfangen, wie ich es noch in Kapitel 8 zeige.
190
(Daten)Typen
Tabelle 3.11 zeigt die wichtigsten Klassenmethoden der Convert-Klasse. Diese Methoden liegen in mehreren Varianten vor, die die unterschiedlichen Typen übergeben bekommen. Ich beschreibe nicht alle diese Varianten und erläutere deswegen die Grundlagen: Die Varianten, die keinen String übergeben bekommen, besitzen nur ein Argument. Der ToInt32-Methode können Sie z. B. im ersten Argument (u. a.) einen float-, short- oder double-Wert übergeben. Die Variante, die einen String übergeben bekommt, kann zusätzlich noch mit einem zweiten Argument aufgerufen werden, an dem Sie einen Format-Provider übergeben, der die länderspezifische Formatierung des Strings festlegt. Übergeben Sie diesen nicht, wird die Systemeinstellung berücksichtigt. Methode
konvertiert in
ToBoolean(...)
bool
ToByte(...)
byte
ToChar(...)
char
ToDateTime(...)
DateTime
ToDecimal(...)
decimal
ToDouble(...)
Double
ToInt16(...)
Short
ToInt32(...)
Int
ToInt64(...)
Long
ToSByte(...)
Sbyte
ToSingle(...)
Float
ToString(...)
String
ToUInt16(...)
ushort
ToUInt32(...)
uint
ToUInt64(...)
ulong
1
2 Tabelle 3.11: Die wichtigsten Klassenmethoden der Convert-Klasse
3 4
5
Für Konvertierungen in Strings können Sie auch die ToString-Methode verwenden, die alle Typen von object geerbt haben. Ob der zurückgegebene String den Inhalt des Objekts darstellt, hängt aber davon ab, ob der jeweilige Typ die ToString-Methode mit einer eigenen Implementierung überschreibt. Bei den Standardtypen und vielen anderen .NET-Typen ist das der Fall. Bei Typen, die ToString nicht überschreiben, wird dann allerdings die Implementierung verwendet, die in object definiert ist. Und die gibt einfach nur den vollen Klassennamen (inkl. Namensraum) zurück.
6
7
8
9
10 TIPP
11
191
Die Sprache C#
3.5.18 Aufzählungen (Enumerationen) Aufzählungen bestehen aus mehreren Konstanten
Aufzählungen sind Typen, die aus mehreren benannten Zahlkonstanten bestehen. Eine Aufzählung wird nach dem folgenden Schema deklariert: [Attribute] [Modifizierer] enum Name [: Basistyp] { Konstantenliste };
In der Liste geben Sie einen oder mehrere Bezeichner an. Das folgende Beispiel deklariert eine Aufzählung, die Himmelsrichtungen verwaltet: Listing 3.30: Einfache Aufzählung private enum Direction { North, South, East, West }
Wenn Sie keinen Typ angeben, besitzt die Aufzählung den Typ int. Sie können den Typ aber auch auf einen anderen festlegen, indem Sie diesen durch einen Doppelpunkt getrennt anhängen: Listing 3.31: Aufzählung mit expliziter Datentypangabe private enum TrafficLightColor: byte { Red, Yellow, Green }
Als Typ können Sie lediglich die Integer-Typen (byte, sbyte, short, ushort, int, uint, long und ulong) angeben. Andere Typen, und damit (leider) auch string, sind nicht möglich. Die neu erzeugten Aufzählungstypen können Sie nun überall da einsetzen, wo diese gültig sind. Zur Gültigkeit erfahren Sie mehr in Kapitel 4. Eine Methode kann z. B. ein Argument dieses Typs besitzen: static void Move(Direction direction) { ... }
Abbildung 3.8: IntelliSense bei der Verwendung einer Aufzählung
192
(Daten)Typen
Beim Aufruf der Methode muss nun ein passender Wert übergeben werden. Dabei greifen Sie über den Namen der Aufzählung auf die einzelnen Konstanten zu: Move(Direction.North);
Visual Studio unterstützt Sie dabei mit IntelliSense. Verwenden Sie Aufzählungen immer da, wo nur eine begrenzte Anzahl an Möglichkeiten besteht. Die Methode mit den Himmelsrichtungen ist dafür ein gutes Beispiel. Sie könnten aber auch auf die Idee kommen, solche Methoden mit einfachen Integer-Argumenten auszustatten. Sie müssten dann innerhalb der Methode im Programm festlegen, dass mit 0 Norden, mit 1 Osten etc. gemeint ist. Das Problem mit einer solchen Programmierung (die ich in meiner Praxis immer wieder sehe) ist aber, dass beim Aufruf der Methode nicht klar ist, was da eigentlich übergeben werden muss. Hinzu kommt, dass der Aufrufer (der auch ein anderer Programmierer sein kann) natürlich auch vollkommen ungeeignete Werte übergeben kann. Aufzählungen verhindern diese Probleme, da die verwendeten Werte zum einen für den Menschen lesbar sind und der C#-Compiler zum anderen nur die in der Aufzählung angegebenen Werte erlaubt (mit der Ausnahme, dass Sie auch ungültige Integer-Werte in die Aufzählung konvertieren können, wie ich unten beschreibe).
1 TIPP
2
3 4
Explizite Vergabe der Aufzählungswerte und bitweise Aufzählungen (Flag-Aufzählungen) Wenn Sie bei der Deklaration der Auflistung keinen Wert für die einzelnen Konstanten angeben, erhalten diese einen bei 0 beginnenden Wert. Sie können den Wert allerdings auch festlegen:
5
Listing 3.32: Aufzählung, bei der die Werte festgelegt sind
6
private enum FavoriteSports { Windsurfing = 1, Snowboarding = 2, Mountainboarding = 4, Landkiting = 8, Biking = 16 }
Wenn Sie die Werte wie im Beispiel so definieren, dass diese die einzelnen Bits des Typs repräsentieren, können Sie die Auflistung bitweise einsetzen. Dazu ist etwas Wissen notwendig, das erst in Abschnitt »Bitoperationen« (Seite 205) vermittelt wird:
7
8 Aufzählungen können Flags verwalten
9 Listing 3.33: Beschreiben einer Aufzählungsvariablen, die Werte bitweise verwalten kann FavoriteSports mySports = FavoriteSports.Windsurfing | FavoriteSports.Snowboarding | FavoriteSports.Landkiting | FavoriteSports.Biking;
10
Sie können solche Aufzählungen natürlich auch auswerten. Das Auswerten setzt wieder Wissen aus dem Abschnitt »Bitoperationen« (Seite 205) voraus:
11
Listing 3.34: Auswerten einer Aufzählungsvariablen, die Werte bitweise verwalten kann if ((mySports & FavoriteSports.Windsurfing) > 0) { Console.WriteLine("Ich mag Windsurfen"); } if ((mySports & FavoriteSports.Snowboarding) > 0) {
193
Die Sprache C#
Console.WriteLine("Ich mag Snowboarden"); } if ((mySports & FavoriteSports.Mountainboarding) > 0) { Console.WriteLine("Ich mag Mountainboarden"); } if ((mySports & FavoriteSports.Landkiting) > 0) { Console.WriteLine("Ich mag Landkiten"); } if ((mySports & FavoriteSports.Biking) > 0) { Console.WriteLine("Ich mag Biken"); }
TIPP
Die ToString-Methode einer Aufzählung gibt normalerweise den Namen der Konstante zurück, wenn nur der Wert einer Konstanten gespeichert ist. Sind aber in einer Aufzählung, die die Konstantenwerte bitweise verwaltet, mehrere Werte gesetzt, wird der numerische Wert zurückgegeben. Im Beispiel wäre das 27. Wenn Sie allerdings Aufzählungen, die Werte bitweise verwalten, mit dem Flags-Attribut belegen, führt dies dazu, dass ToString die Namen der gespeicherten Konstanten kommagetrennt zurückgibt: Listing 3.35: Flags-Aufzählung [Flags] private enum FavoriteSports { Windsurfing = 1, Snowboarding = 2, Mountainboarding = 4, Landkiting = 8, Biking = 16 }
Attribute werden in Kapitel 6 behandelt. REF
Wenn Sie Flags verwalten wollen, können Sie dazu auch die Klassen BitArray und BitVector32 verwenden. Diese Klassen werden in Kapitel 7 beschrieben. INFO
Zusammenfassen von Flag-Werten In Flags-Aufzählungen können Sie die einzelnen Werte auch zusammenfassen um diese einfacher setzen und abfragen zu können: Listing 3.36: Flags-Aufzählung mit zusammengefassten Werten [Flags] private enum FavoriteSports { Windsurfing = 1, Snowboarding = 2, Mountainboarding = 4, Landkiting = 8, Biking = 16, WindSports = Windsurfing | Landkiting, MountainSports = Snowboarding | Mountainboarding | Biking }
194
Variablen und Konstanten
Konvertieren von Aufzählungen Aufzählungen können natürlich auch in ihren Basistyp umgewandelt werden und umgekehrt. Dazu verwenden Sie eine explizite Konvertierung: Listing 3.37: Umwandeln eines Aufzählungswerts in seinen Basistyp und umgekehrt Direction direction = Direction.North; byte directionValue = (byte)direction; Console.WriteLine("Der Wert von " + direction + " ist " + directionValue); // Der Wert von North ist 0 directionValue = 2; direction = (Direction)directionValue; Console.WriteLine("Der Direction-Name von " + directionValue + " ist " + direction); // Der Direction-Name von 2 ist East
Beim expliziten Konvertieren können Sie die Typsicherheit unterlaufen. So können Sie der Direction-Aufzählung z. B. den eigentlich ungültigen Wert 42 zuweisen:
1
2
3 HALT
direction = (Direction)42; Console.WriteLine(direction); // 42
4
Auch wenn das philosophisch betrachtet vielleicht korrekt wäre (42 ist schließlich die Antwort auf die Frage aller Fragen, warum sollte diese wichtige Antwort nicht auch gleich die richtige Richtung (fürs Leben) sein ☺), das Programm funktioniert in einem solchen Fall mit Sicherheit nicht korrekt. Passen Sie bei Konvertierungen von Integer-Werten in Aufzählungswerte also auf.
5
Ein Tipp 6
Der folgende Tipp für die Arbeit mit Aufzählungen ist für die Praxis sehr wichtig, setzt allerdings Programmiertechniken voraus, die erst später behandelt werden. Wenn Sie alle Werte einer Aufzählung ermitteln wollen, können Sie dazu die Methode GetValues der Enum-Klasse verwenden.
7
Listing 3.38: Ermitteln aller Werte einer Aufzählung foreach (Direction d in Enum.GetValues(typeof(Direction))) { Console.WriteLine(d); }
8
Über Enum.GetNames können Sie die Namen der Aufzählungskonstanten ermitteln. Das ist aber eigentlich nicht notwendig, da die ToString-Methode eines Aufzählungstyps den Namen der Konstante sowieso bereits zurückgibt, wie es im obigen Beispiel der Fall ist.
9
10
3.6
Variablen und Konstanten
In Variablen und Konstanten können Sie Werte speichern und Objekte referenzieren. Eine Variable oder eine Konstante besitzt wie Literale oder Ausdrücke einen Typ. Variablen können im Programm verändert werden, der Wert einer Konstanten ist unveränderlich.
11
C# unterscheidet die üblichen Bereiche für die Lebensdauer und die Gültigkeit: Variablen und Konstanten können Sie innerhalb einer Methode deklarieren. Dann gilt diese nur innerhalb der Methode und lebt nur so lange, wie die Methode ausgeführt
195
Die Sprache C#
wird. Eine andere Möglichkeit der Deklaration ist außerhalb von Methoden auf der Ebene der Klasse oder Struktur. »Variablen« werden dann allerdings als Felder bezeichnet. Felder können statisch sein, dann leben sie, solange das Programm ausgeführt wird. »Normale« Felder sind Instanzfelder und leben nur so lange, wie die Instanz der Klasse bzw. Struktur (das Objekt) lebt. Ich beschreibe hier nur die Deklaration von Variablen und Konstanten innerhalb einer Methode, weil Kapitel 4 ausführlich auf normale und statische Felder (und Eigenschaften) eingeht.
3.6.1
Die Deklaration von Variablen
Bei der Deklaration einer Variablen geben Sie optionale Attribute, einen optionalen Modifizierer, den Datentyp und den Bezeichner an. Eine Variable können Sie direkt bei der Deklaration initialisieren: [Attribute] [Modifizierer] {Typ | var} Bezeichner [= Ausdruck];
Über Attribute können Sie zusätzliche Informationen zu einer Variablen definieren, die entweder vom Compiler oder von externen Programmen ausgewertet werden können. Attribute werden in Kapitel 6 behandelt. Der Modifizierer wird bei Feldern (und Eigenschaften) verwendet, die Sie innerhalb von Klassen deklarieren (siehe Kapitel 4). Er legt den Gültigkeitsbereich des Feldes bzw. der Eigenschaft fest. Als Typ für die Variable wird entweder ein vorhandener Typ (z. B. int) oder das Schlüsselwort var angegeben. var führt zu einer implizit typisierten Variable (Seite 198). Innerhalb einer Methode wird kein Modifizierer verwendet. Die folgende Anweisung deklariert eine int-Variable und initialisiert deren Wert auf 0: int i1 = 0;
Sie können in einer Anweisung gleich mehrere Variablen deklarieren, die Sie durch Kommata getrennt angeben: int i2 = 0, j1 = 0, k1 = 0;
Die so deklarierten Variablen erhalten natürlich alle denselben Typ.
Blockweise Gültigkeit Variablen gelten blockweise
In C# gelten Variablen blockweise. Wenn Sie eine Variable in einem Block deklarieren, können Sie außerhalb des Blocks nicht auf die Variable zugreifen: // Deklaration innerhalb einer Abfrage if (i1 == 0) { string s1 = "i1 ist 0"; } Console.WriteLine(s1); // Dieser Zugriff ist nicht möglich
Daneben können Sie innerhalb einer Methode keine Variable erneut deklarieren, die in der Methode bereits deklariert wurde. Dazu gehören auch Variablen, die bereits in Blöcken deklariert wurden, folglich eigentlich nur in dem Block gelten: // Deklaration innerhalb einer Abfrage if (i1 == 0) {
196
Variablen und Konstanten
string s2 = "i1 ist 0"; Console.WriteLine(s2); } string s2 = ""; // Diese Deklaration ist nicht möglich
Sie können aber Felder und Eigenschaften deklarieren, die denselben Namen tragen wie Variablen in einer Methode. Oder andersherum: Die Namen von Variablen können dieselben sein wie die von Feldern oder Eigenschaften. Wenn Sie innerhalb einer Methode auf eine solche Variable zugreifen, verwendet der Compiler immer die auf der niedrigsten Gültigkeitsebene, also die Variable, die innerhalb der Methode deklariert ist. Wenn Sie in einer Methode explizit auf ein Feld oder eine Eigenschaft zugreifen wollen, das/die denselben Namen trägt wie eine Variable, verwenden Sie den this-Operator als Präfix vor dem Namen. Wenn Sie sich an die Konvention halten, diesen Operator beim Zugriff auf Felder und Eigenschaften immer zu verwenden, haben Sie prinzipiell kein Problem beim Zugriff auf Variablen, Felder und Eigenschaften.
3.6.2
1
2 TIPP
3
Konstanten 4
Konstanten werden ähnlich wie Variablen deklariert: [Attribute] [Modifizierer] const Typ Name = Ausdruck;
5
Bei einer Konstantendeklaration müssen Sie immer einen Wert zuweisen. Die Modifizierer gelten wieder für die Deklaration auf Klassenebene. Den Wert einer Konstanten können Sie im Programm nicht mehr ändern (das ist ja auch der Sinn einer Konstanten ☺).
6
Wenn Sie konsequent Konstanten einsetzen, verzichten Sie damit automatisch auf Magic Numbers (Magische Zahlen). Magische Zahlen sind in Programmen verwendete Zahlliterale. Das Magische an diesen Zahlen ist, dass deren Bedeutung an Hand des Werts nicht erfasst werden kann. Setzen Sie stattdessen Konstanten (oder Aufzählungen) ein, ist deren Bedeutung hingegen klar, sofern Sie aussagekräftige Namen verwenden. Ein anderer Grund für den Verzicht auf magische Zahlen ist, dass die Werte, mit denen Sie arbeiten, sich im Laufe der Weiterentwicklung des Programms ändern können. Wenn Sie eine magische Zahl für einen Wert verwenden, der an vielen Stellen im Programm benötigt wird, ist eine Änderung nur sehr schwer möglich und führt in vielen Fällen zu Fehlern. Setzen Sie stattdessen eine aussagekräftig benannte Konstante ein, ist die Änderung einfach.
7
Konstanten erleichtern die Wartung eines Programms
9
Auch die spätere Umsetzung einer Konstanten in eine Variable, in ein Feld oder in eine Eigenschaft ist einfach. Diese Umsetzung ist z. B. dann notwendig, wenn Sie den Wert in der Laufzeit aus der Konfiguration der Anwendung auslesen und diesen so für den Anwender änderbar machen (wie das geht, zeigt Kapitel 8).
3.6.3
10
11
Array-Grundlagen
Arrays erlauben das zusammenhängende Speichern mehrerer gleichartiger Informationen. Sie verhalten sich wie eine Liste einzelner Variablen, auf die Sie über den Arraynamen und einen Integer-Index zugreifen können. Arrays sind in C# komplexe Objekte und besitzen einige Möglichkeiten, die ich in diesem Kapitel noch nicht besprechen will. In Kapitel 7 erfahren Sie wesentlich mehr über Arrays und daneben
8
Arrays speichern Listen von Variablen
197
Die Sprache C#
über die für die Speicherung von Daten-Listen wesentlich flexibleren Auflistungen. Hier beschreibe ich lediglich, wie Sie einfache (eindimensionale) Arrays erzeugen und damit arbeiten. Einfache (eindimensionale) Arrays erzeugen Sie nach dem folgenden Schema: Typ[] Name = new Typ[Anzahl der Elemente];
Als Typ können Sie jeden der verfügbaren Typen einsetzen, auch komplexe Typen wie Strukturen oder Klassen. Der Name eines Arrays entspricht den Namen einer Variablen und muss den Bedingungen für Bezeichner genügen. Arrays sind in C# Referenztypen, die mit new erzeugt werden müssen. Dabei geben Sie hinter dem Typ die Anzahl der Elemente in eckigen Klammern an. So können Sie z. B. ein Array deklarieren, das drei Integer-Werte verwalten kann: Listing 3.39: Deklaration eines Integer-Arrays int[] numbers = new int[3];
Der Index eines Arrays beginnt bei 0
Die einzelnen Elemente eines Arrays können Sie wie Variablen lesen und schreiben. Der Unterschied ist lediglich, dass Sie den Index des Elements angeben müssen. Dazu verwenden Sie die C#-Indexer-Syntax: Schreiben Sie den Index in eckige Klammern hinter den Namen der Variablen. Der Index eines Arrays (und einer Auflistung) beginnt in C# immer mit 0: Listing 3.40: Zugriff auf ein Array numbers[0] = 3; numbers[1] = 7; numbers[2] = 42;
Arrays können durchlaufen werden
Arrays haben gegenüber normalen Variablen zur Speicherung zusammenhängender Daten die Vorteile, dass die Daten über eine Variable erreichbar sind und dass diese über eine Schleife durchlaufen werden können. Die Length-Eigenschaft, die die Anzahl der gespeicherten Elemente zurückgibt, ergibt dabei den größten Index (subtrahiert mit 1): Listing 3.41: Durchgehen eines Arrays for (int i = 0; i < numbers.Length; i++) { Console.WriteLine(numbers[i]); }
Schleifen werden ab Seite 223 behandelt.
3.6.4
NEU
Implizit typisierte lokale Variablen
C# 3.0 ermöglicht die Deklaration von Variablen ohne Typangabe über das varSchlüsselwort. var number = 11.5;
198
Variablen und Konstanten
Auf diese Weise deklarierte Variablen sind allerdings nicht (wie z. B. in JavaScript) typenlos. Der Compiler ermittelt beim Kompilieren den Typ des zugewiesenen Ausdrucks und erzeugt im CIL-Code eine Variable von diesem Typ. Die Variable in der obigen Anweisung besitzt z. B. den Typ double. Beweisen können Sie dies, indem Sie den Typ ausgeben:
Implizit typisierte lokale Variablen sind nicht typlos
Console.WriteLine(number.GetType().Name); // Double
Ein weiterer Beweis ist der erzeugte CIL-Code:
1
Listing 3.42: Auszug aus dem CIL-Code einer Konsolenanwendung mit den oben angegebenen Anweisungen method private hidebysig static void { .entrypoint // Code size 34 (0x22) .maxstack 1 .locals init ([0] float64 number) IL_0000: nop IL_0001: ldc.r8 11.5 IL_000a: stloc.0 IL_000b: ldloc.0 ...
2
Main(string[] args) cil managed
Für implizit typisierte lokale Variablen gilt eigentlich nur eine Regel: Der Compiler muss die Möglichkeit haben, den Typ der Variablen aus dem zugewiesenen Ausdruck zu erkennen. Der Typ des Ausdrucks ist dabei unerheblich, es kann sich um die Standardtypen, um sonstige Werttypen und um Referenztypen handeln. Objektinitialisierer (und die in Kapitel 7 behandelten Auflistungs-Initialisierer) sind nicht direkt möglich, da ein solcher Ausdruck keinen Typ ergibt. Sie können aber neue Objekte über das new-Schlüsselwort erzeugen und damit Objekt- oder Auflistungs-Initialisierer verwenden:
3 4 Der Compiler muss den Typ einer implizit typisierten lokalen Variablen erkennen
5
6
Listing 3.43: Mögliche Zuweisungen an implizit typisierte lokale Variablen
7
// OK, der Ausdruck ergibt float var v1 = 10 * 1.5F;
8
// OK, der Ausdruck ergibt ein DateTime-Objekt var v2 = new DateTime(2010, 12, 31); // OK, der Ausdruck ergibt ein PersonStruct-Objekt var v3 = new PersonStruct { FirstName = "Zaphod", LastName = "Beeblebrox" };
9
// Anmerkung: PersonStruct entstammt dem Abschnitt // »Wert- und Referenztypen« auf Seite _Ref179608779
10
Listing 3.44: Nicht mögliche Zuweisungen an implizit typisierte lokale Variablen // Nicht OK, da Array-Initialisierer nicht unterstützt werden var v4 = { 1, 2, 3 };
11
// Nicht OK, da null keinen Typ besitzt var v5 = null;
Das automatische Erkennen des Variablentyps wird übrigens als Typrückschluss (Type inference) bezeichnet. Implizit typisierte lokale Variablen sind bei anonymen Typen (Kapitel 4) obligatorisch und werden intensiv mit LINQ (Kapitel 11) eingesetzt. Außerdem können Sie
199
Die Sprache C#
diese Variablen sehr gut mit der foreach-Schleife einsetzen (Seite 226), was der foreach-Codeschnipsel von Visual Studio bereits macht.
3.7 Quellcode wird über Namensrichtlinien verständlicher
Namensrichtlinien
Bei der Programmierung macht es immer Sinn, sich an gewisse Richtlinien bei der Vergabe von Bezeichnern zu halten. Damit machen Sie es anderen Programmierern (und auch sich selbst) leichter, Ihren Quelltext zu verstehen. Für die verschiedenen Programmiersprachen haben sich unterschiedliche Notationen für die Benennung entwickelt. C++-Programmierer verwenden z. B. meist die so genannte »Ungarische Notation«, Visual Basic-Programmierer wenden die »ReddikKonvention« an. Für C# beschreibt Microsoft in der C#-Dokumentation eine Richtlinie für die Benennung von Bezeichnern, die recht übersichtlich ist. Diskussionen in verschiedenen Newsgroups und die im Internet verfügbaren C#-Programme zeigen, dass sich diese Richtlinien durchgesetzt haben.
Bezeichner werden mit Pascal und Camel Casing benannt
Grundlage der Benennung von Bezeichnern sind das so genannte Pascal Casing und das Camel Casing. Beim Pascal Casing beginnt der Bezeichner mit einem Großbuchstaben und wird klein weitergeschrieben. Besteht der Bezeichner aus mehreren Worten, wird jedes Wort wieder mit einem Großbuchstaben begonnen: public int LeftMargin;
Das Camel Casing ist ähnlich, nur dass der Bezeichner mit einem Kleinbuchstaben beginnt: int leftMargin;
Pascal Casing wird hauptsächlich bei öffentlichen Elementen verwendet, Camel Casing bei privaten oder lokalen Elementen. Tabelle 3.12 fasst die Richtlinien zusammen. Tabelle 3.12: Namensrichtlinien für C#-Programme
Element
Namensrichtlinie
Klasse
– Pascal Casing
Schnittstelle
– Pascal Casing – »I« als Präfix
Aufzählungen (Enums)
– Pascal Casing (für den Namen der Aufzählung und die Namen der Werte)
Eigenschaften
– Bennennung mit Substantiven (Hauptwörtern) oder Substantiv-Phrasen (z. B. Color, FirstName) – Pascal Casing für öffentliche Eigenschaften – Camel Casing für private und geschützte Eigenschaften
Methoden
– Bennennung mit Verben oder Verb-Phrasen (z. B. Remove, RemoveAll) – Pascal Casing
200
Ausdrücke und Operatoren
Element
Namensrichtlinie
Ereignisse
– Benennen Sie die Delegaten für Ereignisse mit dem »EventHandler«-Suffix (z. B. MouseEventHandler)
Tabelle 3.12: Namensrichtlinien für C#-Programme (Forts.)
– Verwenden Sie die zwei Argumente sender und e – Benennen Sie Ereignisargument-Klassen mit dem Suffix »EventArgs« (z. B. MouseEventArgs)
1
– Verwenden Sie Pascal Casing Argumente
– Verwenden Sie aussagekräftige Namen – Verwenden Sie Camel Casing
Präfixe werden (von fast allen C#-Programmierern, wie Recherchen in Newsgroups zeigten) grundsätzlich nicht verwendet. Die C#-Konvention weicht damit erheblich von anderen Konventionen ab, die eine Präfix vor den eigentlichen Namen setzen, der den Datentyp kennzeichnet. Diese in C++ als ungarische Notation und in Visual Basic als Reddik-Konvention bezeichnete Konvention bringt in der Praxis aber unter C# nicht allzu viele Vorteile. C# ist typsicher, Sie müssen also nicht wissen, welchen Typ eine Variable besitzt: Der Compiler meldet bei falschen Zuweisungen einen Fehler und Sie müssen explizit konvertieren, wenn der Typ nicht implizit konvertiert werden kann.
2 Keine Präfixe für Variablen
3 4
5 Ich benenne die Steuerelemente auf Windows.Forms-, WPF- und ASP.NET-Formularen (und anderen Containern) allerdings mit einem zwei- bis dreistelligen, kleingeschriebenen Präfix, den ich dem Namen der Klasse entnehme. Der Name einer ButtonInstanz beginnt in meinen Anwendungen (und im Buch) z. B. immer mit btn. Der Vorteil dieser Konvention ist, dass ich die verwendeten Steuerelemente im Programm über IntelliSense sehr schnell finde, da ich nur den Präfix eingeben muss, um alle Steuerelemente einer bestimmten Klasse untereinander aufgelistet zu bekommen. Das hat sich in der Praxis bewährt. Sie finden meine Namenskonvention im Anhang.
3.8
INFO
6
7
Ausdrücke und Operatoren
8
Die Typen, die Sie in den vorhergehenden Kapiteln kennen gelernt haben, setzen Sie beim Programmieren häufig in Ausdrücken als Operanden ein, die Sie über Operatoren miteinander verknüpfen. Ausdrücke sind entweder arithmetische (die etwas berechnen) oder logische (die eine Bedingung überprüfen).
3.8.1
9
Der Typ eines Ausdrucks 10
Ein Ausdruck ergibt eigentlich immer einen Wert, der einen Typ besitzt. Eigentlich deswegen, weil es eine Sonderform eines Ausdrucks gibt, die keinen Wert ergibt. Das ist z. B. dann der Fall, wenn eine Methode aufgerufen wird, die nichts zurückgibt, wie z. B. Console.WriteLine. Der einfachste Leer-Ausdruck ist allerdings ein einfaches Semikolon.
11
Um Ausdrücke zu verstehen, müssen Sie wissen, wie der Compiler den Typ eines Ausdrucks bestimmt. Der Compiler muss beim Kompilieren den Typ eines Ausdrucks bestimmen, weil das Ergebnis eines Ausdrucks (und jedes Teilergebnis) in einem temporären Speicherbereich gespeichert wird, bevor dieses weiterverarbeitet wird. Dieser Speicherbereich
Der Compiler bestimmt den Typ eines Ausdrucks
201
Die Sprache C#
muss dazu eine für das Ergebnis (wahrscheinlich) ausreichende Größe besitzen und im weiteren (CIL-)Programm mit dem richtigen Typ ausgewertet werden. Zur Bestimmung des Typs eines Ausdrucks untersucht der Compiler die einzelnen Operanden. Als Ergebnistypen verwendet er einen der enthaltenen Typen, der das Ergebnis am wahrscheinlichsten aufnehmen kann. Der Typ des Ausdrucks (byte)1 + 10 ist demnach int, der Typ des Ausdrucks 1 + 10.5 ist double, bei 1 * 10F kommt ein float-Wert heraus. Dabei werden aber immer nur die im Ausdruck enthaltenen Typen als Basis verwendet. Das kann natürlich auch zu Problemen führen. Wenn Sie z. B. zwei int-Werte addieren oder multiplizieren, kann der vom Compiler verwendete int-Speicherbereich das tatsächliche Ergebnis häufig nicht verwalten und es kommt zu einem Unteroder Überlauf. Das passiert sogar dann, wenn Sie das Ergebnis einem größeren Typen zuweisen: Listing 3.45: Überlauf in einem Integer-Ausdruck int number1 = int.MaxValue; long result1 = number1 * 2; Console.WriteLine(result1); // -2
Das Ergebnis des Ausdrucks ist hier -2, weil die Multiplikation des Maximalwerts von int mit 2 einen Überlauf verursacht.
HALT
Dieses Verhalten sollten Sie bei Ausdrücken immer im Auge behalten. Lösen können Sie das Problem, indem Sie einen der Operanden im Ausdruck zum erwarteten Typ konvertieren: int number2 = int.MaxValue; long result2 = number2 * (long)2; Console.WriteLine(result2); // -2
3.8.2 Arithmetische Ausdrücke ergeben einen Wert
Arithmetische Ausdrücke und Operatoren
Arithmetische Ausdrücke ergeben einen Wert (eine Zahl, ein Datum, eine Zeichenkette), der in weiteren Ausdrücken oder in Zuweisungen verwendet werden kann. Der Ausdruck 1 + 1
ergibt z. B. den Wert 2 (wenn ich richtig gerechnet habe ...). Arithmetische Ausdrücke verwenden die in Tabelle 3.13 beschriebenen Operatoren, von denen einige unär sind (d. h. nur einen Operanden besitzen) und einige binär (d. h. zwei Operanden besitzen). Tabelle 3.13: Die arithmetischen Operatoren
202
Operator
Bedeutung
()
Klammern; Verschieben der Rechenpriorität
+
Addition zweier Operanden
++
Addition eines Operanden mit 1
-
Subtraktion zweier Operanden
Ausdrücke und Operatoren
Operator
Bedeutung
--
Subtraktion von 1 von einem Operanden
*
Multiplikation
/
Division
%
Modulo-Division
Klammern verwenden Sie immer dann, wenn Sie die Rechenpriorität verschieben wollen. In Computern gilt natürlich auch die in der Mathematik verwendete Rechenpriorität (»Punktrechnung vor Strichrechnung«). Der Ausdruck 1 + 2 * 2 ergibt demnach 5, weil erst die Multiplikation ausgerechnet wird.
Tabelle 3.13: Die arithmetischen Operatoren (Forts.)
1 Klammern verschieben die Priorität
Ich könnte jetzt an dieser Stelle eine Prioritätsliste der Operatoren liefern. Stattdessen gebe ich aber lediglich einen wichtigen Praxistipp: Verwenden Sie immer Klammern um die Priorität in Ausdrücken explizit festzulegen. Verlassen Sie sich nie auf die vordefinierte Priorität. Sie müssten ansonsten jeden Ausdruck hinsichtlich der Priorität der einzelnen Operatoren untersuchen um den Ausdruck interpretieren zu können. Sind Teilausdrücke hingegen geklammert, vermeiden Sie zum einen Fehler und machen den Ausdruck zum anderen wesentlich lesbarer. Auch wenn Sie beim Ausdruck 1 + 2 * 2 die Standard-Rechenpriorität verwenden wollen, setzen Sie Klammern: 1 + (2 * 2). Die Operatoren +, –, *, / und ^ müssen wohl nicht erläutert werden … Außer, dass der Operator / nicht unbedingt das vom Programmierer geplante Ergebnis liefert ☺. Das Problem liegt an der im vorigen Abschnitt beschriebenen Auswertung der Operanden eines Ausdrucks zur Ermittlung des Ergebnis-Typs. Wenn Sie zwei IntegerWerte teilen, wird das Ergebnis demnach als Ganzzahl ausgewertet. Deswegen ergeben 1 / 3 und 2 / 3 den Wert 0 und 5 / 3 den Wert 1:
2
3 4 HALT
5
/ führt u. U. zu einer Ganzzahldivision
6
7
Listing 3.46: Beispiele für Ganzzahldivisionen
8
Console.WriteLine("1 / 3 ergibt " + 1 / 3); // 0 Console.WriteLine("2 / 3 ergibt " + 2 / 3); // 0 Console.WriteLine("5 / 3 ergibt " + 5 / 3); // 1
Ganzzahl- und Restwertdivisionen
9
Der Divisionsoperator arbeitet also als Ganzzahldivisionsoperator bei Integer-Werten. Ist im Ausdruck eine Dezimalzahl enthalten, resultiert hingegen ein float-, double- oder decimal-Wert:
10
Listing 3.47: Beispiele für Divisionen mit Dezimalzahlen Console.WriteLine("1 / 3F ergibt " + 1 / 3F); // 0,3333333 Console.WriteLine("2 / 3F ergibt " + 2 / 3F); // 0,6666666 Console.WriteLine("5 / 3F ergibt " + 5 / 3F); // 1,6666666
11
In vielen Fällen ist eine Ganzzahldivision aber auch erwünscht. Wenn Sie z. B. in einem Programm berechnen wollen, wie viele Lagerbehälter für eine bestimmte Anzahl zu lagernde Objekte benötigt werden, wenn fünf Objekte in ein Paket passen, verwenden Sie dazu eine Ganzzahldivision:
203
Die Sprache C#
int numberOfObjects = 552; int numberOfStorgageContainers = numberOfObjects / 5;
Das Ergebnis hier sind 110 Lagerbehälter, die vollständig gefüllt werden. Dabei bleiben allerdings zu lagernde Objekte übrig (im Beispiel 2). Diese können Sie über eine einfache Subtraktion berechnen: int remainingObjects = numberOfObjects (numberOfStorgageContainers * 5);
Restwertdivisionen ergeben den Restwert
Sie können aber auch eine Restwertdivision über den Modulo-Operator ausführen: int remainingObjects = numberOfObjects % 5;
Potenzierungen Für Potenzierungen liefert C# keinen Operator. Potenzierungen können Sie über die Pow-Methode der Math-Klasse vornehmen. Die Potenzierung 25 berechnen Sie z. B. so: double result = Math.Pow(2, 5);
String-Verkettungen Über den Operator + können Sie auch Strings verketten: Listing 3.48: String-Verkettung string firstName = "Donald"; string lastName = "Duck"; Console.WriteLine(firstName + " " + lastName);
INFO
Wenn Sie reine String-Literale verketten, erzeugt der Compiler daraus einen einzigen String. Aus "a" + "b" wird im CIL-Code "ab". Deswegen können Sie größere StringLiterale (z. B. SQL-Anweisungen) im Quellcode beruhigt in mehrere Zeilen auftrennen, ohne befürchten zu müssen, dass Ihr Programm aufgrund der 1/10000000000000 Sekunde, die eine String-Verkettung ansonsten benötigt, zu langsam wird (ok, das war jetzt ironisch an die Performance-Freaks gerichtet …). Bei Verkettungen mit String-Variablen oder bei dynamisch ausgeführten Verkettungen sollten Sie aus Performancegründen allerdings ab etwa fünf Verkettungen die StringBuilder-Klasse verwenden. Diese wird in Kapitel 8 behandelt.
Die unären Operatoren ++ und – – Die Operatoren ++ und -- arbeiten unär. Die Anweisung i++;
addiert z. B. den Wert 1 zu der Variablen i. Diese Operatoren können Sie vor (PräfixNotation) oder hinter den Operanden setzen (Postfix-Notation). Ein Unterschied wird allerdings erst dann sichtbar, wenn die Operatoren in Ausdrücken verwendet werden: Listing 3.49: Beispiele für die Präfix- und Postfix-Notation des unären Operators ++ int i = 1; Console.WriteLine(++i); // 2, da i vor der Auswertung des Ausdrucks // inkrementiert wird Console.WriteLine(i); // 2 i = 1;
204
Ausdrücke und Operatoren
Console.WriteLine(i++); // 1, da i erst nach der Auswertung des Ausdrucks // inkrementiert wird Console.WriteLine(i); // 2
Die Präfix-Notation bewirkt, dass der Compiler zuerst den Wert des Operanden addiert bzw. subtrahiert und danach den Ausdruck auswertet. Der Ausdruck verwendet also den veränderten Wert. Die erste und die zweite WriteLine-Anweisung geben folglich den Wert 2 aus.
1
Wenn Sie die Postfix-Notation verwenden, wird der Wert des Operanden erst addiert bzw. subtrahiert, nachdem der Ausdruck vom Compiler ausgewertet wurde. Die dritte WriteLine-Anweisung gibt also 1 aus, die vierte dann wieder 2.
2 Ich halte diese, von C++ übernommene Auswertung der unären arithmetischen Operatoren für überflüssig, verwirrend und gefährlich. Sehr leicht können Sie in Ihren Programmen logische Fehler produzieren, wenn Sie diese Operatoren unbedacht einsetzen. Achten Sie immer darauf, dass die Präfix-Notation den Wert vor und die Postfix-Notation den Wert erst nach der Auswertung des Ausdrucks verändert, wenn Sie die Operatoren ++ und -- in Ausdrücken einsetzen. Idealerweise vermeiden Sie den Einsatz dieser Operatoren in Ausdrücken. Sie können die Addition/Subtraktion ja auch vor dem Ausdruck ausführen.
3.8.3
HALT
3 4
Bitoperationen 5
C# enthält einige Operatoren, über die Sie die einzelnen Bits eines Integer- oder booleschen Typen bearbeiten können. Operator
Bedeutung
|
Or
&
And
^
Xor
~
Komplementär-Operation
>
Rechtsverschiebung
Tabelle 3.14: Die bitweisen Operatoren
6
7
8
9 Der Or-Operator (|) Mit Hilfe des Or-Operators können Sie einzelne Bits in einem Integer-Wert gezielt setzen, ohne die anderen Bits zu beeinflussen. Im Ergebnis eines Or-Ausdrucks sind alle Bits gesetzt, die entweder in dem einen oder im anderen Operanden gesetzt sind. Dual ausgedrückt ergibt 01012 | 10012 z. B. den Wert 11012, weil die Bits 1 und 3 im ersten und die Bits 1 und 4 im zweiten Operanden gesetzt sind.
Über den OrOperator können Bits gesetzt werden
10
11
So können Sie z. B. gezielt das Bit 3 und 4 (das mit dem Wert 4 und das mit dem Wert 8) einer Variablen setzen, unabhängig vom bereits in der Variablen gespeicherten Wert:
205
Die Sprache C#
Listing 3.50: Beispiel für das Setzen einzelner Bits int i = 0; i = i | 4; // Bit 3 setzen i = i | 8; // Bit 4 setzen // Alternativ: i = i | 4 | 8;
Eine solche Zuweisung kann noch etwas kürzer geschrieben werden (vgl. Seite 207): i |= 4; i |= 8;
Sie benötigen den |-Operator im Besonderen mit Aufzählungen, deren Konstanten die einzelnen Bits des Aufzählungswerts repräsentieren (siehe bei »Aufzählungen (Enumerationen)« ab Seite 192 ).
Der And-Operator (&) Der And-Operator ermöglicht das Abfragen, ob Bits gesetzt sind
Im Ergebnis einer And-Operation sind nur die Bits gesetzt, die in dem einen und in dem anderen Operanden gesetzt sind. Im Dualsystem ausgedrückt ergibt z. B: 01012 And 01002 den Wert 01002, weil nur das dritte Bit (von rechts aus gesehen) in beiden Operanden gesetzt ist. Den &-Operator benötigen Sie immer dann, wenn Sie überprüfen wollen, ob ein bestimmtes Bit in einem Integer-Wert gesetzt ist. Die folgende Anweisung überprüft z. B., ob das vierte Bit (das mit dem Wert 8) in der Variablen i gesetzt ist: Listing 3.51: Beispiel für das Abfragen einzelner Bits if ((i & 8) > 0) { Console.WriteLine("Bit 4 ist gesetzt"); } else { Console.WriteLine("Bit 4 ist nicht gesetzt"); }
Genau wie den |-Operator benötigen Sie den &-Operator häufig bei Aufzählungen, deren Konstanten die einzelnen Bits des Aufzählungswerts repräsentieren, um die Konstanten der Aufzählung abzufragen.
Der XOr-Operator (^) Der XOr-Operator ^ führt eine Exklusiv-Oder-Operation aus. Im Ergebnis ist ein Bit gesetzt, wenn dieses Bit in einem Operanden gesetzt und im anderen Operanden nicht gesetzt ist. Dual ausgedrückt ergibt z. B. 01012 ^ 10012 den Wert 11002. Mit diesem Operator können Sie einzelne Bits in einer Variablen gezielt umschalten. Gesetzte Bits werden so auf 0 gesetzt, Bits mit dem Wert 0 auf 1. Das folgende Beispiel schaltet das Bit 3 um: i = i ^ 4;
Der Komplementär-Operator (~) Mit dem Komplementär-Operator ~ können Sie das Einer-Komplement eines Wertes ermitteln. Dabei werden einfach alle Bits des Wertes umgekippt. Aus 1 wird 0 und aus 0 wird 1.
206
Ausdrücke und Operatoren
Links- und Rechtsschieben Mit den Operatoren >> und
Größer
=
Größer/Gleich
Lediglich die Operatoren == und != können Sie auf alle .NET-Typen anwenden.
INFO
208
Dabei müssen Sie aber beachten, dass diese Operatoren nur bei Werttypen, dem Typ String und bei Referenztypen, die diese Operatoren speziell überladen haben (was das ist und wie das geht, zeigt Kapitel 5) einen Vergleich der gespeicherten Werte ergeben. Vergleichen Sie Referenztypen, vergleichen Sie prinzipiell daraufhin, ob diese dasselbe Objekt referenzieren.
Ausdrücke und Operatoren
Vergleich von Referenztypen Referenzieren zwei Variablen unterschiedliche Referenztyp-Instanzen, die aber dieselben Werte speichern, ergibt ein Vergleich mit == prinzipiell false. Da die Vergleichsoperatoren für einzelne Typen aber auch überladen sein können, kann es auch sein, dass ein Vergleich von Referenztypen doch die gespeicherten Werte miteinander vergleicht. Im .NET Framework ist das zwar laut der Microsoft-Dokumentation nur für den (Referenz-)Typ String der Fall. Für neue Referenztypen, besonders aus Assemblys externer Hersteller, können die Vergleichsoperatoren aber durchaus überschrieben sein. Lesen Sie also die Dokumentation zu den verwendeten Typen oder probieren Sie den Vergleich einfach aus. Oder:
1
2 Verwenden Sie dazu die von Object geerbte Equals-Methode, wenn Sie den Inhalt zweier Referenztyp-Instanzen miteinander vergleichen wollen. Vorausgesetzt, die verwendeten Typen überschreiben diese Methode (was zwar bei den .NET-Typen, aber nicht unbedingt auch bei externen Typen der Fall ist), erhalten Sie einen Vergleich der gespeicherten Werte. Einige Typen wie String besitzen daneben eine CompareTo-Methode, über die Sie herausfinden können, ob ein Objekt kleiner, gleich oder größer ist als ein anderes. Die Rückgabe dieser Methode ist kleiner Null, wenn das Objekt kleiner ist, Null, wenn beide Objekte gleich groß sind, und größer Null, wenn das Objekt größer ist als das übergebene.
TIPP
3 4
5
Wollen Sie hingegen bei zwei Referenzen explizit überprüfen, ob diese dasselbe Objekt referenzieren, auch wenn der Typ die Vergleichsoperatoren überschreibt, setzen Sie die statische ReferenceEquals-Methode der Object-Klasse ein.
6
Vergleich von Strings Beim Vergleich von Strings berücksichtigt C# Groß- und Kleinschreibung. Der Vergleich "a" == "A" ergibt also false. Eine einfache Lösung dieses Problems ist, Strings einfach vor dem Vergleich in Klein- oder Großschrift umzuwandeln:
= = unterscheidet Groß- und Kleinschreibung
7
Listing 3.52: Vergleich von Strings s1 = "ZAPHOD"; s2 = "zaphod"; if (s1.ToLower() == s2.ToLower()) { Console.WriteLine("Beide Strings sind gleich"); }
8
9
Alternativ können Sie auch die Compare-Methode der String-Klasse verwenden: Listing 3.53: Vergleich von Strings über String.Compare
10
string s1 = "ZAPHOD"; string s2 = "zaphod"; if (String.Compare(s1, s2, true) == 0) { Console.WriteLine("Beide Strings sind gleich"); }
11
Am dritten Argument geben Sie an, ob Sie Groß- und Kleinschreibung ignorieren wollen. Die Operatoren >, = und 12)) { Console.WriteLine("Jetzt ist Samstag oder " + "Sonntag nach 12 Uhr"); } else { Console.WriteLine("Jetzt ist nicht Samstag " + "oder Sonntag nach 12 Uhr"); }
10
11
211
Die Sprache C#
C# verwendet einen logischen Kurzschluss
C# wertet zusammengesetzte logische Ausdrücke von links nach rechts aus. Ergibt ein links stehender Ausdruck bereits, dass das Gesamtergebnis auf jeden Fall false ergibt, werden die rechts stehenden Ausdrücke nicht weiter ausgewertet. Dieses KurzschlussVerhalten ist für den Fall, dass (rechts stehende) Teilausdrücke nur dann ausgewertet werden sollen, wenn (links stehende) andere Teilausdrücke zum Erfolg führen, enorm hilfreich. Bei der Ermittlung eines Teilstrings aus einem String darf beispielsweise die Anzahl der extrahierten Zeichen die Länge des Strings nicht überschreiten: string name = "Ford"; if (name.Substring(0, 6) == "Zaphod") // erzeugt eine Ausnahme, weil der { // String nicht genügend Zeichen } // speichert
Wenn Sie vor dem Extrahieren überprüfen, ob der String lang genug ist, wird keine Ausnahme erzeugt, weil der rechte Teilausdruck nicht mehr ausgewertet wird, wenn der String zu kurz ist: Listing 3.56: Sinnvoller Einsatz des Kurzschlusses bei logischen Ausdrücken string name = "Ford"; if (name.Length >= 6 && name.Substring(0, 6) == "Zaphod") { Console.WriteLine("Hallo Zaphod"); }
HALT
Das Kurzschluss-Verhalten bei der Auswertung logischer Ausdrücke kann zu Problemen führen, wenn ein rechts stehender Teilausdruck den Aufruf einer Methode beinhaltet. Ergibt der links stehende Teilausdruck bereits, dass das Gesamtergebnis false ergibt, wird damit die im rechten Ausdruck untergebrachte Methode nicht aufgerufen. Um solche Probleme zu vermeiden, sollten Sie in logischen Ausdrücken Methoden nicht oder nur im linken Teilausdruck aufrufen.
3.8.7
?:, ??, typeof, is, as, sizeof und =>
C# besitzt neben den Standardoperatoren noch ein paar spezielle. Über den Bedingungsoperator ?: können Sie in vielen Fällen auf Abfragen mit if verzichten. ?? ermöglicht die Umwandlung von null in andere Werte. typeof ermittelt den Typ eines Objekts, was für erweiterte Techniken, die mit Reflektion arbeiten, wichtig ist. is vergleicht ein Objekt daraufhin, ob dieses einem angegebenen Typen entspricht. as ermöglicht die Umwandlung eines Referenztypen und, falls dieser nicht umwandelbar ist, die Rückgabe von null. sizeof ermittelt die Speichergröße eines Werttypen, was für den Aufruf von API-Funktionen (Funktionen, die in klassischen DLL-Dateien enthalten sind) oder bei der Arbeit mit COM-Objekten wichtig sein kann (auf die .NET-Programmierer nach Möglichkeit verzichten). Der Lambda-Operator => wird schließlich mit Lambda-Ausdrücken eingesetzt. Lambda-Ausdrücke werden gesondert in Kapitel 6 behandelt.
Der Bedingungsoperator ?: ?: ersetzt einfache Anfragen
Über den Bedingungsoperator ?: können Sie einen Ausdruck schreiben, der eine Bedingung überprüft und der jeweils einen anderen Wert ergibt, wenn die Bedingung wahr oder falsch wird: Bedingung ? Ausdruck1 : Ausdruck2
Der Ausdruck number != 0d ? Math.Sin(number) : 1d überprüft z. B., ob number ungleich 0 ist, und gibt in diesem Fall den Sinus von number zurück, ansonsten 1.
212
Ausdrücke und Operatoren
Das Ergebnis eines solchen Ausdrucks können Sie in weiteren Ausdrücken verwenden oder einer Variablen zuweisen: double number = 0; double result = number != 0d ? Math.Sin(number) : 1d;
Übersichtlicher wird dies, wenn Sie den Ausdruck klammern: double number = 0; double result = (number != 0d ? Math.Sin(number) : 1d);
1
Über den Bedingungsoperator können Sie in vielen Fällen auf if-Abfragen verzichten. Die if-Abfrage, die der obigen Zuweisung entspricht, wäre z. B. deutlich aufwändiger:
2
double result; double number = 0; if (number != 0) { result = Math.Sin(number); } else { result = 1; }
3 4
Der Bedingungsoperator wird allerdings wesentlich schneller ausgeführt und ist einfacher anzuwenden. Richtig interessant wird der Bedingungsoperator, wenn Sie Teilausdrücke damit in anderen Ausdrücken einsetzen oder als Argument an eine Methode übergeben. Sie sollten jedoch beachten, dass Ihr Programmcode bei der Verwendung des Bedingungsoperators nicht unbedingt lesbarer wird.
5
6
Der Operator ?? Über den Operator ?? können Sie den Wert null in einen beliebigen, zum Ausdruck passenden Wert umwandeln. Der Ausdruck x ?? y ergibt x, wenn x nicht null ist, und y, wenn x null ist.
7
?? ist ziemlich hilfreich, wenn Sie mit Referenztypen oder Nullables arbeiten und Sie bei der Speicherung von null einen anderen Wert verwenden wollen.
8 Listing 3.57: Der ??-Operator string name = null; ... Console.WriteLine("Name: " + (name ?? "Nicht angegeben"));
In vielen Fällen haben Methoden (meist externer Programmierer) Probleme damit, dass an einem String-Argument eine null-Referenz übergeben wird. Diese Methoden generieren dann eine NullReferenceException. Mit ?? können Sie für einen String statt null einen Leerstring übergeben. Ich demonstriere das an der WriteLineMethode der Console-Klasse, die allerdings mit null-Werten keine Probleme hat:
9
10
TIPP
11
string name = null; ... Console.WriteLine(name ?? String.Empty);
213
Die Sprache C#
Der typeof-Operator typeof ermittelt Informationen zu einem Typ
Über den typeof-Operator können Sie Informationen zu einem Typ ermitteln: typeof(Typ)
typeof ergibt ein Objekt der Klasse Type. Ein solches Objekt wird auch von der GetType-Methode zurückgegeben, die jede Instanz eines Typs besitzt. typeof wird aber nicht auf Instanzen, sondern auf die Typen direkt angewendet. Die Type-Klasse besitzt eine große Anzahl an Eigenschaften und Methoden, über die Sie Informationen zum Typ ermitteln können. Die Name-Eigenschaft gibt z. B. den Namen des Typs zurück. Wenn Sie beispielsweise eine Klasse besitzen, die die Daten einer Person speichert: class Person { public string FirstName; public string LastName; }
können Sie mit typeof Informationen zu dieser Klasse ermitteln: Listing 3.58: Einsatz von typeof zur Ermittlung von Informationen zu einem Typ Type t = typeof(Person); Console.WriteLine("Name: " + t.Name); Console.WriteLine("Assembly: "+ t.Assembly.FullName); Console.WriteLine("Basistyp: "+ t.BaseType.Name); Console.WriteLine("Voller Name: "+ t.FullName); Console.WriteLine("Typ ist " + (t.IsClass ? "eine" : "keine" ) + " Klasse"); Console.WriteLine("Typ ist " + (t.IsArray ? "ein" : "kein") + " Array"); Console.WriteLine("Typ ist " + (t.IsEnum ? "eine" : "keine") + " Aufzählung"); Console.WriteLine("Typ ist " + (t.IsInterface ? "eine" : "keine") + " Schnittstelle");
Dieser Vorgang wird übrigens als Reflektion (Reflection) bezeichnet. Über Reflektion können Sie u. a. jederzeit Informationen zu allen Typen erhalten, auf die Ihr Programm Zugriff hat. .NET stellt Ihnen dazu Klassen zur Verfügung, die Sie im Namensraum System.Reflection finden. Reflektion wird in Kapitel 22 behandelt.
Der is-Operator is ermittelt, ob ein Ausdruck einem bestimmten Typ angehört
Über den is-Operator können Sie herausfinden, ob ein Ausdruck einen bestimmten Typ besitzt: Ausdruck is Typ
Üblicherweise überprüfen Sie damit Variablen. Sinn macht das u. a., wenn Sie Daten mit object-Referenzen verwalten (bei allen anderen Typen kennen Sie und der Compiler den gespeicherten Typ): Listing 3.59: Überprüfung eines Objekts daraufhin, ob dieses einen bestimmten Typ besitzt object o = (int)10; if (o is int) { Console.WriteLine("o ist ein int."); } else
214
Ausdrücke und Operatoren
{ Console.WriteLine("o ist kein int."); }
is wird in der Praxis jedoch meist für die Überprüfung verwendet, ob ein Typ von einem anderen Typ abgeleitet ist oder ob dieser eine bestimmte Schnittstelle implementiert. Dazu vergleichen Sie einfach mit dem Basistyp oder dem Typ der Schnittstelle. Vererbung und Schnittstellen werden in Kapitel 5 behandelt.
1 Der as-Operator Der as-Operator verbindet die Typumwandlung eines Referenztypen mit einer Abfrage, ob das umzuwandelnde Objekt dem Typ überhaupt entspricht. Mit
2
Variable = Referenztyp as Typ
können Sie einer Variablen eine Referenz auf den Typ zuweisen, falls der Referenztyp dem Typ entspricht. Entspricht der Referenztyp dem Typ nicht, wird automatisch null zugewiesen.
3
as wird wie is meist eingesetzt, um zu überprüfen, ob Objekte von einem Basistyp abgeleitet sind oder eine erwartete Schnittstelle implementieren. Der Vergleichstyp ist dann der Basistyp bzw. die Schnittstelle. as besitzt den (selten genutzten) Vorteil, dass automatisch konvertiert wird, wenn das Objekt vom Basistyp abgeleitet ist bzw. die Schnittstelle implementiert, und null zugewiesen wird, wenn das Objekt nicht vom Basistyp abgeleitet ist bzw. die Schnittstelle nicht implementiert. In einigen Programmiersituationen ist die Verwendung von as deswegen einfacher als die von is.
4
5
Aus Mangel an einem sinnvollen Beispiel, das nicht zu viel Wissen (aus Kapitel 5) voraussetzt, überprüft Listing 3.60, ob eine Instanz der StreamReader-Klasse (aus dem Namensraum System.IO) von TextReader abgeleitet ist und die Schnittstelle IDisposable implementiert (was beides der Fall ist).
6
Listing 3.60: Den as-Operator mit Basisklassen und Schnittstellen einsetzen
7
StreamReader streamReader = new StreamReader("C:\\boot.ini"); // Den StreamReader in den Basistyp TextReader umwandeln TextReader textReader = streamReader as TextReader; if (textReader != null) { Console.WriteLine(textReader.ReadToEnd()); }
8
9
// Den StreamReader in die Schnittstelle IDisposable umwandeln IDisposable disposable = streamReader as IDisposable; if (disposable != null) { disposable.Dispose(); }
10
Das Beispiel ist ziemlich sinnlos, weil Sie die verwendeten Methoden auch direkt über die StreamReader-Instanz aufrufen können. Der as-Operator wird in der Praxis wie is meist in Zusammenhang mit Polymorphismus (Kapitel 5) eingesetzt.
11
Der sizeof-Operator Über den sizeof-Operator können Sie die Größe eines Werttypen ermitteln: sizeof(Typ)
sizeof ermittelt die Größe eines Typen
215
Die Sprache C#
Dieser Operator kann nur in einem unsicheren Bereich des Quellcodes verwendet werden: Listing 3.61: Verwendung des sizeof-Operators unsafe { Console.WriteLine("int besitzt die Größe: " + sizeof(int)); Console.WriteLine("DateTime besitzt die Größe: " + sizeof(DateTime)); }
Der Grund für den Zwang zur Verwendung in einem unsicheren Block ist, dass dieser Operator bereits zur Kompilierungszeit ausgewertet wird und dass die Größe von Typen sich in neueren Versionen des .NET Framework ändern kann. Der DateTimeTyp kann z. B. in neuen Versionen zwölf statt acht Byte groß sein. Ein altes Programm würde unter einer neuen .NET Framework-Version dann fehlerhaft ausgeführt werden.
INFO
Verwenden Sie diesen Operator also idealerweise erst gar nicht. Wenn Sie nur mit den Klassen des .NET Framework arbeiten, benötigen Sie sizeof nicht. Nur wenn Sie (Windows-)API-Funktionen direkt aufrufen (was lediglich zur Lösung sehr seltener Programmier-Probleme notwendig ist), müssen Sie manchen dieser Funktionen Strukturen und deren Größe übergeben. Dann müssen Sie normalerweise sizeof einsetzen um die Größe zu ermitteln.
3.9
Verzweigungen und Schleifen
C# kennt natürlich auch die gängigen Verzweigungen und Schleifen. Bevor ich diese beschreibe, ein Tipp, der eventuelle Probleme vermeidet:
TIPP
Wenn Sie versehentlich eine Endlosschleife produzieren (eine Schleife, die nie beendet wird) und das Programm in Visual Studio ausführen, scheint Ihre Anwendung nicht mehr zu reagieren. Das liegt daran, dass der Prozessor in diesem Fall sehr stark ausgelastet ist und Ihre Anwendung keine Möglichkeit mehr hat, auf Benutzereingaben zu reagieren. Statt das Programm über den Task-Manager »abzuschießen«, sollten Sie die Anwendung in Visual Studio lieber anhalten. Dazu betätigen Sie (Strg) + (ALT) + (Pause), den Schalter Alles unterbrechen in der Symbolleiste oder den entsprechenden Befehl im DEBUG-Menü. Das Unterbrechen hat den Vorteil, dass Sie erkennen, in welcher Schleife Ihr Programm hängt.
3.9.1 if verzweigt ein Programm
Die if-Verzweigung
Über die if-Verzweigung können Sie ein Programm bedingungsabhängig verzweigen. Die if-Verzweigung besitzt die folgende Syntax: if (Bedingung) Anweisungsblock1 [else Anweisungsblock2]
Wenn die Bedingung wahr wird, verzweigt das Programm in den ersten Anweisungsblock. Wenn Sie den optionalen else-Block angeben, wird dieser ausgeführt, wenn die Bedingung false ergibt.
216
Verzweigungen und Schleifen
Das folgende Beispiel überprüft, ob der Wert einer Variablen größer ist als der einer anderen: Listing 3.62: if-Verzweigung mit else-Block ohne Blockklammern int number1 = 1; int number2 = 2; if (number1 > number2) Console.WriteLine("number1 ist größer als number2."); else Console.WriteLine("number1 ist kleiner oder gleich number2.");
1
Wenn Sie mehrere Anweisungen in einem Block unterbringen wollen, müssen Sie diese in geschweifte Klammern einfügen, aber das wissen Sie ja bereits. Und wie Sie auch bereits wissen, sollten Sie der besseren Lesbarkeit Ihrer Programme zuliebe immer Blockklammern verwenden:
2
3
Listing 3.63: Die if-Verzweigung mit else-Block mit Blockklammern int number1 = 1; int number2 = 2; if (number1 > number2) { Console.WriteLine(number1 + " ist größer als " + number2); } else { Console.WriteLine(number1 + " ist kleiner oder gleich " + number2); }
4
5
Die Bedingung kann natürlich mit den logischen Operatoren auch komplex gestaltet werden. Das folgende Beispiel überprüft, ob number1 größer als 1 und kleiner als 100 ist:
6
Listing 3.64: if-Verzweigung mit komplexer Bedingung if (number1 > 1 && number1 < { Console.WriteLine(number1 } else { Console.WriteLine(number1 " ist kleiner/gleich 1 }
7
100) + " ist größer als 1 und kleiner als 100");
8 + oder größer/gleich 100");
if mit mehreren Fallüberprüfungen Mit der if-Verzweigung können Sie natürlich auch mehrere Fälle überprüfen (auch wenn dazu häufig die switch-Verzweigung besser geeignet ist). Im Gegensatz zur switch-Verzweigung, die im nächsten Abschnitt behandelt wird, können Sie mit if beliebige, auch unterschiedliche Vergleiche verwenden. C# stellt dazu aber kein spezielles Schlüsselwort zur Verfügung (wie das ElseIf von Visual Basic). Sie können if-Verzweigungen allerdings schachteln:
if kann mehrere Fallüberprüfungen besitzen
9
10
11
Listing 3.65: Geschachtelte if-Abfrage, die überprüft, ob heute Weihnachten, Samstag, Sonntag oder ein anderer Tag ist if (DateTime.Now.Day == 24 && DateTime.Now.Month == 12) { Console.WriteLine("Heute ist Weihnachten"); } else
217
Die Sprache C#
{ if (DateTime.Now.DayOfWeek == DayOfWeek.Saturday) { Console.WriteLine("Heute ist Samstag"); } else { if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday) { Console.WriteLine("Heute ist Sonntag"); } else { Console.WriteLine("Heute ist weder Weihnachten " + "noch Samstag oder Sonntag"); } } }
Zur besseren Übersicht sollten Sie solche Abfragen allerdings (ausnahmsweise) ohne else-Blöcke (außer im letzten else-Block) und nicht eingerückt schreiben: Listing 3.66: Besser lesbare, geschachtelte if-Abfrage mit mehreren Bedingungen if (DateTime.Now.Day == 24 && DateTime.Now.Month == 12) { Console.WriteLine("Heute ist Weihnachten"); } else if (DateTime.Now.DayOfWeek == DayOfWeek.Saturday) { Console.WriteLine("Heute ist Samstag"); } else if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday) { Console.WriteLine("Heute ist Sonntag"); } else { Console.WriteLine("Heute ist weder Weihnachten " + "noch Samstag oder Sonntag"); }
3.9.2 switch erlaubt beliebig viele Fälle
Die switch-Verzweigung
Die switch-Verzweigung kann einen Ausdruck mit mehreren Werten vergleichen. Jeder erwartete Ergebniswert wird in einem case6-Block abgefangen, der ausgeführt wird, wenn der zu prüfende Ausdruck diesen Wert ergibt. Der optionale defaultBlock behandelt alle anderen Fälle. switch (Prüfausdruck) { case Wert1: /* Anweisungen für den Fall, dass der Prüfausdruck den Wert 1 ergibt */ Sprunganweisung; [case Wert2: /* Anweisungen für den Fall, dass der Prüfausdruck den Wert 2 ergibt */ Sprunganweisung;]
6
218
engl. für »Fall«
Verzweigungen und Schleifen
[...] [default: /* Anweisungen, die ausgeführt werden sollen, wenn keiner der Fälle eingetreten ist */ Sprunganweisung;] }
Der Prüfausdruck kann ein beliebiger Ausdruck sein, der einen ordinalen Wert oder einen String ergibt. Ein ordinaler Wert ist eine Ganzzahl, ein boolescher oder ein Aufzählungswert.
1
Die in den einzelnen Fallblöcken angegebenen Werte müssen konstante Werte sein (keine Ausdrücke oder Variablen), deren Typ zum Prüfausdruck passt. Die am Ende eines Fallblocks angegebene Sprunganweisung ist obligatorisch. Damit geben Sie an, wohin das Programm nach der Abarbeitung eines Falls springen soll. In den meisten Fällen verwenden Sie hier die break-Anweisung. Diese bewirkt, dass das Programm aus dem switch-Block herausspringt und an der ersten Anweisung hinter diesem weiter abgearbeitet wird.
2
3
Das folgende Beispiel lässt den Anwender eine Zahl eingeben und wertet diese dann aus:
4
Listing 3.67: Standard-switch-Verzweigung mit break-Anweisungen Console.Write("Ihre Lieblingszahl: "); int number = Convert.ToInt32(Console.ReadLine());
5
switch (number) { case 7: Console.WriteLine("7 ist eine gute Zahl."); break;
6
case 42: Console.WriteLine("Cool. Trinken Sie einen " + "pangalaktischen Donnergurgler."); break;
7
case 3: Console.WriteLine("3 ist OK."); break;
8
default: Console.WriteLine("Denken Sie noch einmal darüber nach."); break;
9
}
Der Compiler geht bei der Auswertung von switch-Verzweigungen so vor, dass die einzelnen case-Blöcke von oben nach unten der Reihe nach überprüft werden. Entspricht einer der Vergleichswerte dem Ergebnis des Prüfausdrucks, verzweigt das Programm in den Block. Die normalerweise angebrachte Sprunganweisung break am Ende des Blocks bewirkt nach der Abarbeitung der im Block enthaltenen Anweisungen einen Sprung zum Ende der switch-Verzweigung. Weitere Fälle werden nicht mehr überprüft. Tritt keiner der angegebenen Fälle ein, wird der default-Block ausgeführt, falls dieser vorhanden ist.
10
11
219
Die Sprache C#
STEPS
Wenn der Prüfausdruck einen Aufzählungswert ergibt, können Sie sehr gut den switch-Codeausschnitt einsetzen um automatisch case-Blöcke für alle Werte der Aufzählung einzufügen. 1. 2. 3.
4.
Abbildung 3.9: Die drei Stufen des switch-Codeausschnitts
220
Schreiben Sie dazu »sw« bzw. so viele Zeichen, dass IntelliSense den switchCodeausschnitt markiert. Betätigen Sie dann die (ÿ)-Taste um den Codeausschnitt einzufügen. Tragen Sie im grün markierten Bereich innerhalb der Klammern den Ausdruck ein, der den Aufzählungswert zurückgibt. Sie können den Ausdruck auch aus der Zwischenablage einfügen und sogar nach dem Einfügen des switchCodeausschnitts zuerst einmal von einer anderen Stelle im Quellcode in die Zwischenablage kopieren. Solange der Bereich innerhalb der Klammern grün markiert bleibt (bis Sie den Quellcode ändern oder die (ESC)- oder (¢)-Taste betätigen), funktioniert der Codeausschnitt weiterhin. Sobald Sie den grün markierten Bereich innerhalb der Klammern verlassen oder wenn Sie die (¢)-Taste betätigen, fügt Visual Studio case-Blöcke für alle Werte der Aufzählung und einen default-Block ein.
Verzweigungen und Schleifen
case-Blöcke Wenn Sie in den einzelnen case-Blöcken mehrere Anweisungen unterbringen, müssen Sie diese nicht in geschweifte Klammern einfügen. Das wird aber problematisch, wenn Sie in einem case-Block eine Variable deklarieren, deren Name Sie in einem anderen Block oder außerhalb der switch-Anweisung noch einmal benötigen:
Variablen können in case-Blöcken problematisch sein
Listing 3.68: Nicht mögliche switch-Anweisung mit der Deklaration mehrerer Variablen in den einzelnen Blöcken, die denselben Namen tragen
1
switch (number) { case 7: string message = "7 ist eine gute Zahl."; Console.WriteLine(message); break;
2
3
case 42: // Compilerfehler »Eine lokale Variable mit dem Namen "message" ist // bereits in diesem Bereich definiert« string message = "Cool. Trinken Sie einen " + "pangalaktischen Donnergurgler."; Console.WriteLine(message); break;
4
case 3: // Compilerfehler »Eine lokale Variable mit dem Namen "message" ist // bereits in diesem Bereich definiert« string message = "3 ist OK."; Console.WriteLine(message); break;
5
6
default: // Compilerfehler »Eine lokale Variable mit dem Namen "message" ist // bereits in diesem Bereich definiert« string message = "Denken Sie noch einmal darüber nach."; Console.WriteLine(message); break;
7
}
Der Compiler beschwert sich in diesem Fall für die letzten drei Fälle darüber, dass die Variable message bereits in diesem Bereich deklariert wurde.
8
Lösen können Sie das Variablen-Deklarations-Problem, indem Sie in den caseBlöcken Block-Klammern einsetzen:
9
Listing 3.69: switch-Verzweigung mit mehreren gleichnamigen Variablen in den einzelnen case-Blöcken switch (number) { case 7: { string message = "7 ist eine gute Zahl."; Console.WriteLine(message); break; }
10
11
case 42: { string message = "Cool. Trinken Sie einen " + "pangalaktischen Donnergurgler."; Console.WriteLine(message); break; }
221
Die Sprache C#
case 3: { string message = "3 ist OK."; Console.WriteLine(message); break; } default: { string message = "Denken Sie noch einmal darüber nach."; Console.WriteLine(message); break; } }
Zusammenfassen von mehreren Fällen Sie können in einer switch-Verzweigung mehrere Fälle zusammenfassen, indem Sie die entsprechenden case-Anweisungen übereinander schreiben: Listing 3.70: switch-Verzweigung mit mehreren zusammengefassten case-Blöcken switch (number) { case 7: case 42: case 3: Console.WriteLine("Gute Wahl"); break; default: Console.WriteLine("Denken Sie noch einmal darüber nach."); break; }
switch-Verzweigungen mit Strings und Methodenaufrufen Anders als einige andere Sprachen erlaubt C# nicht nur die Überprüfung eines ordinalen Werts, sondern auch von Zeichenketten. Außerdem können Sie im Prüfausdruck auch Methoden aufrufen. Das folgende Beispiel ruft die ReadLine-Methode der Console-Klasse direkt im Prüfausdruck auf, um den Anwender seinen Namen eingeben zu lassen und diesen dann auszuwerten: Listing 3.71: switch-Verzweigung mit Aufruf einer Methode im Prüfausdruck und Auswertung eines Strings Console.Write("Ihr Name: "); switch (Console.ReadLine()) { case "Zaphod": Console.WriteLine("Oh, hallo. " + "Wie geht’s dem Universum so?"); break; case "Arthur": Console.WriteLine("Hast du Dein Handtuch dabei?"); break; default: Console.WriteLine("Guten Tag, Fremder"); break; }
222
Verzweigungen und Schleifen
Mögliche Sprunganweisungen in switch-Verzweigungen Als Sprunganweisung erlaubt C# die in Tabelle 3.18 angegebenen. Sprunganweisung
Bedeutung
break
Sprung aus dem switch-Block
return
Sprung aus der Methode heraus
throw Ausnahme
Die throw-Anweisung wirft eine Ausnahme und führt damit auch zum Sprung aus einem case-Block.
goto Labelname
Sprung zu einem Label, das am Anfang einer Zeile mit Labelname: definiert ist. Nur der Vollständigkeit halber hier aufgeführt! Verwenden Sie goto nicht!
Tabelle 3.18: Die C#-Sprunganweisungen
1
2
goto case Vergleichswert Sprung zu einem bestimmten case-Block, ab dem erneut ausgewertet werden soll. Der Prüfausdruck des Blocks wird natürlich weiterhin ausgewertet. Die Verwendung von goto case macht nur dann Sinn, wenn ein case-Block die im Programm gespeicherten Daten so verändert, dass der Prüfausdruck einen anderen Wert ergibt als zuvor, und wenn dieser noch einmal ab einem bestimmten case-Block überprüft werden soll. Wenn Sie also Programme schreiben wollen, die kein normaler Entwickler versteht, setzen Sie diese verworrene Technik ein. goto default
3 4
Sprung zum default-Block, der dann auf jeden Fall ausgeführt wird.
5 In der Praxis werden in der Regel break, return und throw eingesetzt. break verwenden Sie immer dann, wenn das Programm nach Abarbeitung eines case-Blocks hinter dem switch-Block weiter ausgeführt werden soll. return können Sie einsetzen, wenn der entsprechende Fall dazu führen soll, dass die gesamte Methode beendet wird. throw wird dann verwendet, wenn der entsprechende Fall eine Ausnahme darstellt, die im normalen Programmablauf nicht vorkommen sollte und die in einer Exception gemeldet werden soll. Das eigene Werfen von Ausnahmen wird in Kapitel 8 behandelt.
In der Praxis werden hauptsächlich break, return und throw eingesetzt
Das unstrukturierte, aus uralten Zeiten übernommene goto sollten Sie besser nicht verwenden. Wenn Sie dies trotzdem machen, wird der schwarze Mann Ihr Programm holen ...
Verzichten Sie auf goto
3.9.3
7
Die while-Schleife
Die while-Schleife überprüft die Schleifenbedingung im Kopf und wiederholt die enthaltenen Anweisungen so lange, wie diese Bedingung erfüllt ist: while (Bedingung) { Anweisungen }
6
8
9 Die while-Schleife wiederholt Anweisungen
10
Beachten Sie, dass diese Schleife nicht mit einem Semikolon abgeschlossen werden muss.
11
223
Die Sprache C#
Das folgende Beispiel zählt eine Variable hoch, solange diese einen Wert kleiner als 10 besitzt: Listing 3.72: Beispiel für die Verwendung der while-Schleife int i = 1; while(i < 10) { Console.WriteLine(i); i++; }
EXKURS
Die while-Schleife ist eine kopfgesteuerte Schleife. Solche Schleifen überprüfen ihre Bedingung im Kopf. Die Anweisungen, die im Schleifenkörper enthalten sind, werden nur dann ausgeführt, wenn die Bedingung beim Eintritt in die Schleife wahr ist. Im Gegensatz dazu prüft eine fußgesteuerte Schleife (die do-Schleife) ihre Bedingung im Fuß. Die Anweisungen im Block einer fußgesteuerten Schleife werden auf jeden Fall einmal ausgeführt, auch dann, wenn die Bedingung beim Eintritt in die Schleife nicht wahr ist. Bei der Programmierung entscheiden Sie sich an Hand dieses Kriteriums für eine kopf- oder fußgesteuerte Schleife.
while-Schleife mit Bedingungsprüfung im Körper Eine while-Schleife können Sie (wie auch eine do-Schleife) mit break explizit abbrechen. So können Sie Schleifen erzeugen, die ihre Bedingung im Körper überprüfen (»bauchgesteuerte« Schleifen ☺). Als Bedingung für die eigentliche Schleife wird einfach true angegeben (was ohne break im Schleifenkörper zu einer Endlosschleife führen würde): Listing 3.73: while-Schleife, die ihre Abbruchbedingung im Körper überprüft int i = 0; while (true) { i++; if (i > 9) { break; } Console.WriteLine(i); }
Solche Schleifen benötigen Sie dann, wenn die Bedingung so komplex ist, dass diese nicht im Kopf oder Fuß der Schleife überprüft werden kann.
3.9.4 Die do-Schleife überprüft die Bedingung im Fuß
Die do-Schleife
Die do-Schleife überprüft die Bedingung im Fuß: do { Anweisungen } while (Bedingung);
Im Unterschied zur while-Schleife wird die do-Schleife mindestens einmal durchlaufen, auch wenn die Bedingung zu Anfang der Schleife bereits falsch ist.
224
Verzweigungen und Schleifen
Das folgende Beispiel lässt den Anwender eine Antwort eingeben und schleift so lange, bis dieser die »richtige« Antwort eingegeben hat: Listing 3.74: Verwendung der do-Schleife string answer; int i = 0; do { i++; if (i < 3) { Console.Write("Geben Sie die Antwort ein: " ); } else { Console.Write("Geben Sie die Antwort ein " + "(Tipp: Die richtige Antwort ist 42): " ); } answer = Console.ReadLine(); } while (answer != "42");
1
2
3 4
Wie eine while-Schleife, können Sie eine do-Schleife explizit mit break abbrechen: Listing 3.75: Abbrechen einer do-Schleife
5
int i = 0; do { i++; if (i > 9) { break; } Console.WriteLine(i); } while (true);
3.9.5
6
7
Die for-Schleife
Die for-Schleife wird verwendet, wenn die Schleife eine festgelegte Anzahl Durchläufe besitzen soll. Im Kopf der Schleife können Sie eine Zählvariable initialisieren, geben an, bis zu welchem Wert diese gezählt werden soll und welchen Wert der Compiler in jedem Durchlauf der Schleife zu der Variable hinzuzählen oder von dieser abziehen soll:
Die for-Schleife zählt eine IntegerVariable hoch oder herunter
8
9
for ([Initialisierungsausdruck]; [Bedingung]; [Zählausdruck]) { Anweisungen }
10
Der Initialisierungsausdruck kann eine Variablendeklaration enthalten. Das folgende Beispiel zählt eine Variable von eins bis zehn in Einer-Schritten hoch:
11
Listing 3.76: Hochzählende for-Schleife for (int i = 1; i < 11; i++) { Console.WriteLine(i); }
225
Die Sprache C#
Rückwärts-Zählen ist natürlich auch möglich: Listing 3.77: Rückwärts zählende for-Schleife for (int i = 10; i > 0; i--) { Console.WriteLine(i); }
Genauso können Sie auch um mehr als den Wert 1 hoch- oder herunterzählen: Listing 3.78: for-Schleife, die die Zählvariable bei jedem Durchlauf um zwei erhöht for (int i = 1; i < 10; i += 2) { Console.WriteLine(i); }
Beachten Sie, dass alle drei Teile der for-Schleife optional sind. Sie können z. B. die Zählvariable außerhalb der for-Schleife deklarieren und initialisieren: Listing 3.79: for-Schleife mit Zählvariable, die außerhalb der Schleife deklariert und initialisiert wird int number = 0; for (; number < 10; number++) { Console.WriteLine(number); }
Genauso können Sie aber auch die Bedingung im Schleifenkörper überprüfen (und die Schleife mit break beenden) und die Zählvariable im Schleifenkörper hochzählen: Listing 3.80: for-Schleife mit Bedingung und Hochzählen im Schleifenkörper for (int i = 0; ; ) { if (i >= 10) { break; } Console.WriteLine(i); i++; }
Sie sollten in einem solchen Fall allerdings darüber nachdenken, ob eine while- oder do-Schleife nicht besser angebracht wäre.
3.9.6 Die foreachSchleife erleichtert das Durchgehen von Arrays und Auflistungen
Die foreach-Schleife
Die foreach-Schleife ist eine sehr wichtige Schleife. Mit ihr können Sie die in einer Auflistung oder in einem Array gespeicherten Objekte sequenziell durchgehen. Auflistungen und Arrays werden zwar erst in Kapitel 7 behandelt, die foreach-Schleife gehört aber nun einmal zu den Schleifen und wird deshalb hier beschrieben. Prinzipiell können Sie Auflistungen und Arrays auch mit einer der anderen Schleifen durchgehen, die foreach-Schleife besitzt aber den Vorteil, dass die verwendete Variable die durchlaufenen Objekte direkt referenziert und dass Sie keine Ende-Bedingung überprüfen müssen.
226
Verzweigungen und Schleifen
Die Syntax der foreach-Schleife ist die folgende: foreach ({var | Typ} Variable in {Auflistung | Array}) Anweisungen
Als Typ geben Sie normalerweise var an. In diesem Fall ermittelt der Compiler den Typ der in der Auflistung bzw. im Array gespeicherten Objekte und erzeugt eine Variable von genau dem richtigen Typ. Der Vorteil dieses Vorgehens ist, dass Sie nicht wissen müssen, welchen Typ die Auflistung bzw. das Array verwaltet (was in der Praxis eine große Hilfe ist). Nur wenn der Compiler den Typ nicht aus dem Ausdruck ermitteln kann oder wenn Polymorphismus im Spiel ist (was das ist, klärt Kapitel 5), ist es u. U. notwendig, einen Typen anzugeben (Das ist z. B. der Fall für die Threads-Eigenschaft der Process-Klasse, die zwar ProcessThread-Instanzen verwaltet, bei der der Compiler dies aber nicht erkennt).
1
2
Das folgende Beispiel geht auf diese Weise die Auflistung von Process-Objekten durch, die die GetProcesses-Methode der Process-Klasse (aus dem Namensraum System.Diagnostics) zurückgibt. Process-Objekte repräsentieren einen Prozess im (Windows-)System und geben Informationen zum Prozess und Möglichkeiten, diesen zu steuern. Das Beispiel liest lediglich die ID und den Namen des Prozesses aus:
3 4
Listing 3.81: Verwendung der foreach-Schleife zum Durchgehen aller Prozesse des aktuellen Systems foreach (var process in Process.GetProcesses()) { Console.WriteLine("ID: " + process.Id); Console.WriteLine("Name: " + process.ProcessName); Console.WriteLine(); }
5
6 Die foreach-Schleife wird etwas langsamer ausgeführt als die for-Schleife. Das liegt daran, dass die foreach-Schleife intern Methodenaufrufe beinhaltet (wie ich bei der IEnumerable-Schnittstelle in Kapitel 7 noch näher beschreibe). Der Unterschied liegt zwar nur im Nanosekunden-Bereich, kann aber bei sehr (oder besser: bei extrem) vielen Schleifendurchläufen besonders in performance-kritischen Anwendungen schon einen Unterschied ausmachen. In einem Performance-Test (den Sie im Projekt foreach-Performance-Test in den Beispielen zu diesem Kapitel finden) habe ich ermittelt, dass die for-Schleife auf meinem Laptop (2 GHz Dual Core, 2 GB RAM) für eine int-Liste bei 100.000 gespeicherten Elementen im Durchschnitt 0,21 ms und die foreach-Schleife im Mittel 0,73 ms benötigte. Das ist bei der foreach-Schleife zwar etwa 3,5-mal langsamer, aber bei der benötigten Zeit von 7,3 Nanosekunden für einen Schleifendurchlauf wohl kaum relevant. Deshalb würde ich der foreach-Schleife aufgrund der einfacheren Anwendung immer den Vorzug geben.
INFO
7
for ist insignifikant schneller als foreach
8
9
10
Falls Sie den Test nachvollziehen, beachten Sie, dass Sie das Release des Projekts unter Windows direkt ausführen sollten um korrekte Ergebnisse zu erhalten. Und um Kritik vorzubeugen: Ich habe natürlich auch damit experimentiert, die Schleifen in der anderen Reihenfolge als im Test (foreach-Schleife vor der for-Schleife) auszuführen ☺.
11
227
Die Sprache C#
3.10 PräprozessorDirektiven steuern u. a. die Kompilierung
Tabelle 3.19: Die wichtigen C#-PräprozessorDirektiven
Präprozessor-Direktiven
C# kennt einige Präprozessor-Direktiven, über die Sie die Kompilierung steuern und spezielle Features verwenden können. Präprozessor-Direktiven beginnen immer mit einem #. Das Wesentliche an diesen Direktiven ist, dass die nicht in das Programm übernommen werden. Sie stellen entweder spezielle Informationen für den Compiler oder für Visual Studio dar. Tabelle 3.19 beschreibt die wichtigen Präprozessor-Direktiven. Direktive
Bedeutung
#define Konstante [= {true | false}]
Definition einer Konstante für die bedingte Kompilierung.
#if Bedingung [#elif Bedingung] [#else] #endif
In einem #if-Block können Sie Anweisungen unterbringen, die nur dann kompiliert werden, wenn die Bedingung wahr bzw. falsch wird. Die Bedingung kann alle Vergleichsoperatoren beinhalten und vergleicht mit den Konstanten, die mit #define oder in den Projekteigenschaften angelegt wurden.
#warning Warnung #error Fehler
Diese Anweisung können Sie in einen #if-Block einbauen, um beim Eintritt einer Präprozessor-Bedingung eine Compiler-Warnung oder einen Compiler-Fehler zu generieren.
#region Bezeichnung #endregion
In diese Direktive schließen Sie Anweisungen ein, die als Region verwaltet werden sollen. In Visual Studio können Sie Regionen auf- und zuklappen. Regionen werden in Kapitel 4 behandelt.
#pragma pragma-name Mit #pragma werden dem Compiler spezielle Informationen übergeben. Über argumente #pragma warning disable Warnungsliste können Sie z. B. Warnungen, die der Compiler für eine Klasse ausgibt, deaktivieren. In der Warnungsliste geben Sie dazu die ID der Warnungen kommagetrennt an. Die ID erhalten Sie über das Fehlerfenster, das Visual Studio beim Auftreten von Fehlern oder Warnungen beim Kompilieren anzeigt.
#if erlaubt eine bedingte Kompilierung
Über einen #if-Block können Sie eine bedingte Kompilierung erreichen. Die Arbeitsweise entspricht der der if-Verzweigung, mit dem Unterschied, dass Code in Blöcken, deren Bedingung nicht zutrifft, vom Compiler nicht berücksichtigt wird. Dieser Code kann dann auch für die aktuelle Umgebung nicht benutzbare Anweisungen enthalten. Der Bedingungsausdruck darf lediglich Literale, Operatoren und Konstanten für die bedingte Kompilierung enthalten. In der Debug-Konfiguration aller Projekte ist per Voreinstellung bereits die Konstante DEBUG definiert (was Sie allerdings in den Projekteigenschaften in der Debug-Konfiguration auch abschalten können). So können Sie im Programm sehr einfach abfragen, ob es sich um eine Debug- oder eine Release-Anwendung handelt, und in der Debug-Variante z. B. spezielle Protokollierungen vornehmen: Listing 3.82: Kleine Demo-Konsolenanwendung mit bedingter Kompilierung static void Main(string[] args) { Console.Title = "Präprozessor-Direktiven"; #if DEBUG Console.WriteLine("Das Programm wurde am " + DateTime.Now + " gestartet"); #endif
228
Präprozessor-Direktiven
Console.WriteLine("Das Programm ..."); #if DEBUG Console.WriteLine("Das Programm wurde am " + DateTime.Now + " beendet"); #endif Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine();
1
}
Visual Studio zeigt die Anweisungen, die aufgrund der bedingten Kompilierung nicht kompiliert werden, in einer grauen Farbe an. Sie können dies ausprobieren, indem Sie das obige Beispiel (das Sie natürlich auf der Buch-DVD finden) in die Release-Konfiguration schalten.
2
Abbildung 3.10: Visual Studio zeigt Anweisungen in einer bedingten Kompilierung, die in der aktuellen Konfiguration nicht kompiliert werden, in einer grauen Farbe an
3 4
5
6
7
8
Immer wenn Sie Anweisungen nur zum Test in Ihren Programmen unterbringen, sollten Sie diese in eine bedingte Kompilierung einschließen, die auf das DEBUG-Symbol überprüft. Auf diese Weise verhindern Sie, dass diese Anweisungen in das Release übernommen werden, falls Sie vergessen, diese zu löschen.
9 TIPP
10 Sie können die bedingte Kompilierung aber auch für spezielle Zwecke, wie z. B. zur Erzeugung einer Shareware-Version eine Anwendung, verwenden. Die dazu zu verwendenden Konstanten können Sie im Kopf einer Datei über #define Name [= {true | false}] einrichten. Solche Konstanten gelten dann nur für die entsprechende Datei.
11
Sie können Konstanten für eine bedingte Kompilierung aber auch in den Eigenschaften des Projekts im ERSTELLEN-Register angeben. Diese gelten dann für das gesamte Projekt. Diese Technik wird interessant, wenn Sie spezielle Konfigurationen erstellen, in denen Sie die Konstanten angeben. So können Sie z. B. eine Konfiguration
229
Die Sprache C#
Shareware erstellen, in der Sie die Konstante SHAREWARE unterbringen. Wenn Sie im Programm mit dieser Konstante bedingt kompilieren, können Sie über ein einfaches Umschalten der Projekt-Konfiguration ein normales Release oder eine spezielle Version (im Beispiel eine eingeschränkte Shareware-Version) erstellen.
230
Inhalt
4
Grundlegende OOP 1
C# ist eine objektorientierte Programmiersprache. Das .NET Framework ist objektorientiert aufgebaut. Ein gesundes OOP-Wissen ist deswegen Voraussetzung für das Verständnis von C# und der Features des .NET Framework. Dieses Wissen vermitteln das vorliegende und die nachfolgenden Kapitel.
2
3
Kapitel 4 behandelt dazu zunächst die OOP-Grundlagen. Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■ ■ ■
4
Klassen versus Strukturen Anonyme Typen Daten in Feldern und Eigenschaften verwalten Den Zugriff auf Daten kapseln Indexer Methoden entwickeln Klassen über Konstruktoren initialisieren Objekte korrekt freigeben Statische Klassenelemente und statische Klassen Organisieren von Klassen und Strukturen in Regionen
5
6
7
Neu in .NET 3.5: ■ ■
Anonyme Typen (Seite 260) Automatisch implementierte Eigenschaften (Seite 258)
4.1
8
Klassen und Strukturen 9
In C# schreiben Sie Programme in Klassen oder Strukturen. Bei der Programmierung müssen Sie also entscheiden, ob für Ihr Problem eine Klasse oder eine Struktur besser geeignet ist.
4.1.1
10
Die Unterschiede
Prinzipiell unterscheiden sich Klassen und Strukturen in C# kaum: In beiden können Sie Felder, Eigenschaften und Methoden unterbringen. Im Detail gibt es aber einige Unterschiede. Der wesentliche Unterschied ist, dass Klassen Referenztypen sind und Strukturen Werttypen. Auf die weiteren Unterschiede gehe ich im Verlauf dieses Abschnitts noch ein. An Hand dieser Unterschiede entscheiden Sie, ob für die Lösung Ihres Problems eine Klasse oder eine Struktur besser geeignet ist.
Strukturen sind Werttypen, Klassen sind Referenztypen
231
11
Index
■
Grundlegende OOP
Strukturen werden normalerweise verwendet: ■
■
wenn eher allgemeine Daten gespeichert werden sollen, die im Programm nach einer anfänglichen Initialisierung nicht mehr geändert werden und/oder wenn die Datenmenge relativ klein ist ( regularWorkHours) { salary += (workHours - regularWorkHours) * overtimeBonus; } return salary; }
8
9
10
}
11
Dieses Beispiel zeigt übrigens ein in der Praxis häufiges Vorgehen: Eine Variante der überladenen Methode, der alle Argumente übergeben werden, wird von der anderen Variante, die nur eingeschränkte Argumente besitzt, intern aufgerufen. Damit erreichen Sie, dass Sie die Programmierung der Methode nur an einer Stelle ändern müssen, wenn Sie Fehler beseitigen oder wenn das Programm geändert werden soll. 2
Ob der Rückgabewert zur Signatur einer Methode gehört oder nicht, ist allerdings umstritten.
243
Grundlegende OOP
Bei der Arbeit mit Instanzen dieser Klasse können Sie nun die eine oder die andere Methode zur Berechnung des Gehalts verwenden: Listing 4.14: Anwendung einer überladenen Methode Employee employee1 = new Employee { FirstName = "Ford", LastName = "Prefect" }; decimal salary = employee1.CalculateSalary(48, 40, true); Console.WriteLine(employee1.FirstName + " erhält " + salary + " Euro"); Employee employee2 = new Employee { FirstName = "Arthur", LastName = "Dent" }; salary = employee2.CalculateSalary(48, 40); Console.WriteLine(employee2.FirstName + " erhält " + salary + " Euro");
TIPP
REF
Wenn Sie überladene Varianten einer Methode implementieren, achten Sie darauf, dass die Methoden prinzipiell dieselbe Bedeutung besitzen. Implementieren Sie eine Variante, die alle denkbaren Argumente besitzt. Alle anderen Varianten sollten nichts weiter machen, als diese Haupt-Variante aufzurufen. An den Argumenten, die den anderen Varianten fehlen, sollten diese der Haupt-Variante Standardwerte übergeben. Diese sollten Sie ggf. in (privaten, konstanten) Feldern verwalten, damit sie schnell geändert werden können. An Stelle überladener Methoden mit prinzipiell gleichen Parametern, die sich nur im Typ unterscheiden, sind generische Methoden häufig besser geeignet. Generische Methoden werden in Kapitel 6 behandelt.
4.4.4 Die StandardÜbergabe ist »By Value«
ref- und out-Argumente
Wenn Sie ein Argument einer Methode nicht mit ref oder out deklarieren, ist die Übergabeart dieses Arguments »By Value«. Wenn Sie an einem solchen Argument eine Variable übergeben, erzeugt der Compiler innerhalb der aufgerufenen Methode einen neuen Speicherbereich auf dem Stack und kopiert den Wert der übergebenen Variablen dort hinein. Wenn Sie innerhalb der Methode mit dem Argument (also mit dem Stack-Speicherbereich) arbeiten, wird die äußere Variable davon nicht beeinflusst. Bei Werttypen führt das dazu, dass der Wert einer an eine Methode übergebenen Variablen nicht verändert wird, wenn das Argument innerhalb der Methode geändert wird. Das folgende Beispiel demonstriert dieses Verhalten. Es setzt eine statische Methode ein, die in der Program-Klasse einer Konsolenanwendung implementiert ist: Listing 4.15: Demo für Wert-Argumente class Program { static void Main(string[] args) { int i = 10; Program.ByValDemo(i); Console.WriteLine(i); // Hier ist i immer noch 10 } public static void ByValDemo(int number) { number++; // Das Argument wird im Wert geändert Console.WriteLine(number); // 11 } }
244
Methoden
Nach dem Aufruf der Methode ByValDemo ist der Wert der übergebenen Variablen i immer noch 10. Der Compiler hat eben keine Referenz auf die Variable übergeben, sondern nur den Wert.
Referenzübergabe Wenn Sie ein Argument mit dem Schlüsselwort ref deklarieren, übergibt der Compiler eine Referenz auf die Variable. Die Übergabeart ist dann »By Reference«.
1
Listing 4.16: Methode mit einem ref-Argument public static void RefDemo(ref int number) { number++; // Das Argument wird im Wert geändert Console.WriteLine(number); // 11 }
An ref-Argumente können Sie nur Variablen und Felder übergeben. Die Übergabe eines Werts (in Form eines Literals), eines Ausdrucks oder einer Eigenschaft ist nicht möglich. Innerhalb der Methode steht der Wert der übergebenen Variablen bzw. des übergebenen Feldes zur Verfügung, kann gelesen und geändert werden. Änderungen sind nach außen sichtbar, weil ja in Wirklichkeit eine Referenz auf die Variable übergeben wurde.
2
ref-Argumente sind für die Eingabe und Ausgabe von Werten vorgesehen
3
4
Beim Aufruf müssen Sie die übergebenen Variablen oder Felder mit ref kennzeichnen. Diese Kennzeichnung ist zwar für den Compiler nicht notwendig, er erzwingt sie aber, damit Programmierer beim Lesen des Quellcodes erkennen, dass dort eine Variable bzw. ein Feld By Reference übergeben wird:
5
Listing 4.17: Aufruf einer Methode mit ref-Argument
6
int i = 10; Program.RefDemo(ref i); Console.WriteLine(i); // Hier ist i 11
ref-Argumente machen nur mit Werttypen wirklich Sinn. Bei Referenztypen wird bereits bei der By-Value-Übergabe eine Referenz übergeben. Wenn Sie einen Referenztypen an ein ref-Argument übergeben, übergibt der Compiler genau wie bei Werttypen eine Referenz auf die übergebene Variable an das Argument. Die Variable ist aber selbst eine Referenz. In der Methode arbeiten Sie dann mit einer Referenz auf eine Referenz auf ein Objekt. Und das ist in den meisten Fällen sinnlos. Allerdings wäre es denkbar, dass eine Methode an einem ref-Argument eine Referenz auf ein Objekt übergeben bekommt und diese auf eine neue Instanz desselben Typs setzt. Solch eine verwirrende Programmierung sollten Sie aber vermeiden.
7
INFO
8
9
10
Rückgabeparameter Wenn Sie statt ref den Modifizierer out verwenden, erhalten Sie einen Parameter, der nur die Rückgabe eines Werts bzw. einer Objekt-Referenz erlaubt. Anders als bei ref steht der Wert einer übergebenen Variablen bzw. eines übergebenen Feldes innerhalb der Methode bei out nicht zur Verfügung. Der Compiler beschwert sich, wenn Sie ein out-Argument lesen, bevor die Methode etwas hineingeschrieben hat. Innerhalb der Methode ist ein out-Argument also uninitialisiert. Dafür können Sie auch uninitialisierte Variablen übergeben (was bei ref nicht möglich ist). out-Argumente müssen nämlich innerhalb der Methode initialisiert werden. Sie sind damit für Argumente geeignet, die lediglich Daten zurückgeben.
out-Argumente sind nur für die Ausgabe vorgesehen
245
11
Grundlegende OOP
INFO
out-Argumente machen im Gegensatz zu ref-Argumenten auch bei Referenztypen Sinn, nämlich dann, wenn eine Methode eine Instanz des Typs erzeugt und eine Referenz darauf am out-Argument zurückgibt.
Über ref- und out-Argumente können Sie auch Methoden schreiben, die mehr als ein Objekt zurückgeben, indem Sie neben dem eigentlichen Rückgabewert der Methode mehrere ref- oder out-Argumente verwenden. Ich sehe darin allerdings nicht viel Sinn, da Methoden auch Referenzen auf komplexe Objekte zurückgeben können (die dann die verschiedensten Eigenschaften besitzen können). Sinn machen ref- und out-Argumente u. U. bei Methoden, die zum einen einen booleschen Wert zurückgeben, der aussagt, ob die Methode erfolgreich ausgeführt wurde, und zum anderen einen berechneten Wert oder ein erzeugtes Objekt. Da eine Methode nur ein Objekt als Ergebnis zurückgeben kann, könnte diese so geschrieben werden, dass sie am eigentlichen Rückgabewert lediglich den booleschen Wert zurückgibt. Den berechneten Wert bzw. das erzeugte Objekt könnte diese dann in ein out-Argument schreiben. Im Beispiel sähe das dann so aus: Listing 4.18: Beispiel für die Anwendung von out-Argumenten public static bool GetPerson(string name, out Person person) { if (name == "Zaphod") { // Die Bedingung für das Lesen der Person ist erfüllt: // Person erzeugen, an das out-Argument übergeben // und true zurückgeben person = new Person { FirstName = "Zaphod", LastName = "Beeblebrox" }; return true; } else { // Die Bedingung für das Lesen der Person ist nicht erfüllt: // null an das person-Argument übergeben und false zurückgeben person = null; return false; } }
Das Beispiel arbeitet mit der Person-Klasse aus dem Abschnitt »Felder« auf Seite 235. Die Methode überprüft, ob die Bedingung zur Rückgabe einer Person erfüllt ist. Um das Beispiel nicht zu kompliziert werden zu lassen, habe ich lediglich überprüft, ob der Name gleich »Zaphod« ist. In der Praxis würde hier natürlich eine komplexere Überprüfung vorgenommen werden (z. B., ob in der Datenbank, die die Daten der im Programm verwendeten Personen speichert, eine Person mit dem übergebenen Namen gefunden wird). Ist diese Bedingung erfüllt, erzeugt die Methode eine neue Instanz der Person-Klasse, schreibt die Referenz darauf in das person-Argument und gibt true zurück. Ist die Bedingung nicht erfüllt, schreibt GetPerson null in das person-Argument. Das ist deswegen notwendig, da der Compiler (sinnvollerweise) die Initialisierung von out-Argumenten erzwingt. Außerdem gibt GetPerson in diesem Fall false zurück.
246
Methoden
Beim Aufruf dieser Methode kann nun über den Rückgabewert entschieden werden, ob der Aufruf erfolgreich war: Listing 4.19: Verwendung der Methode mit out-Argument Person p; if (Program.GetPerson("Zaphod", out p) == true) { Console.WriteLine(p.FirstName + " " + p.LastName); } else { Console.WriteLine("Zaphod existiert nicht"); }
1
2
if (Program.GetPerson("Ford", out p) == true) { Console.WriteLine(p.FirstName + " " + p.LastName); } else { Console.WriteLine("Ford existiert nicht"); }
4.4.5
3
4
Variable Argumente mit params
Über das params-Schlüsselwort können Sie Argumente deklarieren, an denen der Aufrufer eine beliebige Anzahl Werte übergeben kann. params wird dazu immer mit einer Array-Deklaration verwendet und muss als letztes Argument der Methode eingesetzt werden. Der Compiler erlaubt nur ein params-Argument pro Methode. Innerhalb der Methode greifen Sie auf params-Argumente zu wie auf ein Array.
params erlaubt beliebig viele Argumente
5
6
So können Sie z. B. eine Methode erzeugen, die eine beliebige Anzahl int-Werte summiert und die Summe zurückgibt:
7
Listing 4.20: Methode mit params-Argument public static int Sum(params int[] numbers) { int result = 0; foreach (int number in numbers) { result += number; } return result; }
8
9
Beim Aufruf übergeben Sie nun kein, ein oder mehrere Argumente, die Sie wie gewohnt durch Kommata trennen:
10
Listing 4.21: Aufruf einer Methode mit params-Argument Console.WriteLine(Program.Sum()); Console.WriteLine(Program.Sum(1, 2, 3, 4, 5, 6, 7, 8, 9));
11
Alternativ könnten Sie das Argument in der Methode auch als normales Array ohne params deklarieren. Dann muss der Aufrufer aber explizit ein Array erzeugen, das er übergeben kann. params erleichtert die Übergabe von Werten mit variabler Anzahl enorm.
247
Grundlegende OOP
4.4.6 Rekursive Methoden rufen sich selbst auf
Rekursive Methoden
Rekursive Methoden sind solche, die sich selbst immer wieder aufrufen, bis eine Ende-Bedingung erreicht ist. Das einfachste Beispiel für eine rekursive Methode ist das Durchgehen aller Dateien eines Ordners. Eine Methode, die alle (oder nur bestimmte) Dateien eines Ordners bearbeiten soll, kann nicht nur die in dem Ordner direkt gespeicherten Dateien durchgehen, sondern muss auch die Unterordner berücksichtigen. Dies ist nur möglich, indem die Methode sich selbst für jeden Unterordner aufruft. Das folgende Beispiel zeigt dies am einfachen Durchgehen aller Dateien eines Ordners und aller seiner Unterordner. Es arbeitet mit der Directory-Klasse aus dem Namensraum System.IO, über deren statische Methoden GetFiles und GetDirectories Sie die Dateien und Unterordner eines Ordners ermitteln können. GetFiles wird im Beispiel mit dem zweiten Parameter verwendet, der ein Suchmuster für die Dateien beinhaltet. Das Suchmuster wird über das Argument searchPattern übergeben: Listing 4.22: Methode, die rekursiv alle Dateien ab einem übergebenen Startordner durchgeht, die dem übergebenen Suchmuster entsprechen private static void EnumFiles(string folderPath, string searchPattern) { // Alle Dateien des Startordners durchgehen try { foreach (var fileName in Directory.GetFiles( folderPath, searchPattern)) { // Den Namen der Datei ausgeben Console.WriteLine(fileName); } // Alle Unterordner durchgehen foreach (var subFolderPath in Directory.GetDirectories(folderPath)) { // Die Methode rekursiv aufrufen Program.EnumFiles(subFolderPath,searchPattern); } } catch (Exception ex) { Console.WriteLine(ex.Message); } }
INFO
Die Methode enthält eine Ausnahmebehandlung, da Windows den Zugriff auf einige Systemordner nicht zulässt. Außerdem kann es sein, dass der gerade angemeldete Benutzer auf bestimmte Ordner keinen Zugriff besitzt. Interessant in diesem Beispiel ist im Moment eher der rekursive Aufruf in der unteren Schleife, in der die Methode alle Unterordner durchgeht und sich selbst für jeden Unterordner aufruft. Die Anwendung einer rekursiven Methode ist genauso einfach wie die einer normalen. Ich zeige dies hier nur der Vollständigkeit halber am Beispiel des Durchlaufens aller Textdateien ab dem Wurzelordner des Laufwerks C: Program.EnumFiles("C:\\", "*.txt");
248
Methoden
Rekursive Methoden müssen Sie immer dann einsetzen, wenn Sie einen Baum durchgehen wollen, wie es in Listing 4.22 der Fall ist. In vielen Fällen können Sie aber auch an Stelle einer normalen Methode eine rekursive einsetzen. Dies ist z. B. der Fall für die Berechnung einer Fakultät. Die Fakultät der Zahl 3 ist 1 * 2 * 3, die der Zahl 4 ist 1 * 2 * 3 * 4. Sie können die Fakultät einer Zahl nun mit einer normalen oder mit einer rekursiven Methode berechnen:
Mit rekursiven Methoden gehen Sie hauptsächlich Bäume durch
1
Listing 4.23: Berechnung einer Fakultät in nicht rekursiver und rekursiver Art /* Normale Berechnung der Fakultät */ private static long CalculateFacultyNormal(byte number) { if (number > 0) { long result = 1; for (int i = 2; i 0) { this.radius = value; } else { // Ungültiger Radius: Eine Ausnahme werfen throw new ArgumentException("Der Radius '" + value + "' ist ungültig. Er muss größer 0 sein"); } } get { return this.radius; } } }
Nun ist Radius eine Eigenschaft und kann direkt beschrieben und gelesen werden.
EXKURS
Abbildung 4.3: Der von ildasm.exe disassemblierte Code der setMethode der Radius-Eigenschaft
252
set und get sind in Wirklichkeit Methoden. Der vom Compiler erzeugte CIL-Code enthält das Feld der Eigenschaft, eine Methode set_Radius, der ein int-Argument mit Namen value übergeben wird, und eine Methode get_Radius, die den Wert zurückgibt. In Anweisungen, in denen das C#-Programm in die Eigenschaft schreibt oder daraus liest, wird im CIL-Code die entsprechende Methode aufgerufen.
Eigenschaften: Kapselung von Daten
Abbildung 4.3 und Abbildung 4.4 zeigen den vom Microsoft .NET Framework ILDisassembler (ildasm.exe) disassemblierten Assemblycode für die set-Methode der Circle-Klasse und das Schreiben in die Radius-Eigenschaft. Abbildung 4.4: Der von ildasm.exe disassemblierte Code der MainMethode des Beispielprogramms
1
2
3
Der CIL-Code ähnelt also dem Code in Listing 4.24.
Das Schema einer Eigenschaft Für eine vollwertige Eigenschaft benötigen Sie ein privates Feld, das den Wert speichert. Der Name dieses Feldes wird per Konvention kleingeschrieben (weil das Feld privat ist). Dann deklarieren Sie eine öffentliche Eigenschaft, ähnlich wie ein Feld, nur dass Sie dieses um Block-Klammern erweitern. Innerhalb der Blockklammern implementieren Sie zwei spezielle Methoden, die als Accessoren (»Zugreifer«) bezeichnet werden. Im set-Accessor wird der geschriebene Wert im impliziten Argument value übergeben. Der get-Accessor ist eine Funktion, die den Wert über return zurückgibt.
Eigenschaften bestehen i. d. R. aus einem Feld, einer set- und einer get-Methode
4 5
6
Das Schema einer vollständigen Eigenschaft, die den Wert nicht überprüft, ist:
7
private Typ feldname; public Typ Eigenschaftsname { set { this.feldname = value; }
8
get {
9
return this.feldname; } }
Abfangen der erzeugten Ausnahme
10
Um das Beispiel zu vervollständigen, muss das Programm noch die Ausnahme abfangen, die von der Radius-Eigenschaft beim Schreiben ungültiger Werte geworfen wird. Das Erzeugen und Abfangen von Ausnahmen wird in Kapitel 8 ausführlich behandelt. Deshalb folgt hier nur eine kleine grundlegende Einführung:
11
Um Ausnahmen abzufangen, platzieren Sie die Anweisungen, die diese erzeugen können, in einen try-Block. Unterhalb dieses Blocks können Sie in beliebig vielen catch-Blöcken die verschiedenen Ausnahme-Typen abfangen. Für unser Beispiel reicht jedoch das Abfangen des allgemeinen Ausnahmetyps Exception aus, der für alle Ausnahmen steht (da diese von Exception abgeleitet sind). Hinter catch schrei-
253
Grundlegende OOP
ben Sie den Ausnahmetyp in Klammern, optional gefolgt von einem Bezeichner für eine Referenz auf die Ausnahme, die abgefangen wird. Über diesen Bezeichner können Sie auf die Ausnahme zugreifen und z. B. aus der Eigenschaft Message die Ausnahmenachricht auslesen. Listing 4.27: Abfangen der Ausnahme, die Radius beim Setzen ggf. wirft // Einen Kreis erzeugen Circle circle = new Circle(); circle.CenterPoint = new Point(10, 10); // Den Radius setzen try { circle.Radius = -100; } catch (Exception ex) { // Beim Setzen ist eine Ausnahme aufgetreten: // Deren Nachricht ausgeben Console.WriteLine(ex.Message); }
Schreibt ein Programm nun einen ungültigen Wert in die Radius-Eigenschaft, erzeugt das Circle-Objekt eine Ausnahme. Das Programm fängt diese ab und gibt den Fehlertext aus. Das Objekt wird nicht in einen ungültigen Zustand versetzt.
INFO
Falls Ihnen an dem Programm etwas auffällt, ist es wahrscheinlich das, dass CircleInstanzen trotzdem einen ungültigen Zustand besitzen können. Nach der Erzeugung ist die Radius-Eigenschaft nämlich auf 0 eingestellt. Dieses Problem können Sie in der Praxis über Konstruktoren lösen, die ab Seite 266 behandelt werden.
Eigenschaften außerhalb der Kapselung Eigenschaften können Programme enthalten oder aufrufen
Eigenschaften werden nicht ausschließlich für Kapselung verwendet. In vielen Fällen erfordern spezielle Programmiersituationen die Implementierung einer Eigenschaft, um beim Schreiben zu erreichen, dass ein Programm ausgeführt wird.
Eigenschaften können Daten berechnen
Andere Eigenschaften berechnen ihren Wert aus den Daten des Objekts und besitzen deswegen selbst kein Feld zur Verwaltung von Daten. Eine Eigenschaft FullName, die den vollen Namen einer Person zurückgibt, wäre ein Beispiel dafür. Diese Eigenschaften sind in der Regel schreibgeschützt, weil ein Schreiben keinen Sinn macht bzw. zu aufwändig zu implementieren wäre (ein Schreiben in FullName wäre denkbar, aber dann müsste der geschriebene String in den Vor- und den Nachnamen aufgesplittet werden).
Objekte, die eine grafische Darstellung besitzen, müssen z. B. beim Schreiben einer Eigenschaft, die das Aussehen beschreibt, eine Neuzeichnung initiieren. Diese Objekte rufen die Methode zum Zeichnen der Oberfläche dann im set-Accessor der Eigenschaft auf.
4.5.2
Schreibgeschützte Felder und Eigenschaften
In vielen Situationen macht es Sinn, dass Felder oder Eigenschaften nur gelesen werden können. Die Eigenschaft Alter einer Person kann z. B. (leider) nicht geändert werden, sondern berechnet sich aus dem Geburtsdatum.
254
Eigenschaften: Kapselung von Daten
Einfache schreibgeschützte Felder erhalten Sie über den Modifizierer readonly. Solche Felder können Sie nur im Konstruktor der Klasse beschreiben oder bei der Deklaration initialisieren. Für das Alter oder den vollen Namen einer Person eignet sich ein einfaches schreibgeschütztes Feld nicht, da beide Werte ja berechnet werden müssen. Eine Klasse, die ein Bankkonto repräsentiert, könnte die Kontonummer aber in einem schreibgeschützten Feld verwalten, dessen Wert am Konstruktor übergeben wird:
readonly erzeugt schreibgeschützte Felder
1
Listing 4.28: Klasse mit einem schreibgeschützten Feld public class BankAccount { /* Die Kontonummer */ public readonly int Number;
2
/* Konstruktor */ public BankAccount(int number) { this.Number = number; }
3
}
4
Konstruktoren werden ab Seite 266 behandelt. Beim Erzeugen einer BankAccount-Instanz übergeben Sie nun die Kontonummer. Danach können Sie diese nicht mehr ändern:
REF
5
Listing 4.29: Verwenden der BankAccount-Klasse
6
BankAccount bankAccount = new BankAccount(12341234); //bankAccount.Number = 99999999; // Dies lässt der Compiler nicht zu
Wesentlich mehr Flexibilität besitzen Sie, wenn Sie an Stelle von Feldern Eigenschaften verwenden. Wenn Sie den set-Accessor einfach weglassen, erhalten Sie eine schreibgeschützte Eigenschaft. Die Person-Klasse kann so um die Eigenschaften Age und FullName erweitert werden, die nur gelesen werden können und ihre Werte dabei berechnen. Age ruft in diesem Beispiel übrigens die bereits existierende Methode GetAge auf:
Schreibgeschützte Eigenschaften sind flexibler
7
8
Listing 4.30: Klasse mit schreibgeschützten Eigenschaften public class Person { public string FirstName; public string LastName; public DateTimeOffset Birthdate; public Address Address;
9
10
/* Gibt das Alter zurück */ public int Age { get { return this.GetAge(); } }
11
/* Gibt den vollen Namen zurück */ public string FullName { get
255
Grundlegende OOP
{ return this.FirstName + " " + this.LastName; } } /* Berechnet das Alter der Person in ganzen Jahren */ public int GetAge() { int age = DateTimeOffset.Now.Year - this.Birthdate.Year; if ((DateTimeOffset.Now.Month < this.Birthdate.Month) || (DateTimeOffset.Now.Month == this.Birthdate.Month && DateTimeOffset.Now.Day < this.Birthdate.Day)) { age--; } return age; } }
Mit einem Objekt der Klasse Person können Sie nun den vollen Namen und das Alter auslesen, aber beide nicht beschreiben: Listing 4.31: Verwendung von schreibgeschützten Eigenschaften Person ich = new Person { FirstName = "Jürgen", LastName = "Bayer", Birthdate = DateTimeOffset.Parse("26.08.1972"), // Gelogen J Address = { City = "Dublin", Country = "Ireland" } }; Console.WriteLine(ich.FullName + " ist " + ich.Age + " Jahre alt"); // ich.Age = 22; // Geht nicht L
4.5.3
Eigenschaften mit unterschiedlichen Gültigkeitsbereichen für das Schreiben und das Lesen
Bei Eigenschaften können Sie für die set- und die get-Methode unterschiedliche Gültigkeitsbereiche einstellen. Dazu schreiben Sie einen der Gültigkeitsbereich-Modifizierer vor set oder get, je nachdem, welchen Zugriff Sie einschränken wollen. So können Sie z. B. eine Eigenschaft erzeugen, deren Wert von außen gelesen, aber nur innerhalb der Assembly geschrieben werden kann: Listing 4.32: Beispiel für Eigenschaften mit unterschiedlicher Sichtbarkeit für die Accessoren public class BankAccount { private int number; /* Diese Eigenschaft kann innerhalb der Assembly gelesen */ /* und geschrieben werden. Außerhalb der Assembly kann */ /* die Eigenschaft allerdings nur gelesen werden */ public int Number { internal set { this.number = value; } get
256
Eigenschaften: Kapselung von Daten
{ return this.number; } } }
Der Sinn von solchen Eigenschaften liegt in einer erweiterten Kapselung. Viele Anwendungen arbeiten z. B. mit einer separaten Assembly, die die Daten der Anwendung aus einer Datenbank liest und der Anwendung zur Verfügung stellt. Dabei werden in den meisten Fällen bestimmte Regeln berücksichtigt, die von den Personen oder der Firma, die die Anwendung einsetzt, definiert werden. In einer Bank darf die Kontonummer eines Bankkontos z. B. nicht ohne weiteres geändert werden. Diese so genannten Geschäftsregeln werden in der Assembly implementiert, die den Datenzugriff realisiert. Im obigen Beispiel wäre die Klasse BankAccount in einer solchen Assembly implementiert. Die Assembly selbst kann die Kontonummer eines Bankkontos schreiben, z. B. bei der Erzeugung eines Objekts. Für eine Anwendung, die diese Assembly referenziert, ist die Kontonummer aber schreibgeschützt. Auf diese Weise können Sie auch komplexe Geschäftsregeln problemlos abbilden.
Falls Sie dies einmal in einem praxisorientierten Beispiel nachvollziehen wollen (das allerdings bereits fortgeschrittenere Themen einsetzt), folgen Sie den folgenden Schritten: 1. 2.
3.
4.
5.
6.
1 EXKURS
2
3
4 5 STEPS
Erzeugen Sie ein neues Konsolenanwendung-Projekt, das Sie z. B. »Eigenschaften mit spezieller Sichtbarkeit« nennen. Fügen Sie der Projektmappe ein neues Klassenbibliothek-Projekt hinzu, indem Sie den Befehl HINZUFÜGEN / NEUES PROJEKT im Kontextmenü ProjektmappenEintrag in Projektmappe-Explorer wählen. Nennen Sie dieses z. B. »Klassenbibliothek« (ein besserer Name ist mir nicht eingefallen ☺). Erstellen Sie im Konsolenanwendung-Projekt eine Referenz auf das Klassenbibliothek-Projekt, indem Sie im Kontextmenü des REFERENZEN-Eintrags dieses Projekts den Befehl REFERENZ HINZUFÜGEN wählen, im Referenz-Dialog das PROJEKTERegister aktivieren, die Klassenbibliothek auswählen und mit OK bestätigen. Erstellen Sie die Klasse aus Listing 4.32 in dem Klassenbibliothek-Projekt. Dazu ändern Sie entweder den Namen der bereits enthaltenen Klasse Class1 ab, oder Sie löschen diese und fügen eine neue Klasse hinzu. Fügen Sie dem Klassenbibliothek-Projekt eine neue Klasse mit Namen Manager hinzu. Über diese Klasse soll das Programm später Bankkonten aus der Datenbank lesen (was allerdings im Beispiel nicht implementiert wird). Implementieren Sie die folgende Methode in der Manager-Klasse:
6
7
8
9
10
public static BankAccount LoadBankAccount(int number) { BankAccount bankAccount = new BankAccount(); bankAccount.Number = number; return bankAccount; }
7.
11
Implementieren Sie den folgenden Code in der Main-Methode des Konsolenanwendungs-Projekts: BankAccount bankAccount = Manager.LoadBankAccount(1234); Console.WriteLine(bankAccount.Number);
8.
Kompilieren und testen Sie das Programm.
257
Grundlegende OOP
Dieses Beispiel ist natürlich nur sehr einfach gehalten. Es zeigt aber das Prinzip von Assemblys, die den Zugriff auf die Daten kontrollieren, die ein Programm verwendet. Die Manager-Klasse der Klassenbibliotheks-Assembly in unserem Beispiel regelt den kompletten Datenzugriff. Über die statische LoadBankAccount-Methode kann ein Bankkonto geladen werden. In der Praxis würde diese Methode die Daten des Bankkontos aus einer Datenbank einlesen. So weit wollte ich in dem Beispiel aber nicht gehen. Das Wichtige für das aktuelle Thema allerdings ist, dass die LoadBankAccountMethode die Eigenschaft Number der BankAccount-Klasse beschreiben kann, weil diese den Gültigkeitsbereich internal besitzt. Von außen (also von der Konsolenanwendungs-Assembly aus) kann zwar noch eine BankAccount-Instanz erzeugt werden (was allerdings auch verhindert werden kann), die Eigenschaft Number kann allerdings nicht geändert werden. Das Programm muss die Manager-Klasse verwenden, um eine BankAccount-Instanz zu erzeugen.
4.5.4
NEU
Automatisch implementierte Eigenschaften
Ab C# 3.0 können Sie einfache Eigenschaften, die nichts weiter machen, als Ihren Wert in einem Feld zu verwalten, auch in einer platzsparenden Version implementieren: [Attribute] [Modifizierer] Typ Name { [Modifizierer] get; [Modifizierer] set; }
Über Attribute, die ich hier nur der Vollständigkeit halber aufgenommen habe, können Sie der Eigenschaft spezielle Informationen für den Compiler oder andere Klassen mitgeben, die mit Ihren Typen arbeiten. Attribute werden in Kapitel 6 behandelt. Dieser Art der Eigenschaftsdeklarationen fehlen das Feld und der Körper der set- und get-Accessoren. Die Klasse BankAccount könnte so auch etwas vereinfacht werden: Listing 4.33: Klasse mit einer automatisch implementierten Eigenschaft public class BankAccount { public int Number { internal set; get; } }
Automatisch implementierte Eigenschaften sind komplette, einfache Eigenschaften
258
Der Compiler erzeugt im CIL-Code allerdings eine komplette implementierte Eigenschaft, die der aus Listing 4.32 ähnelt. Der Name des privaten Feldes (das im CIL-Code tatsächlich erzeugt wird) wird dabei natürlich vom Compiler vergeben. Im Programm können Sie auf das Feld also nicht zugreifen. Automatisch implementierte Eigenschaften können Sie an Stelle von Feldern immer dann verwenden, wenn Sie für das Lesen und Schreiben unterschiedliche Gültigkeitsbereiche einstellen wollen (was ja in unserem Beispiel der Fall ist), wenn Sie planen, die Eigenschaft später mit einer Programmierung zu erweitern, oder wenn
Der Gültigkeitsbereich von Klassen, Strukturen und deren Elementen
ein Programm, mit dem Sie arbeiten, einfach das Vorhandensein einer Eigenschaft an Stelle eines Feldes verlangt. Der dritte Punkt in dieser Auflistung ist nicht so einfach zu erklären und hat etwas mit Reflektion zu tun. Warten Sie also ab, bis Sie in Kapitel 22 etwas darüber erfahren haben.
4.5.5
Lesegeschützte Eigenschaften
Lassen Sie bei einer Eigenschaft den get-Accessor weg, ist diese Eigenschaft lesegeschützt. Obwohl solche Eigenschaften eigentlich selten Sinn machen (ein Programm, das einen Wert in eine Eigenschaft schreiben kann, sollte diesen wohl auch lesen können), sind sie dennoch möglich.
4.5.6
1
2
Konstante Eigenschaften
Eigenschaften können auch als Konstante deklariert werden. Dann können Sie den Wert der Eigenschaft nur in der Deklaration zuweisen und später nicht mehr ändern:
3
public const int MaximumAge = 150;
4.6
4
Der Gültigkeitsbereich von Klassen, Strukturen und deren Elementen
Den Gültigkeitsbereich von Klassen, Strukturen und deren Elementen können Sie über Modifizierer einschränken. Tabelle 4.1 beschreibt die Bedeutung dieser Modifizierer für Klassen. Modifizierer
Bedeutung
internal
legt fest, dass eine Klasse nur innerhalb der Assembly verwendet werden kann. Diesen Modifizierer verwenden Sie hauptsächlich in Klassenbibliotheken für Hilfsklassen, die Sie nicht veröffentlichen wollen.
public
legt fest, dass eine Klasse innerhalb der Assembly und von anderen Assemblys aus, die diese Assembly referenzieren, verwendet werden kann.
private
private wird für Typen verwendet, die innerhalb eines anderen Typs deklariert werden (siehe in Kapitel 5 bei den geschachtelten Typen). Wird ein innerer Typ mit private deklariert, kann er nicht von außen verwendet werden. Nur die Klasse oder Struktur, die den inneren Typen enthält, kann diesen verwenden.
5
Tabelle 4.1: Die Modifizierer für die Sichtbarkeit von Klassen
7
8
9
Tabelle 4.2 beschreibt die Bedeutung der Sichtbarkeitsmodifizierer für Klassenelemente. Modifizierer
Bedeutung
private
Mit private stellen Sie die Sichtbarkeit eines Elements so ein, dass dieses nur innerhalb der Klasse gilt. Solche Elemente können von außen nicht verwendet werden. Innerhalb der Methoden und Eigenschaften der Klasse können Sie auf private Elemente aber zugreifen.
protected
Der Modifizierer protected kennzeichnet Klassenelemente, die zunächst wie private Eigenschaften auftreten, aber bei der Vererbung (Kapitel 5) auch in abgeleiteten Klassen verwendet werden können (was bei privaten Elementen nicht möglich ist).
6
10 Tabelle 4.2: Die Modifizierer für die Sichtbarkeit von Klassenelementen
259
11
Grundlegende OOP
Tabelle 4.2: Die Modifizierer für die Sichtbarkeit von Klassenelementen (Forts.)
Modifizierer
Bedeutung
internal
internal-Elemente gelten innerhalb der Assembly wie public-Elemente, verhalten sich nach außen aber wie private-Elemente. Mit diesem hauptsächlich in Klassenbibliotheken verwendeten Sichtbarkeitsbereich legen Sie fest, dass bestimmte Elemente nur innerhalb der Klassenbibliothek verwendet werden dürfen. Andere Assemblys können auf diese Elemente nicht zugreifen.
protected internal
Elemente, die mit protected internal gekennzeichnet sind, verhalten sich prinzipiell wie internal-Elemente. Klassen, die in anderen Assemblys von Klassen mit protectedinternal-Elementen abgeleitet werden, können auf diese Elemente aber zugreifen.
public
Elemente, die den Modifizierer public besitzen, können von außen (über eine Referenz auf ein Objekt dieses Typs oder – bei statischen Elementen – über den Typnamen) verwendet werden.
4.7
NEU
Anonyme Typen
»Normale« Typen werden in Strukturen oder Klassen beschrieben. Um einen solchen Typen instanzieren zu können, benötigen Sie die entsprechende Struktur oder Klasse, die Sie entweder selbst implementieren oder einer Assembly entnehmen. Solche Typen machen Sinn in Programmen, die Objekte an verschiedenen Stellen benötigen und von einer Methode an eine andere übergeben. In einigen Fällen werden Typen aber nur in einer Methode und damit quasi temporär benötigt. Das ist z. B. dann der Fall, wenn Sie die Daten von Personen innerhalb einer Methode aus einer XML-Datei einlesen, innerhalb dieser Methode verarbeiten und diese Daten in anderen Methoden nicht weiter benötigen. Sie müssten dazu zwar eigentlich nicht extra einen (strukturierten) Typ für die Daten verwenden, ein solcher macht die Arbeit mit zusammenhängenden Daten aber zum einen übersichtlicher und zum anderen in der Regel auch einfacher. Für einen solchen, eher temporären Fall extra eine Struktur oder Klasse deklarieren zu müssen, um die Daten verwalten zu können, wäre in vielen Fällen zu aufwändig.
Anonyme Typen besitzen keinen Namen und benötigen keine Klasse oder Struktur
Hier springen die in C# 3.0 neuen anonymen Typen ein. C# 3.0 erlaubt nämlich die Erzeugung einer Objektinstanz ohne Typangabe. Das erzeugte Objekt muss lediglich über Objektinitialisierer initialisiert werden. Die Referenz auf das Objekt wird einer mit var deklarierten Variablen zugewiesen. Das so erzeugte Objekt besitzt zwar auch einen Typ, da dieser aber lediglich intern erzeugt wird, besitzt er keinen (öffentlichen) Namen (und kann deswegen auch nicht als Typ für die Deklaration weiterer Variablen verwendet werden). Falls Sie sich fragen, ob anonyme Typen überhaupt Sinn machen: Wenn Sie mit LINQ (Kapitel 11) arbeiten, werden Sie anonyme Typen recht häufig benötigen. Anonyme Typen erzeugen Sie nach dem folgenden Schema: var Name = new {Initialisierliste};
In der Initialisierliste geben Sie wie bei Objektinitialisierern Eigenschaftsnamen an, an die Sie Werte oder Objekte zuweisen. Die Namen können Sie frei wählen. Sie müssen natürlich den Bedingungen für Bezeichner genügen.
260
Anonyme Typen
Die einzelnen Name-Wert-Paare der Initialisierliste werden zu Eigenschaften des erzeugten Objekts. Deren Typ wird vom Compiler an Hand der zugewiesenen Werte bzw. Objekte erkannt und entsprechend eingestellt. So können Sie z. B. einen anonymen Typen erzeugen, der die Daten eines Artikels verwaltet: Listing 4.34: Erzeugen eines anonymen Typs var product1 = new { Name = "Basil Tofu", Price = 2.5M, Weight = 150F };
1
Das Beispiel setzt bewusst Zahlliterale ein, die über Typzeichen typisiert sind, um den Typ der Eigenschaften explizit zu bestimmen. Das Price-Feld besitzt im Beispiel den Typ decimal, das Weight-Feld den Typ float.
2
Auf Instanzen anonymer Typen können Sie im Programm zugreifen wie auf normale Objekte. Sogar IntelliSense funktioniert. Listing 4.35: Zugreifen auf anonyme Typen
3
Console.WriteLine(product1.Name + ": " + product1.Price + "_");
Abbildung 4.5: IntelliSense bei einem anonymen Typen
4 5
6 Anonyme Typen haben natürlich auch Einschränkungen. So können diese keine Methoden oder Konstruktoren besitzen (wie sollten diese auch deklariert werden …). Diese Einschränkungen sind aber nicht relevant, da der Sinn anonymer Typen das temporäre strukturierte Speichern von Daten ist, und dafür reichen Eigenschaften vollkommen aus.
7
Der Compiler erzeugt für jeden eindeutigen anonymen Typen im CIL-Code in Wirklichkeit eine Klasse, die in der erzeugten Assembly abgelegt wird.
8
Diese Klasse sieht für unser Beispiel prinzipiell (!) so aus:
9
Listing 4.36: Vereinfachte Version der Klasse, die der Compiler aus dem anonymen Typen im Beispiel generiert
EXKURS
10
internal sealed class f__AnonymousType0 { public string Name; public decimal Price; public float Weight;
11
public f__AnonymousType0(string name, decimal price, float weight) { this.Name = name; this.Price = price; this.Weight = weight; } }
261
Grundlegende OOP
Die tatsächliche Klasse ist wesentlich komplexer, weil diese generisch ist, komplexere Bezeichner verwendet und statt einfacher Felder mit Eigenschaften arbeitet. Wenn Sie dies nachvollziehen wollen: Schreiben Sie ein Programm, das einen anonymen Typen verwendet, kompilieren Sie dies, laden Sie die erzeugte Assembly in den Reflector von Lutz Roeder (www.aisto.com/roeder/dotnet), stellen Sie in den Optionen als Optimierung None ein und dekompilieren Sie den Code der Anwendung, indem Sie auf einer Methode die Leertaste betätigen. An der Stelle der Verwendung des anonymen Typs sieht der Programmcode in etwa so aus: Listing 4.37: Der vom Compiler erzeugte Code bei der Erzeugung anonymer Typen in einer vereinfachten Form var product1; product1 = new f__AnonymousType0("Basil Tofu", 2.5M, 150f);
Wenn Sie im Programm einen weiteren anonymen Typen verwenden, der in den Eigenschaften identisch ist, erkennt dies der Compiler und erzeugt keine separate Klasse. Listing 4.38: Erzeugung eines weiteren anonymen Typen, der mit dem ersten kompatibel ist var product2 = new { Name = "Smoked Tofu", Price = 2.4M, Weight = 145F};
In diesem Beispiel wird im CIL-Code nur eine Klasse für den anonymen Typen erzeugt, da die Eigenschaften beider Typen identisch sind. Der Programmcode im CIL-Code sieht für den zweiten Typen in etwa so aus: Listing 4.39: Der vom Compiler erzeugte Code bei der Erzeugung eines weiteren, kompatiblen anonymen Typs in einer vereinfachten Form var product2; product2 = new f__AnonymousType0("Smoked Tofu ", 2.4M, 145f);
Da die erzeugten Objekte bei kompatiblen anonymen Typen derselben Klasse angehören, können diese sogar aufeinander zugewiesen werden (auch wenn das eigentlich keinen Sinn macht): product2 = product1;
Enthält aber ein Typ mehr oder weniger Eigenschaften, oder unterscheiden sich die Eigenschaften in den Typen, erzeugt der Compiler separate Klassen.
TIPP
262
Das Erstaunliche an anonymen Typen ist, dass diese sogar die von object geerbten Methoden ToString, Equals und GetHashCode in einer logisch korrekten Form überschreiben. ToString gibt die Namen der Eigenschaften gefolgt von den jeweiligen Werten in einer kommabegrenzten Liste zurück. Equals vergleicht alle Eigenschaften mit denen des übergebenen anonymen Typs (derselben anonymen Klasse), GetHashCode berücksichtigt bei der Berechnung des Hashcode alle Eigenschaften (diese Implementierung werde ich für die eigene Implementierung von GetHashCode in Kapitel 5 »ausleihen« ☺).
Indexer
4.8
Indexer
Wenn Sie eine Klasse entwickeln, die Daten als Liste zur Verfügung stellt, können Sie über Indexer einen intuitiven Zugriff auf die gespeicherten Elemente ermöglichen. Implementiert die Klasse einen Indexer, können Sie über die C#-Indexer-Syntax auf die gespeicherten Elemente zugreifen. Dazu geben Sie hinter dem Objektnamen eckige Klammern und in diesen einen Index an. Der Index, der in den meisten Fällen vom Typ int ist, bezieht sich auf ein Objekt in der verwalteten Liste. Bei einem intIndex steht der Index in der Regel für die Position des Objekts, die bei 0 beginnt. 0 steht hier für das erste Objekt, 1 für das zweite etc.
1
2 In der Praxis werden Sie Indexer wahrscheinlich eher selten implementieren. Die generischen Auflistungen von .NET (Kapitel 7) reichen in der Regel für die Speicherung von listenförmigen Daten vollkommen aus, sodass Sie in .NET 3.5 nur noch selten eigene Auflistungsklassen implementieren müssen. Indexer erlauben nicht nur Integer-Werte als Index, sondern alle möglichen Typen als Schlüssel. Außerdem können Sie Indexer so entwickeln, dass mehrere Schlüssel übergeben werden. Im Prinzip handelt es sich bei einem Indexer um eine ganz normale Methode, der Sie beliebige Argumente übergeben können. Diese Methode wird allerdings über die Indexer-Syntax aufgerufen, also mit eckigen Klammern hinter dem Objektnamen.
INFO
3
Indexer erlauben den Zugriff auf Daten über die C#Indexer-Syntax
4 5
Indexer werden ähnlich Eigenschaften nach dem folgenden Schema deklariert: [Attribute] [Modifizierer] Typ this [Parameterliste] { [set { }]
6
[get { }]
7
}
In der Parameterliste geben Sie die verschiedenen möglichen Zugriffstypen an. Ein Parameter wird wie eine Variable angegeben und besitzt die folgende Syntax:
8
Typ Bezeichner
In der set-Methode implementieren Sie das Schreiben, in der get-Methode das Lesen. In set erhalten Sie wie bei Eigenschaften über value Zugriff auf den geschriebenen Wert. Den Index werten Sie über das oder die Argumente aus, die dem Indexer übergeben werden. get gibt den gelesenen Wert über return zurück. Sie können set oder get auch weglassen, um schreib- oder lesegeschützte Indexer zu erzeugen.
9
10
Das folgende Beispiel zeigt eine einfache Klasse zur Verwaltung einer Wertung in Form dreier Einzelwertungen. Die Einzelwerte können über den Indexer hinzugefügt und abgerufen werden. Die Methode GetAverage errechnet den Mittelwert:
11
Listing 4.40: Eine Klasse zur Verwaltung von Wertungen public class Valuation { /* Felder zur Verwaltung der Einzelwerte */ public double Value1;
263
Grundlegende OOP
public double Value2; public double Value3; /* Indexer */ public double this[int index] { set { switch (index) { case 1: this.Value1 = value; break; case 2: this.Value2 = value; break; case 3: this.Value3 = value; break; default: throw new IndexOutOfRangeException( "Der Index darf nicht größer sein als 2"); } } get { switch (index) { case 1: return this.Value1; case 2: return this.Value2; case 3: return this.Value3; default: throw new IndexOutOfRangeException( "Der Index darf nicht größer sein als 2"); } } } /* Methode zur Berechnung des Mittelwerts */ public double GetAverage() { return (this.Value1 + this.Value2 + this.Value3) / 3; } }
Die Klasse arbeitet mit einzelnen Feldern (Value1, Value2, Value3) zur Speicherung der Einzelwerte. Diese Felder sind öffentlich, weswegen ein direkter Zugriff möglich ist: Valuation valuation = new Valuation(); valuation.Value1 = 9.5;
264
Indexer
Der Indexer ermöglicht aber auch einen u. U. intuitiveren Zugriff über die C#Indexer-Syntax: Listing 4.41: Intuitiverer Zugriff über den Indexer valuation[0] = 9.5; valuation[1] = 8.2; valuation[2] = 8.7;
1
Der Indexer erzeugt eine IndexOutOfRangeException für den Fall, dass ein ungültiger Index übergeben wurde. Diesen Fall müssen Sie immer berücksichtigen und dann eigentlich immer auch eine IndexOutOfRangeException mit einer entsprechenden Fehlermeldung werfen. Indexer können jeden Typ als Rückgabetyp und als Index besitzen. Am Index können Sie auch mehrere Argumente deklarieren. Außerdem können Sie Indexer in mehreren Varianten implementieren, die sich in den Index-Argumenten allerdings unterscheiden müssen. Mehr zu diesem Thema erfahren Sie im Abschnitt »Überladene Methoden« ab Seite 242.
2 Indexer können jeden Typ besitzen und überladen werden
Das folgende Beispiel zeigt diese Features an der Klasse FileSize, über die die Größe von Dateien ermittelt werden kann:
3
4
Listing 4.42: Klasse zur Ermittlung der Größe von Dateien, die mit zwei Indexern arbeitet
5
public class FileSize { /* Indexer */ public long this[string fileName] { get { return this[fileName, false]; } }
6
7
/* Indexer */ public long this[string fileName, bool useKB] { get { FileInfo fileInfo = new FileInfo(fileName); if (useKB) { return fileInfo.Length / 1024; } else { return fileInfo.Length; } } }
8
9
10
}
Das Beispiel arbeitet mit der FileInfo-Klasse aus dem Namensraum System.IO, über die Informationen zu Dateien eingelesen werden können.
11
265
Grundlegende OOP
Das Ermitteln der Größe von Dateien läuft bei dieser Klasse über die Indexer: Listing 4.43: Verwendung der Indexer der FileSize-Klasse FileSize fileSize = new FileSize(); string fileName = "C:\\pagefile.sys"; Console.WriteLine(fileName + " ist " + fileSize[fileName] + " Bytes groß"); Console.WriteLine(fileName + " ist " + fileSize[fileName, true] + " KB groß");
Abhängig davon, ob Sie beim Aufruf das zweite Argument angeben oder nicht, rufen Sie den einen oder den anderen Indexer auf.
INFO
Ob dieses Beispiel sinnvoll ist oder nicht, ist diskussionswürdig. In der Praxis würde ich wahrscheinlich eher eine Klasse mit einer statischen Methode GetFileSize implementieren. Der Aufruf einer solchen wäre in meinen Augen intuitiver als die Verwendung eines Indexers. Anders herum können die auf einem System gespeicherten Dateien durchaus auch als Liste gesehen werden, was die Verwendung eines Indexers wiederum rechtfertigt.
4.9
Konstruktoren, Finalisierer, Dispose und using
Konstruktoren haben Sie bereits genutzt, wenn Sie diesem Buch bis hierhin gefolgt sind. Ein Konstruktor sorgt nämlich beim Erzeugen eines Objekts für die Initialisierung desselben. Bei vielen Typen haben Sie die Möglichkeit, den so genannten Standardkonstruktor zu verwenden, dem keine Argumente übergeben werden. Diesen erkennen Sie an den leeren Klammern hinter den Typnamen. Viele Typen besitzen aber auch Konstruktoren, denen Argumente übergeben werden, über die das erzeugte Objekt initialisiert wird. Konstruktoren können Sie für Ihre eigenen Strukturen und Klassen implementieren, um eben genau dies zu erreichen: um die erzeugten Objekte bei der Erzeugung zu initialisieren. Wenn Objekte in einem Konstruktor Ressourcen »öffnen«, die wieder explizit freigegeben werden müssen, können diese über einen so genannten Finalisierer freigegeben werden. Ein Beispiel dafür ist eine Klasse zur Protokollierung, die im Konstruktor eine Textdatei öffnet. Diese Datei muss wieder geschlossen werden, wenn die Instanz der Klasse nicht mehr benötigt wird. Dafür können Sie einen Finalisierer verwenden. Das Dumme an Finalisierern ist aber, dass diese nicht unbedingt auch aufgerufen werden. Woran das liegt, kläre ich im Abschnitt »Finalisierer«. Deswegen sind Finalisierer zum Freigeben von Ressourcen eigentlich gar nicht geeignet. Eine spezielle Dispose-Methode, die Sie in Ihren Typen implementieren können, ist hingegen für das Freigeben von Ressourcen sehr gut geeignet. Diese Methode wird ab Seite 271 behandelt.
4.9.1
Konstruktoren
Ein Konstruktor ist eine spezielle Methode, die automatisch aufgerufen wird, wenn ein Objekt erzeugt wird. Auch wenn Sie keinen eigenen Konstruktor implementieren, besitzt eine Klasse oder Struktur einen Konstruktor. In Typen, die keinen speziellen Konstruktor enthalten, fügt der Compiler nämlich automatisch einen parameterlosen Standardkonstruktor ein.
266
Konstruktoren, Finalisierer, Dispose und using
Sie können aber auch eigene Konstruktoren implementieren, mit denen Sie ein Objekt direkt bei dessen Erzeugung initialisieren. Bei der Person-Klasse wäre es z. B. sinnvoll, einem Person-Objekt bei der Erzeugung gleich den Vornamen und den Nachnamen der Person zu übergeben. An dieser Stelle könnten Sie denken, dass zur Initialisierung von Objekten ja auch Objektinitialisierer verwendet werden können. Das ist auch richtig, die in C# 3.0 neuen Objektinitialisierer lassen Konstruktoren wirklich ein wenig in den Hintergrund treten. Konstruktoren machen aber immer dann Sinn, wenn Sie dem Programmierer eine oder mehrere wichtige Varianten zur Initialisierung eines Objekts anbieten oder die Initialisierung eines Objekts erzwingen wollen. In diesem Fall nutzen Sie die Tatsache, dass C# keinen Standardkonstruktor erzeugt, sobald Sie einen eigenen Konstruktor implementieren. Wenn Sie nur Konstruktoren implementieren, denen Initialisierungs-Argumente übergeben werden müssen, und diese Argumente im Konstruktor auf Korrektheit überprüfen (z. B. keine leeren Strings zulassen), erzwingen Sie die Initialisierung Ihrer Klassen, und Instanzen davon können keinen ungültigen Status aufweisen.
Über Konstruktoren werden Objekte bei der Erzeugung initialisiert
1
INFO
2
3
4
Konstruktoren werden nicht nur dazu verwendet, Objekte zu initialisieren. Sie können in einem Konstruktor z. B. auch eine Datei oder eine Verbindung zu einer Datenbank öffnen, die Ihre Klasse benötigt. Das Beispiel beim Finalisierer im nächsten Abschnitt demonstriert dies.
5
In C# wird ein Konstruktor ähnlich einer Methode deklariert. Dabei wird allerdings kein Rückgabetyp angegeben. Außerdem muss der Name des Konstruktors derselbe sein wie der der Klasse. Konstruktoren können wie Methoden überladen werden. Das folgende Beispiel demonstriert dies anhand der aus den vorhergehenden Beispielen bekannten Person-Klasse (Seite 235), die ich zur Vereinfachung um die Adresse reduziert habe. Das Geburtsdatum habe ich zudem als Nullable deklariert, damit es auch möglich ist, kein Geburtsdatum anzugeben:
6
7
Listing 4.44: Klasse mit zwei Konstruktoren public class Person { public string FirstName; public string LastName; public DateTimeOffset? Birthdate;
8
9
/* Dem ersten Konstruktor werden nur der Vor- und der Nachname übergeben */ public Person(string firstName, string lastName) { this.FirstName = firstName; this.LastName = lastName; }
10
/* Dem zweiten Konstruktor wird neben dem Vor- und dem Nachnamen auch das Geburtsdatum übergeben */ public Person(string firstName, string lastName, DateTimeOffset birthDate) { this.FirstName = firstName; this.LastName = lastName; this.Birthdate = birthDate; }
11
}
267
Grundlegende OOP
Das Beispiel nutzt übrigens die Tatsache, dass Felder vom Compiler mit einem Leerwert initialisiert werden. Das BirthDate-Feld, das im ersten Konstruktor nicht initialisiert wird, steht damit automatisch auf null. Wenn Sie nun Objekte dieser Klasse erzeugen, müssen Sie einen der beiden Konstruktoren verwenden: Listing 4.45: Erzeugen von Person-Instanzen über die eigenen Konstruktoren // Eine Person mit dem ersten Konstruktor erzeugen Person p1 = new Person("Ford", "Prefect"); // Eine Person mit dem zweiten Konstruktor erzeugen Person p2 = new Person("Arthur", "Dent", DateTimeOffset.Parse("15.6.1974"));
Das Geburtsdatum von Arthur Dent habe ich übrigens einer Seite auf myspace.com entnommen: profile.myspace.com/index.cfm?fuseaction=user.viewprofile&friendid=117142324. Eigene Konstruktoren => Kein automatischer Standardkonstruktor
Dass Sie nun nur noch die selbst implementierten Konstruktoren zur Verfügung haben, liegt daran, dass der Compiler den parameterlosen Standardkonstruktor nicht mehr erzeugt, sobald eine Klasse eigene Konstruktoren implementiert. Der Grund dafür ist, dass Sie so in der Lage sind, die Konstruktoren, über die Instanzen ihrer Klassen erzeugt werden können, selbst zu bestimmen. In vielen Fällen will der Entwickler einer Klasse erzwingen, dass Instanzen (über die verfügbaren Konstruktoren) mit Argumenten initialisiert werden müssen. Ein vom Compiler automatisch eingefügter Standardkonstruktor würde diesem Anliegen entgegenstehen. Wenn Sie in Ihren Klassen einen Standardkonstruktor zur Verfügung stellen wollen, müssen Sie diesen selbst implementieren. Für Strukturen gilt dies übrigens nicht, da diese vom Compiler immer mit einem Standardkonstruktor ausgestattet werden und Sie einen solchen selbst nicht implementieren können.
Konstruktoren können andere Konstruktoren aufrufen
Ein Konstruktor kann einen anderen Konstruktor (desselben Typs) aufrufen. Der Aufruf muss vor der ersten Anweisung erfolgen. Deshalb müssen Sie den Aufruf mit einem Doppelpunkt getrennt an den Kopf des Konstruktors anhängen. Verwenden Sie dazu den this-Operator gefolgt von der Argumentliste. Der zweite Konstruktor der Person-Klasse kann z. B. den ersten aufrufen, um den Vornamen und den Nachnamen zu setzen: Listing 4.46: Konstruktor, der einen anderen Konstruktor aufruft public Person(string firstName, string lastName, DateTimeOffset birthDate): this(firstName, lastName) { this.Birthdate = birthDate; }
HALT
268
Achten Sie darauf, dass Sie den richtigen Konstruktor aufrufen. Im obigen Beispiel bietet IntelliSense u. U. beim Aufruf des Konstruktors auch den Konstruktor an, in dem gerade programmiert wird. Wenn Sie aus Versehen diesen aufrufen (this(firstName, lastName, null)), erzeugen Sie eine Endlos-Rekursion, bei der der Konstruktor sich immer wieder selbst aufruft. Das Programm reagiert eine Weile nicht mehr, weil es mit dem endlosen Aufrufen des Konstruktors beschäftigt ist. Zudem werden bei jedem Konstruktoraufruf wie bei normalen Methodenaufrufen Informa-
Konstruktoren, Finalisierer, Dispose und using
tionen auf dem Stack abgelegt (die übergebenen Argumente und die Rücksprungadresse). Nach einigen tausend rekursiven Aufrufen reicht der freie Platz im Stack nicht mehr aus und die CLR erzeugt eine StackOverflowException.
Der Gültigkeitsbereich von Konstruktoren Konstruktoren besitzen normalerweise die Sichtbarkeit public, können aber auch mit private, protected und internal deklariert werden. public-Konstruktoren können als einzige von außen verwendet werden, um eine Instanz einer Klasse zu erzeugen.
1
Ein privater Konstruktor verhindert, dass ein Programm ein Objekt dieser Klasse mit Hilfe dieses Konstruktors erzeugt. Enthält die Klasse lediglich private Konstruktoren, kann niemand (außer die Klasse selbst über eine statische Methode) von dieser Klasse Instanzen erzeugen.
2
protected-Konstruktoren verhalten sich ähnlich, können aber – im Gegensatz zu privaten Konstruktoren – von abgeleiteten Klassen in deren Konstruktoren aufgerufen werden.
3
Mit privaten und geschützten Konstruktoren können Sie einige spezielle Techniken programmieren. So können Sie z. B. eine Instanzierung einer Klasse nur über eine statische Methode der Klasse ermöglichen, damit Sie die Anzahl der erzeugten Instanzen kontrollieren können.
4 5
Ein internal-Konstruktor ermöglicht das Instanzieren eines Objekts innerhalb der Assembly, verhindert das Instanzieren aber von außerhalb. Diese Konstruktoren werden häufig in Klassenbibliotheken verwendet (Kapitel 5). In vielen Fällen ist es dort sinnvoll, Objekte einer Klasse nur von Methoden anderer Klassen innerhalb derselben Assembly erzeugen zu lassen und das Instanzieren von außen zu verhindern.
4.9.2
6
Finalisierer 7
Der Finalisierer einer Klasse wird aufgerufen, kurz bevor das Objekt vom Garbage Collector aus dem Speicher entfernt wird. Finalisierer sind normalerweise für Aufräumarbeiten gedacht, die beim Zerstören des Objekts ausgeführt werden müssen.
8 An Stelle des Begriffs »Finalisierer« wird häufig der Begriff »Destruktor« verwendet. Dieser Begriff ist für C#-Klassen nicht korrekt. Ein Destruktor wird in Programmiersprachen, die solche ermöglichen, garantiert aufgerufen, sobald ein Objekt vom Programm freigegeben wird (in vielen Sprachen, sobald keine Referenz mehr darauf zeigt, in einigen Sprache, sobald das Objekt explizit freigegeben wird). Sprachen, bei denen ein Garbage Collector die Entsorgung der Objekte übernimmt, besitzen allerdings keine echten Destruktoren, sondern nur Finalisierer. Ein Finalisierer ist im Prinzip eine ganz normale Methode, nur dass diese automatisch vom Garbage Collector aufgerufen wird, wenn das Objekt zerstört wird. Der für uns wesentliche Unterschied zwischen Destruktoren und Finalisierern ist, dass der Garbage Collector u. U. entweder keine Zeit hat, nicht mehr benötigte Objekte zu zerstören, oder entscheidet, dass die Freigabe eines solchen Objekts nicht lohnt (weil das Objekt im Speicher sehr wenig Platz benötigt). Finalisierer werden also nicht unbedingt aufgerufen.
INFO
9
10
11
Einen Finalisierer deklarieren Sie ohne Modifizierer für die Sichtbarkeit und mit dem Namen der Klasse, dem allerdings eine Tilde (~) vorangestellt werden muss:
269
Grundlegende OOP
Listing 4.47: Demo-Klasse mit einem Finalisierer public class Demo { ~Demo() { Console.WriteLine("Der Finalisierer wurde aufgerufen"); } }
Wenn Sie diese Klasse in einer Konsolenanwendung implementieren, können Sie Finalisierer einmal ausprobieren: STEPS
1. 2. 3.
Erzeugen Sie dazu in Visual Studio eine neue Konsolenanwendung. Fügen Sie dem Projekt eine Klasse Demo hinzu, die Sie wie in Listing 4.47 implementieren. Schreiben Sie den folgenden Programmcode in der Main-Methode der Konsolenanwendung: Console.WriteLine("Finalisierer"); Demo d1 = new Demo(); Demo d2 = new Demo(); Demo d3 = new Demo();
4. 5.
Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine(); Starten Sie das Projekt mit (STRG)+(F5),
damit die Konsole nach dem Beenden noch so lange geöffnet bleibt, bis Sie die (¢)-Taste betätigen. Betätigen Sie die (¢)-Taste einmal, um das Programm zu beenden. Die Konsole bleibt geöffnet und Sie sehen die Ausgaben des Finalisierers.
Abbildung 4.6: Das BeispielProgramm nach dem Betätigen der (¢_)-Taste zum Beenden des Programms
HALT
270
Finalisierer sind eigentlich recht problematisch: Sie wissen nie genau, ob und wann der Garbage Collector das Objekt freigibt. Es kann sein, dass Ihr Programm gerade sehr beschäftigt ist und das Objekt erst eine halbe Stunde nach der Freigabe der Referenzen auf das Objekt aus dem Speicher entfernt wird. In einigen Fällen werden Finalisierer auch gar nicht aufgerufen. Das ist z. B. dann der Fall, wenn ein Programm beendet wird, bevor der Garbage Collector Zeit hatte, alle freien Objekte zu zerstören. Für den Arbeitsspeicher ist das zwar kein Problem, da der Speicher des Programms komplett freigegeben wird. Hat ein Objekt allerdings eine externe Ressource (wie z. B. eine Datenbankverbindung) verwendet, wird diese u. U. nicht mehr geschlossen und bleibt im System geöffnet. Finalisierer sollten Sie deswegen nur in Zusammenhang mit der im folgenden Abschnitt beschriebenen Dispose-Methode verwenden.
Konstruktoren, Finalisierer, Dispose und using
4.9.3
Die Close- und die Dispose-Methode
Wenn Sie im Konstruktor Ressourcen geöffnet haben, die möglichst schnell wieder freigegeben werden müssen (wie z. B. Datenbankverbindungen), können Sie diese also nicht einfach im Finalisierer freigeben. Verwenden Sie dazu besser eine separate Methode. Microsoft empfiehlt, diese Methode »Close« zu nennen, wenn die Ressource (über eine andere Methode) wieder geöffnet werden kann, und »Dispose«, wenn die Ressource damit endgültig geschlossen wird.
Close und Dispose sind zum Schließen bzw. Freigeben von Ressourcen vorgesehen
Das folgende Beispiel implementiert eine Protokollierungsklasse, die im Konstruktor eine Datei öffnet. Damit die Datei wieder ordnungsgemäß geschlossen werden kann, besitzt die Klasse eine Dispose-Methode. Diese Methode ist in der Schnittstelle IDisposable definiert. Schnittstellen werden in Kapitel 5 behandelt. Wenn ein Typ die IDisposable-Schnittstelle implementiert, kann Dispose in einigen Fällen (wie z. B. über die using-Anweisung im nächsten Abschnitt) automatisch aufgerufen werden. Auch wenn Sie jetzt vielleicht noch nicht wissen, was eine Schnittstelle ist, sollten Sie IDisposable also immer implementieren, wenn Sie eine Dispose-Methode benötigen. Das Protokollierungsbeispiel zeigt, wie das geht:
1
2
3
4
Listing 4.48: Beispiel für den sinnvollen Einsatz einer Dispose-Methode public class LogFile: IDisposable { /* Ein privates Feld verwaltet den StreamWriter, über den */ /* die Datei beschrieben wird */ private System.IO.StreamWriter sw;
5
/* Der Konstruktor öffnet die Datei */ public LogFile(string fileName) { this.sw = new System.IO.StreamWriter(fileName, true); }
6
/* Protokolliert eine Info in die Datei */ public void Log(string info) { this.sw.WriteLine(DateTime.Now.ToString() + ": " + info); }
7
8
/* Eine eigene Dispose-Methode gibt die verwendete Datei frei */ public void Dispose() { this.sw.Dispose(); }
9
/* Im Finalisierer wird Dispose aufgerufen */ ~LogFile() { this.Dispose(); }
10
}
11
Die LogFile-Klasse verwendet zum Schreiben in die Datei eine StreamWriterInstanz. Darüber erfahren Sie mehr in Kapitel 10. Die Dispose-Methode ruft die gleichnamige Methode der StreamWriter-Instanz auf, denn diese ist natürlich auch dazu vorgesehen, Ressourcen freizugeben.
271
Grundlegende OOP
Im Finalisierer, der trotz allem nur für den Fall implementiert ist, dass die DisposeMethode nicht aufgerufen wird, wird die Dispose-Methode aufgerufen. Ein mehrfacher Dispose-Aufruf sollte übrigens problemlos möglich sein und nicht zu Fehlern führen. Dispose sollte – wenn vorhanden – auf jeden Fall aufgerufen werden
Wenn Sie nun eine Instanz dieser Klasse anwenden, können Sie die DisposeMethode explizit aufrufen. Dabei müssen Sie allerdings darauf achten, dass Dispose auf jeden Fall aufgerufen wird, auch für den Fall, dass bei der Verwendung des Objekts eine Ausnahme auftritt. Deswegen sollten Sie das Objekt in einem try-Block erzeugen, der über einen finally-Block abgeschlossen wird. Der finally-Block wird auf jeden Fall ausgeführt, auch wenn der try-Block wegen einer Ausnahme oder explizit (über return) verlassen wird. Das Ganze ist leider ein wenig komplex: Listing 4.49: Expliziter Aufruf der Dispose-Methode LogFile logFile1 = null; try { // Erzeugen einer Instanz der LogFile-Klasse logFile1 = new LogFile("C:\\Log2.txt"); // Ein paar Dinge protokollieren logFile1.Log("Die Antwort auf die Frage aller Fragen ist 42"); } finally { if (logFile1 != null) { // Dispose aufrufen, um die Datei freizugeben logFile1.Dispose(); } } nun verwend
Die Variable logFile1 wird außerhalb der Blöcke deklariert, damit diese in beiden Blöcken zur Verfügung steht. Die LogFile-Instanz wird allerdings erst im try-Block erzeugt, weil beim Erzeugen bereits Fehler auftreten können. Im finally-Block wird die LogFile-Instanz über Dispose freigegeben, wobei allerdings überprüft wird, ob die Referenz nicht null ist. Das ist ein übliches Vorgehen bei der Verwendung von Objekten, die explizit freigegeben oder geschlossen werden müssen. Dieses Vorgehen ist absolut notwendig für die Typen, die IDisposable nicht implementieren (und bei denen Sie dann z. B. eine Close-Methode aufrufen müssen). Typen, die IDisposable implementieren, erlauben using
272
Bei Typen, die IDisposable implementieren (und das sind im .NET Framework die meisten von denen, die externe Ressourcen verwenden), können Sie allerdings auch eine vereinfachte Form des Dispose-Aufrufs verwenden. Über die using-Anweisung (die nichts mit der using-Direktive gemeinsam hat) können Sie für solche Typen nämlich erreichen, dass die Dispose-Methode eines Objekts direkt und automatisch nach der Verwendung des Objekts aufgerufen wird. Wenn Sie nun eine Instanz dieser Klasse in den Klammern der using-Anweisung erzeugen und innerhalb des zu dieser Anweisung gehörenden Blocks mit dieser Instanz arbeiten, ruft der Compiler nach dem Anweisungsblock automatisch die Dispose-Methode auf (die ja Bestandteil der IDisposable-Schnittstelle ist).
Statische Klassenmember und statische Klassen
Die Verwendung der LogFile-Klasse wird damit wesentlich einfacher: Listing 4.50: Die using-Anweisung using (LogFile logFile2 = new LogFile("C:\\Log2.txt")) { // Ein paar Dinge protokollieren logFile2.Log("Die Antwort auf die Frage aller Fragen ist 42"); } // Hier wird automatisch Dispose aufgerufen
1
Der Compiler setzt den Quellcode in einen Code um, der im Wesentlichen dem Code in Listing 4.49 entspricht. Er sorgt also dafür, dass Dispose auf jedem Fall (auch im Fehlerfall) sicher aufgerufen wird. Sie können (und sollten) using grundsätzlich immer für alle Klassen verwenden, die IDisposable implementieren (also eine Dispose-Methode besitzen), auch wenn diese eine Close-Methode zur Verfügung stellen. Dispose sollte nämlich auch dazu führen, dass die verwendeten Ressourcen ordnungsgemäß geschlossen (und nicht einfach verworfen) werden. In den Klassen des .NET Framework, die ich bisher verwendet habe, funktionierte Dispose in diesem Sinne perfekt. Garantiert ist das ordnungsgemäße Schließen in Dispose aber, besonders mit Typen, die nicht zum .NET Framework gehören, nicht. Probieren Sie aus, ob Dispose nicht u. U. alle Änderungen verwirft.
4.10
2
3
INFO
4 5
Statische Klassenmember und statische Klassen
Normale Eigenschaften und Methoden sind immer einer Instanz einer Klasse zugeordnet. Manchmal besteht aber auch der Bedarf, dass eine Eigenschaft nur ein einziges Mal für alle Instanzen dieser Klasse gespeichert ist oder dass eine Eigenschaft oder Methode ohne Instanz der Klasse verwendet werden kann. In diesem Fall können Sie statische Felder, Eigenschaften und Methoden implementieren. Statische Konstruktoren helfen bei der Initialisierung statischer Klassenelemente.
6
4.10.1
8
7
Statische Felder und Eigenschaften (Klasseneigenschaften)
Ein statisches Feld oder eine statische Eigenschaft ist nur einmal für die Klasse und nicht für jedes Objekt separat gespeichert. Damit können Sie Werte zwischen den einzelnen Instanzen der Klasse austauschen oder die globalen Variablen der strukturierten Programmierung simulieren.
Statische Elemente sind pro Klasse gespeichert
In einer Kontoverwaltung ist z. B. der Kontostand jedem Kontoobjekt separat zugeordnet. Der Zinssatz sollte aber für alle Kontoobjekte gemeinsam gespeichert sein. Damit der Zinssatz global für alle Kontoobjekte verwaltet werden kann, muss dieser statisch verwaltet werden. Das Feld bzw. die Eigenschaft deklarieren Sie dazu mit dem Modifizierer static
9
10
11
Listing 4.51: Klasse mit statischer Eigenschaft public class BankAccount { /* Der Zinssatz */ public static decimal InterestRate;
273
Grundlegende OOP
/* Die Kontonummer */ public int Number { private set; get; } /* Der Kontostand */ public decimal Balance; /* Konstruktor */ public BankAccount(int number) { this.Number = number; } /* Berechnet die Zinsen */ public decimal CalculateInterest(int months) { return (this.Balance * BankAccount.InterestRate) * (months / 12M); } }
Alle Methoden (normale und statische) des Objekts können auf statische Elemente der Klasse zugreifen. Die CalculateInterest-Methode des Beispiels demonstriert dies. Die Methode bezieht die Instanz-Eigenschaft Balance, den statischen Zinssatz und das übergebene Argument months in die Berechnung ein.
EXKURS
Bei der Berechnung bin ich übrigens in eine Anfänger-Falle getappt: Ursprünglich sah diese so aus: (this.Balance * BankAccount.InterestRate) * (months / 12)
Was fällt Ihnen auf? OK, Sie sind schon besser als ich. Das ist das immerwährende Problem, dass Schüler irgendwann besser werden als der Lehrer. Aber schon in Kapitel 4? Vielleicht sollte ich doch besser Snowboard-Lehrer werden … Jedenfalls: Die Division des int-Werts months durch den int-Wert 12 ist eine Ganzzahldivision. Deswegen ergibt diese 0 und die berechneten Zinsen sind immer 0. Dadurch, dass ich 12 mit M gekennzeichnet habe, wurde daraus ein decimal und die Division eine normale. Ein Programm kann nun den Zinssatz für alle Instanzen der Klasse setzen: Listing 4.52: Verwenden eines Objekts mit statischem Feld // Festlegen des Zinssatzes BankAccount.InterestRate = 0.055M; // Erzeugen zweier Konten BankAccount bankAccount1 = new BankAccount(1001) { Balance = 1000 }; BankAccount bankAccount2 = new BankAccount(1002) { Balance = 5000 }; // Berechnen der Zinsen für 10 Monate Console.WriteLine(bankAccount1.Number bankAccount1.CalculateInterest(10) Console.WriteLine(bankAccount2.Number bankAccount2.CalculateInterest(10)
+ + + +
" " " "
erhält für 10 Monate " + Euro Zinsen"); erhält für 10 Monate " + Euro Zinsen");
Wenn der Zinssatz nachträglich geändert wird, werden nachfolgende Berechnungen natürlich auch sofort mit diesem neuen Zinssatz ausgeführt.
274
Statische Klassenmember und statische Klassen
Statische Felder und Eigenschaften erlauben also das globale Speichern von Daten, auf die alle Instanzen der Klasse zugreifen können. Bei der Entscheidung, ob Sie ein Feld bzw. eine Eigenschaft normal oder statisch implementieren, müssen Sie lediglich die Frage stellen, ob die darin verwalteten Daten exklusiv für das jeweilige Objekt oder für alle Instanzen der Klasse gelten. Seien Sie aber sehr vorsichtig bei der Implementierung statischer Felder und Eigenschaften. Globale Daten machen ein Programm sehr fehleranfällig, weil diese im Prinzip im gesamten Programm geändert werden können. So kann es schnell passieren, dass eine statische Eigenschaft versehentlich geändert und Ihr Programm danach nicht mehr korrekt ausgeführt wird. Die statische Eigenschaft in unserem Beispiel kann, wie Listing 4.52 zeigt, nicht nur innerhalb der BankAccount-Klasse, sondern auch von außen geändert werden. Solche potenziellen Fehlerstellen können Sie vermeiden, indem Sie statische Felder bzw. Eigenschaften grundsätzlich private oder internal deklarieren. Die Initialisierung eines solchen Feldes könnten Sie in einem statischen Konstruktor (Seite 277) vornehmen, der den Wert z. B. aus einer Datenbank ermittelt. Statische internalFelder wären dann sinnvoll, wenn die Klasse in einer Klassenbibliothek implementiert wird, die intern vollen Zugriff auf dieses Feld besitzen soll. Anwendungen, die diese Klassenbibliothek verwenden, haben dann aber keinen Zugriff auf die globalen Daten. Das Konzept der Kapselung ist hier, wie bei einfachen Klassen, enorm wichtig, damit Sie Programme erstellen, bei denen nicht versehentlich Daten überschrieben werden können, die zu einem Fehlverhalten des Programms führen.
Statische Elemente erlauben globales Speichern
1 INFO
2 Kapseln Sie den Zugriff auf globale Daten!
3
4 5
4.10.2 Statische Klassen: Globale Daten in statischen Eigenschaften
6
In manchen Fällen ist es sinnvoll oder notwendig, Daten so zu verwalten, dass diese global im gesamten Programm gelten. Eigentlich sollten globale Daten grundsätzlich (und besonders bei der OOP) vermieden werden. Globale Daten beinhalten viele potenzielle Fehlerquellen, da jeder Programmteil auf diese Daten zugreifen und diese (eventuell fehlerhaft) verändern kann. Außerdem machen globale Daten ein Programm sehr undurchsichtig. Wenn Sie das Risiko aber eingehen wollen oder müssen, verwenden Sie dazu einfach eine Klasse mit lediglich statischen Eigenschaften. Um zu verhindern, dass eine solche Klasse instanziert werden kann, deklarieren Sie diese mit dem Modifizierer static:
7
8
9
Listing 4.53: Eine Klasse mit ausschließlich statischen Elementen public static class Globals { public static string DatabaseName; public static string UserName; public static string UserPassword; }
10
11
Statische Klassen dürfen nur statische Elemente beinhalten und können, wie gesagt, nicht instanziert werden. Im Programm können Sie die »globalen Variablen« dieser Klasse dann über den Klassennamen referenzieren: Globals.DatabaseName = "C:\\Bestellungen.mdb";
275
Grundlegende OOP
4.10.3 Statische Methoden (Klassenmethoden) Statische Methoden gelten für die Klasse
Statische Methoden werden – wie statische Eigenschaften – direkt über die Klasse (ohne Instanz) verwendet. Solche Methoden verwenden Sie (sehr selten) für Operationen, die sich auf die Klasse und nicht auf einzelne Objekte beziehen. Über statische Methoden können Sie beispielsweise die statischen (ggf. privaten) Felder und Eigenschaften einer Klasse bearbeiten. Ein Beispiel dafür fehlt hier, weil diese Technik doch eher selten verwendet wird. Wesentlich häufiger werden statische Methoden eingesetzt, um globale, allgemein anwendbare »Funktionen« in das Projekt einzufügen. Das .NET Framework nutzt solche Methoden sehr häufig. Die Klasse Math besteht z. B. ausschließlich aus statischen Methoden. Wenn Sie z. B. in Ihren Projekten häufig verschiedene Maße und Gewichte in nationale Einheiten umrechnen müssen, können Sie dazu eine Klasse mit statischen Methoden verwenden: Listing 4.54: Klasse mit statischen Methoden zur Maß- und Gewichtseinheitsumrechnung public static class UnitConversion { /* Rechnet Kilometer in Meilen um */ public static double KmToMile(double value) { return 0.621371 * value; } /* Rechnet Meilen in Kilometer um */ public static double MileToKm(double value) { return 1.609344 * value; } /* Rechnet Liter in US-Gallonen um */ public static double LitreToUSGallon(double value) { return value * 0.264172; } /* Rechnet US-Gallonen in Liter um */ public static double USGallonToLitre(double value) { return value * 3.785412; } }
Die Verwendung statischer Methoden für allgemeine Aufgaben ist wesentlich einfacher, als wenn Sie dazu immer wieder Objekte instanzieren müssten. Um eine Methode der Klasse UnitConversion zu verwenden, müssen Sie lediglich den Klassennamen angeben: Listing 4.55: Anwendung der UnitConversion-Klasse Console.WriteLine("1 km entspricht " + UnitConversion.KmToMile(1) + " Meilen"); Console.WriteLine("1 Liter entspricht " + UnitConversion.LitreToUSGallon(1) + " US-Gallonen");
276
Statische Klassenmember und statische Klassen
Statische Methoden können nicht auf die normalen (Instanz-)Eigenschaften einer Klasse zugreifen. Das ist auch logisch, denn diese Eigenschaften werden erst im Speicher angelegt, wenn eine Instanz der Klasse erzeugt wird.
INFO
4.10.4 Statische Konstruktoren Neben Eigenschaften und Methoden können auch Konstruktoren statisch deklariert werden. Ein statischer Konstruktor wird nur ein einziges Mal aufgerufen, nämlich dann, wenn die Klasse das erste Mal im Programm verwendet wird. Damit können Sie Initialisierungen vornehmen, die für alle Instanzen der Klasse gelten sollen. Deklarieren Sie diesen Konstruktor wie einen normalen Standardkonstruktor, lediglich ohne Sichtbarkeitsmodifizierer und mit dem Modifizierer static.
Statische Konstruktoren werden aufgerufen, wenn die Klasse zum ersten Mal verwendet wird
Statische Konstruktoren werden automatisch aufgerufen, weswegen diese keine Argumente besitzen dürfen. Außerdem können Sie (logischerweise) nur einen statischen Konstruktor implementieren.
1
2
3
Eine Kontoverwaltung könnte z. B. den aktuellen Zinssatz in einem statischen Konstruktor aus einer Datenbank auslesen (im Beispiel wird dies nur simuliert). Die InterestRate-Eigenschaft habe ich dazu so implementiert, dass diese von außen nur gelesen werden kann. Somit kann nur die Klasse selbst (der statische Konstruktor) auf diese Eigenschaft schreibend zugreifen:
4 5
Listing 4.56: Klasse mit statischem Konstruktor public class BankAccount { /* Der Zinssatz */ public static decimal InterestRate { private set; get; }
6
7
/* Die Kontonummer */ public int Number { private set; get; }
8
/* Der Kontostand */ public decimal Balance;
9
/* Konstruktor */ public BankAccount(int number) { this.Number = number; }
10
/* Statischer Konstruktor */ static BankAccount() { // Dieser Konstruktor ermittelt den Zinssatz, wenn das erste Objekt // dieser Klasse instanziert wird BankAccount.InterestRate = 0.055M; }
11
/* Berechnet die Zinsen */ public decimal CalculateInterest(int months)
277
Grundlegende OOP
{ return (this.Balance * BankAccount.InterestRate) * (months / 12M); } }
Ein statischer Konstruktor darf keinen Modifizierer für die Sichtbarkeit und keine Argumente besitzen. Neben einem statischen Konstruktor können Sie natürlich auch normale Konstruktoren deklarieren. Statische Finalisierer sind übrigens nicht möglich.
4.11 Regionen helfen beim Organisieren von Quellcode
Organisieren von Typelementen mit Hilfe von Regionen
Über eigene Typen mit vielen Elementen die Übersicht zu behalten, ist nicht besonders einfach. Hier helfen Regionen, die es Ihnen ermöglichen Teile Ihres Quellcodes zusammenzufassen. In Visual Studio können Sie Regionen über das automatisch links erscheinende Plus- bzw. Minus-Zeichen auf- bzw. zuklappen, um die Übersicht über Ihre Typen zu verbessern. Eine Region deklarieren Sie über die Direktive #region. Dahinter können Sie einen beliebigen einzeiligen Text schreiben, der zum Namen der Region wird. Eine Region schließen Sie über #endregion ab. Ich teile meine Klassen und Strukturen üblicherweise zumindest in Regionen ein, die der Bedeutung der Elemente entsprechen. Eine Klasse besitzt dann z. B. die Regionen Private Felder, Öffentliche Eigenschaften, Konstruktoren, Private Methoden, Öffentliche Methoden und Öffentliche statische Methoden. Ich zeige dies am Beispiel der BankAccount-Klasse aus dem Abschnitt »Statische Konstruktoren« (Seite 277). Listing 4.57: Klasse mit Regionen public class BankAccount { #region Öffentliche statische Eigenschaften /* Der Zinssatz */ public static decimal InterestRate { private set; get; } #endregion #region Öffentliche Eigenschaften /* Die Kontonummer */ public int Number { private set; get; } #endregion #region Öffentliche Felder /* Der Kontostand */ public decimal Balance;
278
Organisieren von Typelementen mit Hilfe von Regionen
#endregion #region Konstruktoren /* Konstruktor */ public BankAccount(int number) { this.Number = number; }
1
#endregion #region Statischer Konstruktor
2
/* Statischer Konstruktor */ static BankAccount() { // Dieser Konstruktor ermittelt den Zinssatz, wenn das erste Objekt // dieser Klasse instanziert wird BankAccount.InterestRate = 0.055M; }
3
#endregion
4
#region Öffentliche Methoden /* Berechnet die Zinsen */ public decimal CalculateInterest(int months) { return (this.Balance * BankAccount.InterestRate) * (months / 12M); }
5
#endregion
6
}
Abbildung 4.7: Die BankAccountKlasse mit einer aufgeklappten Region in Visual Studio
7
8
9
10
11
279
Grundlegende OOP
TIPP
Denken Sie daran, dass Sie alle Regionen einer Datei im Visual Studio über die Tastenkombination (STRG)+(M)+(O) zuklappen können. So können Sie auch sehr große Klassen und Strukturen sehr übersichtlich darstellen. Die Regionen, die den Quellcode enthalten, den Sie bearbeiten wollen, klappen Sie dann einfach wieder auf. Wie Sie der Abbildung entnehmen können, führen Regionen zu wesentlich aufgeräumteren Programmen. Und als kleine Zugabe tun Sie noch etwas Gutes für sich, denn Aufräumen räumt (nach der buddhistischen Lehre) auch Ihre Seele auf ☺.
280
Inhalt
5
Weiterführende OOP 1
Mit den in Kapitel 4 beschriebenen grundlegenden OOP-Techniken können Sie bereits einfache Anwendungen entwickeln. Für komplexe Projekte und zum Verständnis des .NET Framework sind aber die weiterführenden OOP-Techniken notwendig, die in diesem und im nächsten Kapitel behandelt werden.
2
3
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■ ■ ■
Partielle Klassen, Strukturen (und Schnittstellen) Typen in Typen (Verschachtelte Typen) Vererbung und Polymorphismus Abstrakte Klassen Versiegelte Klassen Schnittstellen Operatoren implementieren Wichtige Methoden implementieren Benachrichtigungen über partielle Methoden, Delegaten und Ereignisse Klassenbibliotheken implementieren
4
5 6
Neu in C# 3.0: ■ ■ ■ ■
7
Lambda-Ausdrücke für Delegaten: Seite 324 Partielle Methoden: Seite 334 Neu im .NET Framework 3.5: Die vordefinierten Delegaten Func und Action (in den Varianten mit zwei bis vier Typparametern): Seite 323
5.1
8
9
Partielle Klassen, Strukturen (und Schnittstellen)
C# erlaubt die Aufteilung von Klassen, Strukturen und Schnittstellen (Seite 303) auf mehrere Dateien. Die einzelnen Teile werden mit dem Modifizierer partial gekennzeichnet und müssen lediglich zu einer Assembly gehören (die Aufteilung von Typen über mehrere Assemblys ist nicht möglich). Der Hauptteil eines partiellen Typs wird ansonsten ganz normal, mit allen notwendigen Modifizierern wie z. B. public deklariert, die anderen Teile nur mit partial struct, class oder interface. Der Dateiname des Hauptteils ist (ohne Dateiendung) üblicherweise derselbe wie der Name des Typs. Der Dateiname der weiteren Teile wird meist um einen Punkt gefolgt von einer Bezeichnung des Teils erweitert. Ein Windows.Forms-Formular wird z. B. in den Dateien MainForm.cs und MainForm.Designer.cs verwaltet. Der Compiler fügt vor dem Kompilieren alle Teile einer partiellen Klasse zusammen und kompiliert diese dann wie eine ganz normale Klasse.
Klassen, Strukturen und Schnittstellen können aus mehreren Teilen bestehen
281
10
11
Index
■
Weiterführende OOP
Die Klasse Demo in Listing 5.1 ist z. B. in der Datei Demo.cs gespeichert. Die Beispielmethode in dieser Klasse ruft eine Methode auf, die im anderen Klassenteil enthalten ist. Der andere Klassenteil (Listing 5.2) ist in der Datei Demo.Database.cs gespeichert: Listing 5.1: Hauptteil einer partiellen Klasse public partial class Demo { /* Beispiel-Methode */ public void WriteProductName(int productID) { // Aufruf der Methode im anderen Klassenteil string productName = this.GetProductName(productID); Console.WriteLine(productName); } }
Listing 5.2: Zweiter Teil der partiellen Demo-Klasse partial class Demo { /* Beispiel-Methode */ private string GetProductName(int productID) { // Der Datenbankzugriff ist hier natürlich nicht implementiert return "Tofu"; } }
Die Teile eines partiellen Typs können auf alle Elemente des Typs zugreifen
An dem Beispiel wird deutlich, dass alle Teile einer partiellen Klasse oder Struktur problemlos auf alle Elemente des gesamten Typs zugreifen können. Die Methode WriteProductName des Hauptteils im Beispiel kann z. B. die private Methode GetProductName des Datenbankteils aufrufen. Der Sinn partieller Klassen ist das Aufteilen von Klassen in einzelne Teile, die eine separate Bedeutung besitzen. Das beste Beispiel für den sinnvollen Einsatz solcher Klassen sind die von Visual Studio erzeugten Windows.Forms-Formular-Klassen. Der Hauptteil der Klasse enthält nur den Programmcode, den der Entwickler implementiert. Der Designer-Teil enthält den Programmcode, den der Visual-Studio-FormularDesigner automatisch erzeugt hat. So kann der Entwickler sich auf den von ihm entwickelten Programmcode konzentrieren und den zweiten, automatisch erzeugten Teil der Klasse ignorieren. Partielle Klassen werden deshalb gerne eingesetzt, wenn Programmcode einer Klasse durch ein Tool bzw. einen (Visual-Studio-)Designer automatisch erzeugt wird. Da bei einer Neuerzeugung des Programmcodes nur der Teil der Klasse neu erzeugt wird, der für den automatisch erstellten Programmcode vorgesehen ist, werden eventuell weitere Teile der Klasse davon nicht beeinflusst. Sie können partielle Typen aber auch selbst einsetzen, um große Klassen, Strukturen oder Schnittstellen aufzuteilen oder um die Möglichkeit zu schaffen, dass mehrere Entwickler gleichzeitig an einem Typen arbeiten können (jeder an einem Teil des Typs). Dabei sollten Sie jedoch im Auge behalten, dass sehr große Klassen, Strukturen oder Schnittstellen auf ein schlechtes Design der Anwendung hindeuten. Außerdem machen partielle Typen ein Projekt nicht unbedingt übersichtlicher. Entwickeln Sie lieber kleine und komplette Typen als große und aufgeteilte.
282
Verschachtelte Typen
5.2
Verschachtelte Typen
Manchmal ist es hilfreich, Typen innerhalb von Klassen oder Strukturen zu deklarieren. Das ist z. B. dann der Fall, wenn ein Typ nur von einer einzigen Klasse oder Struktur verwendet wird. Ich habe verschachtelte Typen z. B. bei der Entwicklung von Steuerelementen für Felder, Eigenschaften, Ereignissen oder Methoden verwendet, die exklusiv eigene Typen verwendeten. Ob das so sinnvoll ist, ist diskussionswürdig. Verschachtelte Typen machen ein Programm eher unübersichtlich. Mittlerweile verwende ich lieber separate Typen.
Typen können ineinander geschachtelt werden
1
Deshalb, und da dieses Feature in der Praxis wohl eher selten eingesetzt wird, finden Sie hier nur ein kurzes Beispiel:
2
Listing 5.3: Beispiel für einen verschachtelten Typ /* Äußere Klasse. Verwaltet eine Person. */ public class Person { /* Innere Struktur. Verwaltet eine Adresse. */ public struct PersonAddress { public string Country; public string City;
3
4
/* Eine Methode der inneren Struktur */ public string GetFullName() { return this.Country + ", " + this.City; }
5
}
6 /* Innere Aufzählung */ public enum PersonType { Earthling, Alien }
7
public string FirstName; public string LastName;
8
/* Feld vom Typ der inneren Struktur */ public PersonAddress Address; /* Feld vom Typ der inneren Aufzählung */ public PersonType Type;
9
/* Eine Methode der äußeren Klasse */ public string GetFullName() { return this.FirstName + " " + this.LastName + ", " + this.Address.GetFullName(); }
10
}
11
Die Struktur PersonAddress und die Aufzählung PersonType sind innerhalb der Klasse Person deklariert, weil diese ausschließlich von Person verwendet werden. Beide Typen sind öffentlich, weil diese für öffentliche Felder verwendet werden. Die Felder Address und Type verwenden diese inneren Typen. Ein Problem geschachtelter Typen wird in diesem Beispiel deutlich: Sie können nicht einen Untertypen mit dem gleichen Namen versehen wie ein anderes Element des Typs. Die Struktur
283
Weiterführende OOP
PersonAddress kann deswegen nicht einfach nur Address heißen (da das Feld Address bereits so heißt). Nur der Vollständigkeit halber folgt noch ein Beispiel zur Verwendung, das sich allerdings nur beim Type-Feld von einem Beispiel unterscheidet, das nicht geschachtelte Typen verwendet: Listing 5.4: Anwendung des verschachtelten Typs Person person = new Person { FirstName = "Fred Bogus", LastName = "Trumper" , Type = Person.PersonType.Alien, Address = { City = "New York", Country = "USA" } }; Console.WriteLine(person.GetFullName());
Innere Typen sind normale Typen
Ein innerer Typ ist eigentlich ein ganz normaler Typ. Der wesentliche Unterschied zu normalen Typen ist, dass innere Typen von außen nur über den Namen des äußeren Typen referenziert werden können (wenn diese public oder internal deklariert sind). So können Sie z. B. die PersonAddress-Struktur auch separat verwenden: Listing 5.5: Separate Verwendung eines inneren Typs Person.PersonAddress address = new Person.PersonAddress {City = "Dublin", Country = "Irland"};
Wenn Sie innere Typen separat verwenden, ist der einzige Unterschied zu normalen Typen, dass Sie den äußeren Typen bei der Deklaration und der Erzeugung mit angeben müssen. Das hat viel Ähnlichkeit mit der Organisation von Typen in Namensräumen, nur dass der Typ hier eben in einem anderen Typ gespeichert ist. Innere Typen können auf private Elemente des äußeren Typen zugreifen
Was im Beispiel nicht dargestellt ist, ist die Tatsache, dass innere Klassen und Strukturen auf alle statischen Elemente einer äußeren Klasse bzw. Struktur zugreifen können, unabhängig von deren Sichtbarkeit (also auch auf geschützte und private). Der Zugriff auf nicht statische Elemente ist allerdings nur dann möglich, wenn der äußere Typ eine Klasse ist und dem inneren Typ eine Referenz auf das äußere Objekt übergeben wird (z. B. am Konstruktor). Der innere Typ kann dann auch auf die geschützten und privaten Elemente der äußeren Klasse zugreifen.
5.3
Vererbung und Polymorphismus
Die Vererbung ist neben der Kapselung ein weiteres, sehr wichtiges Grundkonzept der OOP. Vererbung ist nur bei Klassen möglich, nicht bei Strukturen. Klassen können von anderen Klassen erben
INFO
284
Über die Vererbung können Sie neue Klassen erzeugen, die alle Eigenschaften, Methoden und Ereignisse einer Basisklasse erben. So können Sie sehr einfach die Funktionalität einer bereits vorhandenen Klasse erweitern und/oder neu definieren. Da Klassen auch von Klassen erben können, die in einer Klassenbibliothek vorliegen (also kompiliert sind), besitzen Sie damit eine geniale Möglichkeit der Wiederverwendung und Erweiterung bzw. Anpassung bereits geschriebener Programme. Die Vererbung wird in der Praxis für eigene Klassen nur relativ selten eingesetzt. Ein Grund dafür ist, dass selten wirklicher Bedarf für Vererbung besteht. Ein anderer Grund ist, dass Programme, die Vererbung sehr intensiv nutzen, meist recht unübersichtlich und fehleranfällig werden. Da die Klassen des .NET Framework aber intensiv Vererbung einsetzen und Sie manchmal Ihre eigenen Klassen von .NET-Basisklassen ableiten müssen, sollten Sie die Vererbung schon beherrschen.
Vererbung und Polymorphismus
Bei der Vererbung spricht man häufig auch von ableiten: Eine Klasse wird von einer Basisklasse abgeleitet. Die Basisklasse wird auch als Superklasse, die abgeleitete Klasse auch als Subklasse bezeichnet. Wenn Sie Klassen von anderen Klassen ableiten, kommt automatisch auch Polymorphismus ins Spiel. Diesen Begriff und den Umgang damit erläutere ich ab Seite 292. Vererbung ist nur einer von zwei Mechanismen, mit denen Sie in .NET wiederverwendbaren Code erzeugen können. Der andere sind generische Typen, die in Kapitel 6 behandelt werden. In vielen Fällen ist die Entwicklung eines eigenen generischen Typs angebrachter als die Implementierung von Vererbung. Aber Vererbung – und besonders Polymorphismus – sollten Sie natürlich trotzdem beherrschen.
5.3.1
1 INFO
2
Ableiten von Klassen
Wenn Sie eine Klasse von einer anderen Klasse (der Basisklasse) ableiten wollen, geben Sie die Basisklasse einfach mit einem Doppelpunkt getrennt hinter der Subklasse an.
3
public class BankGiroAccount: BankAccount { }
4
Sie können als Subklasse nur eine Klasse angeben, C# unterstützt (wie die meisten OOP-Sprachen) lediglich die Einfachvererbung1.
5
Diese Deklaration reicht bereits aus, um eine neue Klasse zu erzeugen, die alle Elemente der Basisklasse erbt. Die Sichtbarkeit der Klassenelemente wird dabei nicht verändert. Alle öffentlichen Elemente sind in der abgeleiteten Klasse ebenfalls öffentlich, alle privaten bleiben privat.
6
Sinn macht die Vererbung allerdings erst dann, wenn Sie die abgeleitete Klasse erweitern oder geerbte Methoden neu definieren bzw. überschreiben.
5.3.2
Erweitern von Klassen
Das Erweitern von abgeleiteten Klassen ist prinzipiell einfach. In der abgeleiteten Klasse deklarieren Sie dazu lediglich neue Felder, Eigenschaften, Methoden und Ereignisse (siehe Seite 330). Das Beispiel, das ich hier entwerfe, implementiert zuerst eine Basisklasse BankAccount (Bankkonto) mit dem Feld Number (Kontonummer), der nach außen schreibgeschützten Eigenschaft Balance (Kontostand), einem Konstruktor, dem die Kontonummer übergeben wird, und der Methode Lodge (Einzahlen). Die Klasse BankGiroAccount (Girokonto) wird von BankAccount abgeleitet und um eine Eigenschaft Overdraft (Dispobetrag) und eine Methode zum Abheben (Withdraw) erweitert. Die Withdraw-Methode überprüft, ob die Auszahlung des Betrags unter Berücksichtigung des Dispobetrags möglich ist. In diesem Beispiel war ich mir übrigens nicht sicher, ob ich englische oder deutsche Bezeichner verwenden sollte. Deutsche hätten den Programmcode u. U. für Sie lesbarer gemacht. Ich bin aber der Meinung, dass Programme immer englische Bezeichner verwenden sollten, setze im Buch grundsätzlich englische Bezeichner ein und lebe außerdem zurzeit in Dublin ☺. Ich hoffe, Sie sehen mir das nach … 1
7
Abgeleitete Klassen können um neue Elemente erweitert werden
8
9
10
11 INFO
C++ unterstützt auch die Mehrfachvererbung, bei der eine Subklasse von mehreren Superklassen erben kann.
285
Weiterführende OOP
Vor dem Beispiel beschreibe ich noch zwei immer wieder auftretende Probleme beim Erweitern und deren Lösung. Konstruktoren werden nicht öffentlich vererbt
Das erste Problem ist, dass Konstruktoren nicht öffentlich vererbt werden. Wenn Sie eine Klasse von einer anderen Klasse ableiten, erbt die abgeleitete Klasse wohl die Konstruktoren der Basisklasse. Diese werden aber verborgen, sodass sie von außen nicht aufgerufen werden können. Der Compiler sorgt allerdings dafür, dass zumindest der geerbte Standardkonstruktor aufgerufen wird. Wie Sie ja bereits wissen, implementiert der Compiler in jeder Klasse einen neuen Standardkonstruktor, sofern die abgeleitete Klasse keine eigenen Konstruktoren besitzt. Dieser ruft automatisch den geerbten Standardkonstruktor auf, sofern ein solcher vorhanden ist (im anderen Fall meldet der Compiler einen Fehler). Implementiert die abgeleitete Klasse eigene Konstruktoren, rufen diese ebenfalls per Voreinstellung automatisch den geerbten Standardkonstruktor auf. Die neuen Konstruktoren können aber auch explizit andere Konstruktoren der Basisklasse aufrufen. Die Klasse BankGiroAccount soll aber wie die Klasse BankAccount einen Konstruktor besitzen, dem die Kontonummer und der Stand des Kontos übergeben werden können. Dieser Konstruktor muss in der Klasse BankGiroAccount neu implementiert werden.
Geerbte Konstruktoren werden über base aufgerufen
Sie können bei der Neu-Implementierung von Konstruktoren geerbte Konstruktoren aufrufen. Dazu hängen Sie das Schlüsselwort base (das Zugriff auf die geerbten Elemente der Basisklasse gibt), gefolgt von der Parameterliste, durch einen Doppelpunkt getrennt an den Konstruktorkopf an. public BankGiroAccount(int number): base(number)
Zugriff auf eigentlich private Elemente über protected
HALT
286
Ein anderes Problem ist in vielen Fällen, dass eine abgeleitete Klasse auf eigentlich private Elemente der Basisklasse zugreifen muss. Das Girokonto muss z. B. in seiner Withdraw-Methode auf die geerbte Eigenschaft Balance schreibend zugreifen. Von außen soll allerdings nur der lesende Zugriff möglich sein. Solche Elemente können Sie in der Basisklasse nicht privat deklarieren, da eine abgeleitete Klasse keinen Zugriff auf die privaten Elemente einer Basisklasse besitzt. Deklarieren Sie Elemente, die in abgeleiteten Klassen verwendet werden sollen, in der Basisklasse mit dem Modifizierer protected. Solche »geschützten« Elemente gelten nach außen wie private, werden aber so vererbt, dass die abgeleitete Klasse Zugriff darauf hat. Gehen Sie mit protected-Elementen sehr sparsam um. protected-Elemente unterlaufen ggf. eine ansonsten sorgsam geplante Kapselung, da abgeleitete Klassen diese ohne Restriktionen verwenden können. In einigen Fällen scheint die Verwendung geschützter Elemente aber nicht zu umgehen zu sein. Das scheint z. B. dann der Fall zu sein, wenn abgeleitete Klassen Eigenschaften so neu implementieren, dass für das Schreiben neue Regeln gelten. Wenn das Feld, in dem die Eigenschaft ihren Wert verwaltet, auch in anderen Methoden oder Eigenschaften der Basisklasse verwendet wird, kann die abgeleitete Klasse nicht einfach ein neues Feld für die Eigenschaft verwenden. Das Feld muss in der Basisklasse geschützt deklariert werden, damit die abgeleitete Klasse genau dieses beschreibt. Aber auch in solchen Fällen können Sie auf geschützte Elemente verzichten. Dazu müssen Sie allerdings Polymorphismus (Seite 325) einsetzen.
Vererbung und Polymorphismus
Das Beispiel Die folgenden Listings implementieren die beiden Beispielklassen BankAccount und BankGiroAccount unter Berücksichtigung der genannten Probleme. Ich setze für die Balance-Eigenschaft einen geschützten set-Accessor ein, damit BankGiroAccount auf diese Eigenschaft zugreifen kann. Listing 5.6: Die Basisklasse BankAccount /* Klasse für ein einfaches Konto public class BankAccount { /* Die Kontonummer */ public readonly int Number;
1
*/
2
/* Die Eigenschaft Balance ist zum Schreiben als protected deklariert, damit das Girokonto darauf zugreifen kann. Zum Lesen ist diese Eigenschaft öffentlich. */ public decimal Balance { get; protected set; }
3
4
/* Methode zum Einzahlen */ public void Lodge(decimal value) { this.Balance += value; }
5
/* Der Konstruktor */ public BankAccount(int number) { this.Number = number; this.Balance = 0; }
6
}
7
Die Klasse BankGiroAccount (Girokonto) wird von BankAccount abgeleitet und um einen Dispobetrag und eine Methode zum Abheben erweitert. Der Konstruktor muss neu implementiert werden und greift über base auf den geerbten Konstruktor zu:
8
Listing 5.7: Die abgeleitete Klasse BankGiroAccount public class BankGiroAccount : BankAccount { /* Der Dispobetrag. Kann nach außen nur gelesen werden. */ public decimal Overdraft { private set; get; }
9
10
/* C# erzeugt einen parameterlosen Defaultkonstruktor in jede neue Klasse, der alle geerbten Konstruktoren nach außen verbirgt. Deshalb muss ein Konstruktor mit Parametern in der neuen Klasse noch einmal deklariert werden. */ public BankGiroAccount(int number): base(number) { // Dispobetrag einstellen this.Overdraft = -1000; }
11
287
Weiterführende OOP
/* Methode zum Abheben */ public void Withdraw(decimal value) { if (this.Balance - value >= this.Overdraft) { this.Balance -= value; } else { // Wenn das Abheben nicht möglich ist, // wird eine Ausnahme erzeugt throw new Exception("Abheben nicht möglich. " + "Der Dispobetrag würde unterschritten werden."); } } }
INFO
Die Klasse BankGiroAccount erzeugt beim Versuch, einen Betrag abzuheben, der den verfügbaren Kreditrahmen überschreiten würde, eine Ausnahme. Diese Technik ist bei der Implementierung von Klassen sehr wichtig: Klassen sollten die Nicht-Ausführbarkeit einer Methode oder das Nicht-Zulassen des Schreibens eines ungültigen Wertes in eine Eigenschaft immer mit einer Ausnahme an den Aufrufer melden. Theoretisch wäre auch denkbar, stattdessen in einer Windows-Anwendung eine Meldung (in einer MessageBox) aufzugeben. Eine solche Klasse wäre aber zum einen auf eine bestimmte Art von Anwendung eingeschränkt und überließe dem Programmierer zum anderen nicht die eigene Reaktion auf Ausnahmesituationen. Ausnahmen sind genau für diese Fälle vorgesehen. Wenn Sie die Klassen nun verwenden, können Sie auf ein Konto und ein Girokonto einzahlen, von einem Girokonto abheben und von beiden Konten die Nummer und den Betrag auslesen: Listing 5.8: Verwenden der Beispielklassen BankAccount account1 = new BankAccount(1001); account1.Lodge(1000); BankGiroAccount account2 = new BankGiroAccount(1002); account2.Lodge(1000); account2.Withdraw(500); Console.WriteLine("Konto Nummer {0}: {1}", account1.Number, account1.Balance); Console.WriteLine("Konto Nummer {0}: {1}", account2.Number, account2.Balance);
5.3.3 Methoden und Eigenschaften können in Subklassen neu definiert werden
288
Neudefinieren von Methoden und Eigenschaften und Zugriff auf geerbte Elemente
Eine abgeleitete Klasse kann nicht nur neue Elemente hinzufügen, sondern auch geerbte Methoden und Eigenschaften mit einer neuen Implementierung versehen (Falls Sie sich wundern, dass Eigenschaften auch neu definiert werden können: Eigenschaften sind ja intern lediglich Methoden). Neu implementiert wird eine Methode oder Eigenschaft immer dann, wenn eine abgeleitete Klasse eine gleichnamige Methode bzw. Eigenschaft mit derselben Signatur aufweist wie ein entsprechendes Element in der Basisklasse (wobei allerdings bei Methoden der Rückgabetyp nicht berücksichtigt wird). Besitzt eine Methode in der abgeleiteten Klasse zwar denselben Namen, aber eine andere Signatur, handelt es sich nicht um eine neu definierte, son-
Vererbung und Polymorphismus
dern einfach nur um eine neue Methode (bei Eigenschaften ist Überladen ja nicht möglich, weswegen diese nur überschrieben werden können). Beim Neudefinieren von Eigenschaften können Sie natürlich die Programmierung verändern, Sie können aber bei schreibgeschützten Eigenschaften z. B. auch den Schreibschutz entfernen, indem Sie die set-Methode implementieren oder nicht schreibgeschützte schreibschützen, indem Sie die set-Methode weglassen. Wenn Methoden und Eigenschaften neu implementiert werden, macht es häufig Sinn, auf geerbte Methoden bzw. Eigenschaften zuzugreifen, um deren Implementierung zu verwenden. Die Methoden und Eigenschaften einer abgeleiteten Klasse können auf alle Elemente der Basisklasse(n) zugreifen, die nicht privat deklariert sind. Sofern in der abgeleiteten Klasse keine gleichnamigen Elemente mit gleicher Signatur existieren, können Sie dazu einfach den Namen des Elements verwenden. Setzen Sie aber besser this. davor, um klarer zu machen, dass es sich um ein Klassenelement handelt. Existieren in der abgeleiteten Klasse gleichnamige Elemente mit gleicher Signatur, können Sie explizit auf geerbte Elemente zugreifen, indem Sie statt this das Schlüsselwort base verwenden. Sie sollten in Methoden und Eigenschaften prinzipiell mit this auf Klassenelemente zugreifen, auch wenn diese (zurzeit noch) lediglich in der Basisklasse definiert sind. Falls ein geerbtes Klassenelement später einmal neu definiert oder überschrieben wird, stellt dies sicher, dass Ihre Klasse auch die eine neue Variante verwendet. Nur wenn Sie Methoden oder Eigenschaften neu definieren oder überschreiben und innerhalb dieser neuen Varianten explizit auf die Basisversion zugreifen wollen, verwenden Sie base.
Methoden und Eigenschaften können auf alle nicht privaten Elemente der Basisklasse(n) zugreifen
1
2
3
4 INFO
5 6
Eine Klasse Employee könnte z. B. die Daten von Mitarbeitern speichern und das Gehalt (über CalculateSalary) berechnen. Die Eigenschaft OvertimeBonus verwaltet den Bonus für Überstunden (in Prozent / 100). OvertimeBonus lässt lediglich Werte zwischen 0 und 0.2 zu. Das Feld overtimeBonus, das diese Eigenschaft zur Verwaltung des Werts verwendet, ist protected deklariert, damit abgeleitete Klassen darauf zugreifen können, und für den Fall, dass dieses Feld bzw. die Eigenschaft in weiteren Methoden bzw. Eigenschaften der Klasse verwendet werden, die in abgeleiteten Klassen nicht neu definiert werden.
7
8
Mir ist dabei klar, dass das geschützte Feld die Kapselung unterläuft, da abgeleitete Klassen ohne Restriktionen darauf zugreifen können. Da Polymorphismus erst im nächsten Abschnitt behandelt wird, komme ich aber um das geschützte Feld für eine saubere Implementierung nicht herum.
9
Listing 5.9: Klasse zur Verwaltung der Daten eines »normalen« Mitarbeiters
10
public class Employee { public string FirstName; public string LastName;
11
protected decimal overtimeBonus; /* Bonus für Überstunden */ public virtual decimal OvertimeBonus { set {
289
Weiterführende OOP
if (value >= 0 && value regularHours) { return hours * hourlyRate + (hours - regularHours) * (hourlyRate * this.OvertimeBonus); } else { return hours * hourlyRate; } } }
Die von Employee abgeleitete Klasse Manager speichert die Daten von leitenden Angestellten und berechnet das Gehalt anders. Außerdem lässt die OvertimeBonus-Eigenschaft in dieser Klasse Werte zwischen 0 und 1 zu. OvertimeBonus kann auf das geerbte overtimeBonus-Feld zugreifen, da dieses in der Basisklasse geschützt deklariert ist. Listing 5.10: Klasse zur Verwaltung der Daten eines leitenden Angestellten public class Manager : Employee { /* Der Bonus für Überstunden wird neu definiert */ public decimal OvertimeBonus { set { if (value >= 0 && value regularHours) { return hours * hourlyRate + (hours - regularHours) * (hourlyRate * this.OvertimeBonus); } else { return hours * hourlyRate; } }
1
2
}
Bei dieser Variante des Neudefinierens meldet der Compiler allerdings die Warnung, dass die Eigenschaft OvertimeBonus und die Methode CalculateSalary die geerbte Eigenschaft bzw. Methode ausblenden (verbergen). Die Warnung fällt weg, wenn Sie den new-Modifizierer verwenden, der aussagt, dass diese Eigenschaft bzw. Methode eine neue Version der geerbten ist:
new zeigt dem Compiler, dass es sich um eine neue Variante handelt
3
public new decimal OvertimeBonus ... public new decimal CalculateSalary(int hours, int regularHours)
4
Diese Art des Neudefinierens wird auch als »Verbergen« bezeichnet. Die neue Methode verbirgt die geerbte, sodass diese von außen nicht mehr aufgerufen werden kann.
5
Wenn Sie einen Mitarbeiter und einen Manager erzeugen, können Sie bei einem Mitarbeiter einen Überstunden-Bonus zwischen 0 und 0.2 und bei einem Manager einen Überstunden-Bonus zwischen 0 und 1 setzen. Wenn Sie bei beiden Objekten die CalculateSalary-Methode aufrufen, wird das Gehalt unterschiedlich berechnet:
6
Listing 5.11: Verwendung der neu definierten Eigenschaft und Methode
7
// Einen normalen Mitarbeiter erzeugen Employee employee = new Employee {FirstName = "Ford", LastName = "Prefect", OvertimeBonus= 0.2M};
8
// Einen leitenden Angestellten erzeugen Manager manager = new Manager {FirstName = "Zaphod", LastName = "Beeblebrox", OvertimeBonus = 0.3M};
9
// Das Gehalt der Mitarbeiter berechnen und ausgeben Console.WriteLine("{0} {1} erhält für 48 Stunden {2} Euro", employee.FirstName, employee.LastName, employee.CalculateSalary(48, 40)); Console.WriteLine("{0} {1} erhält für 48 Stunden {2} Euro", manager.FirstName, manager.LastName, manager.CalculateSalary(48, 40));
Beachten Sie, dass das einfache Neudefinieren von Methoden und Eigenschaften immer dann zu Problemen führt, wenn Sie die Klasse in polymorphen Situationen verwenden. Polymorphismus – dieses Problem und dessen Lösung werden im folgenden Abschnitt beschrieben.
10
11 INFO
291
Weiterführende OOP
5.3.4 Polymorphismus ist Vielgestaltigkeit
Polymorphismus, virtuelle Methoden und virtuelle Eigenschaften
Polymorphismus (Vielgestaltigkeit) bedeutet, dass ein Objekt mehrere Gestalten haben kann. Das ist zunächst vielleicht nicht so leicht zu verstehen, aber eigentlich ganz einfach ☺. Wenn eine Klasse von einer Basisklasse abgeleitet wird, erbt sie alle Elemente der Basisklasse. Deswegen können Sie sagen, dass eine Instanz der Manager-Klasse ein Manager-Objekt und gleichzeitig auch ein Employee-Objekt ist. Oder anders: Ein leitender Angestellter ist ein leitender Angestellter (Gestalt 1), aber auch ein Mitarbeiter (Gestalt 2). Oder: Ein Girokonto ist ein Girokonto, aber auch ein einfaches Bankkonto. Das ist Polymorphismus ☺. Polymorphismus hat beim Programmieren aber noch eine weitere Bedeutung. In der abgeleiteten Klasse ist es ja möglich, geerbte Methoden und Eigenschaften mit einer neuen Implementierung zu versehen. Objekte der abgeleiteten Klasse verhalten sich also ggf. anders als Objekte der Basisklasse.
Referenzen vom Typ einer Basisklasse können Instanzen abgeleiteter Klassen verwalten
Hinzu kommt, dass eine Referenz vom Typ der Basisklasse Instanzen abgeleiteter Klassen referenzieren kann. Eine Employee-Referenz kann ein Manager-Objekt verwalten. Über eine Employee-Referenz können alle öffentlichen Elemente der EmployeeKlasse verwendet werden, auch wenn ein Manager-Objekt referenziert wird. Das ist deswegen möglich, da die abgeleitete Klasse alle Elemente der Basisklasse erbt. In der Praxis wird dieses Feature sehr häufig eingesetzt. Stellen Sie sich vor, Sie müssten eine Mitarbeiter-Verwaltung entwickeln. Zur Verwaltung der Mitarbeiter benötigen Sie lediglich eine Auflistung vom Typ Employee. An diese Auflistung könnten Sie dann Employee- und Manager-Objekte anhängen (deren Daten Sie z. B. aus einer Datenbank einlesen). Wenn Sie die Auflistung durchgehen, können Sie von allen Objekten die Eigenschaften FirstName und LastName lesen und schreiben und die Methode CalculateSalary aufrufen. Über einen Vergleich über den is-Operator können Sie sogar herausfinden, ob ein Mitarbeiter ein Manager ist. Polymorphismus kann nicht nur durch Vererbung erreicht werden, sondern auch über Schnittstellen. Schnittstellen werden ab Seite 303 beschrieben.
REF
Das Ganze ist am Anfang ziemlich schwierig zu verstehen. Wenn Sie Polymorphismus allerdings einmal am praktischen Beispiel angewendet haben, ist dieser nicht mehr ganz so mysteriös. Ich demonstriere Polymorphismus zunächst an einem einfachen Beispiel. Listing 5.12 verwendet eine generische Auflistung vom Typ List zur Verwaltung von Mitarbeitern. T ist der Typ der verwalteten Objekte. Ich setze hier Employee ein, um normale Mitarbeiter und leitende Angestellte verwalten zu können. Im Programm füge ich der Auflistung ein Employee- und ein Manager-Objekt hinzu. Das Beispiel geht dann die in der Auflistung gespeicherten Objekte durch, gibt deren Namen und eine Information über den Typ aus und berechnet das Gehalt für 48 Stunden: Listing 5.12: Anwendung von (noch nicht ganz korrektem) Polymorphismus über eine Auflistung // Auflistung vom Typ List erzeugen List employees = new List();
292
Vererbung und Polymorphismus
// Einen normalen Mitarbeiter anfügen employees.Add(new Employee { FirstName = "Ford", LastName = "Prefect", OvertimeBonus = 0.02M }); // Einen leitenden Angestellten anfügen employees.Add(new Manager { FirstName = "Zaphod", LastName = "Beeblebrox", OvertimeBonus = 0.03M }); // Die in der Liste verwalteten Mitarbeiter durchgehen foreach (Employee employee in employees) { Console.WriteLine("{0} {1} ist ein {2}", employee.FirstName, employee.LastName, employee is Manager ? "Manager" : "Mitarbeiter"); Console.WriteLine("{0} {1} erhält für 48 Stunden {2} Euro", employee.FirstName, employee.LastName, employee.CalculateSalary(48, 40)); }
1
2
3
Dem Programm ist es möglich, über eine Referenz auf die Basisklasse Employee auch Instanzen der Klasse Manager zu bearbeiten. Die Klasse Manager hat ja alle Elemente der Klasse Employee geerbt. Der Compiler kann sich darauf verlassen, dass die Klasse Manager die Elemente der Klasse Employee besitzt.
4
Falls die Klassen allerdings so aussehen wie in Listing 5.9 und Listing 5.10 (Seite 288), entsteht dabei ein großes Problem: Wenn Sie Ihre Klassen polymorph verwenden wollen, sollten Sie auf das einfache Neudefinieren von Methoden und Eigenschaften verzichten. Der Compiler verwendet dann nämlich immer die Version der Methode bzw. Eigenschaft, die zu dem Typ der Referenz passt (und nicht die Version, die zum Typ des tatsächlich verwalteten Objekts passt). Im Beispiel könnten Sie für einen Manager keinen ÜberstundenBonus setzen, der größer ist als 0.2, wenn Sie die OvertimeBonus-Eigenschaft über die Employee-Referenz der Auflistung beschreiben und die Klassen so aussehen wie in Listing 5.9 und Listing 5.10:
5 HALT
6
7
foreach (Employee employee in employees) { if (employee is Manager) { employee.OvertimeBonus = 0.3M; // Nicht möglich } }
8
9
Das Setzen würde allerdings funktionieren, wenn Sie die Employee-Referenz in eine Manager-Referenz umwandeln: foreach (Employee employee in employees) { if (employee is Manager) { ((Manager)employee).OvertimeBonus = 0.3M; // OK } }
10
11
Sauber ist das aber nicht, weil es in Programmen, die Polymorphismus einsetzen, immer vorkommen kann, dass lediglich eine Referenz vom Typ einer Basisklasse verwendet wird. Viel problematischer ist allerdings die Tatsache, dass für Mitarbeiter und Manager dasselbe Gehalt berechnet wird.
293
Weiterführende OOP
Abbildung 5.1: Das fehlerhafte Beispielprogramm
Virtuelle Methoden und Eigenschaften lösen das PolymorphismusProblem
Um dieses Problem zu verhindern, müssen Sie Methoden und Eigenschaften überschreiben statt diese einfach nur neu zu definieren. Dazu kennzeichnen Sie die Methoden und Eigenschaften, die überschrieben werden sollen, in der Basisklasse über den virtual-Modifizierer als virtuelle Methoden bzw. virtuelle Eigenschaften. In der abgeleiteten Klasse geben Sie statt des new-Modifizierers nun den Modifizierer override an, damit diese überschrieben werden (statt nur verborgen). Virtuelle Methoden und Eigenschaften sorgen dafür, dass das Programm immer die ggf. in der Klasse eines Objekts überschriebene Variante verwendet, unabhängig vom Typ der Referenz. Virtuelle Methoden und Eigenschaften bleiben übrigens auch in abgeleiteten Klassen immer virtuell. Listing 5.13: Auszug aus der geänderten Basisklasse mit einer virtuellen Eigenschaft und einer virtuellen Methode public class Employee { ... public virtual decimal OvertimeBonus ... public virtual decimal CalculateSalary(int hours, int regularHours) ... }
Listing 5.14: Auszug aus der abgeleiteten Klasse mit überschriebener Eigenschaft und überschriebener Methode public class Manager : Employee { ... public override decimal OvertimeBonus ... public override decimal CalculateSalary(int hours, int regularHours) ... }
Wenn Sie nun im Programm mit einer Referenz auf die Employee-Klasse einen Manager verwalten und das Gehalt berechnen, ruft der Compiler die Methode auf, die in der Manager-Klasse deklariert ist. Nun erhält ein Manager auch das, was er (hoffentlich …) verdient. Abbildung 5.2: Das korrigierte Beispielprogramm
294
Vererbung und Polymorphismus
In den Beispielen zu diesem Kapitel finden Sie das Projekt »Paint«, das ein einfaches Zeichenprogramm implementiert (mit dem Kreise und Rechtecke gezeichnet werden können). Dieses nutzt (neben abstrakten Methoden) auch Polymorphismus. Das Beispiel ist sehr gut dazu geeignet, Polymorphismus in der Praxis zu sehen, leider aber für dieses Buch zu aufwändig.
DISC
1 Virtuelle Methoden werden in einer so genannten »VTable« oder »VMT« (Virtual Method Table) verwaltet. Diese Tabelle, die vom Programm in der Laufzeit abgefragt wird, stellt sicher, dass das Programm immer die korrekte, überschriebene Methode der abgeleiteten Klasse verwendet, unabhängig vom Typ der Referenz.
EXKURS
2
Der Compiler sorgt dafür, dass bei der Ausführung des Programms für jede Klasse mit virtuellen Methoden eine eigene VMT im Speicher angelegt wird. Alle virtuellen Methoden der Klasse werden in dieser Tabelle, zusammen mit deren Adresse, eingetragen. An der Stelle des Aufrufs virtueller Methoden erzeugt der Compiler Programmcode, der in der VMT nachschaut, welche Methode für das jeweilige Objekt nun tatsächlich aufgerufen werden soll. Dabei wird nicht der Typ der Referenz, sondern der tatsächliche Typ des Objekts verwendet, um die VMT zu identifizieren. Der Aufruf einer virtuellen Methode führt also über die VMT immer zu der korrekten Variante. Diese Aufrufart wird übrigens auch als »späte Bindung« bezeichnet, weil das Programm erst in der Laufzeit die Adresse der Methode ermittelt.
3
4
5
Im Zusammenhang mit Polymorphismus sollten Sie die folgenden Punkte beachten: –
Setzen Sie virtuelle Methoden und Eigenschaften immer dann ein, wenn Sie auch nur die Ahnung haben, dass Ihre Klassen abgeleitet und in polymorphen Situationen eingesetzt werden könnten. Der Aufruf virtueller Methoden und Eigenschaften benötigt zwar etwas mehr Zeit (wegen des Umwegs über die VMT), bringt Ihnen aber die Sicherheit, dass alles funktioniert.
–
Wenn Sie sicherstellen, dass Ihre Klassen nicht abgeleitet werden können, können Sie auf virtuelle Methoden und Eigenschaften verzichten. Das Ableiten können Sie über den sealed-Modifizierer verhindern (Seite 303).
–
Wenn Sie in Eigenschaften und Methoden auf Eigenschaften zugreifen, verwenden Sie dazu in der Regel die Eigenschaft, und nicht das ggf. existierende Feld, in dem die Eigenschaft ihren Wert verwaltet. Ist die Eigenschaft virtuell deklariert (was immer dann der Fall sein sollte, wenn die Klasse abgeleitet werden kann und die Eigenschaft in den Elementen der Klasse oder von abgeleiteten Klassen verwendet wird), verwendet Ihre Klasse immer die aktuelle Eigenschaft. Wird die Eigenschaft in einer abgeleiteten Klasse überschrieben, greifen Methoden und Eigenschaften, die diese verwenden, auch in der Basisklasse auf die neue Version zu.
6 HALT
7
8
9
10
Der letzte Punkt ist wahrscheinlich etwas schwierig zu verstehen, weswegen ich diesem einen extra Abschnitt widme.
5.3.5
11
Polymorphismus extrem
Polymorphismus wird noch auf eine andere Art verwendet, als bisher dargestellt. Dabei wird die Tatsache genutzt, dass innerhalb einer Klasse virtuelle Methoden und Eigenschaften, die in der Klasse selbst definiert sind, zunächst einmal wie normale Methoden
295
Weiterführende OOP
und Eigenschaften verwendet werden können. Eine öffentliche Methode der Klasse könnte z. B. eine virtuelle geschützte Methode aufrufen. Wird die virtuelle Methode oder Eigenschaft in einer abgeleiteten Klasse aber überschrieben, ruft der Compiler bei deren Verwendung immer die neue Variante auf. Das gilt auch für den Fall, dass die Methode oder Eigenschaft, die das virtuelle Element verwendet, selbst nicht überschrieben wird. Ein Beispiel klärt dieses Mysterium vielleicht ein wenig auf: Die Employee-Klasse könnte zur Ermittlung des Stundensatzes eine schreibgeschützte Eigenschaft HourlyRate und zur Überprüfung des Überstunden-Bonus eine geschützte Methode CheckOvertimeBonus verwenden: Listing 5.15: Basisklasse mit virtuellen Eigenschaften und Methoden public class Employee { public string FirstName; public string LastName; private decimal hourlyRate = 30; /* Der Stundensatz */ public virtual decimal HourlyRate { get { return this.hourlyRate; } } protected decimal overtimeBonus; /* Bonus für Überstunden */ public virtual decimal OvertimeBonus { set { if (this.CheckOvertimeBonus(value) == true) { this.overtimeBonus = value; } else { throw new ArgumentException("Der übergebene Überstunden-" + "Bonus ist ungültig"); } } get { return this.overtimeBonus; } } /* Überprüft den Überstunden-Bonus */ protected virtual bool CheckOvertimeBonus(decimal value) { return value >= 0 && value regularHours) { return hours * this.HourlyRate + (hours - regularHours) *
296
Vererbung und Polymorphismus
(this.HourlyRate * this.OvertimeBonus); } else { return hours * this.HourlyRate; } } }
HourlyRate und CheckOvertimeBonus sind virtuell deklariert, damit diese in abgeleiteten Klassen überschrieben werden können. Die Eigenschaft OvertimeBonus setzt CheckOvertimeBonus ein, um den geschriebenen Wert zu überprüfen. Die Methode CalculateSalary fragt die Eigenschaft HourlyRate (nicht das Feld hourlyRate!) ab, um den Stundensatz zu ermitteln. In der abgeleiteten Klasse Employee müssen jetzt nur noch die Eigenschaft HourlyRate und die Methode CheckOvertimeBonus überschrieben werden:
1
2
3
Listing 5.16: Abgeleitete Klasse mit überschriebener Eigenschaft und überschriebener geschützter Methode public class Manager : Employee { private decimal hourlyRate = 50; /* Der Stundensatz wird überschrieben */ public override decimal HourlyRate { get { return this.hourlyRate; } }
4
5 6
/* Überprüft den Überstunden-Bonus in einer neuen Version */ protected override bool CheckOvertimeBonus(decimal value) { return value >= 0 && value , = und , >=,< und , >=, < und/oder x +1 definiert z. B. die anonyme Methode delegate(int x) {return x + 1;} oder anders gesagt so etwas wie die folgende normale Methode: int (int x) { return x +1; }
Ein .NET-Lambda-Ausdruck arbeitet mit dem Lambda-Operator =>. Dieser Operator ist binär, besitzt also zwei Operanden. Links stehen die eventuellen Parameter der (anonymen) Methode, rechts steht die Implementierung. Die Implementierung kann in Form eines Ausdrucks oder als in geschweifte Klammern eingeschlossene Anweisungsliste angegeben werden: [Parameterliste] => {Ausdruck | {Anweisungsliste}}
Für Lambda-Ausdrücke gelten die folgenden Regeln: ■
■ ■
324
Wird eine Methode mit mehr als einem Argument definiert, müssen die Argumente in Klammern angegeben werden. Bei Methoden mit genau einem Argument können Sie die Klammern auch weglassen. Wird eine Methode ohne Argumente definiert, müssen leere Klammern statt der Argumentliste angegeben werden. Die Typen der Argumente müssen nicht angegeben werden, weil der Compiler diese aus dem Typ des Delegaten entnimmt, dem der Lambda-Ausdruck zugewiesen wird. Sie können für einzelne Argumente aber auch einen Typ angeben (den
Delegaten, anonyme Methoden und Lambda-Ausdrücke
■
■
Sie wie gewohnt vor den Namen des Arguments schreiben). Die Namen der Argumente können Sie natürlich (wie bei normalen oder anonymen Methoden) frei vergeben. Enthält die Implementierung auf der rechten Seite eine Anweisungsliste, muss diese in geschweifte Klammern eingeschlossen werden. Jede Anweisung wird dann wie gewohnt mit einem Semikolon abgeschlossen. Definiert der Delegat einen Rückgabetyp, muss entweder ein Ausdruck verwendet werden, der den Typ ergibt, oder die Anweisungsliste ein return beinhalten, das den entsprechenden Typ zurückgibt.
1
Der Compiler beschwert sich in den Fällen, in denen der Lambda-Ausdruck nicht den Delegaten spricht.
2
Listing 5.46 zeigt einige Beispiele für Lambda-Ausdrücke, die mit Ausdrücken arbeiten. Um das Ganze kompilieren zu können, weist das Beispiel die erzeugten Lambda-Ausdrücke Variablen zu, deren Typ eine Variante des Func-Delegaten ist, der dem jeweiligen Lambda-Ausdruck entspricht.
3
Listing 5.46: Beispiele für Lambda-Ausdrücke, die mit Ausdrücken arbeiten
4
// Lambda-Ausdruck mit Ausdruck ohne Parameter int number = 10; Func f1 = () => number * 2;
5
// Lambda-Ausdruck mit Ausdruck mit einem implizit typisierten Parameter Func f2 = x => x + 1; // Lambda-Ausdruck mit Ausdruck und zwei explizit typisierten Parametern Func f3 = (int x, int y) => x + y;
6
Die folgenden Beispiele verwenden eine Anweisungsliste auf der rechten Seite des Lambda-Ausdrucks:
7
Listing 5.47: Lambda-Ausdrücke mit Anweisungsblock // Lambda-Ausdruck mit Anweisungsblock und nur einer Anweisung Func f4 = x => { return x + 1; };
8
// Lambda-Ausdruck mit Anweisungsblock und mehreren Anweisungen Func f5 = (x) => { if (x < 10) { return 1; } else if (x < 100) { return 2; } else { return 3; } };
9
// Lambda-Ausdruck mit Anweisungsblock ohne Rückgabe Action f6 = info => { Console.WriteLine(info); };
10
Die EnumFiles-Methode aus dem Delegat-Beispiel kann nun noch einfacher aufgerufen werden:
11
Listing 5.48: Aufruf einer Methode mit einem Lambda-Ausdruck an einem Delegat-Argument FileUtils.EnumFiles(new DirectoryInfo(startFolder), fileInfo => Console.WriteLine(fileInfo.FullName));
325
Weiterführende OOP
Das Beispiel setzt übrigens auf der rechten Seite des Lambda-Ausdrucks einen Ausdruck ein (und keine Anweisungsliste). Dieser Ausdruck ist ein spezieller, denn er ergibt keinen Wert. Aber es ist ein Ausdruck. Der rechte Teil muss deswegen nicht in geschweifte Klammern eingeschlossen werden. Wenn Sie wollen, können Sie den Lambda-Ausdruck auch zuvor einer Referenz vom Typ des Delegaten zuweisen: ProcessFileHandler processFile = fileInfo => Console.WriteLine(fileInfo.FullName); FileUtils.EnumFiles(new DirectoryInfo(startFolder), processFile);
5.9.5
Kovarianz und Kontravarianz
Bei der Arbeit mit Delegaten muss die Signatur einer übergebenen Methode zum Delegat passen. In .NET 1.x mussten der Rückgabetyp und die Signatur noch genau eingehalten werden. Kovarianz und Kontravarianz erlauben ab .NET 2.0 aber auch den flexibleren Umgang mit Delegaten (auch wenn Kovarianz und Kontravarianz in der Praxis relativ selten eingesetzt werden). Kovarianz erlaubt abgeleitete Klassen als Rückgabetyp
Kovarianz bedeutet, dass einem Delegaten mit Rückgabetyp auch Methoden zugewiesen werden können, deren Rückgabetyp eine von dem eigentlichen Rückgabetyp abgeleitete Klasse ist. Kovarianz gilt (wie Kontravarianz auch) also nur für Referenztypen. Definiert ein Delegat, dass eine Instanz der Klasse Employee zurückgegeben wird, kann eine Methode auch eine Instanz der davon abgeleiteten Klasse Manager zurückgeben um zum Delegat kompatibel zu sein. Der Programmteil, der mit dem Delegat arbeitet, kann dann problemlos die Elemente der Basisklasse verwenden. Bei korrekt überschriebenen Methoden wird aber natürlich die in der abgeleiteten Klasse verwendete Implementierung aufgerufen (was ja normaler Polymorphismus ist).
Kontravarianz erlaubt Basisklassen als Argument
Kontravarianz bedeutet, dass der Typ eines Referenztyp-Arguments einer Methode auch eine Basisklasse sein kann. Erwartet ein Delegat ein Manager-Objekt an einem Argument, kann auch eine Methode übergeben werden, die an diesem Argument eine Employee-Referenz erwartet. Die Methode kann mit der Employee-Referenz arbeiten, auch wenn über den Delegate später ein Manager-Objekt übergeben wird. Die abgeleitete Klasse besitzt ja schließlich alle Elemente der Basisklasse. Eigentlich handelt es sich bei Kovarianz und Kontravarianz um ganz normalen Polymorphismus, nur dass Microsoft dafür zwei kompliziert klingende Begriffe erfunden hat. In der Praxis wird Polymorphismus in Zusammenhang mit Delegaten recht selten und in komplexen Situationen eingesetzt. Das folgende Kovarianz-Beispiel ist daher sehr abstrakt und nicht praxisorientiert. Eine Methode erwartet einen Delegat vom Typ Func, also einer Methode, die ein Demo-Objekt zurückgibt: Listing 5.49: Kovarianz–Demo public static void CovarianceDemo(Func getDemo) { // Die Methode aufrufen, die ein Demo-Objekt liefert Demo demo = getDemo(); // Das Demo-Objekt verwenden demo.Message(); }
326
Delegaten, anonyme Methoden und Lambda-Ausdrücke
Neben der Klasse Demo existiert noch die davon abgeleitete Klasse ExtendedDemo: public class Demo { public virtual void Message() { Console.WriteLine("Demo"); } }
1
public class ExtendedDemo : Demo { public override void Message() { Console.WriteLine("Erweiterte Demo"); } }
2
Ein Programm kann nun CovarianceDemo aufrufen und z. B. einen Lambda-Ausdruck übergeben, der ein ExtendedDemo-Objekt zurückgibt:
3
CovarianceDemo(() => { return new ExtendedDemo(); });
Ehrlich gesagt fällt es mir schon sehr schwer, für Kovarianz eine Anwendung in der Praxis zu finden. Deswegen ist das Beispiel auch so abstrakt …
4
Bei Kontravarianz sieht es ähnlich aus. Eine abstrakte Beispielmethode erwartet einen Delegat vom Typ Action mit einem ExtendedDemo-Objekt am einzigen Argument:
5
public static void ContravarianceDemo(Action demo) { // Die übergebene Methode mit einer ExtendedDemo-Instanz // aufrufen ExtendedDemo exendedDemo = new ExtendedDemo(); demo(exendedDemo); }
6
7
Eine Methode erfüllt die Anforderungen des Delegaten, allerdings mit einem DemoArgument: public static void PrintMessage(Demo demo) { demo.Message(); }
8
Diese Methode kann als Delegat nun an ContravarianceDemo übergeben werden:
9
ContravarianceDemo(PrintMessage);
5.9.6
Multicast-Delegaten
Wenn ein Delegat einen void-Rückgabetyp besitzt, kann dieser mehrere Methodenreferenzen verwalten und damit auch mehrere Methoden aufrufen. Sinn macht das (in der Praxis allerdings eher selten), wenn die Delegat-Referenz ein Feld oder eine Eigenschaft einer Klasse bzw. Struktur ist.
10 Multicast-Delegaten erlauben mehrere Methoden
327
11
Weiterführende OOP
In der folgenden Worker-Klasse ist das Feld Log z. B. zur Ausgabe von Informationen während der Ausführung der ProcessJob-Methode vorgesehen: Listing 5.50: Klasse, die die Ausführung eines Jobs simuliert und die über ein Delegat-Feld Informationen ausgibt public class Worker { /* Delegate-Referenz zur Ausgabe von Informationen */ public Action Log; /* Führt den Job aus */ public void ProcessJob() { // Info ausgeben if (this.Log != null) { this.Log("Starte die Job-Bearbeitung"); } // Den Job ausführen for (int i = 0; i < 1000000; i++) { ; // Nur simuliert } // Info ausgeben if (this.Log != null) { this.Log("Fertig"); } } }
Bei der Verwendung der Klasse können Sie über den Operator += nun beliebig viele Methoden an das Log-Feld übergeben: Listing 5.51: Zuweisen von mehreren Methoden an einen Multicast-Delegaten private static void LogToConsole(string info) { Console.WriteLine("Konsolen-Logger: " + info); } private static void LogToFile(string info) { Console.WriteLine("Datei-Logger:" + info); } static void Main(string[] args) { // Worker-Instanz erzeugen Worker worker = new Worker(); // Die Methoden zur Protokollierung zuweisen worker.Log += LogToConsole; worker.Log += LogToFile; // Den Job ausführen worker.ProcessJob(); }
Die ProcessJob-Methode ruft nun automatisch beide Methoden auf.
328
Benachrichtigung des Aufrufers über Ereignisse und partielle Methoden
Für den Fall, dass das aufrufende Programm eine Methode wieder entfernen soll, verwenden Sie den Operator -= mit derselben Syntax: worker.Log -= LogToFile;
Multicast-Delegaten werden im Wesentlichen als Ereignisse für Klassen eingesetzt, die beim Auftreten von Ereignissen das verwendende Programm benachrichtigen sollen. Sie haben solche Ereignisse bereits verwendet, wenn Sie dem Buch gefolgt sind und in Kapitel 2 in einer Windows.Forms-Anwendung das Click-Ereignis eines Schalters mit einer Methode verknüpft haben. Multicast-Delegaten ermöglichen die Verknüpfung eines Ereignisses mit beliebig vielen Methoden. In der Praxis wird diese Technik eher selten eingesetzt, ist aber eben möglich. Bei der Verwendung eines reinen Delegaten müssen Sie bei der Zuweisung von Methoden aufpassen: Wenn Sie nur einmal eine Methode mit = statt += zuweisen, gehen alle vorherigen Zuweisungen verloren. Dieses Problem lösen Ereignisse, die eigentlich nur spezielle Delegaten sind, dadurch, dass nur die Operatoren += und -= zulassen. Mit Multicast-Delegaten können Sie noch wesentlich mehr machen. Eine Delegat-Referenz besitzt z. B. die Methode GetInvocationList, die ein Array von Delegate-Referenzen zurückgibt. Dieses Array können Sie durchgehen um die zugewiesenen Methoden einzeln aufzurufen. Dabei können Sie die Methoden auch asynchron aufrufen, was u. U. sinnvoll ist, wenn diese sehr viel Zeit in Anspruch nehmen und das eigentliche Programm dadurch nicht behindert werden soll. In einigen speziellen Fällen ist es auch sinnvoll, das Array der zugewiesenen Delegaten durchzugehen und zu überprüfen, ob die Methode überhaupt noch vorhanden ist. Es kann nämlich sein, dass die zugewiesenen Methoden aus Instanzen einer Klasse stammen, die zwischenzeitlich freigegeben wurde. Für diese erweiterten Techniken ist in diesem Buch leider kein Platz. Der asynchrone Methodenaufruf wird in Kapitel 20 behandelt.
5.9.7
1
2
HALT
3
4 INFO
5 6
7
Delegaten im Vergleich zu Schnittstellen
Ein Problem, das Sie mit Delegaten (oder Ereignissen) lösen können, können Sie häufig auch über eine Schnittstelle lösen. Der Unterschied für den Benutzer Ihres Typs ist allerdings, dass er im Schnittstellenfall einen Typ entwickeln muss, der die geforderte Schnittstelle implementiert. Der Vorteil von Delegaten ist in vielen Fällen, dass diese – besonders mit Lambda-Ausdrücken – wesentlich einfacher zu implementieren sind und dass die erzeugten Methoden im Gültigkeitsbereich des anwendenden Typs liegen.
Delegaten definieren Ereignisse und funktionale Programmierung, Schnittstellen definieren Fähigkeiten
Verwenden Sie Delegaten immer dann, wenn Ihre Typen Ereignisse melden sollen oder Sie funktionale Programmierung ermöglichen wollen. Verwenden Sie Schnittstellen immer dann, wenn Ihre Typen eine bestimmte Fähigkeit besitzen sollen.
5.10
8
9
10
Benachrichtigung des Aufrufers über Ereignisse und partielle Methoden
11
In vielen Programmen müssen Objekte die Programme, die sie verwenden, über (neue) Zustände benachrichtigen. Die Steuerelemente von Windows.Forms besitzen z. B. eine Vielzahl an Ereignissen, die größtenteils dann aufgerufen werden, wenn der Anwender mit dem Steuerelement arbeitet. Das TextChanged-Ereignis einer TextBox
329
Weiterführende OOP
wird z. B. dann aufgerufen, wenn der Text der TextBox geändert wird. Ein Programm, das solche Objekte verwendet, kann diese Ereignisse in Methoden abfangen und auswerten. Zur Benachrichtigung des Aufrufers stehen Ihnen Delegaten, Ereignisse und die in .NET 3.0 neuen partiellen Methoden zur Verfügung. Delegaten haben im Vergleich zu Ereignissen allerdings einen wesentlichen Nachteil, den ich im folgenden Abschnitt erkläre.
5.10.1 Ereignisse sind spezielle Multicast-Delegaten
Ereignisse
Ereignisse verhalten sich ähnlich wie Multicast-Delegaten. Ein kleiner Unterschied ist allerdings vorhanden: Bei Delegaten kann ein Programm, das Zugriff auf ein Objekt hat, die Delegat-Methode nachträglich entfernen oder eine neue DelegatMethode setzen: worker.Log = null; ... worker.Log = OtherLogHandler;
In größeren Projekten kann das zum Problem werden. Die zuvor gesetzten DelegatMethoden werden dann schließlich nicht mehr aufgerufen. Ereignisse fügen dem Ganzen eine gewisse Sicherheit hinzu, indem diese nur die Operatoren += und -= erlauben. So kann eine Anwendung Ereignisbehandlungsmethoden nur hinzufügen oder explizit einzeln entfernen. Ereignisse besitzen per Konvention die Argumente object sender und EventArgs e
Für Ereignisse ist unter .NET zudem eine Konvention definiert: Ereignisse sollten immer zwei Argumente besitzen. Im ersten Argument übergibt ein Ereignis eine object-Referenz auf das Objekt, das das Ereignis generiert, im zweiten ein Objekt der Klasse EventArgs oder einer davon abgeleiteten Klasse. Außerdem sollten Sie sich an die Namensrichtlinien für Ereignisse halten (siehe Kapitel 3). Das EventArgs-Argument wird bei Ereignissen verwendet, die Informationen an den Aufrufer übergeben. Soll dies der Fall sein, leiten Sie eine eigene Klasse von EventArgs ab. EventArgs ist eine leere Klasse, die lediglich dem Polymorphismus dient. In Ihrer Ereignisargument-Klasse implementieren Sie Felder oder Eigenschaften, die Sie später mit den Informationen füllen. Diese Klasse verwenden Sie später für das zweite Argument des Delegaten. Ereignisse werden über ein Feld oder eine Eigenschaft verwaltet, so wie ich dies beim Multicast-Delegat-Beispiel auch schon programmiert habe. Ereignisse unterscheiden sich von Delegaten neben der Konvention nur durch die Deklaration mit dem event-Schlüsselwort.
STEPS
Daneben verwendet Microsoft noch eine weitere Konvention. Die folgende Schritt-fürSchritt-Anleitung fasst das Implementieren von Ereignissen nach diesen Konventionen zusammen: 1.
2.
330
Soll Ihr Ereignis Argumente besitzen, implementieren Sie eine von EventArgs abgeleitete Klasse mit schreibgeschützten Feldern oder Eigenschaften für jedes Argument. Der Name dieser Klasse sollte mit dem Ereignisnamen beginnen und mit »EventArgs« enden. Implementieren Sie einen internen Konstruktor, dem Argumente übergeben werden. In der Klasse bzw. Struktur deklarieren Sie ein öffentliches Feld vom Typ EventHandler oder EventHandler. EventHandler verwenden Sie, wenn Ihr Ereignis
Benachrichtigung des Aufrufers über Ereignisse und partielle Methoden
3.
4.
mit einem EventArgs-Argument arbeitet, also keine eigenen Argumente besitzt. Wenn Sie eine eigene Ereignisargument-Klasse implementiert haben, verwenden Sie EventHandler, wobei T diese Klasse ist. EventHandler und EventHandler sind Delegaten, die der Ereignis-Konvention entsprechen. In einer Klasse implementieren Sie eine geschützte virtuelle Methode, deren Name mit »On« beginnt und mit dem Namen des Ereignisses endet. Diese Methode bekommt eine Instanz Ihrer Ereignisargument-Klasse übergeben, falls Ihr Ereignis mit Argumenten arbeitet. Die Methode macht nichts anderes als zu überprüfen, ob das Ereignis-Feld zugewiesen ist, und in diesem Fall das Ereignis aufzurufen. Schließlich rufen Sie an den Stellen, an denen das Ereignis aufgerufen werden soll, die On-Methode auf. In einer Struktur, die die On-Methode nicht unbedingt enthalten muss, können Sie das Ereignis auch direkt aufrufen.
1
2
Die On-Methode hat natürlich einen Sinn. Wenn von Ihrer Klasse andere Klassen abgeleitet werden, kann die abgeleitete Klasse die On-Methode einfach überschreiben, um das entsprechende Ereignis auszuwerten. Die abgeleitete Klasse kann dann auch entscheiden, ob die geerbte Methode aufgerufen wird, was schließlich dazu führt, dass das Ereignis an den Aufrufer weitergegeben wird (oder eben nicht).
3
4
Listing 5.52 setzt das Genannte am Worker-Beispiel aus dem Multicast-DelegatAbschnitt (Seite 327) um:
5
Listing 5.52: Klasse mit komplett nach Microsoft-Konvention implementiertem Ereignis /* Ereignisargumente für das Log-Ereignis der Worker-Klasse */ public class LoggerEventArgs : EventArgs { /* Die Info */ public readonly string Info;
6
/* Konstruktor */ internal LoggerEventArgs(string info) { this.Info = info; }
7
}
8
/* Klasse, die die Ausführung eines Jobs simuliert */ public class Worker { /* Ereignis zur Ausgabe von Informationen */ public event EventHandler Log;
9
/* Ruft das Log-Ereignis auf */ protected virtual void OnLog(LoggerEventArgs e) { if (this.Log != null) { this.Log(this, e); } }
10
11
/* Führt den Job aus */ public void ProcessJob() { // Info ausgeben this.OnLog(new LoggerEventArgs("Starte die Job-Bearbeitung"));
331
Weiterführende OOP
// Den Job ausführen for (int i = 0; i < 1000000; i++) { ; // Nur simuliert } // Info ausgeben this.OnLog(new LoggerEventArgs("Fertig")); } }
Die benutzende Anwendung kann nun eine Behandlungsmethode erzeugen (wie schon bei Delegaten) und diese übergeben. Bei der Implementierung der Ereignisbehandlungsmethode hilft Visual Studio. Wenn Sie im Code-Editor mit einer ObjektReferenz arbeiten und den Namen des Ereignisses schreiben, gefolgt von +=, erhalten Sie die Möglichkeit, eine passende Methode zu erzeugen, indem Sie zweimal die (ÿ)-Taste betätigen. Beim ersten Tab wird eine Delegat-Zuweisung erzeugt, beim zweiten Tab die Methode. Vor dem zweiten Tab können Sie den vorgeschlagenen Namen der Methode übrigens auch ändern. Abbildung 5.4: Der Code-Editor bei der Zuweisung eines Ereignisses vor der ersten Betätigung der Tab-Taste
Abbildung 5.5: Der Code-Editor bei der Zuweisung eines Ereignisses nach dem Ändern des Methodennamens und vor der zweiten Betätigung der TabTaste
Der erzeugte und erweiterte Quellcode sieht dann so aus: Listing 5.53: Konsolenanwendung mit Ereignismethode static void Main(string[] args) { // Worker-Instanz erzeugen Worker worker = new Worker(); // Log-Ereignis zuweisen worker.Log += new EventHandler(Worker_Log); // Die ProcessJob-Methode aufrufen worker.ProcessJob(); } /* Ereignismethode für das Log-Ereignis der Worker-Klasse */ static void Worker_Log(object sender, LoggerEventArgs e) { Console.WriteLine(e.Info); }
332
Benachrichtigung des Aufrufers über Ereignisse und partielle Methoden
Ereignis-Accessoren Wenn Sie ein Feld mit einem Delegaten und dem event-Schlüsselwort deklarieren, erzeugt der Compiler daraus automatisch eine Eigenschaft. Diese sieht für das Worker-Beispiel folgendermaßen aus: Listing 5.54: Ereignis-Deklaration mit Accessoren private EventHandler log; public event EventHandler Log { add { this.log = (EventHandler) Delegate.Combine(this.log, value); } remove { this.log = (EventHandler) Delegate.Remove(this.log, value); } }
1
Eine Ereignis-Eigenschaft arbeitet nicht wie eine normale Eigenschaft mit einem setund einem get-Accessor. Sie enthält stattdessen einen add- und einen remove-Accessor. Im add-Accessor wird dem privaten Delegat-Feld ein Delegat angefügt, im remove-Accessor wird eine Delegat-Instanz entfernt.
4
2
3
5
Sie können ein Ereignis natürlich genauso deklarieren. Dabei können Sie den addund remove-Accessor vereinfachen, indem Sie den Delegaten mit += hinzufügen und mit -= entfernen.
6
Listing 5.55: Vereinfachte Ereignis-Deklaration mit Accessoren private EventHandler log; public event EventHandler Log { add { this.log += value; } remove { this.log -= value; } }
Die Frage ist nun nur noch, warum Sie dieses benötigen. Ereignis-Accessoren werden relativ selten benötigt: ■
Ereignis-Accessoren setzen Sie primär dann ein, wenn Ihre Klasse oder Struktur ein Ereignis nicht selbst aufruft, sondern ein entsprechendes Ereignis von einem intern verwendeten, anderen Objekt direkt weitergibt. Dies ist z. B. dann der Fall, wenn Sie in Ihren Klassen die Fähigkeiten anderer Typen nutzen wollen, aber aus irgendwelchen Gründen nicht von diesen Typen ableiten wollen oder können (z. B. wenn der andere Typ eine Struktur ist oder Sie in Ihrer Klasse nicht alle öffentlichen Elemente des anderen Typs veröffentlichen wollen). In diesem Fall würden Sie den anderen Typen in einem privaten Feld verwalten und für alle Methoden, Eigenschaften, Ereignisse etc., die Sie in Ihrer Klasse veröffentlichen wollen, eigene Elemente implementieren, die die entsprechenden
7
8
9 Ereignis-Accessoren können Ereignisse weitergeben
10
11
333
Weiterführende OOP
■
■
Elemente des inneren Objekts aufrufen. Für Ereignisse implementieren Sie dann eben Ereignis-Accessoren. Ein anderer Grund kann der folgende sein: Für Ereignisse, die ohne Accessoren implementiert sind, erzeugt der Compiler pro Ereignis ein Datenfeld. Dieses benötigt auch dann Speicher, wenn das Ereignis nicht zugewiesen ist. Bei Typen, die sehr viele Ereignisse besitzen, von denen in der Praxis aber nur wenige zugewiesen werden, bedeutet das, dass (recht wenig, aber immerhin …) Speicher verschwendet wird. Dieses »Problem« (was sind schon ein paar Byte bei Systemen mit 2 GB Standard-RAM?) wird in einigen Klassen des .NET Framework dadurch gelöst, dass die Delegat-Referenzen nicht in Feldern, sondern in einer assoziativen Auflistung verwaltet werden, die nur für zugewiesene Ereignisse Elemente enthält. Ereignis-Accessoren sind außerdem notwendig, wenn Sie ein in einer Schnittstelle deklariertes Ereignis explizit implementieren.
Den ersten Punkt, der in der Praxis wohl am wahrscheinlichsten auftritt, erläutere ich an einem kurzen Beispiel. Die Klasse HardWorker soll die Fähigkeiten von Worker nutzen, aber nicht von Worker abgeleitet werden, damit nicht alle öffentlichen Elemente zur Verfügung stehen. OK: Im Beispiel besitzt Worker außer ProcessJob keine weiteren Elemente. Stellen Sie sich einfach vor, das wäre so … HardWorker soll die ProcessJobs-Methode von Worker aufrufen und das Log-Ereignis zur Verfügung stellen. Das Problem löse ich über Ereignis-Accessoren: Listing 5.56: Klasse, die die Fähigkeiten einer anderen Klasse nutzt und ein Ereignis delegiert public class HardWorker { /* Die intern verwendete Worker-Instanz */ private Worker worker = new Worker(); /* Ereignis zur Ausgabe von Informationen */ public event EventHandler Log { add { this.worker.Log += value; } remove { this.worker.Log -= value; } } /* Führt den Job aus */ public void ProcessJob() { this.worker.ProcessJob(); } }
5.10.2 Partielle Methoden
NEU
334
Partielle Methoden sind ein neues Feature von C# 3.0. Eine partielle Methode ist eine Methode, die in einem Teil einer partiellen Klasse lediglich deklariert und in einem anderen Teil ggf. implementiert wird.
Benachrichtigung des Aufrufers über Ereignisse und partielle Methoden
Der Sinn partieller Methoden ist ein leichtgewichtiger Ereignis-Mechanismus in Verbindung mit partiellen Klassen oder Strukturen: Der Hauptteil einer partiellen Klasse oder Struktur enthält dazu partielle Methodendeklarationen (ohne Implementierung). Normale Methoden oder Eigenschaften der Klassen bzw. Struktur rufen diese partiellen Methoden auf, um ein Ereignis zu signalisieren. Zunächst passiert aber nichts, da die partielle Methode noch nicht implementiert ist und der Compiler den Aufruf einfach wegoptimiert.
Partielle Methoden ermöglichen einen leichtgewichtigen Ereignis-Mechanismus
1
Wird aber ein weiterer Teil des partiellen Typs implementiert und in diesem der zweite Teil von partiellen Methoden, werden diese aufgerufen. Der zweite Teil kann diese leichtgewichtigen Ereignisse also recht einfach abfangen, indem er die entsprechenden partiellen Methoden implementiert.
2
Diese Technik wird hauptsächlich dann eingesetzt, wenn (neuere) Tools oder Designer automatisch Klassen oder Strukturen erstellen, die Ereignisse enthalten. Es ist jedoch auch denkbar, Klassen und Strukturen, die in Klassenbibliotheken implementiert werden, über partielle Methoden mit einem leichtgewichtigen EreignisSchema auszustatten. Die einzelnen Klassen bzw. Strukturen werden als partielle Typen implementiert, die »Ereignisse« als partielle Methoden. Der LINQ-to-SQL-Designer erzeugt z. B. für die einzelnen Tabellen einer Datenbank eine separate partielle Klasse mit Eigenschaften für jedes Feld der Tabelle. Beim Schreiben jedes Feldes wird vor dem Schreiben des Werts eine partielle Methode aufgerufen, deren Name mit »On« beginnt, mit dem Namen der Eigenschaft weitergeht und mit »Changing« endet. Diese Methode informiert also darüber, dass eine Eigenschaft (und damit ein Datenbank-Feld) geändert werden soll. Zusätzlich wird übrigens auch noch ein Ereignis aufgerufen, das PropertyChanging heißt.
3
LINQ to SQL nutzt partielle Methoden
4
5 6
Wenn Sie mit solchen generierten Klassen arbeiten, können Sie nun einfach einen weiteren Teil einer Klasse erzeugen und in diesem den Teil der Methode, die Sie abfangen wollen, implementieren. In dieser Methode können Sie das »Ereignis« dann abfangen. Für den Compiler ist das allerdings nur der einfache Aufruf einer Methode und deswegen wesentlich einfacher und schneller als der Aufruf eines Ereignisses.
7
Die Deklaration einer partiellen Methode enthält das Schlüsselwort partial, aber (wie gesagt) keine Implementierung:
8
partial {Rückgabetyp | void} Name([Argumentliste]);
Bei der Implementierung einer partiellen Methode wird der Kopf genauso deklariert, aber natürlich der Rumpf angegeben:
9
partial {Rückgabetyp | void} Name([Argumentliste]) { Implementierung }
10
Zur Erstellung partieller Methoden existieren einige Regeln: ■ ■
■ ■
Partielle Methoden müssen Instanzmethoden sein. Eine partielle Methode kann nicht mit Sichtbarkeitsmodifizierern deklariert werden und ist immer ausschließlich privat. Deswegen kann eine private Methode auch nicht virtuell sein. Eine partielle Methode kann ref-Parameter besitzen, aber keine out-Parameter. Eine partielle Methode kann eine Instanzmethode oder statisch sein.
11
335
Weiterführende OOP
■
Eine partielle Methode muss nicht implementiert werden. Fehlt eine Implementierung, optimiert der Compiler Aufrufe der partiellen Methode einfach weg. Soll eine partielle Methode implementiert werden, kann dies nur genau einmal geschehen.
■
Als Beispiel soll hier die Klasse FileUtils mit ihrer Methode EnumFiles aus dem Delegaten-Beispiel (Listing 5.41 auf Seite 321) dienen. Diese Klasse wird so umgebaut, dass für jede gefundene Datei kein Delegat, sondern die partielle Methode OnFileFound aufgerufen wird: Listing 5.57: Klasse zum Durchgehen aller Dateien eines Ordners mit einer partiellen Methode, die für jede gefundene Datei aufgerufen wird public partial class FileUtils { /* Partielle Methode, die für jede gefundene Datei aufgerufen wird */ partial void OnFileFound(FileInfo fileInfo); /* Geht alle Dateien des übergebenen Ordners inkl. aller Unterordner durch */ public void EnumFiles(DirectoryInfo startDirectory) { // Alle Dateien des Startordners durchgehen try { foreach (var file in startDirectory.GetFiles()) { // Die partielle Methode aufrufen this.OnFileFound(file); } // Alle Unterordner durchgehen foreach (var subDirectory in startDirectory.GetDirectories()) { // Die Methode rekursiv aufrufen this.EnumFiles(subDirectory); } } catch (UnauthorizedAccessException) { // UnauthorizedAccessExceptions (Kein Zugriff) werden ignoriert } } }
Diese Klasse wird sinnvollerweise in einer Klassenbibliothek gespeichert (siehe Seite 337), damit sie über eine Referenz auf die Klassenbibliothek einfach verwendet werden kann. In einem Programm, das Zugriff auf die Klasse hat, kann nun ein weiterer Teil entwickelt werden, der die partielle Methode implementiert: Listing 5.58: Zweiter Teil einer partiellen Klasse mit Implementierung einer partiellen Methode partial class FileUtils { /* Partielle Methode, die für jede gefundene Datei aufgerufen wird */ partial void OnFileFound(FileInfo fileInfo) { Console.WriteLine(fileInfo.FullName); } }
336
Klassenbibliotheken
Das Programm kann nun eine Instanz der Klasse erzeugen und die EnumFilesMethode aufrufen: Listing 5.59: Anwendung der partiellen Klasse mit partieller Methode // Eine Instanz der FileUtils-Klasse erzeugen FileUtils fileUtils = new FileUtils();
1
// Die Methode zum Durchgehen der Dateien eines Ordners aufrufen fileUtils.EnumFiles(new DirectoryInfo("C:\\"));
Wie bereits gesagt, müsste die partielle Methode nicht implementiert werden. Ein Programm könnte trotzdem eine Instanz der Klasse erzeugen und verwenden. In unserem Fall würde das natürlich keinen Sinn machen.
2
5.10.3 Der Unterschied zwischen Delegaten, Ereignissen und partiellen Methoden
3
Vielleicht haben Sie ein wenig den Überblick verloren und wissen nicht, wann Sie einen Delegaten, ein Ereignis oder eine partielle Methode einsetzen sollen, um Informationen an den Aufrufer weiterzugeben. Deswegen kläre ich hier die Unterschiede: ■
■
■
4
Delegaten sind für funktionale Programmierung ideal. Wenn eine Klasse erst später eine individuell definierte Bearbeitungsmethode für bestimmte Daten oder Umstände erhalten soll, setzen Sie dazu einen Delegaten ein. Ereignisse sind für die Benachrichtigung des Aufrufers ideal. Ereignisse kann der Aufrufer in einer Methode abfangen, die an einer beliebigen Stelle im Programm deklariert wird. Er ist nicht gezwungen, einen weiteren Teil einer Klasse zu implementieren. Zudem können Ereignisbehandlungsmethoden auf alle Elemente des Typs zugreifen, in dem sie implementiert werden. Mit partiellen Methoden ist dies nicht möglich, da diese in einem weiteren Teil der partiellen Klasse implementiert werden müssen. Partielle Methoden machen zur Benachrichtigung immer dann Sinn, wenn ein partieller Typ mit großer Wahrscheinlichkeit (später) über weitere Teile erweitert wird, und die »Ereignisse«, die die partiellen Methoden melden, nur innerhalb des Typs verwendet werden. Eine partielle Methode, die beim Schreiben in eine Eigenschaft vor dem Speichern des übergebenen Werts aufgerufen wird, kann z. B. den geschriebenen Wert überprüfen und im Fall ungültiger Werte das Schreiben abbrechen.
5 6
7
8
9
Machen Sie sich aber keine Gedanken, es fällt selbst mir schwer, die richtige Strategie im Gewimmel von Schnittstellen, Delegaten, Ereignissen und partiellen Methoden zu finden ☺. Die Praxis und der Umgang mit den .NET-Typen, die die entsprechenden Technologien einsetzen, geben Ihnen mit der Zeit ein Gefühl dafür.
5.11
10
Klassenbibliotheken
Nun da Sie in der Lage sind, komplexe Typen zu erzeugen, ist es an der Zeit, die wieder verwendbaren davon in Klassenbibliotheken auszulagern. Klassenbibliotheken verwalten beliebig viele Typen in einer Assembly. Ein Projekt benötigt lediglich eine Referenz auf eine solche Assembly, um alle öffentlichen Typen der Bibliothek verwenden zu können.
11 Klassenbibliotheken verwalten Typen in einer Assembly
337
Weiterführende OOP
Klassenbibliotheken können Sie sehr einfach erzeugen. In Visual Studio verwenden Sie dazu ein Projekt vom Typ KLASSENBIBLIOTHEK. Vorhandene Projekte anderer Art können Sie über deren Eigenschaften auch nachträglich zu einer Klassenbibliothek umbauen (im Register ANWENDUNG finden Sie dazu die Einstellung AUSGABETYP). Dabei sollen Sie natürlich beachten, dass Klassenbibliotheken keine ausführbaren Programme sind. Innerhalb einer Klassenbibliothek können Sie beliebige Typen unterbringen. Alle Typen, die mit dem Sichtbarkeitsmodifizierer public versehen sind, sind öffentlich und können damit von außen referenziert werden. Sinnvollerweise packen Sie alle zusammengehörigen Typen zu einem Thema, die in mehreren Projekten verwendet werden (oder später verwendet werden könnten), grundsätzlich in eine Klassenbibliothek. Arbeiten Sie lieber mit zu vielen als mit zu wenigen Klassenbibliotheken. Eine thematisch gut organisierte Aufteilung von Anwendungen in eine relativ kleine Anwendungs-Assembly und einige Klassenbibliotheken, die einen Großteil der Programmierung enthalten, macht Anwendungen in vielen Fällen übersichtlicher. Außerdem erhöhen Sie mit Klassenbibliotheken die Wiederverwendbarkeit von Programmcode, verbessern die Wartbarkeit und sorgen dafür, dass mehrere Entwickler gleichzeitig an einem Projekt arbeiten können, ohne sich gegenseitig zu behindern.
5.11.1
Klassenbibliotheken entwickeln
Beim Entwickeln einer Klassenbibliothek werden Sie diese gleich auch testen wollen. Deswegen ermöglicht Visual Studio mehrere Projekte in einer Projektmappe. Ein Projekt ist das (Test-)Anwendung-Projekt, die anderen können Klassenbibliothek-Projekte sein. Damit können Sie Klassenbibliotheken gleichzeitig entwickeln, testen und debuggen. Die folgende Auflistung zeigt, wie Sie eine Klassenbibliothek inklusive Testanwendung erzeugen: STEPS
1.
2.
3.
Erstellen Sie in Visual Studio ein neues Projekt vom Typ einer Anwendung, mit der es Sinn macht, die geplante Klassenbibliothek zu testen (in unserem Fall eine Konsolenanwendung). Fügen Sie der Projektmappe ein Klassenbibliothek-Projekt hinzu. Der Name der Klassenbibliothek sollte dem geplanten Namensraum entsprechen. Ein Namensraum sollte immer mit dem Firmennamen (bzw. mit Ihrem eigenen Namen) beginnen und danach ähnlich dem .NET Framework sinnvoll strukturiert sein. Nennen Sie die Beispiel-Klassenbibliothek (die die FileUtils-Klasse enthalten wird) z. B. Kompendium.IO. Erstellen Sie für das Anwendung-Projekt eine neue Referenz auf das Klassenbibliothek-Projekt, indem Sie im Kontextmenü des VERWEISE-Eintrags den Befehl VERWEIS HINZUFÜGEN wählen, im erscheinenden VERWEIS HINZUFÜGEN-DIALOG das Register PROJEKTE aktivieren, in der Liste das Klassenbibliothek-Projekt auswählen und mit OK bestätigen.
In der Klassenbibliothek können Sie nun die vordefinierte Klasse Class1 umbenennen oder löschen und beliebige neue Typen implementieren. Im Testprojekt erzeugen Sie ggf. Instanzen der Typen und testen diese.
338
Klassenbibliotheken
Ein Beispiel dazu spare ich mir hier, weil die Klassen sich nicht von denen unterscheiden, die bisher implementiert wurden. Das Einzige, was Sie beachten müssen, ist, dass Sie die Klassenbibliothek referenzieren müssen und den Namensraum einbinden sollten (über using). In der Beispielprojektmappe »Klassenbibliotheken«, die Sie auf der Buch-DVD im Ordner zu diesem Kapitel finden, habe ich die FileUtils-Klasse aus dem Delegaten-Beispiel in einer Klassenbibliothek implementiert und beispielhaft in einem Testprojekt verwendet (Abbildung 5.6).
1
DISC
Abbildung 5.6: Die Beispielprojektmappe mit einem Konsolenanwendungs-Test- und einem Klassenbibliothek-Projekt
2
3
4
5 6
7
5.11.2
Klassenbibliotheken referenzieren 8
Fertige Klassenbibliotheken können Sie natürlich in anderen Projekten verwenden. Sie können die Assembly an beliebiger Stelle speichern. So können Sie Ihre eigenen Assemblys z. B. in einem eigenen Ordner gemeinsam ablegen, damit Sie diese bei Bedarf schnell wieder finden.
9
Eine Assembly, die eine Klassenbibliothek ist, müssen Sie referenzieren, damit Sie deren Typen verwenden können. In Visual Studio machen Sie dies – wie beim Testen – über das Kontextmenü des VERWEISE-Eintrags im Projektmappen-Explorer. Fügen Sie einen Verweis auf die Assembly hinzu, indem Sie die Datei über das Register DURCHSUCHEN suchen und hinzufügen.
10
Wenn Sie eine Anwendung, die eine Klassenbibliothek verwendet, in Visual Studio kompilieren, kopiert Visual Studio die Assembly in den Ordner bin/debug bzw. bin/release (je nach Konfiguration). Die Assembly befindet sich also im Ordner der Anwendung. Um die Anwendung auf einem anderen Rechner zu installieren, müssen Sie lediglich alle Assemblys aus diesem Ordner kopieren.
11
339
Inhalt
6
OOP-Specials 1
Die objektorientierte Programmierung ist in C# 3.0 bei den Standard-Themen, die in den vorherigen Kapiteln behandelt wurden, noch nicht am Ende. C# und das .NET Framework bieten noch einige spezielle Features, die in diesem Kapitel beschrieben werden.
2
3
Dazu gehören Erweiterungsmethoden, über die Sie vorhandene Typen (!) um Methoden erweitern können. Über generische Typen können Sie Typen entwickeln, die mit beliebigen oder eingeschränkten anderen Typen arbeiten. Damit können Sie in vielen Fällen auf Vererbung verzichten. Lambda-Ausdrücke, die bereits in Kapitel 5 grundlegend behandelt wurden, ermöglichen nicht nur die Definition von anonymen Methoden. Sie können auch als Ausdrucksbäume ausgewertet werden. Ausdrucksbäume selbst sind ein interessantes Thema, das ebenfalls in diesem Kapitel behandelt wird. Über diese können Sie einen Ausdruck in Form spezieller, verknüpfter Objekte auch im Speicher darstellen und damit das dynamische Erzeugen von Ausdrücken ermöglichen.
4
5
6
Schließlich behandelt dieses Kapitel noch das eher klassische Thema »Attribute«, mit denen Sie Typen und deren Elemente mit Metainformationen ausstatten können. Die Stichworte dieses Kapitels sind: ■ ■ ■ ■
7
Erweitern vorhandener Typen über Erweiterungsmethoden Entwickeln generischer Typen und Methoden Ausdrucksbäume und Lambda-Ausdrücke Metainformationen über Attribute
8
Neues in .NET 3.5: ■ ■
Ausdrucksbäume: Seite 357 Neues in C# 3.0: Erweiterungsmethoden: Seite 341 Lambda-Ausdrücke: Seite 356
6.1
9
10
Erweiterungsmethoden
Erweiterungsmethoden sind ein neues Feature des .NET Framework 3.5. Eine Erweiterungsmethode ist eine besondere Methode, die der Compiler in eine vorhandene Struktur, Klasse oder Schnittstelle (siehe unter »Erweiterungsmethoden für Schnittstellen«, Seite 344) einblendet, so als würde diese eine Instanzmethode sein. Mit einer Erweiterungsmethode kann die String-Klasse z. B. um eine Methode Left erweitert werden, die den linken Teil des String zurückgibt.
11 NEU
341
Index
■
OOP-Specials
Erweiterungsmethoden erweitern vorhandene Typen
Eine Erweiterungsmethode wird wie eine normale statische Methode in einer beliebigen statischen Klasse implementiert. Der Unterschied ist, dass das erste Argument dieser Methode mit dem Schlüsselwort this gekennzeichnet wird. Der Typ dieses Arguments ist dann auch der Typ, der erweitert wird: Listing 6.1: Klasse mit einer Erweiterungsmethode für Strings public static class StringExtensions { /* Gibt die linken Zeichen eines String zurück */ public static string Left(this string s, int count) { return s.Substring(0, count); } }
Steht die Klasse in einem Projekt zur Verfügung (weil z. B. die Assembly referenziert wird, die diese Klasse enthält) und wird der Namensraum der Erweiterungsmethoden-Klasse mit using eingebunden (oder ist der Namensraum der Erweiterungsklasse derselbe wie der des Typs, in dem programmiert wird), werden die Erweiterungsmethoden automatisch importiert. Die Left-Methode kann dann z. B. so aufgerufen werden: Listing 6.2: Verwendung einer Erweiterungsmethode string s1 = "Das ist ein Test-String"; string s2 = s1.Left(16);
Erweiterungsmethoden können aber auch wie normale statische Methoden aufgerufen werden: string s3 = StringUtils.Left(s1, 16);
Erweiterungsmethoden werden vom C#-Compiler speziell behandelt. Im CIL-Code ist aus der Erweiterungsmethode eine normale statische Methode geworden. Der Aufruf dieser Methode entspricht ebenfalls der normalen Verwendung einer statischen Methode. Erweiterungsmethoden sind also eigentlich kein neues Feature des .NET Framework, sondern eines von C# 3.0. Erweiterungsmethoden arbeiten intern allerdings mit einem speziellen Attribut, das in der Assembly System.Core definiert ist. Und die ist Teil von .NET 3.5. Erweiterungsmethoden machen an vielen Stellen Sinn. Immer dann, wenn Sie eine statische Hilfsmethode schreiben, die die Instanz eines Typs bearbeitet und die ein Ergebnis liefert, sollten Sie daraus eine Erweiterungsmethode machen. Erweiterungsmethoden sind wesentlich einfacher und intuitiver anzuwenden als statische Methoden. Erweiterungsmethoden helfen auch für den Fall, dass Sie Typen erweitern wollen, die aus einer externen Assembly stammen und entweder Strukturen oder versiegelte Klassen sind (von denen Sie keine neuen Typen ableiten können). So können Sie die DateTimeOffset-Struktur z. B. sehr einfach um fehlende Methoden erweitern: Listing 6.3: Klasse mit Methoden zur Erweiterung der DateTimeOffset-Struktur public static class DateTimeOffesetExtensions { /* Gibt den Datumsteil eines Datums in kurzer Form zurück */ public static string ToShortDateString(this DateTimeOffset date) {
342
Erweiterungsmethoden
return date.ToString("d"); } /* Gibt den Datumsteil eines Datums in langer Form zurück */ public static string ToLongDateString(this DateTimeOffset date) { return date.ToString("D"); }
1
/* Gibt den Zeitteil eines Datums in kurzer Form zurück */ public static string ToShortTimeString(this DateTimeOffset date) { return date.ToString("t"); }
2
/* Gibt den Zeitteil eines Datums in langer Form zurück */ public static string ToLongTimeString(this DateTimeOffset date) { return date.ToString("T"); }
3
}
Das .NET Framework setzt Erweiterungsmethoden noch nicht für ältere Typen ein. Erweiterungsmethoden werden allerdings sehr intensiv in LINQ verwendet (LINQ besteht eigentlich im Wesentlichen aus Erweiterungsmethoden). Dabei wird die Tatsache genutzt, dass Erweiterungsmethoden auch für Schnittstellen definiert werden können. Auf Erweiterungsmethoden für Schnittstellen gehe ich ab Seite 344 ein.
Erweiterungsmethoden werden intensiv in LINQ eingesetzt
5
Erweiterungsmethoden sind einfach, weil es sich um statische Methoden handelt. Ein paar Regeln gelten allerdings: ■
■
■
■
4
Erweiterungsmethoden stehen dann zur Verfügung, wenn der Namensraum der Klasse, die diese enthält, in dem aktuellen Kontext verfügbar ist. Ggf. muss dieser mit using importiert werden. Besitzt der erweiterte Typ bereits eine Instanzmethode mit demselben Namen und derselben Signatur wie eine Erweiterungsmethode, wird beim Aufruf immer die Instanzmethode verwendet. Soll stattdessen die Erweiterungsmethode aufgerufen werden, muss diese als normale statische Methode aufgerufen werden. Stehen in den importierten Namensräumen für einen Typ zwei Erweiterungsmethoden mit demselben Namen und derselben Signatur zur Verfügung, kann die Erweiterungsmethode normalerweise nur als normale statische Methode aufgerufen werden (wobei ja die unterschiedlichen Klassen die Methoden voneinander trennen). Sind beide Varianten in der Signatur unterschiedlich, aber kompatibel, ruft der Compiler die Methode auf, die besser zu den übergebenen Argumenten passt. Das gilt übrigens auch für den Typ selber, denn eine Methode, die z. B. für den Object-Typ geschrieben wurde, kann auch auf einem String aufgerufen werden. Existiert aber eine gleichwertige Erweiterungsmethode für die String-Klasse, wird diese verwendet, wenn sie auf einem String aufgerufen wird. Obwohl es nach außen den Anschein hat, eine Erweiterungsmethode würde zu dem erweiterten Typ gehören, kann diese nicht auf die privaten, geschützten und internen Elemente des Typs zugreifen. Es handelt sich schließlich eigentlich um eine ganz normale statische Methode.
6 7
8
9
10
11
Der zweite Punkt muss wahrscheinlich etwas erläutert werden:
343
OOP-Specials
6.1.1
Erweiterungsmethoden und Polymorphismus
Wird eine Erweiterungsmethode für eine Klasse geschrieben, von der Klassen abgeleitet werden, steht die Methode auch für alle abgeleiteten Klassen zur Verfügung. Das folgende Beispiel zeigt dies an einer einfachen Methode für den Object-Typ: Listing 6.4: Erweiterungsmethode für den Object-Typ public static void Print(this object o) { Console.WriteLine(o.ToString()); }
Diese Methode steht nun allen anderen Typen zur Verfügung (da ja alle Typen von Object abgeleitet sind): Listing 6.5: Verwendung der polymorphen Erweiterungsmethode ("Die Antwort ist").Print(); 42.Print();
Steht allerdings eine Erweiterungsmethode zur Verfügung, die für den verwendeten Typ besser geeignet ist, wird diese aufgerufen: Listing 6.6: Spezialisiertere Erweiterungsmethode public static void Print(this string s) { Console.WriteLine(s); }
("Die Antwort ist").Print() ruft jetzt die String-Variante auf, 42.Print() aber immer noch die Object-Variante.
6.1.2
Erweiterungsmethoden für Schnittstellen
Erweiterungsmethoden können nicht nur für Klassen und Strukturen, sondern auch für Schnittstellen definiert werden. Eine Schnittstelle ist ja ein Vertrag, der garantiert, dass die definierten Elemente in den Objekten, die die Schnittstelle implementieren, vorhanden sind. Eine Erweiterungsmethode kann also mit allen Elementen arbeiten, die die Schnittstelle enthält. Der C#-Compiler kümmert sich (beim Kompilieren) genau wie Visual Studio (IntelliSense) dann automatisch darum, dass Objekte, die die Schnittstelle implementieren, mit den Erweiterungsmethoden erweitert werden. Die folgende Erweiterungsmethode ist z. B. für die Schnittstelle IEnumerable definiert. IEnumerable wird von Arrays und von den meisten Auflistungen des .NET Framework implementiert und ermöglicht das Durchgehen der verwalteten Objekte. In C# ermöglicht IEnumerable die Verwendung der foreach-Schleife für Arrays und Auflistungen. Die Erweiterungsmethode geht die Liste durch und gibt die einzelnen Objekte an der Konsole aus: Listing 6.7: Erweiterungsmethode für eine Schnittstelle public static void Print(this IEnumerable list) { foreach (object o in list) {
344
Generische Typen und Methoden
Console.WriteLine(o); } }
Diese Erweiterungsmethode kann nun auf allen Objekten aufgerufen werden, die IEnumerable implementieren, also auf allen Arrays und den meisten Auflistungen: Listing 6.8: Verwendung der Erweiterungsmethode für IEnumerable mit einem Array und einer Auflistung
1
int[] numbers = { 1, 3, 5, 7, 9 }; numbers.Print(); List words = new List { "C#", "ist", "cool" }; words.Print();
2
Besonders interessant sind Erweiterungsmethoden für Schnittstellen als generische Methoden. Darauf gehe ich im Abschnitt »Generische Methoden« ab Seite 348 ein.
3
6.2
Generische Typen und Methoden
Generische Typen sind neben der Vererbung eine weitere Möglichkeit, wieder verwendbaren Code zu erzeugen, und deshalb sehr interessant für die Programmierung.
4
Generische Typen sind so etwas wie eine Vorlage für einen Typen. Bei generischen Typen sind einer oder mehrere der Typen von Feldern, Eigenschaften, Methodenparametern, Methodenrückgabewerten oder anderen Elementen nicht festgelegt. Erst bei der Verwendung des generischen Typs werden diese intern verwendeten Typen definiert.
5
6
Generische Typen wurden im Buch bereits an verschiedenen Stellen eingesetzt, meist in Form der generischen List-Auflistung. T steht hier für den einzigen Typparameter. Bei der Verwendung muss dieser in spitzen Klammern angegeben werden:
7
List intList = new List();
Generische Typen können Klassen, Strukturen, Schnittstellen, Delegaten und Ereignisse sein. Daneben können Sie auch generische Methoden implementieren. Die Typen, die an den Typparametern eingesetzt werden können, können Sie dabei nahezu beliebig einschränken. So können Sie z. B. festlegen, dass der Typ ein Referenztyp sein muss, von einer bestimmten Klasse abgeleitet sein oder eine bestimmte Schnittstelle implementieren muss. Damit haben Sie die Möglichkeit, Code zu entwickeln, der mit verschiedenen Typen eingesetzt werden kann, ohne diesen Code für alle diese Typen separat entwickeln zu müssen. Und das Ergebnis ist absolut typsicher und vermeidet das mit Object-Elementen ansonsten notwendige Boxing und Unboxing. Das ist richtig cool ☺.
6.2.1
Klassen, Strukturen, Schnittstellen, Delegaten und Ereignisse können generisch sein
8
9
10
Generische Typen
Generische Typen erzeugen Sie, indem Sie dem Typnamen einen oder mehrere Typparameter in spitzen Klammern anhängen: public class Stack {...} public class Dictionary {...}
Generische Typen verwalten Daten, deren Typ erst später festgelegt wird
Innerhalb des Typs können Sie diese Typparameter wie normale Typen einsetzen, z. B. für die Deklaration von Feldern oder Eigenschaften oder als Methodenparame-
345
11
OOP-Specials
ter. Der Name der Typparameter kann frei vergeben werden. Microsoft verwendet aber die Konvention, dass der Name von Typparametern immer mit einem T beginnt. Ein einzelner Typparameter wird auch nur »T« genannt. Die folgende Beispielklasse Stack soll einen Stack implementieren. Ein Stack ist eine Liste, der über eine Methode (Push) Objekte angehängt werden können. Über eine andere Methode (Pop) kann das jeweils letzte Objekt ausgelesen werden.
INFO
Eine der hier implementierten Klasse ähnliche Klasse Stack existiert bereits im Namensraum System.Collections.Generic und wird in Kapitel 7 behandelt. Stack ist aber ein einfaches Beispiel für einen generischen Typ (weswegen diese Klasse auch in allen möglichen Büchern und Artikeln immer wieder auftaucht ☺). Listing 6.9: Generische Klasse zur Implementierung eines einfachen Stack public class Stack { /* Verwaltet die Einträge */ protected T[] items; /* Die aktuelle Position */ protected int position = -1; /* Konstruktor */ public Stack(int capacity) { this.items = new T[capacity]; } /* Fügt dem Stack ein Element an */ public void Push(T item) { this.position++; this.items[this.position] = item; } /* Liest das letzte Element aus dem Stack aus */ public T Pop() { T item = this.items[this.position]; this.position--; return item; } }
INFO
An dem Beispiel sehen Sie sehr schön, dass der Typparameter T an vielen Stellen in der Klasse eingesetzt wird. Ich denke, dass es wichtig ist, sich klarzumachen, dass ein Typparameter grundsätzlich wie ein normaler Typ eingesetzt werden kann. Er kann als Feldttyp, Eigenschaftstyp, Typ eines Methodenrückgabewerts, Typ eines Methodenparameters und als Variablen-Typ eingesetzt werden. Er kann sogar wieder als Typparameter für intern verwendete generische Typen oder generische Typen, von denen abgeleitet wird, verwendet werden. Da ein über einen Typparameter definierter Typ generisch ist, sind Operationen, die spezifische Typen voraussetzen (wie z. B. das Addieren zweier generischer Typen) zunächst nicht möglich. Über Einschränkungen der möglichen generischen Typen (siehe Seite 349) können Sie aber bestimmte Fähigkeiten wie z. B. das Vorhandensein eines Standardkonstruktors erzwingen und entsprechende Operationen ermöglichen.
346
Generische Typen und Methoden
Bei der Anwendung eines generischen Typs müssen die Typparameter in spitzen Klammern angegeben werden: Listing 6.10: Anwendung der generischen Stack-Klasse // Den generischen Stack verwenden Stack intStack = new Stack(2); intStack.Push(42); intStack.Push(7); Console.WriteLine(intStack.Pop()); Console.WriteLine("und"); Console.WriteLine(intStack.Pop());
1
2
Stack stringStack = new Stack(3); stringStack.Push("Zahlen"); stringStack.Push("coole"); stringStack.Push("sind"); Console.WriteLine(stringStack.Pop()); Console.WriteLine(stringStack.Pop()); Console.WriteLine(stringStack.Pop());
3
Für den Compiler ist der Typ Stack ein Stack, der int-Werte verwaltet. Das zeigt auch Visual Studio beim Aufruf von Methoden oder bei der Verwendung von Feldern oder Eigenschaften, die mit den Typparametern arbeiten (Abbildung 6.1).
4 Abbildung 6.1: IntelliSense mit einem generischen Typ
5
6 6.2.2
Generische Typen im Vergleich zu normalen Typen
7
Das Stack-Problem könnte auch mit einer normalen (nicht generischen) Klasse gelöst werden. Um sicherzustellen, dass diese alle Typen verwalten kann, müsste aber der Typ Object zur Speicherung der Elemente verwendet werden. Ein Beispiel erspare ich mir hier.
8
Ein Problem mit diesem Typen wäre aber, dass Object in der Laufzeit alle Typen zulässt. So könnte der Push-Methode ein int-Wert, ein String und jedes andere Objekt übergeben werden. Die Object-Rückgabe von Pop müsste bei jedem Aufruf auf den zurückgegebenen Typen überprüft werden. Die schöne Typsicherheit von C# wäre dahin.
9
Ein anderes Problem wäre, dass Werttypen geboxt werden, damit diese über ObjectReferenzen verwaltet werden können. Boxing und Unboxing führen aber zu einem Speicher-Overhead und zu einer geringeren Performance beim Lesen und Schreiben.
10
Die Typsicherheit und das Vermeiden von Boxing sind die Gründe für generische Typen. Diese erlauben eine hohe Flexibilität in der Kompilierzeit, aber eine absolute Typsicherheit in der Laufzeit. Ein Stack kann nur int-Werte verwalten. Nichts anderes.
11
Beide Probleme können auch mit normalen Klassen gelöst werden, wenn diese mit Typen arbeiten, die voneinander abgeleitet sind und einen sauberen Polymorphismus erlauben. Ein EmployeeStack, der auf Employee-Objekte spezialisiert ist, wäre
347
OOP-Specials
demnach denkbar. Diesem könnten dann auch Instanzen der abgeleiteten Klasse Manager zugewiesen werden. Ein sauberer Polymorphismus (mit virtuellen und überschriebenen Methoden) führt dann zu einer sauberen und klaren Programmierung. In der Praxis müssen Sie also entscheiden, ob Sie lieber einen generischen Typen einsetzen oder einen, der mit Polymorphismus arbeitet (den Sie auch über Schnittstellen erreichen können). Diese Entscheidung ist in der Praxis leider nicht allzu einfach, weil ein Problem in der Regel auf beide Arten recht gut gelöst werden kann. Generische Typen sind aber in vielen Fällen eleganter und besser verständlich (weil keine Vererbung und keine Schnittstellen im Spiel sind).
6.2.3
Ableiten von generischen Klassen und Schnittstellen
Von generischen Klassen und Schnittstellen können Sie auch ableiten. Die Typparameter können Sie dabei weitergeben oder fest definieren: Listing 6.11: Von der generischen Klasse Stack abgeleitete, ebenfalls generische Klasse public class ExtendedStack: Stack { /* Konstruktor */ public ExtendedStack(int capacity) : base(capacity) { } /* Ersetzt einen Eintrag */ public void ReplaceItem(T newItem, int position) { base.items[position] = newItem; } }
Listing 6.12: Von der generischen Klasse Stack abgeleitete, nicht generische Klasse public class StringStack : Stack { /* Konstruktor */ public StringStack(int capacity): base(capacity) { } }
6.2.4
Generische Methoden
Generische Methoden arbeiten nach demselben Prinzip wie generische Typen. Eine generische Methode definieren Sie mit der Angabe von Typparametern hinter dem Methodennamen. Diese Typparameter beziehen sich nur auf die Methode und sind prinzipiell unabhängig von dem Typ, der die Methode enthält. Eine generische Methode kann deswegen auch in einem nicht generischen Typen implementiert werden oder mit vollkommen anderen Typparametern arbeiten als die Klasse bzw. Struktur, in der sie implementiert wird.
348
Generische Typen und Methoden
Die folgende generische Methode Swap löst das Problem, dass zwei Objekte gegeneinander vertauscht werden sollen (was z. B. bei der eigenen Implementierung eines Sortier-Algorithmus notwendig ist): Listing 6.13: Generische Methode zum Vertauschen zweier Objekte private static void Swap(ref T x, ref T y) { T temp = x; x = y; y = temp; }
1
2
Diese Methode kann nun mit beliebigen Objekten (auch Werttypen) eingesetzt werden: Listing 6.14: Einsatz der generischen Methode
3
int i1 = 42; int i2 = 7; Swap(ref i1, ref i2);
Generische Methoden ersetzen in vielen Fällen überladene Methoden. Stellen Sie sich vor, Sie müssten eine Swap-Methode in einer Sprache implementieren, die keine generischen Methoden kennt. Dann müssten Sie für jeden Typen, mit dem Swap verwendet werden soll, eine eigene überladene Variante entwickeln. Das wäre im Vergleich zu der einen generischen Version sehr aufwändig.
6.2.5
Generische Methoden ersetzen u. a. überladene Methoden
4
Der Compiler erkennt in vielen Fällen Typen automatisch
6
5
Automatischer Typrückschluss
Typrückschluss (Type inference) bedeutet, dass der Compiler einen Typen automatisch erkennt. Typrückschluss wird bereits verwendet, wenn Sie Variablen mit dem var-Schlüsselwort deklarieren. Der Compiler erkennt den Typ des zugewiesenen Ausdrucks und erzeugt eine entsprechend typisierte Variable.
7
Typrückschluss funktioniert auch mit generischen Methoden und Delegaten. Wenn Sie eine generische Methode aufrufen, erkennt der Compiler den Typ der verwendeten Typparameter an den übergebenen Argumenten. Die Swap-Methode aus dem vorhergehenden Abschnitt kann deshalb auch ohne Typparameter aufgerufen werden:
8
Swap(ref i1, ref i2);
9
Typrückschluss ist in der Praxis sehr interessant. Besonders bei der Arbeit mit LINQ rufen Sie sehr viele generische (Erweiterungs-)Methoden auf. Obwohl die Syntax dieser Methoden Typparameter enthält, können Sie beim Aufruf in der Regel auf die Angabe der Typparameter verzichten.
6.2.6
10
Typparameter einschränken
Bei der Entwicklung generischer Typen ist es häufig notwendig, die verwendeten Typparameter einzuschränken. Sollen z. B. neue Instanzen eines über einen Typparameter definierten Typs erzeugt werden, muss der Typ einen parameterlosen Standardkonstruktor zur Verfügung stellen.
Typparameter können eingeschränkt werden
Einschränkungen geben Sie am Ende der Typdeklaration über das where-Schlüsselwort an: where Typparameter: Einschränkungsliste
349
11
OOP-Specials
In der Einschränkungsliste können Sie beliebig viele Einschränkungen angeben, die Sie durch Kommata voneinander trennen. Dazu stehen die in Tabelle 6.1 angegebenen Einschränkungen zur Verfügung. Tabelle 6.1: Einschränkungen für generische Typen und Methoden
Einschränkung
Bedeutung
class
Der Typ muss ein Referenztyp sein.
struct
Der Typ muss ein Werttyp sein.
new()
Der Typ muss einen parameterlosen Standardkonstruktor besitzen.
Basisklasse
Der Typ muss der angegebenen Basisklasse entsprechen oder davon abgeleitet sein.
Schnittstelle
Der Typ muss die angegebene Schnittstelle als Einschränkung implementieren.
Anderer Typparameter
Der Typ muss von dem Typen abgeleitet sein, der über den anderen Typparameter definiert ist (»Offene Einschränkung«).
Was bei den Einschränkungen leider fehlt, sind: INFO
–
solche, die auf das Vorhandensein eines bestimmten Operators (+, –, * etc.) einschränken,
–
solche, die auf das Vorhandensein eines Konstruktors mit bestimmten Argumenten einschränken
–
und solche, die auf numerische Typen einschränken (da diese leider nicht von einer gemeinsamen Basisklasse abgeleitet sind).
Enthält der Typ mehrere Typparameter, können Sie jeden einzelnen über eine separate where-Einschränkung begrenzen, wobei Sie die einzelnen where-Deklarationen durch zumindest ein Leerzeichen trennen. Die folgenden Beispiele zeigen die Deklaration von Klassen mit jeweils mehreren Einschränkungen: public class Customer where TCustomerId : new() where TCustomerData: ICustomerData {/*...*/} public class PersonList where T : Person, IComparable {/*... */}
Einschränken auf Referenz- oder Werttypen Die Methode Swap aus Listing 6.13 kann z. B. auf Werttypen eingeschränkt werden: Listing 6.15: Auf Werttypen eingeschränkte generische Methode zum Vertauschen zweier Objekte private static void Swap(ref T x, ref T y) where T: struct { T temp = x; x = y; y = temp; }
350
Generische Typen und Methoden
Mit Referenztypen kann Swap nun nicht mehr eingesetzt werden: StreamReader sr1 = ... StreamReader sr2 = ... Swap(ref sr1, ref sr2); // Kompilierfehler
Einschränken auf Basisklassen oder Schnittstellen Bei Einschränkungen auf Basisklassen oder Schnittstellen können Sie auch generische Typen als Basistyp verwenden. Den Typparameter können Sie dann sogar weitergeben. In den Methoden des generischen Typs bzw. in der generischen Methode können Sie auf die Elemente des Basistyps bzw. der Schnittstelle zugreifen. Listing 6.16 zeigt dies an einer generischen Klasse, deren einziger Typparameter auf die Schnittstelle IComparable eingeschränkt ist.
Einschränkungen auf Basisklassen oder Schnittstellen erlauben auch generische Typen
Diese Schnittstelle enthält die Methode int CompareTo(T other), die das Objekt, in der sie definiert ist, mit dem übergebenen vergleicht. Bei der Rückgabe eines Werts kleiner 0 ist das aktuelle Objekt kleiner, bei der Rückgabe von 0 sind beide Objekte gleich und bei der Rückgabe eines Werts größer 0 ist das aktuelle Objekt größer als das übergebene.
1
2
3
4
Listing 6.16: Generische Klasse mit Einschränkung auf eine Schnittstelle public static class BasicMath where T: IComparable { /* Ermittelt das größere von zwei Objekten */ public static T Max(T x, T y) { return x.CompareTo(y) > 0 ? x : y; } }
5
6
Da die Standardtypen IComparable implementieren, können Sie die Max-Methode z. B. mit int-Werten verwenden:
7
int max = BasicMath.Max(10, 11);
Einschränkungen auf das Vorhandensein eines Standardkonstruktors Wenn Sie auf einen Standardkonstruktor einschränken, kann Ihr generischer Typ bzw. Ihre generische Methode Instanzen dieses Typs erzeugen. Die folgende Methode soll z. B. eine Liste von Objekten mit neuen Instanzen versorgen:
new() erlaubt das Erzeugen von Instanzen
Listing 6.17: Auf Objekte, die einen Standardkonstruktor besitzen, eingeschränkte Methode
8
9
private static void InitializeList(IList list) where T : new() { for (int i = 0; i < list.Count; i++) { list[i] = new T(); } }
10
Bei der Anwendung der Methode muss der am Typparameter definierte Typ nun einen Standardkonstruktor besitzen.
11
Der Methode wird eine Auflistung übergeben, die IList implementiert. IList erlaubt das Durchgehen über einen Integer-Index. IList wird z. B. von Arrays implementiert. Näheres dazu finden Sie in Kapitel 7.
351
OOP-Specials
Das folgende Beispiel nutzt InitializeList um ein int-Array zu initialisieren: Listing 6.18: Verwendung der generischen Methode, die auf Objekte eingeschränkt ist, die einen Standardkonstruktor besitzen int[] intArray = new int[10]; InitializeList(intArray);
INFO
Beachten Sie, dass das Beispiel keinen Typparameter einsetzt, weil der Compiler diesen automatisch aus dem übergebenen Argument erkennt. Im Beispiel ist InitializeList(intArray) identisch mit InitializeList(intArray).
Offene Einschränkungen Offene Einschränkungen erzwingen, dass ein Typ einem anderen entspricht oder von diesem abgeleitet ist
Bei offenen Einschränkungen (»Naked Constraints«) geben Sie einen anderen Typparameter als Einschränkung an. Der eingeschränkte Typ muss dann dem anderen Typparameter entsprechen oder von diesem abgeleitet sein. Mit offenen Einschränkungen können Sie also erreichen, dass einer der später angegebenen Typen von einem anderen später angegebenen abgeleitet sein muss. Wozu auch immer Sie das benötigen … Offene Einschränkungen werden in der Praxis wohl eher selten eingesetzt. Deswegen folgen hier auch nur abstrakte Beispiele: Angenommen, Sie programmieren eine DataManager-Klasse, die Instanzen verschiedener Klassen in einer Datenbank verwalten soll. Die Read-Methode soll aber auch Instanzen davon abgeleiteter Klassen zurückgeben können. Verwaltet eine DataManager-Instanz BankAccount-Objekte, sollen z. B. auch BankGiroAccount-Objekte zurückgegeben werden können. Die Deklaration dieser Klasse würde der folgenden entsprechen: Listing 6.19: Beispielklasse für offene Typparameter public static class Manager { /* Liest ein Objekt aus der Datenbank */ public static U Read(int id) where U: T, new() { // Das Auslesen ist hier nicht implementiert return new U(); } }
Diese Klasse kann dann z. B. so verwendet werden (wenn BankGiroAccount von BankAccount abgeleitet ist): BankAccount account1 = Manager.Read(1001); BankGiroAccount account2 = Manager.Read(1002);
Aber nicht so (da Customer nicht von BankAccount abgeleitet ist): Customer customer = Manager.Read(1001);
Sinn machen offene Einschränkungen z. B. in solchen Situationen: Wenn Sie DatenManager-Klassen entwickeln und die Daten-Klassen (teilweise) voneinander abgeleitet sind. Die Verwaltung und Erzeugung solcher Objekte ist mit Sicherheit nicht trivial. Generische Klassen mit offenen Typparametern helfen aber bei der Imple-
352
Generische Typen und Methoden
mentierung dadurch, dass sie ermöglichen, nur eine einzige Daten-Manager-Klasse implementieren zu müssen. Außerdem erhöhen generische Klassen die Typsicherheit. Innerhalb einer Methode, die mit einem Typparameter mit offener Einschränkung arbeitet, können Sie allerdings mit dem Typen nicht viel machen. Da der Basistyp selbst ein Typparameter ist (und damit zur Kompilierzeit unbekannt), stellt der Compiler nur die Elemente zur Verfügung, die von Object geerbt wurden.
1
Offene Einschränkungen können auch direkt in einer generischen Klassendeklaration eingesetzt werden:
2
class Demo where T2: T1 {/*...*/}
Dafür findet aber selbst die Microsoft-C#-Dokumentation kein Beispiel ...
6.2.7
Das default-Schlüsselwort
3
Über das default-Schlüsselwort können Sie Felder bzw. Argumente, die mit einem Typparameter deklariert sind, mit dem Defaultwert des jeweiligen Typs (der ja später definiert wird) initialisieren. Den Typparameter übergeben Sie dazu in Klammern. Die InitializeList-Methode aus Listing 6.17 (Seite 351) kann damit so umgeschrieben werden, dass sie keinen Standardkonstruktor mehr verlangt:
4
Listing 6.20: Methode, die einen generischen Typen mit seinem Defaultwert initialisiert
5
private static void InitializeList(IList list) { for (int i = 0; i < list.Count; i++) { list[i] = default(T); } }
6
6.2.8
7
Generische Schnittstellen
Generische Schnittstellen machen dann Sinn, wenn ihre Elemente (Methoden, Eigenschaften) mit Typen arbeiten, die erst später festgelegt werden sollen. Angenommen, Sie entwickeln eine Anwendung für mathematische Aufgaben. Eine Schnittstelle IAddAndSubtractable könnte für die Fähigkeit stehen, dass Objekte auf Instanzen ihres eigenen Typs addiert und davon subtrahiert werden können. Diese Schnittstelle könnte später von Klassen oder Strukturen wie einer für komplexe Zahlen oder einer für einen Punkt (X/Y-Koordinaten) implementiert werden.
8
9
Listing 6.21: Generische Schnittstelle
10
public interface IAddAndSubtractable { T Add(T other); T Substract(T other); }
11
Generische Schnittstellen können wie normale Schnittstellen von Klassen oder Strukturen implementiert werden. Dabei wird in der Regel der implementierende Typ als Typparameter der Schnittstelle eingesetzt:
353
OOP-Specials
Listing 6.22: Klasse, die eine generische Schnittstelle implementiert public class IntPoint: IAddAndSubtractable { public int X; public int Y; public IntPoint Add(IntPoint other) { return new IntPoint{ X = this.X + other.X, Y = this.Y + other.Y}; } public IntPoint Substract(IntPoint other) { return new IntPoint{ X = this.X - other.X, Y = this.Y - other.Y}; } }
Eine generische Schnittstelle kann natürlich auch in einer Klasse oder Struktur implementiert werden, die selbst generisch ist. Als Typ wird dann häufig der generische Klassen- bzw. Strukturtyp angegeben. Im .NET Framework ist dies z. B. bei der List-Auflistung der Fall, die die Schnittstelle IList implementiert: public class List : IList {/*...*/}
Beim IAddAndSubtractable-Beispiel müssten Sie zur Implementierung in einer generischen Klasse oder Struktur den Typparameter derselben jedoch so einschränken, dass nur Typen zugelassen werden, für die die Operatoren + und – definiert sind und die einen parameterlosen Konstruktor besitzen (damit diese erzeugt werden können). Einschränkungen auf einen Standardkonstruktor sind möglich, Einschränkungen für Operatoren aber leider nicht. Darauf gehe ich ab Seite 349 ein. Generische Schnittstellen werden hauptsächlich mit oder in generischen Typen eingesetzt. Die IComparable-Schnittstelle wird z. B. von der Sort-Methode verschiedener generischer Auflistungen wie List eingesetzt. Sort geht die in der Auflistung verwalteten Objekte durch und überprüft, ob diese IComparable implementieren. T ist dabei der Typ, mit dem die Auflistung deklariert und erzeugt wurde. Ist dies der Fall, wird das Objekt nach IComparable umgewandelt und über deren CompareTo-Methode mit einem anderen Objekt verglichen. Ein Beispiel dazu finden Sie in Listing 6.16 auf Seite 351.
6.2.9 Generische Delegaten sind ebenfalls möglich
Generische Delegaten
Delegaten können ebenfalls generisch sein. So können Sie Delegaten erstellen, die mit Typen arbeiten, die erst später festgelegt werden. Die bereits vorhandenen generischen Delegaten Action, Func und EventHandler haben Sie ja bereits kennen gelernt. Und sind wahrscheinlich von deren Nützlichkeit überzeugt. In den seltenen Fällen, wo diese für Ihre Zwecke nicht ausreichen, können Sie auch eigene, generische Delegaten deklarieren. Der folgende Delegat ist z. B. dafür vorgesehen, dass eine Methode ein Objekt auf eine Bedingung testet:
354
Generische Typen und Methoden
Listing 6.23: Generischer Delegat public delegate bool ConditionTestHandler(T item);
Eine Methode in einer Klasse arbeitet mit diesem Delegaten als eines der Argumente. Am anderen Argument wird eine Auflistung übergeben (in Form der IEnumerable-Schnittstelle, die von allen Auflistungen implementiert wird und die ein Durchgehen mit foreach ermöglicht):
1 Listing 6.24: Klasse mit einer Methode, die Objekte in einer Auflistung auf eine Bedingung testet, die in Form eines Delegaten übergeben wird public static class ConditionTester { public static void TestValuesForCondition(IEnumerable values, ConditionTestHandler testMethod) { foreach (var value in values) { if (testMethod(value) == true) { Console.WriteLine("Der Wert " + value + " wurde bestätigt"); } } } }
2
3
4
5
Ich habe den Delegaten übrigens lediglich als Beispiel implementiert. In der Praxis hätte ich stattdessen den Func-Delegaten in der Form Func eingesetzt. INFO
Eine Anwendung kann nun z. B. (anonyme) Methoden in Form von Lambda-Ausdrücken zum Testen auf eine Bedingung zur Verfügung stellen, die den Delegaten erfüllen und die TestValuesForCondition-Methode mit diesen Aufrufen um eine Auflistung von Objekten auf verschiedene Bedingungen zu testen:
6 7
Listing 6.25: Verwendung der Methode, die mit einem generischen Delegaten arbeitet // Erzeugen des Beispiel-Arrays int[] testValues = new int[10]; Random random = new Random(); for (int i = 0; i < 10; i++) { testValues[i] = random.Next(1, 100); }
8
9
// Aufruf der Testmethode mit einem Lambda-Ausdruck, der die Werte // daraufhin überprüft, ob diese gerade sind Console.WriteLine("Gerade Zahlen: "); ConditionTester.TestValuesForCondition(testValues, value => value % 2 == 0); Console.WriteLine();
10
// Aufruf der Testmethode mit einem Lambda-Ausdruck, der die Werte // daraufhin überprüft, ob diese ungerade sind Console.WriteLine("Ungerade Zahlen: "); ConditionTester.TestValuesForCondition(testValues, value => value % 2 != 0); Console.WriteLine();
11
355
OOP-Specials
INFO
Für den Fall, dass Sie mit Lambda-Ausdrücken (Kapitel 5 und folgender Abschnitt) noch nicht so sicher umgehen können: Das Ganze funktioniert deswegen, weil die Lambda-Ausdrücke dem Delegaten entsprechen und der Compiler diese deswegen implizit in eine Instanz des Delegaten umwandelt. Der Ausdruck value => value % 2 == 0
entspricht der anonymen Methode delegate(int value) { return value % 2 == 0; }
Und die entspricht dem Delegaten, weil die Signatur stimmt und der C#-Compiler den Typparameter über einen Typrückschluss automatisch erkennt. An diesem Beispiel erkennen Sie auch die Eleganz der funktionalen Programmierung. Der Testmethode können einfache Lambda-Ausdrücke übergeben werden um auch die komplexesten Bedingungen zu testen. Und die Testmethode muss dazu nicht umgeschrieben werden. Das ist cool ☺.
6.3
Lambda-Ausdrücke und Ausdrucksbäume
Das .NET Framework enthält mit Lambda-Ausdrücken und Ausdrucksbäumen wichtige Features der funktionalen Programmierung. Um dieses interessante Konzept (das ich hier nicht weiter ausführen kann) zu verstehen und umsetzen zu können und um mit modernen .NET-Features wie LINQ arbeiten zu können, sollten Sie sich mit diesen Themen auskennen. Da ich denke, dass das Verständnis besonders von Ausdrucksbäumen sehr wichtig ist, behandle ich diese zwar eher theoretisch, aber so umfangreich, dass Sie später wissen, wie z. B. LINQ to SQL einen Lambda-Ausdruck in eine SQL-Anweisung umsetzt.
6.3.1
Lambda-Ausdrücke
Lambda-Ausdrücke sind ein neues Feature von C# 3.0 bzw. des .NET Framework 3.5. Lambda-Ausdrücke werden sehr intensiv mit LINQ eingesetzt, können aber (wie in dem Beispiel dieses Abschnitts) auch für eigene (funktionale) Programmierung verwendet werden. LambdaAusdrücke können in Delegaten oder Ausdrucksbäume konvertiert werden
Ein Lambda-Ausdruck ist ein Ausdruck, der zum einen in einen so genannten Ausdrucksbaum konvertiert werden kann. Ausdrucksbäume werden ab Seite 357 behandelt. Lambda-Ausdrücke können zum anderen aber auch in einen Delegaten konvertiert werden. Dieser Delegat referenziert eine anonyme Methode, die implizit über den Lambda-Ausdruck erzeugt wird, sobald dieser als Delegat verwendet wird. Die Syntax von Lambda-Ausdrücken habe ich bereits in Kapitel 5, bei der Behandlung von Lambda-Ausdrücken für Delegaten im Abschnitt »Delegaten, anonyme Methoden und Lambda-Ausdrücke«, beschrieben. Hier folgt deswegen nur eine kurze Zusammenfassung: ■ ■ ■
356
Ein Lambda-Ausdruck hat die Form [Parameterliste] => {Ausdruck | {Anweisungsliste}}. Ist die Parameterliste leer oder enthält diese mehr als einen Parameter, muss sie in Klammern eingeschlossen werden. Die Typen der Parameter müssen nicht, können aber angegeben werden.
Lambda-Ausdrücke und Ausdrucksbäume
■ ■
Steht rechts eine Anweisungsliste, muss diese in geschweifte Klammern eingeschlossen werden. Steht rechts ein Ausdruck oder enthält die Anweisungsliste ein return Wert, gibt der Lambda-Ausdruck das Objekt zurück, das der Ausdruck ergibt bzw. das mit return zurückgegeben wird.
Listing 6.2 zeigt (als Wiederholung von Kapitel 5) einige Beispiele für Lambda-Ausdrücke, die mit Ausdrücken und Anweisungsblöcken arbeiten. Die Zuweisung an die Variablen vom Typ des Func- bzw. Action- Delegaten habe ich im Beispiel nur deswegen vorgenommen, um die Anweisungen kompilieren zu können.
1
2
Listing 6.26: Beispiele für Lambda-Ausdrücke, die mit Ausdrücken und Anweisungsblöcken arbeiten // Lambda-Ausdruck mit Ausdruck ohne Parameter int number = 10; Func f1 = () => number * 2;
3
// Lambda-Ausdruck mit Ausdruck mit einem implizit typisierten Parameter Func f2 = x => x + 1; // Lambda-Ausdruck mit Ausdruck und zwei explizit typisierten Parametern Func f3 = (int x, int y) => x + y;
4
// Lambda-Ausdruck mit Anweisungsblock und nur einer Anweisung Func f4 = x => { return x + 1; };
5
// Lambda-Ausdruck mit Anweisungsblock und mehreren Anweisungen Func f5 = (x) => { if (x < 10) { return 1; } else if (x < 100) { return 2; } else { return 3; } };
6
// Lambda-Ausdruck mit Anweisungsblock ohne Rückgabe Action f6 = info => { Console.WriteLine(info); };
7
Den Einsatz von Lambda-Ausdrücken an Delegaten-Referenzen habe ich bereits in Kapitel 5 behandelt. Im Abschnitt »Generische Delegaten« auf Seite 354 finden Sie ein weiteres Beispiel.
8
Lambda-Ausdrücke können aber auch in Ausdrucksbäume konvertiert werden. Und diesem Thema widmet sich der folgende Abschnitt.
6.3.2
9
Ausdrucksbäume
Ausdrucksbäume (Expression Trees) sind ein neues Feature im .NET Framework 3.5, allerdings kein neues Feature in der allgemeinen Informatik. Das Prinzip von Ausdrucksbäumen ist in der Informatik (bzw. in der Mathematik) schon seit längerem bekannt.
10 NEU
11
Was sind Ausdrucksbäume? Über Ausdrucksbäume können Ausdrücke dargestellt bzw. gespeichert werden. Der folgende Ausdruck zur Berechnung eines Bruttowerts: net * (1 + (vat / 100))
kann z. B. wie in Abbildung 6.2 in einem Ausdrucksbaum dargestellt werden.
Ausdrucksbäume sind ein Hilfsmittel zur Darstellung von komplexen Ausdrücken
357
OOP-Specials
Abbildung 6.2: Ein Ausdrucksbaum zur Berechnung eines Bruttobetrags
Der Baum im Beispiel besteht aus drei Berechnungsausdrücken (Multiplikation, Addition und Division), zwei konstanten Ausdrücken für die in der Berechnung verwendeten Konstanten 1 und 100 und zwei Parameterausdrücken für die Parameter net und vat. Bei der späteren Berechnung werden die Parameterausdrücke mit Werten versehen. Jeder Baum beginnt bei einem Wurzelausdruck. Im Beispiel ist das der Multiplikationsausdruck. Die Berechnungsausdrücke im Beispiel besitzen einen linken und einen rechten Operanden. Der linke Operand des Multiplikationsausdrucks ist der Parameterausdruck für den Parameter net, der rechte der Additionsausdruck. Der linke Operand des Additionsausdrucks ist der Konstantenausdruck für den Wert 1, der rechte der Divisionsausdruck. Etc. Ein Ausdrucksbaum wird ausgehend vom Wurzelknoten ausgewertet. Dabei werden zuerst die Knoten an den Enden ausgerechnet, dann die darüberliegenden etc. Im Beispiel ergäbe das die folgenden (mathematisch korrekten) Einzelschritte:
1. Zwischenergebnis1 = vat / 100 2. Zwischenergebnis2 = 1 + Zwischenergebnis1 3. Endergebnis = net * Zwischenergebnis2 Die Parameter müssen für die Berechnung natürlich mit Werten versehen werden.
Der Sinn von Ausdrucksbäumen Mit Ausdrucksbäumen können beliebige Ausdrücke in einem Programm gespeichert und ausgewertet werden. Das ist noch nichts Besonderes, weil Ausdrücke ja auch direkt in Programmen verwenden werden können. Ausdrucksbäume erlauben aber zum einen das dynamische Zusammensetzen eines Ausdrucks und zum anderen individuelle, vom Standard abweichende Auswertungen eines Ausdrucks.
358
Lambda-Ausdrücke und Ausdrucksbäume
Das dynamische Zusammensetzen eines Ausdrucks wird z. B. in Ausdrucksparsern benötigt, die eine Benutzereingabe in einen auswertbaren Ausdruck umwandeln. Stellen Sie sich vor, Sie müssten eine einfache Anwendung schreiben, in die der Anwender einen mathematischen Ausdruck eingeben kann, der nach der Eingabe ausgewertet werden soll. Wenn Sie die Eingabe parsen, müssen Sie die einzelnen Teile des Gesamtausdrucks so speichern, dass diese später ausgewertet werden können. Ein Ausdrucksbaum ist dafür ideal geeignet. Sie bräuchten lediglich Klassen, die die unterstützten atomaren Ausdrücke (Addition, Subtraktion, Multiplikation etc.) abbilden und die Referenzen auf ihre Operanden(ausdrücke) erlauben.
Ausdrucksbäume erlauben das dynamische Zusammensetzen und das dynamische Auswerten von Ausdrücken
1
Das .NET Framework stellt dazu im Namensraum System.Linq.Expressions entsprechende Klassen zur Verfügung, gemeinsam mit der Möglichkeit, einen Ausdruck dynamisch auszuführen. Sie müssten »nur« noch den Parser schreiben (was allerdings nicht allzu einfach ist).
2
Neben dem dynamischen Zusammensetzen und dem Ausführen eines Ausdrucks erlauben Ausdrucksbäume auch eine dynamische Auswertung. Dieses Feature wird z. B. in LINQ to SQL genutzt.
LINQ to SQL wird erst in Kapitel 19 behandelt. LINQ to SQL ermöglicht die einfache Arbeit mit Datenbanken. LINQ (Language Integrated Query) ist die Basis für LINQ to SQL. LINQ wird in Kapitel 11 behandelt. LINQ besteht im Wesentlichen aus generischen Erweiterungsmethoden für die IEnumerable-Schnittstelle. Diese Methoden können also auf den meisten Auflistungen und auf allen Arrays angewendet werden. T ist dabei der Typ, der der Erweiterungsmethode übergeben wird, und der Typ, der in der Auflistung verwaltet wird. Die Where-Methode wird z. B. ein Delegat der Form Func übergeben, der die in der Auflistung gespeicherten Objekte auf eine Bedingung überprüft (ähnlich wie in Listing 6.24 auf Seite 355). Where gibt eine neue Auflistung zurück, die nur die Objekte enthält, die der Bedingung entsprechen. Die LINQ-Erweiterungsmethoden stehen dann zur Verfügung, wenn Sie den Namensraum System.Linq einbinden.
3
4 EXKURS
5
6 7
LINQ erlaubt also z. B. die Verwendung der Where-Methode auf allen Objekten, die IEnumerable implementieren. Als Bedingungs-Delegat wird natürlich in der Praxis zur Vereinfachung ein Lambda-Ausdruck eingesetzt.
8
Das folgende Beispiel sucht in einer Auflistung von Personen nach denen, die in Dublin wohnen:
9
List persons = GetPersons(); foreach (Person personFromDublin in persons.Where( person => person.City == "Dublin")) { Console.WriteLine(personFromDublin.FirstName + " " + personFromDublin.LastName); }
Beachten Sie einmal wieder, dass die Where-Methode ohne Typparameter verwendet wird, weil der Compiler den Typ über einen Typrückschluss implizit einsetzt. Ich hätte auch Where verwenden können, aber warum mehr schreiben als nötig ...
10
11 TIPP
Die Where-Methode verwendet die übergebene Prüfmethode zum Test, ob die einzelnen Objekte in der Auflistung der Bedingung entsprechen. Das hat noch nichts mit Ausdrucksbäumen zu tun.
359
OOP-Specials
In LINQ to SQL werden LambdaAusdrücke in SQLAnweisungen umgewandelt
Wird aber LINQ to SQL verwendet, sieht das Ganze anders aus. Die Abfrage der Daten erfolgt dann gegen eine Auflistung von Daten-Objekten, die in einem DataContext verwaltet wird. Ich will hier nicht näher auf dieses in Kapitel 19 behandelte Thema eingehen. Der DataContext kümmert sich im Wesentlichen um die Abfrage und Aktualisierung der in einer Datenbank verwalteten Daten. Eine zum vorhergehenden Beispiel äquivalente Abfrage würde in etwa folgendermaßen aussehen: foreach (Person personFromDublin in dataContext.Persons.Where( person => person.City == "Dublin")) { Console.WriteLine(personFromDublin.FirstName + " " + personFromDublin.LastName); }
Würden bei LINQ to SQL allerdings zunächst immer erst alle Objekte aus der Datenquelle abgefragt und dann erst gegen die Prüfmethode geprüft werden, wäre dies sehr ineffizient. Deswegen geht LINQ to SQL anders vor: Die LINQ-To-SQL-Where-Methode (die eine andere ist als die LINQ-Methode!) wertet den Ausdrucksbaum aus, den der übergebene Lambda-Ausdruck ergibt, und setzt diesen in eine entsprechende SQL-WHEREKlausel um. Diese wird dann verwendet, um die Daten gezielt abzufragen. Damit werden nur die Daten abgefragt, die der übergebenen Bedingung entsprechen. Das ist effizient und damit ein guter Grund für den Einsatz von Ausdrucksbäumen. Wie LINQ to SQL das macht, erläutert das Beispiel auf den folgenden Seiten.
Ausdrucksbäume in .NET Das .NET Framework unterstützt Ausdrucksbäume über Klassen im Namensraum System.Linq.Expressions. Der Namensraum deutet darauf hin, dass Ausdrucksbäume vorwiegend in LINQ verwendet werden, was auch richtig ist. Ausdrucksbäume können allerdings (natürlich) überall dort verwendet werden, wo sie sinnvoll erscheinen. Anwendungsbereiche zu finden ist allerdings nicht allzu einfach, wenn Sie in der »normalen« (objektorientierten) Programmierung zuhause sind, und nicht in der funktionalen. Ausdrucksbäume werden vorwiegend in LINQ to SQL eingesetzt
LINQ to SQL nutzt Ausdrucksbäume sehr intensiv, indem die bei der Abfrage von Daten verwendeten Lambda-Ausdrücke in passende SQL-Anweisungen umgesetzt werden. Das Verständnis dieser Technik war der Hauptgrund dafür, Ausdrucksbäume in diesem Kapitel zu behandeln. Aber vielleicht finden Sie auch weitere Verwendung dafür.
Erstellung eines Ausdrucksbaums Ein Lambda-Ausdruck, der einem Delegaten D entspricht, entspricht immer einer Instanz der Klasse System.Linq.Expressions.Expression. Ausdrucksbäume können also implizit über einen Lambda-Ausdruck erzeugt werden (was ja z. B. bei LINQ to SQL genutzt wird). Ein Ausdrucksbaum für eine Bruttoberechnung kann z. B. folgendermaßen über einen Lambda-Ausdruck erzeugt werden: Expression grossLambdaExpression = (net, vat) => net * (1 + (vat / 100));
Um dieses etwas komplexe Konstrukt zu erklären: Expression ist eine generische Klasse, die einen Lambda-Ausdruck als Ausdrucksbaum darstellt. Der Typparameter TDelegate ist darauf eingeschränkt, ein Delegat zu sein. Im Beispiel wird am Typparameter von Expression der generische Delegat Func eingesetzt, der mit double als Parameter- und als Rückgabe-
360
Lambda-Ausdrücke und Ausdrucksbäume
typ definiert ist. Der der Variablen grossLambdaExpression zugewiesene LambdaAusdruck entspricht diesen Delegaten. Der Compiler konvertiert den Lambda-Ausdruck in eine Delegat-Referenz vom Typ Func. Diese kann der Variable vom Typ Expression zugewiesen werden, weil Expressioneinen Konvertierungsoperator für Delegaten besitzt, der den Delegaten in eine Expression-Instanz umwandelt. Sie können Ausdrucksbäume aber auch explizit über die Klassen des Namensraums System.Linq.Expressions erzeugen. Das erscheint Ihnen vielleicht etwas komplex und u. U. unsinnig. Wie gesagt: Anwendungsbereiche dafür zu finden ist nicht so einfach. Aber ich denke, um die Möglichkeiten einschätzen zu können, sollten wir (Sie und auch ich) mit Ausdrucksbäumen umgehen können.
1
2
System.Linq.Expressions enthält einige Klassen zur Definition von Ausdrücken. Expression ist dabei die (abstrakte) Basisklasse aller Ausdrucksklassen. Die spezialisierten Ausdrucksklassen wie z. B. BinaryExpression können nicht direkt instanziert werden, sondern müssen über statische Methoden der Expression-Klasse erzeugt werden. Die wichtigsten Ausdrucksklassen stellt Tabelle 6.2 vor. Klasse
Bedeutung
Expression
Expression ist die abstrakte Basisklasse aller speziellen Ausdrucksklassen. Sie stellt die Eigenschaft NodeType zur Verfügung, die den Typ des Knotens definiert, den der jeweilige Ausdruck darstellt. Die Eigenschaft Type gibt den Typ des Ausdrucks an. Expression enthält außerdem eine Menge statischer Methoden zur Erzeugung spezialisierter Ausdruck-Objekte, wie z. B. Add zur Erzeugung eines BinaryExpression-Objekts für eine Addition.
Expression< TDelegate>
BinaryExpression
3
Tabelle 6.2: Die wichtigen Klassen zur Darstellung von Ausdrucksbäumen
5
6
Diese generische Klasse erwartet eine Typangabe in Form eines Delegaten. Sie verwaltet den Ausdrucksbaum eines Lambda-Ausdrucks. Ein Lambda-Ausdruck, der dem Delegaten TDelegate entspricht, kann in eine Instanz von Expression konvertiert werden, um den Ausdrucksbaum auswerten zu können, den der Lambda-Ausdruck ergibt. Expression ist von LambdaExpression abgeleitet und kann deswegen kompiliert und ausgeführt werden.
7
8
Diese Klasse steht für Ausdrücke mit zwei Operanden. Die Operanden werden in den Eigenschaften Left und Right verwaltet und referenzieren Expression-Instanzen, also wieder alle möglichen anderen Ausdrucks-Objekte. Binary Expression-Instanzen werden über verschiedene statische Methoden der Expression-Klasse, wie z. B. Add, Divide, Modulo, Multiply, Power, Subtract, And und Or erzeugt.
9
10
ConditionalExpression Diese Klasse ermöglicht die Definition einer Bedingung im Ausdrucksbaum. Die Bedingung wird in der Eigenschaft Test verwaltet. Ergibt die Bedingung true, wird der Baum an dem Ausdruck weiter ausgeführt, der in der Eigenschaft IfTrue referenziert wird, im anderen Fall an dem Ausdruck, den die Eigenschaft IfFalse referenziert. ConditionalExpression-Instanzen erzeugen Sie über die statische Condition-Methode der Expression-Klasse. ConstantExpression
4
11
Instanzen dieser Klasse verwalten in ihrer Eigenschaft Value konstante Werte, die in einem Ausdruck verwendet werden.
361
OOP-Specials
Tabelle 6.2: Die wichtigen Klassen zur Darstellung von Ausdrucksbäumen (Forts.)
Klasse
Bedeutung
LambdaExpression
LambdaExpression stellt einen Lambda-Ausdruck dar. Im Wesentlichen handelt es sich dabei um einen Ausdrucksbaum mit einer Auflistung der verwendeten Parameter. Der Ausdrucksbaum wird in der Eigenschaft Body verwaltet, die Parameter in der Eigenschaft Parameters. LambdaExpressionInstanzen erlauben das Kompilieren und das Ausführen des Ausdrucksbaums.
MethodCallExpression
Diese Klasse steht für Ausdrücke, die einen Methodenaufruf darstellen. Die aufzurufende Methode wird in der Eigenschaft Method verwaltet. Die Eigenschaft Object referenziert das Objekt, auf dem die Methode ausgeführt werden soll. Object ist null, wenn es sich um eine statische Methode handelt. Die Eigenschaft Arguments verwaltet die Argumente, die der Methode übergeben werden sollen. MethodCallExpression-Instanzen werden über die statischen Methoden Call, ArrayIndex oder ArrayIndex der Expression-Klasse erzeugt.
ParameterExpression
ParameterExpression-Objekte verwalten die in einem Ausdruck verwendeten Parameter. Der Name des Parameters wird in der Eigenschaft Name verwaltet, der Typ in der (von Expression geerbten) Eigenschaft Type.
UnaryExpression
Diese Klasse repräsentiert einen unären Ausdruck, also einen Ausdruck mit nur einem Operanden. Das kann z. B. ein Vorzeichenwechsel sein. Der Operand wird in der Eigenschaft Operand verwaltet.
Neben den in Tabelle 6.2 angegebenen Klassen enthält der Namensraum System. Linq.Expressions noch die Klassen InvocationExpression, ListInitExpression, MemberExpression, MemberInitExpression, NewArrayExpression, NewExpression und TypeBinaryExpression, die ich hier nicht weiter beschreiben kann. Um die Arbeit mit Ausdrucksbäumen zu demonstrieren, erzeugt das folgende Beispiel einen Ausdrucksbaum für die Bruttoberechnung vom Anfang dieses Abschnitts. Listing 6.27: Explizite Erzeugung eines Ausdrucksbaums für eine Bruttoberechnung ParameterExpression netParameterExpression = Expression.Parameter(typeof(double), "net"); ParameterExpression vatParameterExpression = Expression.Parameter(typeof(double), "vat"); Expression grossExpression = Expression.Multiply( netParameterExpression, Expression.Add( Expression.Constant(1D), Expression.Divide( vatParameterExpression, Expression.Constant(100D))));
Bei diesem Beispiel sollten Sie beachten, dass die verwendeten ParameterExpression-Instanzen deswegen separat erzeugt werden, weil diese später, beim Kompilieren und Ausführen des Ausdrucks, noch einmal benötigt werden. Den erzeugten Baum können Sie sich über einen Visual-Studio-Visualisierer anschauen. Den dazu notwendigen Expression Tree Visualizer müssen Sie allerdings zunächst installieren. Entpacken Sie dazu das Archiv CSharpSamples.zip im Ordner Samples\1031 des Visual-Studio-Ordners (normalerweise C:\Programme\Microsoft Visual Studio 9.0). Wenn Sie lediglich eine der Express-Editionen installiert haben, müssen Sie die Beispiele zunächst ggf. herunterladen (code.msdn.microsoft.com/ csharpsamples/Release/ProjectReleases.aspx).
362
Lambda-Ausdrücke und Ausdrucksbäume
Kompilieren Sie die Projektmappe ExpressionTreeVisualizer.sln im Ordner Linq Samples\ExpressionTreeVisualizer. Das in diese Projektmappe integrierte Projekt ExpressionTreeGuiHost können Sie starten, um den Expression Tree Visualizer zu testen. Kopieren Sie dann die Dateien aus dem Ordner LinqSamples\ExpressionTreeVisualizer\ExpressionTreeVisualizer\bin\Debug (oder Release) in den Ordner Visual Studio 2008\Visualizers im Eigene-Dateien-Ordner. Den Ordner Visualizers müssen Sie u. U. erst anlegen. Nach einem Neustart von Visual Studio bzw. einer der Express-Editionen steht der Visualisierer zur Verfügung.
1
Halten Sie das Programm dazu an einer Anweisung hinter der Erzeugung des Expression-Objekts an, bewegen Sie den Cursor auf die Variable, klicken Sie auf den Pfeil neben dem Lupen-Symbol und wählen Sie den EXPRESSION TREE VISUALIZER. Der Expression Tree Visualizer zeigt den Ausdrucksbaum in seiner grafischen Struktur an.
2 Abbildung 6.3: Anzeige des BruttoberechnungsAusdrucksbaums über den Expression Tree Visualizer
3
4
5
6 7
8
9
10
11
363
OOP-Specials
Ausdrucksbäume können nur über LambdaExpressionInstanzen ausgeführt werden
Ausdrucksbäume können Sie nun ausführen oder auswerten. Zum Ausführen eines Ausdrucksbaums muss dieser allerdings in eine LambdaExpression-Instanz eingehüllt werden. Eine solche erlaubt das Kompilieren des Ausdrucks über die CompileMethode. Compile erzeugt aus dem Ausdruck eine Methode und gibt eine DelegateReferenz zurück. Über die DynamicInvoke-Methode des Delegaten können Sie die Ausdrucksmethode dann ausführen. Eine LambdaExpression-Instanz verwaltet den auszuführenden Ausdrucksbaum in der Eigenschaft Body. Da Ausdrucksbäume auch Parameter enthalten können, die an beliebigen Stellen im Baum angelegt sein können, müssen diese auch übergeben werden können. Dazu erwartet eine LambdaExpression-Instanz zunächst eine Auflistung der verwendeten ParameterExpression-Objekte in der Eigenschaft Parameters. Diese Auflistung bestimmt lediglich die Reihenfolge der Parameter, in der deren Werte später der DynamicInvoke-Methode übergeben werden. Den Ausdrucksbaum und die Parameter übergeben Sie der statischen Lambda-Methode der Expression-Klasse um eine LambdaExpression-Instanz zu erzeugen.
HALT
Die an die LambdaExpression-Instanz übergebenen ParameterExpression-Instanzen müssen dieselben sein, die in dem Ausdrucksbaum verwendet werden. Laut einem Posting1 von Anders Hejlsberg, dem Chef-Entwickler von C#, werden Parameterausdrücke nicht über deren Namen identifiziert, sondern über die Objekt-Referenz. Der Name hat laut Anders nur informativen Charakter. Wenn Sie stattdessen neue ParameterExpression-Instanzen (mit denselben Namen) übergeben, erhalten Sie beim Kompilieren des Ausdrucks u. U.2 den Fehler »Der Lambda-Parameter liegt nicht im Bereich« (Lambda Parameter not in scope). Das ist in meinen Augen sehr verwirrend und fehlerträchtig. Aber sei’s drum ... Der Beispiel-Ausdrucksbaum kann also folgendermaßen ausgeführt werden: Listing 6.28: Ausführen eines LambdaExpression-Objekts ParameterExpression[] parameters = new ParameterExpression[] { netParameterExpression, vatParameterExpression }; LambdaExpression grossLambdaExpression = Expression.Lambda(grossExpression, parameters); double net = 1000; double vat = 19; object result = grossLambdaExpression.Compile().DynamicInvoke(net, vat);
Ausdrucksbäume können auch ausgewertet werden
Wenn ein Ausdrucksbaum nicht ausgeführt, sondern so ausgewertet werden soll, dass es in eine spezielle Sprache übersetzt wird (z. B. in SQL), könne Sie dies erreichen, indem Sie die einzelnen Knoten des Baums vom Wurzel-Ausdruck aus durchgehen. Jeden Knoten müssen Sie dann auf die von Ihrem Konvertierer unterstützten Expression-Klassen überprüfen, in diese umwandeln und ggf. die weiteren Informationen der jeweiligen Instanz auswerten. Da dieser Vorgang recht komplex ist, zeige ich in Listing 6.29 nur beispielhaft, wie ein Ausdrucksbaum in sein SQL-Äquivalent umgesetzt werden könnte. Das Beispiel ist allerdings unvollständig und müsste für die Praxis noch wesentlich erweitert werden.
1 2
364
Auch wenn der Link nicht für ewig funktionieren wird: forums.microsoft.com/MSDN/ShowPost. aspx?PostID=1349121&SiteID=1 In meinen Tests trat der Fehler nur dann auf, wenn der Ausdrucksbaum mehr als einen Parameter enthielt
Lambda-Ausdrücke und Ausdrucksbäume
Listing 6.29: Beispiel für die Auswertung eines Ausdrucksbaums (als SQL-String) private static string EvaluateExpressionAsSql(Expression expression) { string result = null; // Überprüfen auf die unterstützten Typen if (expression is BinaryExpression) { BinaryExpression binaryExpression = expression as BinaryExpression;
1
// Ermittlung des Operators string op = null; switch (binaryExpression.NodeType) { case ExpressionType.Add: op = "+"; break; case ExpressionType.And: op = "AND"; break; case ExpressionType.Divide: op = "/"; break; case ExpressionType.Equal: op = "="; break; case ExpressionType.GreaterThan: op = ">"; break; case ExpressionType.GreaterThanOrEqual: op = ">="; break; case ExpressionType.LessThan: op = "(?: |\t)*)(?.*?)(?: |\t)+" + "=(?: |\t)+(?.+?)(?: |\t)*;(?.*)"; string replacement = "${spaces}${right} = ${left}; ${comments}"; string result = Regex.Replace(input, pattern, replacement, RegexOptions.Multiline); Console.WriteLine("Ergebnis nach dem Ersetzen:"); Console.WriteLine(result); Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine();
9
10
11
491
Grundlegende Programmiertechniken
Abbildung 8.7: Das ErsetzenProgramm in Aktion
8.3.14 Flexibles Ersetzen über einen Match-Evaluator Die Replace-Methode der Regex-Klasse erlaubt am zweiten bzw. dritten Argument die Übergabe eines Delegaten vom Typ MatchEvaluator. Diesem Delegaten übergeben Sie die Adresse einer Methode, die die geforderte Signatur besitzt. Replace ruft diese Methode dann für jede Fundstelle im durchsuchten String auf und übergibt ein Match-Objekt mit Informationen zur Fundstelle. Die Methode gibt den u. U. veränderten Teilstring zurück und Replace setzt diesen an der Fundstelle ein. Dieses Feature ist sehr flexibel beim Ersetzen in Strings, da Sie die Methode beliebig programmieren können. Zur Vereinfachung übergeben Sie einen Lambda-Ausdruck. Dieser muss dem Delegaten MatchEvaluator entsprechen, der folgendermaßen definiert ist: public delegate string MatchEvaluator(Match match);
Das folgende Beispiel ersetzt alle Zahlen innerhalb eines Strings durch deren doppelten Wert: Listing 8.39: Ersetzen mit einem MatchEvaluator string input = "125;120,5;15;10,25"; string pattern = @"\d+(?:,\d+){0,1}"; string result = Regex.Replace(input, pattern, match => (Convert.ToDouble(match.Value) * 2).ToString());
Das Ergebnis dieses Programms ist der String "250;241;30;20,5".
8.3.15 Strings splitten Über die Split-Methode können Sie einen String an einem Muster auftrennen. Split gibt, wie die gleichnamige Methode der String-Klasse, ein String-Array zurück. Ein Beispiel dafür finden Sie im Abschnitt »Teilstrings extrahieren, Strings kürzen und auffüllen« (Seite 466).
8.3.16 Einige Beispiele Tabelle 8.14 zeigt einige Beispiele für reguläre Ausdrücke. Tabelle 8.14: Einige Beispiele für reguläre Ausdrücke
Muster
Bedeutung
^\d{5}$
Deutsche Postleitzahl
^(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0IP-Adresse 9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[19]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[01]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[04][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])$
492
Formatierungen
Muster
Bedeutung
^(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?
URL
^[0-9]+(?:,[0-9][0-9]?)? ?_$
Euro-Angabe
Tabelle 8.14: Einige Beispiele für reguläre Ausdrücke (Forts.)
1 Diese Beispiel-Ausdrücke finden Sie neben dem Muster zur Prüfung einer E-MailAdresse, das ich der Seite www.ex-parrot.com/~pdw/Mail-RFC822-Address.html entnommen habe, im Visual-Studio-Projekt »Einige Beispiele«, das auf der Buch-DVD im Beispiel-Ordner zum Reguläre-Ausdrücke-Abschnitt gespeichert ist.
8.4
DISC
2
Formatierungen
Vielen Methoden, denen Strings übergeben werden, können Sie eine Zeichenkette übergeben, die spezielle Platzhalter enthält. Ein Platzhalter ist dabei ein IntegerIndex, der bei 0 beginnt und der in geschweifte Klammern eingefügt wird. Optional können Sie an den Index eine Minimalbreite und/oder eine Formatangabe anhängen. Das Schema eines Platzhalters ist:
Viele Methoden erlauben Strings mit Platzhaltern
3
4
{Parameterindex[, Minimalbreite][:Formatstring]}
Der Parameterindex bestimmt den Index eines Arguments, das Sie der Methode am letzten params-Argument an der entsprechenden Position übergeben. Der Wert des Arguments wird an dem Platzhalter mit dem entsprechenden Index ausgegeben und ggf. formatiert. Im String können Platzhalter an beliebiger Stelle stehen. Die Position der übergebenen Argumente muss aber dem Index des dazugehörigen Platzhalters entsprechen.
5
6
Mit der optionalen Minimalbreite können Sie angeben, wie breit der erzeugte (Teil-) String minimal sein soll. Ist der hier angegebene Wert positiv, wird der String nach links mit Leerzeichen aufgefüllt, ansonsten nach rechts. Im Formatstring können Sie schließlich die Formatierung angeben.
7
Die Format-Methode der String-Klasse und die WriteLine-Methode der ConsoleKlasse arbeiten z. B. mit Format-Platzhaltern:
8
Listing 8.40: Formatieren mit String.Format und Console.WriteLine
9
double number = 10; string result = String.Format("Die Quadratwurzel aus {0} ist " + "gerundet {1:0.00}", number, Math.Sqrt(number)); Console.WriteLine(result); Console.WriteLine("Der Sinus von {0} ist gerundet {1:0.00}", number, Math.Sin(number));
10
Das Ergebnis dieses Programms ist: Die Quadratwurzel aus 10 ist gerundet 3,16 Der Sinus von 10 ist gerundet -0,54
11
Die verwendeten Strings enthalten in diesem Beispiel je zwei Platzhalter, an die die Werte der Variablen number und des Ergebnisses der Methode Math.Sqrt bzw. Math.Sin übergeben werden. Der zweite Wert wird dabei jeweils auf zwei Stellen hinter dem Komma und mit führender Null formatiert.
493
Grundlegende Programmiertechniken
Der Formatstring kann festgelegte Zeichen enthalten, die eine vordefinierte Formatierung spezifizieren, oder aus speziellen Formatierzeichen bestehen, die eine benutzerdefinierte Formatierung erlauben. Das Zeichen C steht z. B. für eine Formatierung im länderspezifischen Währungsformat. Das benutzerdefinierte Format 0.00 formatiert einen Wert mit führender 0 und zwei Nachkommastellen. Die Formatierzeichen sind sehr umfangreich. In den folgenden Abschnitten beschreibe ich zwar alle Formatierzeichen, wegen des begrenzten Platzes aber nur knapp. Die vollständige Übersicht der Formatierzeichen mit Beispielen finden Sie in der Visual Studio-Dokumentation unter .NET FRAMEWORK / PROGRAMMIEREN MIT .NET FRAMEWORK / ARBEITEN MIT BASISTYPEN / FORMATIERUNG VON TYPEN. Beim Formatieren wird gerundet
Wie Sie im obigen Beispiel sehen, werden Zahlen beim Formatieren automatisch gerundet, wenn die Zahl nicht vollständig darstellbar ist. Beim Runden verwendet das .NET Framework das bei uns übliche kaufmännische Rundungsverfahren und nicht das eigentlich ausgewogenere mathematische. Beim kaufmännischen Rundungsverfahren werden Zahlen, bei denen rechts neben der zu rundenden Ziffer eine 5 steht, immer aufgerundet. Zum mathematischen Rundungsverfahren, das Sie über die Round-Methode der Math-Klasse verwenden können, erfahren Sie mehr bei der Beschreibung dieser Methode im Abschnitt »Mathematische Berechnungen« (Seite 513).
Die ToStringMethode verschiedener Typen erlaubt Formatie rungen
Zum Formatieren von Werten können Sie neben den speziellen Formatiermethoden bei den numerischen und Datumstypen auch die ToString-Methode verwenden, der Sie einen Formatstring übergeben können. Dieser arbeitet ähnlich dem String, den Sie String.Format oder Console.WriteLine übergeben, mit dem Unterschied, dass keine Platzhalter angegeben werden. Die Formatangaben in diesem String beziehen sich immer auf den Wert des entsprechenden Objekts. So können Sie z. B. einen double-Wert formatiert ausgeben: Listing 8.41: Formatieren mit der ToString-Methode double number = 10; string result = number.ToString( "Der Wert der verwendeten Zahl ist 0.00");
8.4.1
Zahlformatierungen
Zur Formatierung von Zahlen stehen einige vordefinierte Standard-Formatzeichen (Tabelle 8.15) und Formatzeichen für benutzerdefinierte Formate zur Verfügung (Tabelle 8.16). Beachten Sie, dass Formatierungen mit den Standardformaten kulturspezifisch ausgeführt werden und dass Zahlen (außer beim Format R) implizit (kaufmännisch) gerundet werden, wenn diese nicht komplett dargestellt werden können. Das in der Tabelle dargestellte Ergebnis gilt für ein deutsches System. Tabelle 8.15: Die StandardFormatzeichen für Zahlwerte
494
Formatzeichen
Bedeutung
C[n] oder c[n]
D[n] oder d[n]
Beispielwert
Format
Ergebnis
Währung mit vordefinierter oder 1000.955 (in n) angegebener Anzahl Dezimalstellen
C C4
1.000,96 € 1.000,9550 €
Ganzzahl mit vordefinierter oder 1 (in n) angegebener Anzahl Stellen. Kann nur mit Integer-Typen verwendet werden.
D D4
1 0001
Formatierungen
Formatzeichen
Bedeutung
Beispielwert
Format
Ergebnis
E[n] oder e[n]
Wissenschaftliches Format mit vordefinierter oder (in n) angegebener Anzahl Dezimalstellen
1000.955
E E4
1,000955E+003 1,0010E+003
F[n] oder f[n]
Festkommazahl mit vordefinier- 1000.955 ter oder (in n) angegebener Anzahl Dezimalstellen
F F4
1000,96 1000,9550
G[n] oder g[n]
In diesem Format wird die Zahl je nach Größe, Anzahl der Dezimalstellen und angegebener Anzahl der Dezimalstellen entweder als Festkommazahl oder in der wissenschaftlichen Schreibweise formatiert. n gibt die Anzahl der Gesamt-Stellen (Vorkomma- und Nachkommastellen) an.
1000.955 0.00001 1000.955 1000.955
G G G2 G4
1000,955 1E-05 1E+03 1001
Zahl mit vordefinierter oder (in n) angegebener Anzahl Dezimalstellen und Tausendertrennzeichen
1000.955
Prozentzahl mit vordefinierter oder (in n) angegebener Anzahl Dezimalstellen
0.125
Dieses Format ergibt einen String, der eine double- oder float-Zahl so enthält, dass diese ohne Veränderung der Zahl wieder in einen doublebzw. float-Wert zurückkonvertiert werden kann.
1.234567F
Hexadezimale Darstellung mit vordefinierter oder (in n) angegebener Anzahl Stellen
255
N[n] oder n[n]
P[n] oder p[n]
R oder r
X[n] oder x[n]
Tabelle 8.15: Die StandardFormatzeichen für Zahlwerte (Forts.)
1
2
3
4 N N4
1.000,96 1.000,9550
5 P P4
12,50% 12,5000%
6 R
1,234567
7
8 X X4
FF 00FF
9
Wenn Sie eine Zahl ohne Format ausgeben, entspricht dies dem Format G. Da in allen Formaten die Zahl gerundet wird, wenn diese nicht komplett ausgegeben werden kann, besitzt das Format R eine Bedeutung, wenn Sie eine Zahl in einen String und wieder zurück in eine Zahl umwandeln müssen (z. B. bei der Speicherung in einer XML-Datei). Bei diesem Format wird die Zahl nicht gerundet und entspricht nach dem Zurückkonvertieren wieder demselben Wert.
10
11
Zusätzlich zu den Standard-Formatzeichen können Sie auch ein benutzerdefiniertes Format einstellen. Dazu stehen Ihnen die in Tabelle 8.16 angegebenen Formatzeichen zur Verfügung. Neben diesen Sonderzeichen können Sie auch alle anderen Zeichen als normales Zeichen im Formatstring verwenden.
495
Grundlegende Programmiertechniken
Tabelle 8.16: Die Formatzeichen für benutzerdefinierte Zahlformate
496
Formatzeichen
Bedeutung
Beispielwert
Format
Ergebnis
#
steht für eine Ziffer oder, falls keine Ziffer an der Position vorhanden ist, an der # angegeben ist, für einen leeren String. Wird u. a. für Formate verwendet, bei denen die einzelnen Ziffern einer Zahl umpositioniert werden müssen (z. B. mit Strichen dazwischen). Wird auch für das Tausendertrennzeichen verwendet.
1.55 0.55 1.55 1.55 1234 123 1234 123.4
.# .# .## .### ## – ## #-#-# #-#-# #-#-#
1,6 ,6 1,55 1,55 12 – 34 1-2-3 12-3-4 1-2-3
0
steht für eine Ziffer oder, falls keine Ziffer vorhanden ist, für die 0.
1.55 0.55 1.55 1.55 0.5 0.5
.0 .0 .00 .000 0.00 00.00
1,6 ,6 1,55 1,550 0,50 00,50
.
steht für das Dezimaltrennzeichen.
1.55
0.0
1,6
,
steht für das Tausendertrennzeichen, wenn das Komma zwischen # angegeben ist: #,#. Wenn Sie Kommata links neben dem expliziten oder impliziten Dezimaltrennzeichen positionieren, bewirken diese, dass pro Komma die Zahl durch 1000 geteilt wird (wozu auch immer das sinnvoll ist …).
#,# #,#0.00 #,#.00
1234 1234.5 0.555
1.234 1.234,57 ,56
0,.00 0,,.0000
1234 1234
1,23 0,0012
%
steht für das Prozentzeichen. Die 0.00% formatierte Zahl wird gleichzeitig mit 100 multipliziert.
0.125
12,50%
E0 E+0 E-0 e0 e+0 e-0
Wenn Sie ein E oder e gefolgt von 0, +0 oder -0 am Ende des Formatstrings angeben, wird die Zahl im wissenschaftlichen Format ausgegeben.
1234
1E3 1E+3 1,23E3 1,23E+3
;
Über das Semikolon können Sie 0.00;(0.00);Null den Formatstring in mehrere Sektionen einteilen. Die erste Sektion steht für positive Zahlen, die zweite für negative und die optionale dritte für die Zahl 0. Ist die dritte Sektion nicht vorhanden, wird die Zahl 0 entsprechend der ersten Sektion formatiert.
123 -123 0
123,00 (123,00) Null
0E0 0E+0 0.00E0 0.00E+0
Formatierungen
Formatzeichen
Bedeutung
'xyz' oder "xyz"
\
Beispielwert
Format
Ergebnis
Über Apostrophe oder Anfüh0'…' rungszeichen können Sie in einem Formatstring Zeichenketten einfügen, die auch Zeichen mit Sonderbedeutung besitzen können. Macht eigentlich nur Sinn in Verbindung mit der ToString-Methode der Zahltypen.
50
50…
Der Backslash hebt die Sonder- 0\.\.\. bedeutung von Zeichen auf. Macht eigentlich nur Sinn in Verbindung mit der ToStringMethode der Zahltypen.
50
Tabelle 8.16: Die Formatzeichen für benutzerdefinierte Zahlformate (Forts.)
1
2
50…
3
Ein paar Beispiele sollen die Arbeit mit Formatstrings verdeutlichen. Die Anweisungen nutzen die Formatierfähigkeit der WriteLine-Methode der Console-Klasse:
4
Listing 8.42: Formatierte Ausgabe an der Konsole Console.WriteLine("{0:C}", 100.955); Console.WriteLine("{0:0.00}", 0.345); Console.WriteLine("{0:#,#0.00}", 1234.567); Console.WriteLine("{0:0.00;(0.00)}", 123); Console.WriteLine("{0:0.00;(0.00)}", -123); Console.WriteLine("{0:0.00;(0.00)}", 0); Console.WriteLine("{0:0.00;(0.00);Null}", 0);
8.4.2
// // // // // // //
5
100,96 _ 0,35 1.234,57 123,00 (123,00) 0,00 Null
6
Datumsformatierungen 7
Zur Formatierung von Datumswerten können Sie wie bei den Zahlwerten vordefinierte Formate verwenden oder das Format selbst definieren. Tabelle 8.17 beschreibt die Standard-Formatzeichen. Die Formatierungen beziehen sich auf das Datum 1.12.2010 13:30 (Winterzeit in Deutschland mit einer Stunde UTC-Offset). Beachten Sie, dass die Standardformate kulturspezifisch sind und dass einige Formatzeichen wie z. B. das D auch bei Zahlwerten verwendet werden, dort aber eine andere Bedeutung besitzen. Formatzeichen
Bedeutung
Ergebnis
D
Langes Datumsformat
Mittwoch, 1. Dezember 2010
d
Kurzes Datumsformat
01.12.2010
F
Langes Datum mit langer Zeit
Mittwoch, 1. Dezember 2010 13:30:00
f
Langes Datum mit kurzer Zeit
Mittwoch, 1. Dezember 2010 13:30
G
Kurzes Datum mit langer Zeit
01.12.2010 13:30:00
g
Kurzes Datumsformat mit kurzer Zeit
01.12.2010 13:30
M oder m
Tag mit Monat
01 Dezember
8 9 Tabelle 8.17: Die StandardFormatzeichen für Datumswerte
10
11
497
Grundlegende Programmiertechniken
Tabelle 8.17: Die StandardFormatzeichen für Datumswerte (Forts.)
Formatzeichen
Bedeutung
Ergebnis
O oder o
Mit diesem Format wird das Datum in einen kulturneutralen String konvertiert, der beim Zurückkonvertieren in ein Datum keine Verluste ergibt.
2010-12-01T13:30:00.0000000+1:00 (Dieses Ergebnis bezieht sich auf Deutschland im Winter = UTC-Zeit + 1 Stunde)
R oder r
RFC-1123a-Format. Das Datum wird in Wed, 01 Dec 2010 12:30:00 GMT (Dieses die aktuelle UTC-Zeit umgerechnet. Im Ergebnis bezieht sich auf Deutschland im Winter ergibt die Zeit 13:30 in Deutsch- Winter = UTC-Zeit + 1 Stunde) land (UTC+1) die UTC-Zeit 12:30.
s
Datum im Format »Sortable Date« (Sortierbares Datum). Achtung: Der UTC-Offset geht verloren!
2010-12-01T13:30:00
t
Kurzes Zeitformat
13:30
T
Langes Zeitformat
13:30:00
U oder u
Universales sortierbares Datumsformat (UTC-Datum). Das Datum wird in die aktuelle UTC-Zeit umgerechnet.
2010-12-01 12:30:00Z (Dieses Ergebnis bezieht sich auf Deutschland im Winter = UTC-Zeit + 1 Stunde)
Y oder y
Jahr und Monat
Dezember 2010
a. Dieser »Request For Comment« beschreibt die Anforderungen für Internet-Hosts (siehe www.ietf.org/ rfc/rfc1123.txt)
Das Format G ist wie bei Zahlformaten Standard bei der Konvertierung von Datumswerten in Strings. Zu der in der Tabelle häufiger verwendeten UTC-Zeit finden Sie mehr im Abschnitt »Arbeiten mit Datumswerten und Zeitspannen« (Seite 506). Datumswerte können Sie wie Zahlwerte auch über benutzerdefinierte Formate formatieren (Tabelle 8.18). Tabelle 8.18: Die Formatzeichen für benutzerdefinierte Datumsformate
498
Formatzeichen
Bedeutung
:
Das Zeit-Separatorzeichen
/
Das Datums-Separatorzeichen
%
Über das Prozentzeichen können Sie ein Formatzeichen, das auch als Standard-Formatzeichen interpretiert werden kann, explizit als Formatzeichen für ein benutzerdefiniertes Format definieren.
Beispielformat
Ergebnis für das Beispieldatum
%d
1
Formatierungen
Formatzeichen
Bedeutung
'xyz' oder "xyz"
Über Apostrophe oder Anführungszeichen können Sie in einem Formatstring Zeichenketten einfügen, die auch Zeichen mit Sonderbedeutung besitzen können.
\
Beispielformat
Ergebnis für das Beispieldatum
Tabelle 8.18: Die Formatzeichen für benutzerdefinierte Datumsformate (Forts.)
1
Über den Backslash können Sie die Sonderbedeutung von Zeichen aufheben.
2
d
Der Tag
d/MM
1.12
dd
Tag in zweistelliger Form. Tage unter 10 werden mit führender 0 ausgegeben.
dd/MM
01.12
3
ddd
Abgekürzter, lokalisierter Name ddd, dd/ des Tags
Mi, 01.
4
dddd
Lokalisierter Name des Tags
Mittwoch, 01.
M
Der Monat als ein- bis zweistel- d/M/ lige Zahl
1.12.
MM
Der Monat als zweistellige Zahl
d/MM
1.12.
MMM
Der abgekürzte Name des Monats
MMM
Dez
MMMM
Der volle Name des Monats
MMMM
Dezember
y, yy, yyy, yyyy, yyyyy
Das Jahr in ein- bis vierstelliger dd/MM/yyyy Form. Einige Kulturen besitzen eine andere Zeitrechnung als wir, weswegen ein- oder fünfstellige Jahre durchaus Sinn machen.
01.12.2010
h, hh
Die Stunde im 12-Stundenhh:mm Format (!) in der Form ohne und mit führender Null für einstellige Zahlen
01:30
H, HH
Die Stunde im 24-StundenHH:mm Format in der Form ohne und mit führender 0 bei einstelligen Zahlen
13:30
Die Minute in der Form ohne und mit führender 0 bei einstelligen Zahlen
13:30
m, mm
dddd, dd/
HH:mm
5
6
7
8 9
10
11
499
Grundlegende Programmiertechniken
Formatzeichen
Bedeutung
s, ss
Die Sekunde in der Form ohne HH:mm:ss und mit führender Null für einstellige Zahlen
13:30:00
f, ff … fffffff
repräsentiert 1/10 bis 1/10.000.000 einer Sekunde.
13:30:00.00
F, FF … FFFFFFF
repräsentiert 1/10 bis HH:mm:ss.FF 1/10.000.000 einer Sekunde. Rechts stehende Nullen werden aber nicht ausgegeben.
13:30:00
t, tt
Der in Kulturen ohne 24-Stun- Hh:mm tt den-Format verwendete Bezeichner für AM und PM. Ein t steht für den ersten Buchstaben, zwei t stehen für den ausgeschriebenen Namen.
01:30 PM (als Beispiel für die englische Kultur, in der deutschen resultiert nur 01:30)
%K
Der aktuelle Offset zur UTC-Zeit in Stunden und Minuten
%K
+01:00
%z, zz
Der aktuelle Offset zur UTC-Zeit in Stunden
%z
+1
%g, gg
repräsentiert die aktuelle Ära.
gg dd.MM.yyyy %g
n. Chr. 2010 n. Chr.
8.4.3 Formatierungen werden kulturspezifisch ausgeführt
Beispielformat
HH:mm:ss.ff
Ergebnis für das Beispieldatum
Kulturspezifisches Formatieren
Das .NET Framework verwendet bei Formatierungen kulturspezifische Einstellungen. Die Standardformate werden grundsätzlich kulturspezifisch ausgegeben, bei benutzerdefinierten Formaten gelten kulturspezifische Unterschiede für Dezimaltrennzeichen, Tausendertrennzeichen, Zeit- und Datumstrennzeichen, für den AM/PM-String und für die Namen von Tagen, Monaten und Äras. Auf einem deutschen System wird ein Datum z. B. per Voreinstellung im deutschen Format ausgegeben, wenn Sie dieses über die ToString-Methode der DateTimeOffset-Struktur ohne oder mit einem der Standardformate formatieren. Den Methoden zur Formatierung können Sie in der Regel aber auch ein Objekt übergeben, das die IFormatProvider-Schnittstelle implementiert. Einen solchen Typen werden Sie kaum selbst implementieren. Die Instanzen der CultureInfo-Klasse aus dem Namensraum System.Globalization, die Sie über CultureInfo.CreateSpecificCulture für alle Kulturen erzeugen können, implementieren diese Schnittstelle aber bereits. Ich will hier nicht näher auf das Thema »Lokalisierung« eingehen, da dieses in Kapitel 15 separat behandelt wird. Deshalb zeige ich nur kurz, wie Sie ein Datum im US-amerikanischen, im irischen und im spanischen Format formatieren können:
500
Eingaben überprüfen und parsen
Listing 8.43: Kulturspezifisches Formatieren DateTime date = new DateTime(2010, 12, 31); Console.WriteLine(String.Format( CultureInfo.CreateSpecificCulture("en-US"), "Datum im US-amerikanischen Format: {0:d}", date)); Console.WriteLine(String.Format( CultureInfo.CreateSpecificCulture("en-IE"), "Datum im irischen Format: {0:d}", date)); Console.WriteLine(String.Format( CultureInfo.CreateSpecificCulture("es-ES"), "Datum im spanischen Format: {0:d}", date));
8.5
1
2
Eingaben überprüfen und parsen
Eingaben müssen häufig daraufhin überprüft werden, ob diese einen gültigen Wert ergeben. Dies gilt besonders dann, wenn Zahl- oder Datumswerte eingegeben werden. Die Konvertierung eines Strings, der keine gültige Zahl bzw. kein gültiges Datum verwaltet, in einen numerischen oder Datums-Typ führt ansonsten zu einer Ausnahme vom Typ FormatException. Eingaben können Sie über das Abfangen dieser überprüfen, was aber in der Praxis keine allzu gute Idee ist. Sie sollten dazu besser die TryParse-Methode verwenden, die viele Standardtypen besitzen und die die Eingabe gleich auch parst und konvertiert. Beim Parsen, das auch über die Parse-Methode möglich ist, haben Sie zudem weitere Möglichkeiten, wie die Angabe des Stils von numerischen oder Datumswerten und das Parsen spezifischer Datumsformate.
8.5.1
3
4
5
Eingaben überprüfen
Zur Überprüfung einer Eingabe auf einen gültigen Wert können Sie wie gesagt einfach die erzeugte FormatException abfangen:
6
Listing 8.44: Überprüfen einer Eingabe über das Abfangen der bei ungültigen Werten geworfenen FormatException
7
Console.Write("Geben Sie eine Dezimalzahl ein: "); string input = Console.ReadLine(); double number = 0; try { number = Convert.ToDouble(input); Console.WriteLine("Die Eingabe ist gültig: " + number); } catch (FormatException) { Console.WriteLine("Die Eingabe ist ungültig"); }
Eine wesentlich bessere Möglichkeit ist aber, die TryParse-Methode zu verwenden, die alle numerischen und die Typen Boolean, DateTime, DateTimeOffset und TimeSpan besitzen. Mit TryParse machen Sie Hackern das Leben schwer. Bei einer Eingabeprüfung über das Abfangen einer Ausnahme kann es nämlich möglich sein, dass Hacker bei Versuchen mit bewusst ungültigen Eingaben ein Fehlverhalten der Anwendung provozieren. Das kann z. B. dann der Fall sein, wenn nicht alle Ausnahmen abgefangen werden und der Hacker bewusst eine bestimmte Ausnahme mit speziellen, ungültigen Daten auslöst.
8 9
TryParse ist zur Überprüfung besser geeignet
10
11 TIPP
501
Grundlegende Programmiertechniken
TryParse versucht eine Konvertierung des übergebenen Werts und gibt bei Erfolg true zurück. In diesem Fall wird der konvertierte Wert in die am out-Argument result übergebene Variable geschrieben: Listing 8.45: Überprüfen einer Eingabe über die TryParse-Methode Console.Write("Geben Sie eine Dezimalzahl ein: "); string input = Console.ReadLine(); double number; if (Double.TryParse(input, NumberStyles.Float, CultureInfo.CurrentCulture, out number)) { Console.WriteLine("Die Eingabe ist gültig: " + number); } else { Console.WriteLine("Die Eingabe ist ungültig"); }
HALT
Dass ich die komplexere Variante der TryParse-Methode verwende, der neben dem zu parsenden String und der Rückgabe-Variablen zwei weitere Argumente übergeben werden, hat seinen Sinn: Beim Parsen und Konvertieren verwenden die entsprechenden Methoden für Dezimalzahlen nämlich einen Stil, der das Tausendertrennzeichen zulässt. Da dieses Zeichen beim Parsen einfach ignoriert wird, kann es in der Praxis zu Problemen kommen. Wenn der Anwender nämlich aus Versehen bei der Eingabe einer Zahl das englische Format verwendet und z. B. auf einem deutschen System 1.23 (statt 1,23) eingibt, resultiert ein vollkommen falscher Wert (im Beispiel 123). Um dieses potenzielle Problem zu vermeiden, übergebe ich am zweiten Argument den Stil NumberStyles.Float, der das Tausendertrennzeichen ausschließt. Da es keine Überladung der TryParse-Methode gibt, in der nur die Eingabe, der Stil und die RückgabeVariable übergeben werden, muss am dritten Argument zusätzlich noch die Kultur definiert werden. Deshalb übergebe ich an diesem Argument die aktuelle Kultur. Sie sollten beachten, dass das Tausendertrennzeichen-Problem grundsätzlich bei einem Konvertieren von Strings in Dezimalwerte auftritt, auch wenn Sie mit den Methoden der Convert-Klasse konvertieren. Die folgende Auflistung fasst die Varianten der TryParse-Methode zusammen: ■ ■ ■ ■ ■ ■ ■
bool Zahltyp.TryParse(string s, [NumberStyles style, IFormatProvider provider,] out int result) bool Datumstyp.TryParse(string input, out Datumstyp result) bool Datumstyp.TryParse(string input, IFormatProvider formatProvider, DateTimeStyles styles, out Datumstyp result) bool TimeSpan.TryParse(string s, out TimeSpan result) bool Char.TryParse(string s, out char result) bool Boolean.TryParse(string value, out bool result) bool Enum.TryParse(string s, [NumberStyles style, IFormatProvider provider,] out uint result)
Ein kleiner Nachteil von TryParse ist, dass diese Methode langsamer ausgeführt wird als die Prüfung der Exception (vergleichen Sie hierzu das Beispielprogramm zu diesem Abschnitt). Das Ganze bewegt sich aber im Mikrosekundenbereich, weswegen Sie diesen Nachteil wohl vernachlässigen können.
502
Eingaben überprüfen und parsen
Der neben der Verhinderung von Hacker-Angriffen weitere Vorteil ist, dass Sie optional ein Objekt übergeben können, das die IFormatProvider-Schnittstelle implementiert, um zu erreichen, dass beim Parsen eine bestimmte Formatierung (oder eher: eine bestimmte Kultur) verwendet wird. Ein solches Objekt kann auch der ParseMethode übergeben werden, bei deren Besprechung ich auch darauf eingehe.
8.5.2
Parsen von Strings
Strings können Sie mit TryParse in einen Zahl-, Datums-, TimeSpan- oder BooleanWert parsen, wie ich es im vorhergehenden Abschnitt gezeigt habe. Sie können dazu aber auch die Parse-Methode verwenden, die wie TryParse in allen numerischen, in Boolean, DateTime, DateTimeOffset und TimeSpan zur Verfügung steht. Diese Methode gibt das Ergebnis als Rückgabewert zurück. Kann der String nicht konvertiert werden, resultiert eine FormatException.
1 Parse erlaubt bei numerischen und Datumstypen die Definition des Stils und der Kultur
Die folgende Auflistung fasst die einzelnen Parse-Varianten der verschiedenen Typen zusammen: ■ ■ ■ ■ ■ ■ ■
2
3
Zahltyp Zahltyp.Parse(string s [, NumberStyles style [, IFormatProvider provider]]) Zahltyp Zahltyp.Parse(string s, IFormatProvider provider) Datumstyp Datumstyp.Parse(string input [[, IFormatProvider formatProvider [, DateTimeStyles styles]]) TimeSpan TimeSpan.Parse(string s) char Char.Parse(string s) bool Boolean.Parse(string value) Object Enum.Parse(Type enumType, string value [, bool ignoreCase])
4
5
6
Den zu parsenden String übergeben Sie am ersten Argument. Beim Parsen von numerischen und Datumswerten können Sie am Argument style(s) definieren, wie der String aussehen darf. So können Sie z. B. festlegen, ob Whitespace-Zeichen am Anfang und Ende des Strings erlaubt sind. Am Argument provider können Sie bei Zahl- und Datumswerten ein Objekt übergeben, das die Formatierung eines Strings für den jeweiligen Wert bestimmt.
7
8
Parse hat gegenüber den Methoden der Convert-Klasse die Vorteile, dass Sie bei den numerischen und den Datumstypen den Stil der Eingabe definieren können (indem Sie z. B. Whitespace-Zeichen zulassen) und dass Sie beim Parsen auch eine andere als die aktuelle Kultur angeben können.
9
Wenn Sie ohne Angabe des Stils und der Kultur parsen, werden die aktuelle Kultur und der Stil der aktuellen Kultur verwendet. Auf einem deutschen System wird eine Zahl z. B. mit Komma als Dezimaltrennzeichen erwartet. Ein Datum wird im deutschen Format oder (was sehr interessant ist) in einem der neutralen Formaten O, R, s und U (siehe bei »Datumsformatierungen« auf Seite 497) erwartet:
10
Listing 8.46: Parsen von Zahl- und Datumswerten in der aktuellen Kultur
11
string input = "1,234"; double number = Double.Parse(input); Console.WriteLine("{0} wurde nach {1} geparst", input, number); input = "1.12.2010 13:30"; // Deutsches Format DateTimeOffset date = DateTimeOffset.Parse(input); Console.WriteLine("{0} wurde nach {1} geparst", input, date);
503
Grundlegende Programmiertechniken
input = "2010-12-01T13:30:00.0000000+1:00"; // Das verlustlose Format "O" date = DateTimeOffset.Parse(input); Console.WriteLine("{0} wurde nach {1} geparst", input, date); input = "Wed, 01 Dec 2010 12:30:00 GMT"; // Das RFC-1123-Format "R" date = DateTimeOffset.Parse(input); Console.WriteLine("{0} wurde nach {1} geparst", input, date); input = "2010-12-01T13:30:00"; // Das Sortable-Date-Format "s" date = DateTimeOffset.Parse(input); Console.WriteLine("{0} wurde nach {1} geparst", input, date); input = "2010-12-01 12:30:00Z"; // Das UTC-Format "U" date = DateTimeOffset.Parse(input); Console.WriteLine("{0} wurde nach {1} geparst", input, date); 1,234 wurde nach 1,234 geparst 1.12.2010 13:30 wurde nach 01.12.2010 13:30:00 +01:00 geparst 2010-12-01T13:30:00.0000000+1:00 wurde nach 01.12.2010 13:30:00 +01:00 geparst Wed, 01 Dec 2010 12:30:00 GMT wurde nach 01.12.2010 12:30:00 +00:00 geparst 2010-12-01T13:30:00 wurde nach 01.12.2010 13:30:00 +01:00 geparst 2010-12-01 12:30:00Z wurde nach 01.12.2010 12:30:00 +00:00 geparst
HALT
Das style(s)-Argument erlaubt die Definition spezieller Stile
Wie bereits gesagt sollten Sie beim Parsen und Konvertieren von Zahlwerten vorsichtig sein, da per Voreinstellung Tausendertrennzeichen erlaubt sind und beim Parsen ignoriert werden. Geben Sie beim Parsen von Dezimalzahlwerten besser den Stil NumberStyles.Float an. Um den Stil zu definieren, erlauben die TryParse- und die Parse-Varianten der numerischen Typen am Argument style die Übergabe von Werten der NumberStylesAufzählung. Bei den Datumstypen werden am Argument styles Werte der DateTimeStyles-Aufzählung übergeben. Beide Aufzählungen entstammen dem Namensraum System.Globalization. Die Werte sind recht vielfältig und werden in der Praxis sehr selten benötigt, weil der Defaultstil den in der jeweiligen Kultur verwendeten Literalen entspricht. Das style(s)-Argument ist ein Bitfeld, Sie können also mehrere der Konstanten über | kombinieren. Bei Zahlen können Sie über die NumberStyles-Konstante AllowDecimalPoint z. B. festlegen, dass ein Dezimaltrennzeichen erlaubt ist, AllowThousands erlaubt ein Tausender-Trennzeichen, AllowLeadingSign ein führendes Vorzeichen und AllowCurrencySymbol ein Währungssymbol. Mit diesen und den anderen Konstanten können Sie recht genau festlegen, welches Format die Zahl besitzen darf. NumberStyles besitzt daneben aber auch Konstanten für vordefinierte Formate, die sich größtenteils über ihren Namen erklären: Currency, Float, HexNumber, Integer und Number (wie Float, nur mit Tausendertrennzeichen). Ähnlich sieht es bei der DateTimeStyles-Aufzählung aus mit den (wichtigen) Werten AllowInnerWhite (innere Whitespace-Zeichen zulassen), AllowLeadingWhite (ein führendes Whitespace-Zeichen zulassen), AllowTrailingWhite (ein abschließendes Whitespace-Zeichen zulassen) und AllowWhiteSpaces (Whitespace-Zeichen grundsätzlich zulassen). Das folgende Listing parst einen Double-Wert an der Konsole. Es erlaubt einmal nur eine Zahl mit Dezimalzeichen, ohne Tausendertrennzeichen und ohne WhitespaceZeichen, und dann eine Zahl mit Dezimaltrennzeichen, ohne Tausendertrennzeichen und mit Whitespace-Zeichen:
504
Eingaben überprüfen und parsen
Listing 8.47: Parsen mit speziell definierten Stilen string input = ... // Zahlen nur mit Dezimalpunkt zulassen (kein Tausendertrennzeichen, // keine Whitespace-Zeichen, kein Währungssymbol) double number = Double.Parse(input, NumberStyles.AllowDecimalPoint); // Zahlen mit Dezimalpunkt, ohne Tausendertrennzeichen und mit optionalem // Whitespace-Zeichen am Anfang und Ende zulassen number = Double.Parse(input, NumberStyles.Float);
Viel interessanter als die Angabe des Stils ist die Möglichkeit, am Argument provider ein Objekt zu übergeben, das die IFormatProvider-Schnittstelle implementiert. Sie können theoretisch hier spezielle Objekte für spezifische Formate übergeben. In der Praxis muss eine Zahl oder ein Datum aber meist lediglich für eine spezielle Kultur geparst werden. In diesem Fall übergeben Sie eine Instanz der CultureInfo-Klasse (aus dem Namensraum System.Globalization), die die jeweilige Kultur repräsentiert. Diese Möglichkeit wurde bereits im Abschnitt »Kulturspezifisches Formatieren« (Seite 500) behandelt.
1 Am Argument provider können Sie eine spezifische Kultur übergeben
2
3
So können Sie z. B. ein Datum parsen, das im US-amerikanischen Format vorliegt:
4
Listing 8.48: Parsen mit einer spezifischen Kultur input = "12/31/2010 01:30 PM"; date = DateTimeOffset.Parse(input, CultureInfo.CreateSpecificCulture("en-US"));
8.5.3
5
Parsen spezifischer Datumsangaben
Die Methoden ParseExcact und TryParseExcact der Datumstypen ermöglichen ein Parsen von Datumsangaben, die in einem spezifischen Format vorliegen. Dazu geben Sie am Argument format bzw. formats ein bzw. mehrere Strings an, die das Format beschreiben.
TryParseExact und ParseExact ermöglichen das Parsen exotischer Datums-Formate
6
7
Damit können Sie auch exotische oder spezielle Datumsformate umwandeln. Die folgende Auflistung zeigt die Syntax dieser Methoden: ■ ■ ■ ■
8
bool Datumstyp.TryParseExact(string input, string format, IFormatProvider formatProvider, DateTimeStyles styles, out Datumstyp result) bool Datumstyp.TryParseExact(string input, string[] formats, IFormatProvider formatProvider, DateTimeStyles styles, out DateTimeOffset result) Datumstyp Datumstyp.ParseExact(string input, string format, IFormatProvider formatProvider [, DateTimeStyles styles]) Datumstyp Datumstyp.ParseExact(string input, string[] formats, IFormatProvider formatProvider, DateTimeStyles styles)
9
10
Ein Datum im (erfundenen) englischsprachigen Spezialformat »Fri, 2010-31-12 12:30« können Sie z. B. so parsen:
11
Listing 8.49: Parsen eines speziellen Datumsformats string input = "Fri, 2010-31-12 13:30"; string format = "ddd, yyyy-dd-MM HH:mm"; DateTimeOffset date = DateTimeOffset.ParseExact(input, format, CultureInfo.CreateSpecific¬Culture("en"));
505
Grundlegende Programmiertechniken
8.6 DateTime und DateTimeOffset verwalten ein Datum als Anzahl Ticks
Arbeiten mit Datumswerten und Zeitspannen
Zur Arbeit mit Datumswerten stehen Ihnen die Strukturen DateTime und DateTimeOffset aus dem Namensraum System zur Verfügung. Beide Strukturen verwalten ein Datum als Anzahl von Ticks, die seit dem 01.01.0001 00:00:00 (UTC-Zeit) vergangen sind. Ein Tick entspricht 100 Nanosekunden. Die aktuellen Ticks können Sie aus der Ticks-Eigenschaft auslesen. DateTimeOffset ist neuer und verwaltet grundsätzlich immer neben dem Datum und der Zeit den Offset zur UTC-Zeit. Bei DateTime ist dies allerdings nicht immer gegeben.
8.6.1 Der Offset zur UTCZeit ist wichtig für Datenübertragungen
Der Offset zur UTC-Zeit
UTC (Universal Time Coordinated), die früher als Greenwich Mean Time (GMT) bezeichnet wurde, bezieht sich auf die aktuelle Zeit in der englischen Ortschaft Greenwich und wird als internationale Basis für Zeitangaben verwendet. Die Zeit in Deutschland besitzt im Herbst/Winter einen Offset von +1 und im Frühling/Sommer einen Offset von +2 Stunden zu UTC. UTC ist wichtig für den Datenaustausch mit anderen Zeitzonen. Wenn Sie z. B. mit einem Partner in Neuseeland eine Zeit ausmachen, können Sie nicht einfach nur 12:00 angeben, da damit nicht ausgesagt ist, welche Zeitzone Sie meinen. Wenn Sie allerdings 12:00 UTC-Zeit angeben, kann diese problemlos in eine lokale Zeitangabe umgerechnet werden, da auf einem System die Zeitzone (und damit der UTC-Offset) bekannt ist.
DateTimeOffset verwaltet immer einen UTC-Offset
Damit beim Datenaustausch mit anderen Systemen keine Probleme auftreten können, verwaltet die neue DateTimeOffset-Struktur grundsätzlich immer einen Offset zur UTC-Zeit. Wenn Sie in Deutschland im Winter z. B. eine neue DateTimeOffsetInstanz über die Parse-Methode erzeugen, wird als UTC-Offset +1 eingetragen. Den Offset können Sie aus der Offset-Eigenschaft auslesen, das auf UTC umgerechnete Datum aus UtcDateTime: Listing 8.50: Ausgeben des Datums, des UTC-Offset und des entsprechenden UTC-Datums mit einer DateTimeOffset-Instanz DateTimeOffset date1 = DateTimeOffset.Parse("31.12.2010 12:30"); Console.WriteLine("Datum: " + date1.ToString()); // 31.12.2010 12:30:00 +01:00 Console.WriteLine("UTC-Offset: " + date1.Offset); // 01:00:00 Console.WriteLine("UTC-Datum: " + date1.UtcDateTime.ToString()); // 31.12.2010 11:30:00
Eine DateTimeOffset-Instanz kann problemlos beim Datenaustausch zwischen verschiedenen Systemen verwendet werden. Wenn Sie ein solches Datum z. B. in einer XML-Datei speichern, enthält der resultierende String neben dem lokalen Datum auch den UTC-Offset. Wird die XML-Datei auf einem anderen System mit einer anderen Zeitzone eingelesen und das String-Datum in eine DateTimeOffset-Instanz konvertiert, wird das Datum inklusive UTC-Offset korrekt gespeichert. Über die ToLocalTime-Methode können Sie das Datum in die lokale Zeit konvertieren. Listing 8.51 zeigt dies am Beispiel eines Strings, der mit dem UTC-Offset +10 (Zeitzone von Sydney) definiert ist:
506
Arbeiten mit Datumswerten und Zeitspannen
Listing 8.51: Arbeiten mit Datumswerten in einer anderen Zeitzone string sydneyDate = "31.12.2010 12:30:00 +10:00"; DateTimeOffset date2 = DateTimeOffset.Parse(sydneyDate); Console.WriteLine("Das Sydney-Datum: " + date2); // 31.12.2010 12:30:00 +10:00" Console.WriteLine("In ein lokales Datum konvertiert: " + date2.ToLocalTime()); // 31.12.2010 03:30:00 +01:00"
Bei der Arbeit mit DateTimeOffset müssen Sie also immer aufpassen, da es sich nicht unbedingt um ein lokales Datum handeln muss. Wenn im obigen Beispiel das Datum z. B. mit ToString("G") ausgegeben wird, resultiert "31.12.2010 12:30:00". Vom UTCOffset sehen Sie dann nichts mehr. Verwenden Sie ToLocalTime, um das Datum als lokales Datum auszuwerten. ToLocalTime gibt eine DateTimeOffset-Instanz mit dem aktuellen UTC-Offset zurück. Berücksichtigen Sie bei eigenen Berechnungen mit einer DateTimeOffset-Instanz immer den UTC-Offset, den Sie aus der Offset-Eigenschaft auslesen können. Bei Standardoperationen wie dem Hinzuzählen von Tagen auf ein Datum oder der Berechnung der Differenz zwischen zwei Datumswerten wird dieser Offset allerdings automatisch berücksichtigt. Die ältere DateTime-Struktur ist etwas schwieriger in Bezug auf die Berücksichtigung von Zeitzonen. DateTime besitzt nämlich einen internen Typ, den Sie aus dem Feld Kind auslesen können. Der Wert kann einer der folgenden sein: ■ ■ ■
1 HALT
2
3
DateTime verwaltet nicht immer einen UTCOffset
5
DateTimeKind.Local: Lokales Datum DateTimeKind.Utc: UTC-Datum DateTimeKind.Unspecified: Unspezifiziertes Datum
6
Die ersten beiden Typen machen keine Probleme. Bei einem lokalen Datum wird der UTC-Offset intern gespeichert. Ein lokales Datum kann (über ToUniversalTime) nach UTC, ein UTC-Datum (über ToLocalTime) in ein lokales konvertiert werden. Bei der Auswertung müssen Sie natürlich wissen, was für einen Typ das Datum besitzt. Ansonsten geben Sie ggf. ein UTC-Datum aus und nehmen an, es handle sich um ein lokales. Wie bei DateTimeOffset hilft hier die Methode ToLocalTime.
7
Ein lokales Datum erhalten Sie z. B. über DateTime.Now, ein UTC-Datum über DateTime.UtcNow oder die ToUniversalTime-Methode einer DateTime-Instanz. Der Typ Unspecified ist aber problematisch, denn bei einem solchen Datum können Sie nicht sagen, ob es sich um ein lokales oder ein UTC-Datum handelt. Außerdem verwaltet ein unspezifiziertes Datum keinen Offset zur UTC-Zeit.
4
8 Unspecified ist problematisch
9
Diesen DateTime-Typ erhalten Sie in der Praxis sehr schnell, nämlich immer dann, wenn Sie eine DateTime-Instanz über den Konstruktor initialisieren oder über die Parse-Methode oder über Convert.ToDateTime erzeugen:
10
DateTime date3 = new DateTime(2010, 12, 31, 12, 30, 0); // Unspecified DateTime date4 = DateTime.Parse("31.12.2010 12:30"); // Unspecified DateTime date5 = Convert.ToDateTime("31.12.2010 12:30"); // Unspecified
11
DateTime date6 = DateTime.Now; // Local DateTime date7 = DateTime.UtcNow; // Utc
Mit einem unspezifizierten Datum können Sie eigentlich nichts anfangen. Und genau das war der Grund für die neue DateTimeOffset-Struktur, die ein solches Datum nicht kennt.
507
Grundlegende Programmiertechniken
Falls Sie mit DateTime arbeiten wollen (was ich nicht empfehle), sollten Sie beim Erzeugen den Typ angeben. Beim Parsen oder Konvertieren können Sie den Typ nicht spezifizieren, aber Sie können den UTC-Offset mit angeben: DateTime date8 = new DateTime(2010, 12, 31, 12, 30, 0, DateTimeKind.Local); // Local DateTime date9 = DateTime.Parse("31.12.2010 12:30 +01:00"); // Local DateTime date10 = Convert.ToDateTime("31.12.2010 12:30 +01:00"; // Local
TIPP
Vergessen Sie DateTime aber besser. Die neuere DateTimeOffset-Struktur verwendet beim Parsen oder Konvertieren immer den lokalen UTC-Offset, wenn kein UTC-Offset angegeben ist. Über die Felder DateTime (mit Einschränkung, siehe Seite 510) oder UtcDateTime können Sie bei Bedarf eine DateTime-Instanz mit dem lokalen Datum bzw. dem UTC-Datum ermitteln, z. B. um diese an eine ältere Methode zu übergeben, die DateTimeOffset nicht kennt. Beachten Sie dazu den Abschnitt »Konvertieren nach DateTime« auf Seite 510.
8.6.2
Wichtige Eigenschaften und Methoden der DateTimeOffset-Struktur
Datumswerte speichern Sie also idealerweise in einer Instanz der DateTimeOffsetStruktur. Diese Struktur besitzt einige statische Eigenschaften und Methoden, die in Tabelle 8.19 beschrieben werden. Die wichtigen Instanzmethoden und -eigenschaften finden Sie in Tabelle 8.20 und Tabelle 8.21. Tabelle 8.19: Die wichtigen Klasseneigenschaften und -methoden der DateTimeOffsetStruktur
Eigenschaft / Methode
Beschreibung
DateTimeOffset Now
liefert das aktuelle lokale Datum.
DateTimeOffset UtcNow
liefert das aktuelle UTC-Datum.
DateTimeOffset MinValue
liefert das kleinstmögliche Datum.
DateTimeOffset MaxValue
liefert das größtmögliche Datum.
DateTimeOffset Parse( string input [[, IFormatProvider
parst einen String in eine DateTimeOffset-Instanz. Am zweiten Argument können Sie eine CultureInfo-Instanz übergeben, um einen String zu parsen, der in einer anderen als der aktuellen Kultur vorliegt. Am dritten Argument können Sie mit den Werten der DateTimeStyles-Aufzählung aus dem Namensraum System.Globalization Regeln zum Parsen festlegen. AllowWhiteSpaces legt z. B. fest, dass Whitespace-Zeichen erlaubt sind. Wenn Sie dieses Argument nicht angeben, werden die Regeln der aktuellen bzw. übergebenen Kultur verwendet. Neben Strings, die der verwendeten Kultur entsprechen, können Sie auch Strings in den neutralen Formaten O, R, s und U (siehe bei »Datumsformatierungen« auf Seite 497) parsen. Parse erzeugt eine FormatException, wenn der String kein gültiges Datum speichert.
formatProvider [, DateTimeStyles styles]]
DateTimeOffset ParseExact(…) bool TryParse(…) bool TryParseExact(…)
508
Diese Methoden verwenden Sie zum Parsen von Datumswerten. Das Parsen wurde bereits auf Seite 501 behandelt.
Arbeiten mit Datumswerten und Zeitspannen
Instanzmethode
Beschreibung
DateTimeOffset Add( TimeSpan timespan)
gibt ein um die angegebene Zeitspanne (siehe Seite 512) oder Anzahl Millisekunden, Sekunden, Minuten, Stunden, Tage, Monate bzw. Jahre addiertes Datum als DateTimeOffset-Instanz zurück. Um zu subtrahieren übergeben Sie einen negativen Wert.
DateTimeOffset AddMilliseconds( double millisecods)
Tabelle 8.20: Die wichtigsten Instanzmethoden der DateTimeOffsetStruktur
1
DateTimeOffset AddSeconds( double secods) DateTimeOffset AddMinutes( double minutes)
2
DateTimeOffset AddHours( double hours) DateTimeOffset AddDays( double days)
3
DateTimeOffset AddMonths( double months) DateTimeOffset AddYears( double years)
4
DateTimeOffset ToLocalTime()
gibt ein DateTimeOffset-Objekt zurück, dessen Datum in die lokale Zeit konvertiert wurde.
DateTimeOffset ToUniversalTime()
gibt ein DateTimeOffset-Objekt zurück, dessen Datum in die UTC-Zeit konvertiert wurde.
DateTimeOffset ToOffset( TimeSpan offset)
gibt ein DateTimeOffset-Objekt zurück, dessen Datum in ein Datum mit dem angegebenen UTC-Offset konvertiert wurde.
Instanzeigenschaft
Bedeutung
long Ticks
liefert die Ticks (100 Nanosekunden-Intervalle), die das Datum ausmachen.
Millisecond, Second, Minute, Hour, Day, Month, Year
Über diese Eigenschaften vom Typ int können Sie die Teile des Datums ermitteln.
DayOfWeek DayOfWeek
gibt den Wochentag als DayOfWeek-Typ zurück. DayOfWeek ist eine Aufzählung mit den Werten Monday, Tuesday, Wednesday, Thursday, Friday, Saturday und Sunday.
int DayOfYear
gibt die Nummer des Tages bezogen auf das Jahr zurück.
TimeSpan Offset
liefert den UTC-Offset.
DateTime DateTime
liefert ein DateTime-Objekt mit dem Typ Unspecified, das mit dem Datum initialisiert wird, ohne den UTC-Offset zu berücksichtigen. Vorsicht: Der UTC-Offset geht dabei verloren und das zurückgegebene DateTimeObjekt ist vom Typ Unspecified. Wenn das DateTimeOffset-Objekt z. B. das Datum 31.12.2010 23:00 -02:00 verwaltet, ergibt DateTime für die deutsche Kultur (mit dem UTC-Offset +1 im Winter) das Datum 31.12.2010 23:00:00, obwohl es sich eigentlich um den 1.1.2011 02:00:00 handelt. Auf Seite 510 finden Sie eine Lösung.
5
6 Tabelle 8.21: Die wichtigsten Instanzeigenschaften der DateTimeOffsetStruktur
7
8 9
10
11
509
Grundlegende Programmiertechniken
Tabelle 8.21: Die wichtigsten Instanzeigenschaften der DateTimeOffsetStruktur (Forts.)
Instanzeigenschaft
Bedeutung
DateTime UtcDateTime
liefert ein DateTime-Objekt vom Typ DateTimeKind.Utc mit dem UTC-Datum.
TimeSpan TimeOfDay
liefert den Zeitanteil des Datums. Vorsicht: Wie bei DateTime geht der UTC-Offset verloren.
DateTime Date
liefert den Datumsteil des Datums (mit 00:00:00 als Zeit). Vorsicht: Wie bei DateTime geht der UTC-Offset verloren und das DateTime-Objekt ist vom Typ Unspecified:
8.6.3
Grundlegende Arbeit mit DateTimeOffset
Die folgenden Beispiele sollen den grundlegenden Umgang mit der DateTimeOffsetStruktur verdeutlichen: Listing 8.52: Grundlegendes Arbeiten mit DateTimeOffset // Aktuelles Datum ermitteln DateTimeOffset now = DateTimeOffset.Now; Console.WriteLine("Heutiges Datum: " + now); // 3 Monate aufaddieren DateTimeOffset date = now.AddMonths(3); Console.WriteLine("Heute + 3 Monate: {0}", date.ToString()); // 3 Monate abziehen date = now.AddMonths(-3); Console.WriteLine("Heute - 3 Monate: {0}", date.ToString()); // Wochentag ermitteln DayOfWeek dayOfWeek = now.DayOfWeek; Console.WriteLine("Tag in der Woche {0}: {1}", (int)dayOfWeek, dayOfWeek.ToString()); // Tag im Jahr ermitteln Console.WriteLine("Tag im Jahr: {0}", now.DayOfYear); // Tag im Monat ermitteln Console.WriteLine("Tag im Monat: {0}", now.Day); // Monat ermitteln Console.WriteLine("Monat: {0}", now.Month); // Jahr ermitteln Console.WriteLine("Jahr: {0}", now.Year);
8.6.4 Bei DateTime, Date und TimeOfDay geht der UTCOffset verloren
510
Konvertieren nach DateTime
Für den Fall, dass Sie ein DateTimeOffset-Objekt nach DateTime konvertieren müssen, weil Sie dieses z. B. an eine Methode übergeben müssen, sollten Sie vorsichtig sein. Wie ich bereits angemerkt habe, geht bei der Verwendung der Eigenschaften DateTime, Date und TimeOfDay der UTC-Offset verloren. DateTime und Date ergeben zudem ein Datum vom Typ Unspecified:
Arbeiten mit Datumswerten und Zeitspannen
Listing 8.53: Demonstration des Problems mit der einfachen Konvertierung nach DateTime DateTimeOffset date = DateTimeOffset.Parse("31.12.2010 23:00 -02:00"); Console.WriteLine("Datum: " + date.ToString()); Console.WriteLine("Lokales Datum: " + date.ToLocalTime().ToString()); Console.WriteLine("DateTime ergibt:" + date.DateTime.ToString()); Console.WriteLine("Date ergibt:" + date.Date.ToString()); Console.WriteLine("TimeOfDay ergibt:" + date.TimeOfDay.ToString()); Console.WriteLine("Typ von DateTime:" + date.DateTime.Kind); Console.WriteLine("Typ von Date:" + date.Date.Kind);
1
Das Beispiel ergibt: Datum: 31.12.2010 23:00:00 -02:00 Lokales Datum: 01.01.2011 02:00:00 +01:00 DateTime ergibt: 31.12.2010 23:00:00 Date ergibt: 31.12.2010 00:00:00 TimeOfDay ergibt: 23:00:00 Typ von DateTime: Unspecified Typ von Date: Unspecified
2
3
Wie Sie sehen, sind die Datumswerte, die Date, DateTime und TimeOfDay ergeben, falsch. Der Typ der resultierenden DateTime-Objekte ist außerdem Unspecified, was daran liegt, dass dieser wegen des flexiblen UTC-Offset von DateTimeOffset gar nicht festgelegt werden kann. Das Problem können Sie lösen, indem Sie das DateTimeOffset-Objekt vor dem Konvertieren nach DateTime über ToLocalTime in ein lokales oder über ToUniversalTime in ein UTC-Datum konvertieren. Dummerweise geben DateTime und Date dann aber immer noch ein Datum vom Typ Unspecified zurück. Meine Lösung des Problems ist die folgende:
4 Konvertieren Sie selbst in ein lokales oder UTCDatum
5
6 Listing 8.54: Lösung des Problems des Konvertierens nach DateTime, ohne den UTC-Offset zu verlieren Console.WriteLine("Lösung: Konvertieren in ein lokales Datum:"); DateTime dateTimeValue = new DateTime(date.ToLocalTime().Ticks, DateTimeKind.Local); Console.WriteLine(dateTimeValue.ToString()); // 01.01.2011 02:00:00 Console.WriteLine(dateTimeValue.Kind); // Local
7
8
Console.WriteLine("Lösung: Konvertieren in ein UTC-Datum:"); dateTimeValue = new DateTime(date.ToUniversalTime().Ticks, DateTimeKind.Utc); Console.WriteLine(dateTimeValue.ToString()); // 01.01.2011 01:00:00 Console.WriteLine(dateTimeValue.Kind); // Utc
9
Die Methoden, denen Sie ein DateTime-Objekt übergeben, sollten mit einem lokalen oder UTC-Datum korrekt umgehen können.
8.6.5
Datumswerte vergleichen
10
Datumswerte können Sie über die Vergleichsoperatoren direkt vergleichen:
11
Listing 8.55: Vergleichen von Datumswerten DateTimeOffset date = DateTimeOffset.Parse("31.12.2010"); if (DateTimeOffset.Now >= date) { Console.WriteLine("Schön, dass Sie dieses " + "Beispiel noch Ende 2010 testen."); }
511
Grundlegende Programmiertechniken
else { Console.WriteLine("Schade, da müssen wir wohl noch warten ..."); }
8.6.6
Mit Zeitspannen arbeiten
Die Struktur TimeSpan verwaltet eine Zeitspanne. Eine TimeSpan-Instanz erhalten Sie u. a. dann, wenn Sie eine DateTime- oder DateTimeOffset-Instanz von einer anderen subtrahieren. Listing 8.56: Ermitteln der Differenz zwischen zwei Datumswerten DateTimeOffset date1 = DateTimeOffset.Parse("1.1.2010"); DateTimeOffset date2 = DateTimeOffset.Now; TimeSpan difference = date1 - date2; Console.WriteLine("Zwischen {0} und {1} liegt die Zeitspanne {2}", date2, date1, difference);
Das Ergebnis dieses Programms ist für das Datum, an dem ich diese Zeilen geschrieben habe: Zwischen 16.01.2008 18:53:08 +01:00 und 01.01.2010 00:00:00 +01:00 liegt die Zeitspanne 715.05:06:51.5468750
Eine TimeSpan-Instanz verwaltet ähnlich DateTime und DateTimeOffset eine Anzahl von Ticks (1 Tick = 100 Nanosekunden). Diese kann in Tage, Stunden, Minuten etc. umgerechnet werden. Die Angabe 715.05:06:51.5468750 in der Ausgabe des Beispielprogramms bedeutet z. B. 715 Tage, 5 Stunden, 6 Minuten und 51,5468750 Sekunden. Tabelle 8.22 zeigt die wichtigen Instanzeigenschaften und -methoden, Tabelle 8.23 die wichtigsten statischen Eigenschaften und Methoden der TimeSpan-Struktur. Tabelle 8.22: Die wichtigen Instanzeigenschaften und -Methoden der TimeSpan-Struktur
512
Eigenschaft /Methode
Beschreibung
Days, Hours, Minutes, Seconds, Milliseconds
Über diese Eigenschaften (vom Typ int) erhalten Sie den Anteil der Tage, Stunden, Minuten, Sekunden oder Millisekunden. Dabei handelt es sich aber nur um den jeweiligen Anteil an der Zeitspanne, nicht um die Gesamtanzahl. Bei der Zeitspanne 01:01:00 ergibt Minutes z. B. 1.
TotalDays, TotalHours, TotalMinutes, TotalSeconds, TotalMilliseconds
Über diese double-Eigenschaften erhalten Sie den Gesamtwert der Zeitspanne in Tagen, Stunden, Minuten, Sekunden oder Millisekunden. Bei der Zeitspanne 01:01:00 ergibt TotalMinutes z. B. 61, TotalHours ergibt 1,0166666666666666.
long Ticks
gibt die Anzahl der Ticks zurück, die die Zeitspanne verwaltet.
TimeSpan Add( TimeSpan ts )
gibt ein TimeSpan-Objekt zurück, bei dem der übergebene Wert zu dem aktuellen addiert ist.
TimeSpan Duration()
ergibt einen TimeSpan-Wert, der die absolute Zeitspanne (ohne Vorzeichen) enthält.
Mathematische Berechnungen
Eigenschaft /Methode
Beschreibung
TimeSpan Negate()
ergibt einen TimeSpan-Wert, der die negierte Zeitspanne enthält.
TimeSpan Subtract( TimeSpan ts )
gibt einen TimeSpan-Wert zurück, bei dem der übergebene Wert vom aktuellen subtrahiert ist.
Tabelle 8.22: Die wichtigen Instanzeigenschaften und -methoden der TimeSpan-Struktur (Forts.)
1 Tabelle 8.23: Die wichtigen statischen Eigenschaften und Methoden der TimeSpanStruktur
Eigenschaft / Methode
Beschreibung
TimeSpan Zero
gibt ein TimeSpan-Objekt zurück, das 0 Ticks verwaltet.
TimeSpan MinValue
gibt ein TimeSpan-Objekt zurück, das den kleinstmöglichen Wert verwaltet.
TimeSpan MaxValue
gibt ein TimeSpan-Objekt zurück, das den größtmöglichen Wert verwaltet.
3
TimeSpan FromDays( double value)
Diese Methoden ergeben einen TimeSpan-Wert aus der entsprechenden Anzahl Tage, Stunden, Minuten, Sekunden, Millisekunden oder Ticks.
4
TimeSpan FromHours( double value) TimeSpan FromMinutes( double value)
2
5
TimeSpan FromSeconds( double value) TimeSpan FromMillisecods( double value)
6
TimeSpan FromTicks( long value )
7
TimeSpan Parse( string s )
parst einen String in einen TimeSpan-Wert. Kann der String nicht umgewandelt werden, resultiert eine FormatException.
bool TryParse( string s, out TimeSpan result )
parst einen String in einen TimeSpan-Wert und gibt true zurück, wenn das Parsen erfolgreich war.
8 9
8.7
Mathematische Berechnungen 10
Über die statischen Methoden und Eigenschaften der Math-Klasse können Sie die verschiedensten mathematischen Berechnungen durchführen. Im folgenden Abschnitt finden Sie zunächst eine tabellarische Übersicht. Die sich nicht unbedingt selbsterklärenden und für die Praxis wichtigen Methoden wie Sin, Cos, Tan und Round behandle ich ab Seite 516 separat.
8.7.1
11
Übersicht über die Methoden und Eigenschaften der Math-Klasse
In Tabelle 8.24 finden Sie eine Beschreibung der Methoden und Eigenschaften der Math-Klasse, die Sie für mathematische Berechnungen verwenden können.
513
Grundlegende Programmiertechniken
Tabelle 8.24: Die Methoden und Eigenschaften der Math-Klasse
Methode / Eigenschaft
Beschreibung
Zahltyp Abs( Zahltyp number)
liefert den Absolutwert der übergebenen Zahl (= ohne Vorzeichen). Diese Methode ist mit den verschiedenen Zahltypen überladen und gibt einen entsprechenden Typ zurück.
Acos, Asin, Atan
berechnen den Arcuskosinus, Arcussinus, Arcustangens. Die Arcusfunktionen sind die Umkehrfunktionen von Sinus, Kosinus und Tangens, allerdings in einer eingeschränkten Form. Näheres dazu finden Sie bei Wikipedia: de.wikipedia.org/wiki/Arkusfunktion. Die Rückgabe ist ein Winkel im Bogenmaß. Den normalen Winkel berechnen Sie folgendermaßen: Winkel = Bogenmaß / (Math.PI / 180).
double Atan2( double y, double x)
Mit dieser Methode können Sie den Winkel berechnen, den eine Linie ergeben würde, die auf eine horizontale Grundlinie bezogen ist und die am angegebenen Punkt endet. Die Rückgabe ist wie bei Acos, Asin und Atan ein Winkel im Bogenmaß. Vorsicht: Atan2 erwartet den y-Wert an erster Stelle, was recht ungewöhnlich ist.
Cos, Sin, Tan
berechnen den Kosinus, Sinus, und Tangens. Diese Methoden erwarten eine Gradangabe im Bogenmaß. Das Bogenmaß berechnet sich aus Winkel * Math.PI / 180. Auf die Methoden Sin, Cos und Tan gehe ich noch ein.
Cosh, Sinh, Tanh
berechnen den hyperbolischen Kosinus, hyperbolischen Sinus und hyperbolischen Tangens. Die hyperbolischen Methoden beziehen sich auf die hyperbolische Trigonometrie (Trigonometrie der gekrümmten Kurven), über die selbst im Internet nicht viel zu finden ist.
long BigMul( int a, int b )
multipliziert zwei int-Zahlen und ergibt einen long-Wert. Diese Methode ist hilfreich beim Multiplizieren von int-Werten, da bei einer normalen Multiplikation nur ein (ggf. übergelaufener) int-Wert herauskommt: int intValue = int.MaxValue; long result = intValue * 2; // -2 result = Math.BigMul(intValue, 2);
// 4294967294 decimal Ceiling( decimal d ) double Ceiling( double d ) int DivRem( int a, int b, out int result )
liefert den kleinsten Integer-Wert, der größer oder gleich dem übergebenen decimal- oder double-Wert ist. Bei der Übergabe von 1.5 und 1.00000000000001 resultiert z. B. 2, bei der Übergabe von 1.0 resultiert 1.
dividiert zwei Zahlen mit einer Ganzzahl-Division und ergibt außerdem in result den Restwert.
long DivRem( long a, long b, out long result ) double Exp( double d )
514
liefert die Eulersche Zahl e (2,718281828459…) potenziert mit der übergebenen Zahl. Exp ist die Umkehrfunktion zum natürlichen Logarithmus.
Mathematische Berechnungen
Methode / Eigenschaft
Beschreibung
decimal Floor( decimal d )
liefert den größten Integer-Wert, der kleiner oder gleich dem übergebenen decimal- oder double-Wert ist. Bei der Übergabe von 1.5, 1.00000000000001 und 1.0 resultiert z. B. 1, bei der Übergabe von 0.99999999999999 resultiert 0.
double Floor( double d ) double IEEERemainder( double x, double y )
liefert den Restwert der Division von x durch y nach ANSI/IEEE Std 7541985.
double Log( double a [double newBase])
liefert den Logarithmus der am Argument a übergebenen Zahl zu einer gegebenen Basis. Wenn Sie das Argument newBase nicht übergeben, wird der natürliche Logarithmus berechnet, der als Basis die Eulersche Zahl e (2,718281828459…) verwendet. Der Logarithmus einer Zahl ist die Potenz zu der gegebenen Basis, die die Zahl ergibt. Der Logarithmus von 16 zur Basis 2 ist z. B. 4 (24 = 16).
double Log10( double d )
liefert den Logarithmus der am Argument a übergebenen Zahl zur Basis 10.
Max(Zahltyp val1, Zahltyp val2 )
liefern den kleineren bzw. größeren von zwei übergebenen Zahlen. Diese Methoden sind mehrfach für die numerischen Typen überladen.
Tabelle 8.24: Die Methoden und Eigenschaften der Math-Klasse (Forts.)
1
2
3
4
5
Min(Zahltyp val1, Zahltyp val2 ) PI
Diese Eigenschaft gibt den annähernd genauen Wert von PI zurück.
double Pow( double x, double y )
liefert die Potenz einer Zahl. Am ersten Argument übergeben Sie die Basis, am zweiten den Exponenten. Pow(2, 3) bedeutet 23.
decimal Round( decimal d [, int digits ])
Round rundet die Zahl, die Sie am ersten Argument übergeben. Wenn Sie das Argument digits bzw. decimals nicht übergeben, wird auf eine ganze Zahl gerundet, ansonsten auf die angegebenen Dezimalstellen.
double Round( double d [, int decimals ])
Achtung: Wenn Sie das Argument mode nicht oder mit MidpointRounding.ToEven angeben, rundet Round mathematisch. Wenn Sie an diesem Argument allerdings MidpointRounding.AwayFromZero angeben, wird kaufmännisch gerundet. Mehr dazu finden Sie auf Seite 520.
decimal Round( decimal d, [int decimals,] MidpointRounding mode)
6
7
8 9
10
double Round( double value, [int digits,] MidpointRounding mode ) int Sign(
Zahltyp value )
11 ermittelt, ob der übergebene Zahlwert kleiner als 0 ist (Rückgabe = -1), gleich 0 (Rückgabe = 0) oder größer als 0 (Rückgabe = 1). Das Ergebnis können Sie verwenden, um das Vorzeichen eines Resultats, das Sie mit dem Absolutwert einer Zahl berechnet haben, wieder zu setzen.
515
Grundlegende Programmiertechniken
Tabelle 8.24: Die Methoden und Eigenschaften der Math-Klasse (Forts.)
Methode / Eigenschaft
Beschreibung
double Sqrt( double d )
liefert die Quadratwurzel der übergebenen Zahl.
decimal Truncate( decimal d ) double Truncate( double d )
ermittelt den Ganzzahl-Wert der übergebenen double- oder decimalZahl.
Die meisten der Methoden der Math-Klasse (wie Min und Max) und die Eigenschaft PI bedürfen wohl keiner näheren Erklärung. Einige wie Sin, Cos und Tan, die in der Praxis z. B. beim Zeichnen mit den Grafik-Methoden von .NET sehr hilfreich sind, erläutere ich aber ein wenig. Wenn Sie diese Erläuterung nicht benötigten, gut so. Ich werde auf jeden Fall immer wieder mal hier nachlesen müssen ☺.
8.7.2 Die trigonometrischen Methoden helfen bei der Berechnung von Dreiecken
DISC
EXKURS
Winkelberechnungen
Die Methoden Sin, Cos, Tan und Atan2 helfen enorm bei der Berechung von Punkten in einem Dreieck oder Kreis. Ich selbst brauchte diese Methoden bei der Entwicklung eines Steuerelements, das eine Analog-Uhr darstellt. Dabei musste ich für den Stunden-, Minuten- und Sekunden-Zeiger für die gegebene Zeit nämlich den Endpunkt der jeweiligen Linie berechnen. Ohne die trigonometrischen Funktionen wäre das nicht möglich gewesen. In diesem Abschnitt erläutere ich, wie Sie mit den trigonometrischen Methoden eine einfache Uhr zeichnen. Ein komplexeres Analog-Uhr-Beispiel finden Sie im Beispiele-Ordner zu diesem Abschnitt auf der Buch-DVD. Wenn Sie sich dieses Beispiel anschauen, konzentrieren Sie sich auf die Methode OnPaint in der Klasse AnalogClock.
Ein kleiner Exkurs in die Trigonometrie: Die (ebene) Trigonometrie bezieht sich auf zweidimensionale Objekte. Die Trigonometrie des rechtwinkligen Dreiecks ist dabei die einfachste Variante und die in der Praxis wohl am meisten eingesetzte (mit Dreiecken kann eigentlich fast alles abgebildet werden, Dreiecke werden z. B. in der 3DTechnik eingesetzt, um komplexe Objekte darzustellen). Die Seite eines rechtwinkligen Dreiecks, die dem rechten Winkel gegenüberliegt, wird als Hypothenuse (oder auch als c) bezeichnet, die anderen Seiten als Katheten (oder auch als a und b). Der 90-Grad-Winkel heißt Gamma (γ), der Winkel im Uhrzeigersinn links davon Alpha (α) und der Winkel rechts davon Beta (β). Die Kathete, die einem Winkel anliegt, wird als Ankathete bezeichnet, die andere als Gegenkathete. Abbildung 8.8 zeigt ein solches Dreieck aus der Sicht des Winkels α.
Abbildung 8.8: Ein rechtwinkliges Dreieck aus der Sicht des Winkels α
516
Mathematische Berechnungen
Die trigonometrischen Funktionen berechnen sich nun wie folgt: –
Sin(α oder β) = Gegenkathete / Hypothenuse
–
Cos(α oder β) = Ankathete / Hypothenuse
–
Tan(γ) = Gegenkathete / Ankathete
Eine in der Praxis ebenfalls immer wieder hilfreiche Rechenregel ist der Satz des Pythagoras: a2 + b2 = c2. Damit können Sie in einem rechtwinkligen Dreieck sehr schön die Länge einer Seitenlinie berechnen, wenn die Längen der beiden anderen Linien bekannt sind. Um ein wenig Praxis in das Buch zu bringen, will ich mit Ihnen nun eine einfache Version meiner Analog-Uhr nachprogrammieren. Wenn Sie das relativ komplexe (aber praxisnahe und Spaß machende) Beispiel nicht abschreckt, gehen Sie zunächst folgendermaßen vor, um eine entsprechende Anwendung zu erzeugen: Erstellen Sie ein neues Windows.Forms-Projekt, das Sie vielleicht Analog-Uhr nennen. 2. Nennen Sie das Formular Form1.cs um in StartForm.cs oder in einen Namen, der Ihnen besser gefällt. Bestätigen Sie die Nachfrage, ob auch die Klasse umbenannt werden soll, mit JA. 3. Öffnen Sie ggf. das Eigenschaftenfenster ((F4)) und stellen Sie die Eigenschaft Text des Formulars auf »Analog-Uhr« ein, um dem Formular einen sinnvollen Titel zu geben. 4. Klicken Sie im Eigenschaftenfenster auf das Symbol mit dem Blitz-Pfeil, um die Ereignisse anzuzeigen. Suchen Sie das Ereignis Paint und klicken Sie doppelt auf den Eintrag. Visual Studio erzeugt eine passende Ereignismethode. Das Paint-Ereignis ist dazu vorgesehen, dass Sie eigene Zeichnungen ausgeben. Dieses Ereignis wird von Windows immer dann aufgerufen, wenn das Fenster neu gezeichnet werden muss. Die Eigenschaft Graphics des übergebenen Ereignisargument-Objekts (e) verwaltet dazu ein Graphics-Objekt, über dessen Methoden Sie zweidimensional zeichnen können. Ich gehe hier nicht näher auf diese Methoden ein, weil das Zeichen hier nicht Thema ist.
1 TIPP
2
3
STEPS
1.
4
5
6
In Paint wird auf einem GraphicsObjekt gezeichnet
7
8
Beim Zeichnen einer Uhr müssen Sie aber verschiedene trigonometrische Berechnungen ausführen. Und das ist Thema dieses Abschnitts. Die in der Programmierung verwendeten Math-Methoden habe ich hervorgehoben. Zunächst nehmen Sie ein paar Initialisierungen vor:
9
Listing 8.57: Beginn der Paint-Ereignismethode eines Windows.Forms-Formulars mit Initialisierungen zum Zeichnen einer Analog-Uhr
10
private void MainForm_Paint(object sender, PaintEventArgs e) { // Das Graphics-Objekt referenzieren und so einstellen, // dass die Grafik eine gute Qualität besitzt (mit Anti-Aliasing): Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
11
// Den Mittelpunkt des Formulars und den Radius der Uhr berechnen int xCenter = this.ClientRectangle.Width / 2; int yCenter = this.ClientRectangle.Height / 2; int radius = (Math.Min(this.ClientRectangle.Width, this.ClientRectangle.Height) / 2);
517
Grundlegende Programmiertechniken
Nun beginnt der schwierige Teil: das Zeichnen der Zeiger. Zum Zeichnen einer Linie verwenden Sie die Methode DrawLine des mit dem Ereignisargument e übergebenen Graphics-Objekts. Beim Zeichnen geben Sie den Start- und den Endpunkt an und außerdem ein Pen-Objekt, das die Farbe und die Breite der Linie definiert. Der Startpunkt ist kein Problem, dabei handelt es sich einfach um den berechneten Mittelpunkt des Formulars. Der Endpunkt muss allerdings berechnet werden. Die Berechnung ist aber einfacher, als Sie vielleicht denken (auf jeden Fall einfacher, als ich ursprünglich dachte ☺). Die Stunden, Minuten und Sekunden der Uhrzeit sind ein Teiler von 360
Die Stunden, Minuten und Sekunden der Uhrzeit haben nicht ohne Hintergedanken Werte, deren Vielfaches 360 ergibt. Eine Minutenzahl kann z. B. in eine Gradangabe umgerechnet werden, indem sie mit 6 multipliziert wird. Damit können Sie recht einfach trigonometrische Berechnungen ausführen, die auf der Uhrzeit basieren. Diese Einteilung der Uhr basiert auf den Berechnungen der ersten Seefahrer mit ihren Sextanten, indem aus der Position der Sterne und der Uhrzeit die Position ermittelt wurde. Die Endposition des Minutenzeigers für den Fall zu berechnen, dass dieser im ersten Quartal der Uhr liegt (0 bis 15 Minuten), ist mit den trigonometrischen Methoden Sin und Cos sehr einfach, wie Abbildung 8.9 demonstriert.
Abbildung 8.9: Schema einer Analog-Uhr mit der Basis für die Berechnung der Position eines Zeigers
Die Berechnung des Endpunktes für den Zeiger basiert nun auf dem dargestellten Dreieck. Die x- und y-Position kann (mit Hilfe der Trigonometrie) folgendermaßen berechnet werden: x = Sin(β) * Radius y = Cos(β) * Radius
Der Winkel β kann ganz einfach berechnet werden: Für den Minuten- und den Sekundenzeiger wird dessen Wert mit 6 (360 Grad / 60 Minuten) multipliziert, für den Stundenzeiger wird dessen Wert mit 30 (360 Grad / 12 Stunden) multipliziert. Das Interessante ist, dass die Berechnung der x- und y-Werte sogar einwandfrei auch für Zeiger-Werte funktioniert, die in einem der anderen Quadranten liegen und die folglich gar kein Dreieck mehr ergeben. Die trigonometrischen Methoden erwarten eine BogenmaßAngabe
518
Nun setzen wir dieses Wissen in das Programm um. Bei der Verwendung der trigonometrischen Methoden der Math-Klasse müssen Sie beachten, dass diese einen Winkel nicht im Gradmaß, sondern im Bogenmaß erwarten. Das Bogenmaß berechnet sich aus Winkel * Math.PI / 180.
Mathematische Berechnungen
Listing 8.58: Berechnen der Zeigerendpunkte der Analog-Uhr // Winkel des Sekunden-, Minuten- und Stundenzeigers berechnen int secondAngle = DateTimeOffset.Now.Second * 6; int minuteAngle = DateTimeOffset.Now.Minute * 6; int hourAngle = (DateTimeOffset.Now.Hour + (DateTimeOffset.Now.Minute / 60)) * 30; // Winkel in Bogenmaß umrechnen double secondRadian = secondAngle * Math.PI / 180; double minuteRadian = minuteAngle * Math.PI / 180; double hourRadian = hourAngle * Math.PI / 180;
1
// x und y für den jeweiligen Endpunkt berechnen int xSecond = xCenter + (int)(0.9 * radius * Math.Sin(secondRadian)); int ySecond = yCenter - (int)(0.9 * radius * Math.Cos(secondRadian)); int xMinute = xCenter + (int)(0.8 * radius * Math.Sin(minuteRadian)); int yMinute = yCenter - (int)(0.8 * radius * Math.Cos(minuteRadian)); int xHour = xCenter + (int)(0.5 * radius * Math.Sin(hourRadian)); int yHour = yCenter - (int)(0.5 * radius * Math.Cos(hourRadian));
2
3
Nun müssen Sie nur noch zeichnen. Das Zeichnen unter Windows.Forms wird in diesem Buch nicht weiter behandelt. Trotzdem wollte ich ein sinnvolles Beispiel einsetzen:
4
Listing 8.59: Zeichnen der Zeiger und des Mittelpunkts der Analog-Uhr
5
// Stunden-, Minuten- und Sekundenzeiger zeichnen using (Pen pen = new Pen(Color.Black, 8)) { e.Graphics.DrawLine(pen, xCenter, yCenter, xHour, yHour); } using (Pen pen = new Pen(Color.Navy, 6)) { e.Graphics.DrawLine(pen, xCenter, yCenter, xMinute, yMinute); } using (Pen pen = new Pen(Color.Red, 4)) { e.Graphics.DrawLine(pen, xCenter, yCenter, xSecond, ySecond); }
6
7
8
// Uhr-Mittelpunkt zeichnen using (Brush brush = new SolidBrush(Color.Red)) { e.Graphics.FillEllipse(brush, xCenter - 6, yCenter - 6, 2 * 6, 2 * 6); } using (Brush brush = new SolidBrush(Color.Black)) { e.Graphics.FillEllipse(brush, xCenter - 2, yCenter - 2, 2 * 2, 2 * 2); }
9
10
}
So ganz reicht das Programm noch nicht aus. Das Problem ist, dass die Uhr nur dann gezeichnet wird, wenn Windows das Fenster neu zeichnet. Um die Uhr jede Sekunde neu zu zeichnen, gehen Sie folgendermaßen vor: 1.
11 STEPS
Ziehen Sie eine Timer-Komponente auf das Formular. Diese Komponente finden Sie in der Toolbar im Register KOMPONENTEN. Da diese Komponente in der Laufzeit nicht sichtbar ist, wird diese nicht auf dem Formular dargestellt, sondern unterhalb.
519
Grundlegende Programmiertechniken
2. 3.
Benennen Sie die Timer-Instanz mit tmrClock o. Ä. Setzen Sie die Eigenschaft Enabled auf true und die Eigenschaft Intervall auf 1000 (ms). Erzeugen Sie eine Ereignisbehandlungsmethode für das Tick-Ereignis. Programmieren Sie Folgendes in dieser Methode:
private void tmrClock_Tick(object sender, EventArgs e) { // Das Neuzeichnen initiieren this.Invalidate(); }
Das Tick-Ereignis wird nun in dem angegebenen Intervall regelmäßig aufgerufen. Mit der Methode Invalidate erklären Sie den gesamten Bereich des Formulars für ungültig, woraufhin Windows das Paint-Ereignis aufruft und die Uhr neu gezeichnet wird. Das Ergebnis lässt sich sehen (Abbildung 8.10). Abbildung 8.10: Die Analog-Uhr
8.7.3
Zahlen runden
Zum Runden von Zahlen können Sie die Round-Methode der Math-Klasse verwenden. Diese Methode rundet auf eine ganze Zahl oder auf die angegebenen Dezimalstellen. Round rundet per Voreinstellung, oder wenn Sie am letzten Argument MidpointRounding.ToEven angeben, mathematisch: Listing 8.60: Mathematisches Runden von Zahlen double double double double double
result1 result2 result3 result4 result5
= = = = =
Math.Round(1.15, 1); // 1.2 Math.Round(1.25, 1); // 1.2 Math.Round(1.251, 1); // 1.3 Math.Round(1.15, 1, MidpointRounding.ToEven); // 1.2 Math.Round(1.25, 1, MidpointRounding.ToEven); // 1.2
Wie Sie dem Beispiel entnehmen können, ist das mathematische Rundungsverfahren anders als das bei uns eigentlich übliche kaufmännische: Die Zahl 1.25 wird nämlich nicht auf-, sondern abgerundet.
520
Meldungen mit Hilfe der MessageBox-Klasse ausgeben
Das mathematische Rundungsverfahren stellt sicher, dass keine Ungleichheit beim Runden entsteht. Beim kaufmännischen Rundungsverfahren wird immer aufgerundet, wenn die Ziffer rechts von der Stelle, auf die gerundet wird, eine Fünf ist. Befindet sich rechts neben der Stelle, auf die gerundet wird, aber lediglich eine 5 und keine weiteren Ziffern, liegt die Zahl genau zwischen der nächsthöheren und der nächstniedrigeren gerundeten Zahl. 1,25 liegt z. B. genau zwischen 1,2 und 1,3, wenn auf eine Stelle hinter dem Komma gerundet wird. Da in solchen Fällen immer aufgerundet wird, werden beim kaufmännischen Rundungsverfahren mehr Zahlen auf- als abgerundet (was den Kaufleuten nur recht sein kann ☺).
Mathematisches Runden vermeidet Ungleichheiten
1
Das mathematische Rundungsverfahren löst dieses Problem, indem aufgerundet wird, wenn die Ziffer an der Stelle, auf die gerundet werden soll, ungerade ist. Ist die Ziffer gerade, wird abgerundet. Das gilt natürlich nur dann, wenn hinter der Stelle, auf die gerundet werden soll, lediglich die Ziffer 5 folgt. 1,15 wird demnach auf 1,2 aufgerundet, 1,25 wird auf 1,2 abgerundet. Im Beispiel resultiert deshalb für alle Zahlen das Ergebnis 1,2. 1.251 wird allerdings auf 1,3 aufgerundet, da hinter der 5 noch eine Ziffer folgt.
2
3
Wenn Sie kaufmännisch runden wollen bzw. müssen, geben Sie am letzten Argument der Round-Methode den Wert MidpointRounding.AwayFromZero an:
4
Listing 8.61: Kaufmännisches Runden von Zahlen double result6 = Math.Round(1.15, 1, MidpointRounding.AwayFromZero); // 1.2 double result7 = Math.Round(1.25, 1, MidpointRounding.AwayFromZero); // 1.3 double result8 = Math.Round(1.251, 1, MidpointRounding.AwayFromZero); // 1.3
8.8
5
6
Meldungen mit Hilfe der MessageBoxKlasse ausgeben
Die MessageBox-Klasse ermöglicht mit ihrer statischen Show-Methode die Ausgabe von Meldungen mit verschiedenen Schaltern und Icons. Abbildung 8.11 stellt die in der Praxis wichtigsten Möglichkeiten der MessageBox dar.
7 MessageBox ermöglicht verschiedene Meldungen
8
Die ersten drei MessageBox-Meldungen in Abbildung 8.11 sind einfache Meldungen ohne die Möglichkeit, eine Entscheidung zu treffen. Die nächsten drei Varianten ermöglichen dem Benutzer die Entscheidung zwischen zwei oder drei Optionen. Die Entscheidung des Anwenders spiegelt sich in der Rückgabe der Show-Methode wider. Die letzten beiden Varianten sind für die Ausgabe von Meldungen in einem Windows-Dienst oder von wichtigen Meldungen vorgesehen, die nicht ignoriert werden dürfen. Eine Dienst-Benachrichtigungs-MessageBox wird immer auf dem aktiven Desktop eines Systems im Vordergrund angezeigt, auch dann, wenn zurzeit gerade kein Benutzer angemeldet ist. Eine Default-Desktop-MessageBox wird auf dem Default-Desktop des Systems im Vordergund angezeigt. Andere Anwendungen können eine Dienst-Benachrichtigungs- bzw. Default-Desktop-MessageBox nicht überdecken.
9
10
11
521
Grundlegende Programmiertechniken
Abbildung 8.11: Die in der Praxis wichtigen Möglichkeiten der MessageBox-Klasse
MessageBox existiert für Windows. Forms und WPF
Die MessageBox-Klasse existiert gleich zweimal: einmal im Namensraum System. Windows.Forms (für Windows.Forms-Anwendungen, in der Assembly System. Windows.Forms.dll) und zum anderen im Namensraum System.Windows (für WPFAnwendungen in der Assembly Presentation.Framework.dll). Beide Klassen sind prinzipiell identisch und basieren auf der Windows-System-MessageBox. Bei WPF hat Microsoft die Klasse aber mit teilweise neuen Namen und etwas eingeschränkten Möglichkeiten überarbeitet.
Show ist mehrfach überladen
Die Show-Methode der Windows.Forms-Variante sieht in ihren zwölf wichtigsten Überladungen (ohne die Überladungen, die mit einem Hilfe-Schalter arbeiten) so aus: DialogResult Show([IWin32Window owner,] string text [, string caption [, MessageBoxButtons buttons [, MessageBoxIcon icon [, MessageBoxDefaultButton defaultButton [, MessageBoxOptions options]]]]])
Die Show-Methode der WPF-Variante sieht mit ihren insgesamt »nur« zwölf Varianten etwas vereinfacht, aber ähnlich aus: MessageBoxResult Show([Window owner,] string messageBoxText [, string caption [, MessageBoxButton button [, MessageBoxImage icon [, MessageBoxResult defaultResult [, MessageBoxOptions options]]]]])
Tabelle 8.25 beschreibt die Argumente.
522
Meldungen mit Hilfe der MessageBox-Klasse ausgeben
Argument
Bedeutung
owner
gibt das Besitzer-Formular bzw. -Fenster der MessageBox an. Wenn Sie die Varianten der Show-Methode verwenden, die kein owner-Argument besitzen, ist das Formular bzw. Fenster, von dem aus die MessageBox geöffnet wird, der Besitzer. Eine MessageBox wird immer im Vordergrund vor ihrem Besitzer angezeigt und verhindert, dass Benutzereingaben zum Besitzer-Formular durchdringen (die MessageBox ist modal). Normalerweise müssen Sie keinen speziellen Besitzer angeben, da in der Praxis meist das Formular oder Fenster, von dem aus die MessageBox geöffnet wurde, der sinnvolle Besitzer ist. In einigen sehr speziellen Programmen, die mit mehreren unmodalen Formularen arbeiten, macht es u. U. Sinn, ein anderes Formular als Besitzer anzugeben. Ehrlich gesagt fällt mir dafür aber kein sinnvolles Beispiel ein. In meiner Praxis habe ich noch nie den Besitzer einer MessageBox angeben müssen.
Tabelle 8.25: Die Argumente der Show-Methode der MessageBox-Klasse
1
2
text bzw. messageBoxText definiert die auszugebende Meldung. caption
bestimmt den Titel der MessageBox.
buttons bzw. button
bestimmt die Schalter, die auf der MessageBox angezeigt werden. Dieses Argument unterscheidet sich in Windows.Forms- und WPF-Anwendungen. Die Windows.Forms-Aufzählung MessageBoxButtons enthält die (sich selbst erklärenden Werte) OK, OKCancel, YesNo, YesNoCancel, AbortRetryIgnore und RetryCancel. Die WPF-Aufzählung MessageBoxButton ist mit ihren Werten OK, OKCancel, YesNo und YesNoCancel etwas eingeschränkt, weil die (in der Praxis wohl nur sehr selten eingesetzten) Möglichkeiten mit den Wiederholen-Schaltern fehlen.
icon
3
4
5
bestimmt das Icon. Die Werte der verwendeten Aufzählungen MessageBoxIcon und MessageBoxImage sind gleich:
6
– None: Kein Icon – Information oder Asterisk: Info-Icon
7
– Warning oder Exclamation: Ausrufezeichen – Error, Stop oder Hand: Fehler-Icon – Question: Fragezeichen
8
Dass einige Werte dieselbe Bedeutung (und denselben int-Wert) besitzen, liegt wahrscheinlich daran, dass Microsoft die Kompatibilität zu Visual Basic 6 wahren wollte. Ein Unterschied zwischen den Ausrufezeichen- und den Fehler-Werten ist auf jeden Fall nicht erkennbar und auch nicht dokumentiert.
defaultButton bzw. defaultResult
9
bestimmt den Schalter, der der Default-Schalter ist. Der Default-Schalter wird betätigt, wenn der Anwender die (¢_)- oder die (______) betätigt. In Windows.Forms geben Sie die Position des Defaultschalters über die Werte der MessageBoxDefaultButton-Aufzählung an (Button1, Button2 oder Button3). In WPF geben Sie den Schalter über dessen Bedeutung in Form eines der Werte der MessageBoxResult-Aufzählung an. Diese Aufzählung behandle ich unter der Tabelle, beim Rückgabewert der Show-Methode.
10
11
523
Grundlegende Programmiertechniken
Tabelle 8.25: Die Argumente der Show-Methode der MessageBox-Klasse (Forts.)
Argument
Bedeutung
options
An diesem Argument können Sie Optionen angeben. Die Werte der jeweils verwendeten Aufzählung MessageBoxOptions sind prinzipiell identisch, nur dass WPF den zusätzlichen Wert None besitzt: – ServiceNotification: Die MessageBox ist eine Dienst-Benachrichtigungs-Meldung (siehe in und den der Abbildung folgenden Erläuterungen). – DefaultDesktopOnly: Die MessageBox ist eine Default-DesktopMessageBox (siehe in und den der Abbildung folgenden Erläuterungen). – RightAlign: Der Text der MessageBox ist nach rechts ausgerichtet. – RtlReading: Die MessageBox wird in der Rechst-nach-Links-Leserichtung, die in einigen Ländern verwendet wird, dargestellt.
Die Rückgabe von Show sagt aus, welcher Schalter betätigt wurde
Der Rückgabewert der Show-Methode ist ein Wert der Aufzählung DialogResult bzw. MessageBoxResult, der aussagt, welchen Schalter der Benutzer betätigt hat. Die möglichen Werte sind None, OK, Cancel, Yes und No bei beiden Aufzählungen und Abort, Retry und Ignore zusätzlich bei der Windows.Forms-Aufzählung DialogResult. Die MessageBox ist also recht vielfältig. So können Sie dem Benutzer in einer Windows.Forms-Anwendung z. B. eine Meldung ohne Icon mit Default-Schalter, eine Info-Meldung und eine Nachfrage mit der Möglichkeit abzubrechen stellen: Listing 8.62: Verschiedene MessageBox-Varianten in einer Windows.Forms-Anwendung // Einfache MessageBox ohne Icon mit OK-Schalter MessageBox.Show("Das ist eine einfache MessageBox", Application.ProductName); // MessageBox mit OK-Schalter und Info-Icon MessageBox.Show("Das ist eine Info-MessageBox", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information); // MessageBox mit OK- und Abbrechen-Schalter, Fragezeichen-Icon // und Default auf dem Abbrechen-Schalter switch (MessageBox.Show("Wollen Sie die Datei wirklich überschreiben", Application.ProductName, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question)) { case DialogResult.Yes: // Datei überschreiben ... break; case DialogResult.No: // Datei nicht überschreiben ... break; case DialogResult.Cancel: // Abbrechen ... break; }
Die am Argument eingesetzte Eigenschaft ProductName der Application-Klasse verwaltet übrigens den Namen der Anwendung, der in der AssemblyInfo.cs-Datei definiert ist.
524
Meldungen mit Hilfe der MessageBox-Klasse ausgeben
In einer WPF-Anwendung sieht das Ganze ähnlich aus: Listing 8.63: Verschiedene MessageBox-Varianten in einer WPF-Anwendung // Einfache MessageBox ohne Icon mit OK-Schalter MessageBox.Show("Das ist eine einfache MessageBox", "Die WPF-MessageBox"); // MessageBox mit OK-Schalter und Info-Icon MessageBox.Show("Das ist eine Info-MessageBox", "Die WPF-MessageBox", MessageBoxButton.OK, MessageBoxImage.Information);
1
// MessageBox mit OK- und Abbrechen-Schalter, Fragezeichen-Icon // und Default auf dem Abbrechen-Schalter switch (MessageBox.Show("Wollen Sie die Datei wirklich überschreiben", "Die WPF-MessageBox", MessageBoxButton.YesNoCancel, MessageBoxImage.Question)) { case DialogResult.Yes: // Datei überschreiben ... break;
2
3
4
case DialogResult.No: // Datei nicht überschreiben ... break;
5
case DialogResult.Cancel: // Abbrechen ... break; }
6
Über den Code-Schnipsel mbox können Sie eine sehr einfache Windows.FormsMessageBox ohne viel Aufwand einfügen. Dummerweise wird diese einfache MessageBox in der Praxis nicht besonders häufig verwendet. Deshalb finden Sie auf der Buch-DVD im Ordner Kompendium-Code-Snippets verschiedene Codeschnipsel zur Erzeugung der in der Praxis am meisten benötigten MessageBox-Varianten für Windows.Forms und WPF. Die Windows.Forms-Schnipsel beginnen mit mbox, die WPF-Schnipsel mit wmbox. Kopieren Sie diese Dateien in den Ordner Visual Studio 2008\Code Snippets\Visual C#\My Code Snippets im EigeneDateien-Ordner. Danach sollten die Codeschnipsel direkt zur Verfügung stehen. Eventuell müssen Sie auch Visual Studio neu starten falls es geöffnet war.
7
TIPP
8
DISC
9
Einschränkungen der MessageBox
10
Die MessageBox besitzt auch einige Einschränkungen: ■
■
Die Texte der Schalter können (leider) nicht ohne einen großen Aufwand geändert werden. Den entsprechenden Trick habe ich in meinem C# Codebook beschrieben. Neben den Beispielen zu diesem Abschnitt finden Sie das Projekt MessageBox mit anderen Schalterbeschriftungen, das diesen Trick einsetzt. Die WPF-Variante besitzt nicht die Schalterkombinationen Wiederholen, Abbrechen und Wiederholen, Abbrechen, Ignorieren der Windows.Forms-Variante.
11
525
Grundlegende Programmiertechniken
8.9
Wichtige Diagnose-Hilfsmittel
Der Namensraum System.Diagnostics bietet einige Hilfsmittel zur Diagnose von Anwendungen. Die zur Protokollierung verwendeten Klassen werden in Kapitel 9 vorgestellt. Daneben sind jedoch zwei Klassen aus diesem Namensraum für die tägliche Arbeit sehr interessant, die ich hier kurz vorstelle: Process und Stopwatch. Process erlaubt die Arbeit mit Prozessen
Die Klasse Process erlaubt die Arbeit mit Prozessen. Die wesentliche Methode dieser Klasse ist die Methode Start, über die Sie eine Anwendung starten können. Dazu können Sie den kompletten Dateinamen der Anwendung übergeben. In Fällen, in denen der Pfad zur Anwendung im Windows-Systempfad verwaltet wird, reicht auch der einfache Dateiname: Listing 8.64: Einfacher Start einer Anwendung Process.Start("Notepad.exe");
Am zweiten Argument können Sie optional Befehlszeilenargumente übergeben. Start können Sie auch eine Instanz der ProcessStartInfo-Klasse übergeben, die Startinformationen für den Prozess verwaltet. Damit können Sie selten genutzte Tricks wie die Umleitung der Standardausgabe programmieren. Wichtiger ist aber, dass Sie auch erreichen können, dass die Anwendung auf das Ende des anderen Prozesses wartet. Dazu rufen Sie die WaitForExit-Methode der Process-Instanz auf, die Start zurückgibt: Listing 8.65: Start einer anderen Anwendung mit Warten auf deren Ende string fileName = "C:\\Logs\\MyLog.txt"; Process process = Process.Start("Notepad.exe", fileName); process.WaitForExit();
WaitForExit erlaubt auch die Übergabe eines Timeouts übergeben. Über die HasExited-Eigenschaft der Process-Instanz können Sie dann abfragen, ob der Prozess innerhalb des Timeouts beendet wurde. Stopwatch implementiert eine Stoppuhr
Die Klasse Stopwatch implementiert eine Stoppuhr und eignet sich deshalb für Performancemessungen. Über die Start-Methode starten Sie die Stoppuhr, Stop stoppt diese. Wichtig ist, dass Start eine gestoppte Stoppuhr weiterlaufen lässt. Deswegen müssen Sie vor einer erneuten Messung die Reset-Methode aufrufen, um die Stoppuhr zurückzusetzen. Über verschiedene Eigenschaften erhalten Sie eine Information über die abgelaufene Zeit. ElapsedMilliseconds liefert z. B. die Millisekunden. Da diese Einheit aber für echte Performancemessungen zu ungenau ist, hilft ein kleiner Trick: Die Eigenschaft ElapsedTicks liefert die abgelaufenen »Ticks«. Ein Tick ist bei der Stopwatch-Klasse eine Einheit in der Grundfrequenz des Motherboards. Diese können Sie aus der statischen Eigenschaft Frequency auslesen. So können Sie über eine einfache Berechnung den genauen Zeitbedarf einer Aktion ermitteln: Listing 8.66: Hochgenaue Zeitmessung mit der Stopwatch-Klasse // Stoppuhr erzeugen und starten Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // Demo-Aktion
526
Auswerten von Befehlszeilenargumenten
for (int i = 0; i < 1000000; i++) { } // Stoppuhr stoppen stopwatch.Stop(); // Die genaue Zeit ermitteln double seconds = stopwatch.ElapsedTicks / (double)Stopwatch.Frequency; Console.WriteLine("Zeit: {0:0.0000}ms", seconds * 1000);
1
Wichtig bei der Berechnung der abgelaufenen Sekunden(bruchteile) ist, dass Sie einen der Operanden in double umwandeln, damit es sich nicht um eine Ganzzahldivision handelt. Die Genauigkeit hängt von der Motherboard-Grundfrequenz ab, ist aber bei modernen Motherboards sehr hoch.
2
8.10
3
Auswerten von Befehlszeilenargumenten
Viele Standardanwendungen können mit Befehlszeilenargumenten aufgerufen werden. Dem Windows-Explorer können Sie z. B. beim Aufruf den Pfad zu einem Ordner übergeben, den dieser anzeigen soll: explorer C:\Windows
Befehlszeilenargumente werden beim Start einer Anwendung übergeben
Befehlszeilenargumente werden dabei durch Leerzeichen vom Programmdateinamen und von anderen Befehlszeilenargumenten getrennt. Beim folgenden Aufruf eines Programms:
4
5
Demo.exe Das ist ein Test
6
werden die Argumente "Das", "ist", "ein" und "Test" übergeben. Werden Argumente in Anführungszeichen eingeschlossen, resultiert ein einziges Argument: Demo.exe "Das ist ein Test"
Dieses Beispiel resultiert in dem einzigen Argument "Das ist ein Test". Argumente, die selbst Leerzeichen enthalten, müssen beim Aufruf also in Anführungszeichen eingeschlossen werden:
7
explorer "C:\Dokumente und Einstellungen"
8
In Ihren (Windows-)Anwendungen können Sie Befehlszeilenargumente ebenfalls auswerten. In einer Windows.Forms- oder Konsolenanwendung können Sie dazu die Main-Methode, die normalerweise in der Klasse Program implementiert ist, um ein Argument vom Typ String-Array erweitern:
9
Listing 8.67: Erweitern der Main-Methode um ein String-Array-Argument
10
[STAThread] static void Main(string[] arguments) {
In diesem Array werden alle Argumente übergeben, die beim Aufruf des Programms angegeben wurden. Dieses Vorgehen ist jedoch nicht zu empfehlen, da Sie zum einen auf die Auswertung in der Main-Methode eingeschränkt sind. Zum anderen wird die Main-Methode in einer mit Visual Studio entwickelten WPF-Anwendung automatisch erzeugt und Sie haben keine Möglichkeit, darin zu programmieren.
11
527
Grundlegende Programmiertechniken
Environment.GetComman dLineArgs ermöglicht das flexible Auslesen
Der flexiblere Weg ist der über die GetCommandLineArgs-Methode der EnvironmentKlasse aus dem Namensraum System. Diese Methode liefert ein String-Array zurück, das (anders als der Name vermuten lässt) den kompletten Aufruf der Anwendung enthält. Im ersten Element steht immer der Dateiname der Anwendung. In den folgenden Elementen werden alle Befehlszeilenargumente verwaltet. Damit ist es recht einfach, die einzelnen Argumente auszulesen. Um keine Probleme bei der Auswertung zu haben, sollten Sie die Syntax der einzelnen Argumente so festlegen, dass diese prinzipiell keine Leerzeichen enthalten. In der Praxis bestehen viele Befehlszeilenargumente aus einem Binde- oder Schrägstrich, gefolgt von einem Namen, einem Doppelpunkt und einem Wert: -Argumentname:Argumentwert
Beim Aufruf der Anwendung kann der Argumentwert in Anführungszeichen eingeschlossen werden, sofern dieser Leerzeichen enthält: Imager.exe -imageFolder:"C:\Bilder für die Website"
Die Auswertung solcher Argumente ist dann relativ einfach. Dazu gehen Sie die Elemente des von GetCommandLineArgs zurückgegebenen String-Array ab dem Index 1 durch und vergleichen die einzelnen Argumente mit den erwarteten. Die Programmierung kann an beliebiger Stelle innerhalb der Anwendung erfolgen, wird aber meistens in der Load-Methode des Start-Formulars bzw. -Fensters oder in der MainMethode untergebracht. Das folgende Beispiel liest auf diese Weise in einer Windows.Forms-Anwendung die erwarteten Argumente -debugmode und -imagefolder:Ordnerangabe aus. Beim imagefolder-Argument wird die Angabe eines Ordners erwartet, der mit einem Doppelpunkt vom Argumentnamen getrennt wird. Um die Groß-/Kleinschreibung nicht zu berücksichtigen, werden die Argumente über die ToLower-Methode in Kleinschreibung umgewandelt. Zur Sicherheit werden unbekannte Argumente in der String-Variablen unknownArguments gesammelt und falls vorhanden nach der Auswertung in einer MessageBox gemeldet. Das Beispiel basiert auf einer Windows.Forms-Anwendung mit den üblichen Referenzen und using-Direktiven. Im Formular ist ein Label angelegt, das lblArguments heißt. In diesem Label werden die übergebenen Befehlszeilenargumente ausgegeben. Die Auswertung der Befehlszeilenargumente erfolgt in der Methode für das Load-Ereignis des Formulars. Dieses Ereignis wird aufgerufen, wenn das Formular geladen wird, und ist für Initialisierungen vorgesehen. Listing 8.68: Auswerten von Befehlszeilenargumenten private void MainForm_Load(object sender, EventArgs e) { // Auswerten der Befehlszeilenargumente bool debugMode = false; string imageFolder = null; string unknownArguments = null; string[] args = Environment.GetCommandLineArgs(); for (int i = 1; i < args.Length; i++) { string loweredArgument = args[i].ToLower(); if (loweredArgument == "-debugmode") { debugMode = true; } else if (loweredArgument.StartsWith("-imagefolder:"))
528
Dokumentation der Programmierung
{ // Den Argumentwert auslesen imageFolder = args[i].Substring(13, args[i].Length - 13); } else { // Unbekanntes Argument if (unknownArguments != null) { unknownArguments += ", "; } unknownArguments += args[i]; }
1
}
2
// Unbekannte Argumente auswerten if (unknownArguments != null) { MessageBox.Show("Die folgenden Argumente sind ungültig: " + unknownArguments, "Befehlszeilenargumente auswerten", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); }
3
4
// Die Argumente auswerten this.lblArguments.Text = "debugmode: " + debugMode + Environment.NewLine + "imageFolder: " + imageFolder; }
Sehr nett von Windows ist, dass in Anführungszeichen eingeschlossene Argumente automatisch so ausgewertet werden, dass die Anführungszeichen entfernt werden. Beim Aufruf mit den Argumenten
5
-debugMode -imageFolder:"C:\Bilder für die Website"
6
werden z. B. "-debugMode" und "-imageFolder:C:\Bilder für die Website" übergeben. Zum Testen von Befehlszeilenargumenten können Sie diese in Visual Studio in den Eigenschaften des Projekts im Register DEBUGGEN in das Feld BEFEHLSZEILENARGUMENTE eintragen.
8.11
7 INFO
8
Dokumentation der Programmierung
Mit den speziellen Dokumentationskommentaren von C# können Sie sehr einfach eine XML-Dokumentation erzeugen, die Sie mit externen Tools in eine professionelle HTML- oder MSDN-ähnliche Dokumentation transformieren können. Damit können Sie Ihre Programme ganz einfach gleichzeitig kommentieren und dokumentieren. Die ausführliche Dokumentation von Klassen und deren Elementen ist besonders für Klassenbibliotheken sinnvoll, die von anderen Programmierern verwendet werden.
Dokumentationskommentare erlauben eine vielfältige Dokumentation
9
10
Auf die Grundlagen der Dokumentation bin ich bereits in Kapitel 3 eingegangen. Hier folgt nun die vertiefte Einführung.
8.11.1
11
Die Elemente der C#Dokumentationskommentare
Zur Dokumentierung verwenden Sie Dokumentationskommentare. Diese werden mit drei Schrägstrichen eingeleitet und bestehen aus vordefinierten und eigenen XML-Elementen (eigene Elemente werden von den entsprechenden Tools aber nicht
529
Grundlegende Programmiertechniken
ausgewertet). Mit Dokumentationskommentaren können Sie alle Typen (Klassen, Strukturen, Aufzählungen etc.) und deren Elemente (Methoden, Felder, Eigenschaften, Ereignisse) dokumentieren. Das XML-Element summary ist z. B. für eine zusammenfassende Überschrift vorgesehen, das Element param beschreibt die einzelnen Argumente einer Methode. Einige Elemente wie summary werden auf der obersten Ebene der Dokumentation verwendet. Tabelle 8.26 beschreibt diese oberen XMLElemente. Tabelle 8.26: Die vordefinierten XML-Elemente der ersten Ebene einer XML-Dokumentation
530
Element
Bedeutung
summary verwenden Sie für eine zusammenfassende Beschreibung als Überschrift. Schreiben Sie hier nur kurz, welche Bedeutung der Typ bzw. das Typ-Element hat. Nähere Informationen zur Anwendung des Typs bzw. Elements sollten Sie im remarks-Element beschreiben.
Beschreibung eines Parameters (Arguments) einer Methode
Beschreibung des Rückgabewerts einer Methode
In diesem Element beschreiben Sie die Ausnahmen, die eine Methode, Eigenschaft oder ein Konstruktor werfen kann.
seealso verweist auf ein Quellcode-Element, das separat beschrieben wird. seealso-Verweise erscheinen in einer Dokumentation üblicherweise unter einer separaten »Siehe auch«-Überschrift,
example ist für Beispiele vorgesehen, z. B. für die Verwendung einer Klasse. Der Inhalt des example-Elements erscheint in einer Dokumentation üblicherweise unter der Überschrift »Beispiel«.
remarks ist für Bemerkungen zu einem Typ oder Element vorgesehen. Hier sollten Sie eine nähere Beschreibung geben. Ich beschreibe für Klassen und Strukturen z. B., wie diese grundsätzlich angewendet werden.
Dieses Element können Sie verwenden, um den Wert zu beschreiben, den eine Eigenschaft repräsentiert. Das Microsoft-Beispiel zu diesem Element zeigt, dass Sie damit das Datenfeld beschreiben, in dem eine Eigenschaft ihren Wert verwaltet. In meinen Augen macht das in der Regel nicht viel Sinn, da ein Programmierer mit dieser Information nichts anfangen kann, wenn das Feld privat ist. Ist das Feld, das die Eigenschaft verwendet, allerdings öffentlich, macht value u. U. Sinn.
beschreibt einen Typparameter eines generischen Typs oder einer generischen Methode.
Dokumentation der Programmierung
Element
Bedeutung
Ein solches Element enthält Text, der nicht als XML interpretiert wird. Dieses Element sollten Sie immer dann verwenden, wenn der Text eines Elementes viele der XMLSonderzeichen (, ", ' und &) verwendet. XML-Sonderzeichen sind innerhalb von XML-Elementen nämlich nicht erlaubt und müssten maskiert werden. Ein CDATA-Element erlaubt aber alle Zeichen, da dessen Inhalt nicht als XML interpretiert wird.
bindet eine Dokumentation ein, die in einer anderen XMLDatei gespeichert ist.
Tabelle 8.26: Die vordefinierten XML-Elemente der ersten Ebene einer XML-Dokumentation (Forts.)
1
2
3
4
5
Andere Elemente sind zur Verwendung innerhalb des Textes der oberen Elemente vorgesehen (Tabelle 8.27). Element
Bedeutung
see definiert mit dem cref-Attribut einen Verweis zu einem Bezeichner, der im aktuellen Programm gültig ist. Hier können Sie auf dokumentierte eigene Typen bzw. Elemente (Methoden, Eigenschaften etc.) oder auf Typen des .NET Framework verweisen.
Tabelle 8.27: Die vordefinierten XML-Elemente für die Verwendung innerhalb der oberen Elemente
6
7
Mit diesem Element können Sie ein Wort innerhalb des Textes eines der Elemente der oberen Ebene als Parameter einer Methode kennzeichnen.
8
oder
kennzeichnet den enthaltenen Text als Quellcode. Wird in der Regel in der Schriftart Courier ausgegeben, damit alle Zeichen die gleiche Breite haben.
9
Mit diesem Element können Sie den Text innerhalb eines anderen Elements in Absätze (Paragraphs) strukturieren.
Eintrag 1 Eintrag 2 ...
definiert eine Auflistung.
referenziert ein typeparam-Element im Text eines Elements über dessen Namen.
10
11
531
Grundlegende Programmiertechniken
XML kennt Sonderzeichen, die im Text nicht direkt verwendet werden können Tabelle 8.28: Die XML-EntityReferenzen
Bei der Dokumentation müssen Sie darauf achten, dass XML fünf reservierte Zeichen verwendet: , &, das Apostroph und das Anführungszeichen. Diese Zeichen können Sie innerhalb eines XML-Elements nicht direkt verwenden. Sie können diese Zeichen aber durch so genannte Entity-Referenzen ersetzen. Entity-Referenz
Zeichen
<
<
>
>
&
&
'
'
"
"
Alternativ können Sie den Text in einer CDATA-Sektion unterbringen, deren Inhalt nicht als XML interpretiert wird. In Listing 8.69 habe ich das für den Programmcode gemacht, der im ersten remarks-Element angegeben ist. Das Beispiel nutzt die Elemente, die in der Praxis am häufigsten verwendet werden. Listing 8.69: Beispiel für eine Dokumentation /// /// Klasse zum Protokollieren /// /// /// /// Beim Erzeugen einer Instanz dieser Klasse übergeben Sie am /// Konstruktor den Namen der Datei, in die protokolliert werden soll. /// Diese Datei wird automatisch zum Anfügen geöffnet. /// /// /// Über und /// können Sie Protokolleinträge /// schreiben. /// /// /// Über wird die Protokolldatei schließlich /// geschlossen. /// /// /// /// /// /// /// public class LogFile : IDisposable { /// /// Der StreamWriter, der zum Schreiben verwendet wird. /// private StreamWriter logWriter;
532
Dokumentation der Programmierung
/// /// Konstruktor. /// /// Der Name der Protokolldatei. /// Wird geworfen, wenn die Datei /// nicht geöffnet werden kann. public LogFile(string filename) { this.logWriter = new StreamWriter(filename, true); }
1
/// /// Schreibt eine Protokoll-Nachricht. /// /// Die Nachricht. /// /// Diese Version der Write-Methode stellt der Nachricht /// automatisch das aktuelle Datum voran. /// /// public void Write(string message) { this.Write(message, true); }
2
3
4
/// /// Schreibt eine Protokoll-Nachricht. /// /// Die Nachricht. /// Gibt an, ob das aktuelle Datum /// vorangestellt werden soll. /// public void Write(string message, bool prependDate) { if (prependDate) { this.logWriter.Write(DateTimeOffset.Now + ": "); } this.logWriter.WriteLine(message); }
5
6
7
/// /// Schließt die Protokolldatei. /// public void Dispose() { this.logWriter.Close(); }
8 9
}
8.11.2
Erstellen der Dokumentation
Zum Erstellen der Dokumentation müssen Sie im Projekt zunächst einstellen, dass eine XML-Dokumentation erzeugt wird. Die dazu verwendete Option XML-DOKUMENTATIONSDATEI finden Sie in den Projekteigenschaften im Register ERSTELLEN. Wählen Sie die Option aus und lassen Sie den vorgeschlagenen Dateinamen idealerweise stehen. Diese Option können Sie für die Debug- und die Release-Konfiguration separat einstellen. Bei sehr großen Klassenbibliotheken macht es u. U. Sinn, nur bei der Release-Konfiguration eine XML-Dokumentationsdatei erzeugen zu lassen, wenn das Erzeugen recht lange dauert. Ich stelle diese Option allerdings prinzipiell für beide Konfigurationen ein.
Zunächst muss eine XML-Dokumentationsdatei erzeugt werden
533
10
11
Grundlegende Programmiertechniken
Kompilieren Sie das Projekt dann, um die XML-Dokumentationsdatei zu erzeugen. Diese Datei dient als Basis für Tools zur Erstellung einer HTML- oder WindowsHilfe-Dokumentation und ist für den Menschen schwer lesbar. Ich will auch gar nicht auf das Format dieser Datei eingehen, weil ein Programmierer eigentlich nie direkt damit in Kontakt kommt. Sandcastle und Doxygen sind die aktuellen OpenSource-Tools
Tabelle 8.29: Die zurzeit aktuellen freien Tools zur Erzeugung einer HTML- oder Windows-Hilfe-Dokumentation
Der letzte Schritt zur Erstellung einer für den Menschen lesbaren Dokumentation ist die Verwendung eines der Tools, die aus der XML-Datei eine HTML- oder WindowsHilfe-Dokumentation erzeugen. Die Auswahl an Tools ist begrenzt. Hinzu kommt, dass (frei erhältliche) Dokumentations-Tools häufig nicht besonders benutzerfreundlich sind und/oder schlecht dokumentiert. Tabelle 8.29 stellt die beiden in meinen Augen wichtigen verfügbaren freien Tools vor. Tool
Beschreibung
Sandcastle (www.codeplex.com/Sandcastle)
Sandcastle ist eine von Microsoft entwickelte Sammlung an Kommandozeilen-Tools zur Erzeugung von MSDN-ähnlichen Dokumentationen. Sandcastle wird von Microsoft intern benutzt, um die Dokumentation des .NET Framework zu erzeugen.
Doxygen (www.stack.nl/~dimitri/doxygen)
Doxygen ist ein allgemeines Dokumentationssystem, das nicht nur für C#, sondern auch für andere Sprachen wie Java, Python und PHP Dokumentation erzeugen kann. Doxygen hat vielfältige Möglichkeiten und erlaubt u. a. das eigene Design der erzeugten Dokumentations-Seiten.
Ich verwende in diesem Kapitel Sandcastle, weil dieses Tool für die Erzeugung der Dokumentation des .NET Framework verwendet wird. Damit sind wohl keine Kompatibilitäts-Probleme zu erwarten. Außerdem wird Sandcastle in Zukunft höchstwahrscheinlich mit dem .NET Framework weiterentwickelt, was bei externen Tools (wie dem hervorragenden NDoc, das ab .NET 2.0 nicht mehr weiterentwickelt wurde) nicht unbedingt der Fall ist. Doxygen ist aber auf jeden Fall auch sehr interessant, weil es einen anderen Ansatz hat und viele Möglichkeiten bietet. Laden Sie also zunächst Sandcastle von der Seite www.codeplex.com/Sandcastle/ Release/ProjectReleases.aspx herunter und installieren Sie diese Werkzeugsammlung. Die Sandcastle-Dateien finden Sie nach der Installation im Ordner Sandcastle in Ihrem Programme-Ordner. Ein Startmenü-Eintrag wird nicht erstellt.
REF
Sandcastle wird per Kommandozeile verwendet
534
Die Dokumentation von Sandcastle selbst ist etwas versteckt. Sie finden diese auf der Seite www.codeplex.com/DocProject/Wiki/View.aspx?title=Sandcastle%20 Help&referringTitle=Home. Sandcastle selbst können Sie prinzipiell nur per Kommandozeile verwenden. Im Ordner Examples\generic finden Sie allerdings auch das Programm SandcastleGui. exe, das eine einfache Windows-Oberfläche besitzt. Die Erstellung einer Dokumentation ist auf diese Weise nicht allzu einfach. Beim Erzeugen einer Dokumentation helfen aber einige weitere Tools. Dazu gehören der Sandcastle Help File Builder (www.codeplex.com/SHFB/WorkItem/View.aspx?WorkItemId=14921), eine WindowsAnwendung, mit der Sie recht einfach eine Dokumentation erzeugen können, und DocProject for Sandcastle (www.codeplex.com/DocProject), ein Add-In für Visual Studio.
Dokumentation der Programmierung
Ich verwende den Sandcastle Help File Builder, weil er mit der (zurzeit des Schreibens dieser Zeilen) aktuellen Sandcastle-Version funktioniert und (relativ) einfach zu bedienen ist. Laden Sie diesen also von der Adresse www.codeplex.com/SHFB/ Release/ProjectReleases.aspx herunter und installieren Sie ihn. Alternativ zu der Windows-Installer-Installation können Sie auch den Quellcode herunterladen und über Visual Studio ausführen. Für den Fall, dass Sie den Quellcode verwenden wollen: Öffnen Sie die Projektmappendatei SandcastleBuilder.sln im Ordner SandcastleBuilder und fügen Sie in den Eigenschaften des Projekts SandcastleComponents den Pfad \Sandcastle\ProductionTools zu den Verweispfaden hinzu.
1
2
Der Microsoft Help Compiler Sandcastle verwendet den Microsoft Help Compiler zur Erzeugung einer Dokumentation im Windows-Hilfe-Format. Die Version 1.0 sollte in Form der Datei hhc.exe bereits auf Ihrem System installiert sein. Sie finden diese Datei im Ordner HTML Help Workshop im Programme-Ordner. Diese Version verwendet Sandcastle zur Erstellung einer Dokumentation im HTML-Help-1.0-Format, für das auf jedem WindowsSystem ein Reader zur Verfügung stehen sollte. Das neuere (und schönere) Format 2.0 wird nur von Visual Studio unterstützt. Auf den meisten Systemen steht für das neue Format kein Reader zur Verfügung. Wenn Sie eine Dokumentation für Entwickler erstellen, die Visual Studio auf ihrem System haben, können Sie aber auch das neue Format problemlos verwenden.
Sandcastle verwendet den Help Compiler 1.x und 2.x
3
4
5
Für eine Dokumentation im neuen Hilfe-Format benötigen Sie den HTML Help Compiler 2, der auf einem System üblicherweise noch nicht installiert ist. Dieses Tool ist leider nicht separat erhältlich. Außerdem verteilt Microsoft den HTML Help Workshop 2, der noch Bestandteil des Visual Studio 2005 SDK war, nicht mehr mit dem aktuellen Visual Studio 2008 SDK, sondern verweist auf das kommerzielle Tool HelpStudio von Innovasys (www.innovasys.com). Den Help Compiler 2 zu erhalten ist leider nicht allzu einfach. Sie können diesen als Teil des Visual Studio 2005 SDK herunterladen (msdn.microsoft.com/vstudio/extend) und dieses installieren, wenn Sie (noch) Visual Studio 2005 installiert haben. Oder Sie können den Anweisungen folgen, die Sie an der folgenden Adresse finden: www.helpware.net/mshelp2/h2faq. htm#novsnet2. Sie müssen selbst entscheiden, ob Ihnen das neuere Hilfe-Format (das auch in die Hilfe von Visual Studio 2008 integriert werden kann, was für Komponentenentwickler wichtig ist) die Mühe wert ist.
6
7
8
Der Sandcastle Help File Builder
9
Der Sandcastle Help File Builder arbeitet mit Projekten, in denen alle Einstellungen zur Erzeugung einer Dokumentation verwaltet werden. Er beginnt beim ersten Start mit einem leeren Projekt, in dem Sie die notwendigen Einstellungen vornehmen können.
10
Im oberen Bereich geben Sie über den ADD-Schalter zunächst die Assembly(s) an, die Sie dokumentieren wollen. Der Sandcastle Help File Builder geht davon aus, dass die XML-Datei denselben Namen besitzt und im selben Ordner gespeichert ist.
11
Über den Schalter PRJSUMMARY können Sie zudem eine zusammenfassende Beschreibung für das gesamte Projekt, über NAMESPACES eine Beschreibung der einzelnen in der dokumentierten Assembly enthaltenen Namensräume hinzufügen.
535
Grundlegende Programmiertechniken
Abbildung 8.12: Der Sandcastle Help File Builder
In der Liste in der Mitte geben Sie Optionen zur Erstellung der Dokumentation an. Die wichtigen sind: ■ ■ ■ ■ ■ ■ ■
■
536
BUILD / FRAMEWORKVERSION: Die Version des .NET Framework. BUILD / HELPFILEFORMAT: Das Format der zu erzeugenden Hilfedateien (HTML Help 1, HTML Help 2 und/oder HTML). HELP FILE / HELP TITLE: Der Titel der Dokumentation. HELP FILE / HTMLHELPNAME: Der Dateiname der Windows-Hilfe-Startseite. HELP FILE / LANGUAGE: Die Sprache der von Sandcastle erzeugten Texte in der Dokumentation. HELP FILE / PRESENTATIONSTYLE: Der Stil der Dokumentation. PATHS / HTMLHELP1XCOMPILERPATH: Pfad zum HTML-Help-1-Compiler (/HTML Help Workshop/hhc.exe). Wenn Sie diesen Pfad eintragen, wird der Compiler schneller gefunden. Ansonsten muss der Sandcastle Help File Builder nach diesem Compiler suchen, wenn Sie eine HTML-Help-1Dokumentation erzeugen. PATHS / HTMLHELP2XCOMPILERPATH: Wenn Sie den HTML-Help-2-Compiler installiert haben, sollten Sie hier den Pfad eintragen, damit der Sandcastle Help File Builder diesen schneller findet.
Dokumentation der Programmierung
■
■ ■
PATHS / OUTPUTPATH: In dieser wichtigen Einstellung tragen Sie den Pfad zu dem Ordner ein, in dem die erzeugten Dokumentations-Dateien gespeichert werden sollen. Relative Angaben wie die Voreinstellung (.\Help) beziehen sich auf den Ordner, in dem der Sandcastle Help File Builder ausgeführt wird. PATHS / SANDCASTLEPATH: Hier sollten Sie den Pfad zum Sandcastle-Ordner eintragen. Alle Einstellungen in VISIBILITY: In diesen Optionen können Sie einstellen, welche Typen und Elemente in die Dokumentation aufgenommen werden. Per Voreinstellung werden z. B. private Typen und Elemente nicht dokumentiert.
1
Über (ª) + (STRG) + (B) (wie in Visual Studio) oder den vierten Schalter in der Symbolleiste können Sie die Dokumentation dann erzeugen. Wenn Sie eine HTML-Dokumentation erzeugen, erreichen Sie diese anschließend über die Datei Index.html in dem Ordner, den Sie als Zielordner angegeben haben. Die Datei Index.aspx ist für die Verwendung in einer ASP.NET-Webanwendung vorgesehen. Eine Windows-Hilfe hat den Dateinamen, den Sie in der Option HTMLHELPFILENAME angegeben haben. Die Windows-Help-1.x-Version besitzt die Dateiendung .chm.
2
3
Abbildung 8.13 zeigt die Windows-Hilfe-Version der Beispiel-Dokumentation.
4 Abbildung 8.13: Die erzeugte Beispiel-Dokumentation (mit auf nicht vererbte Member eingeschränkter Sicht)
5
6
7
8 9
10
11
537
Grundlegende Programmiertechniken
TIPP
Über ein gespeichertes Sandcastle-Help-File-Builder-Projekt können Sie eine Dokumentation auch an der Kommandozeile erzeugen. Dazu rufen Sie die Konsolen-Version SandcastleBuilderConsole.exe, die Sie in dem Ordner finden, in dem Sie den Sandcastle Help File Builder installiert haben, mit dem Projekt als Argument auf: "C:\Programme\EWSoftware\Sandcastle Help File Builder\SandcastleBuilderConsole.exe" Projektdatei
Diese Variante eignet sich hervorragend für das automatisierte Erzeugen einer Dokumentation über Build-Tools wie z. B. den FinalBuilder oder MsBuild.
8.12
Umgang mit dem Garbage Collector
Der Garbage Collector (GC) verrichtet seine Arbeit normalerweise sehr zuverlässig im Hintergrund. Immer dann, wenn der Speicher für neue Objekte nicht mehr ausreicht oder wenn der freie Heap zu klein wird, geht er durch den Speicher und gibt Objekte frei, die nicht mehr referenziert werden. In manchen Fällen müssen Sie aber auch dafür sorgen, dass der GC den Speicher freigibt. Dabei hilft zunächst das Verständnis seiner Arbeitsweise.
8.12.1 Der Heap wird von unten nach oben gefüllt
Die Arbeitsweise des Garbage Collectors
Um den Garbage Collector zu verstehen, sollten Sie zunächst wissen, wie Objekte auf dem Heap gespeichert werden. Der Just-In-Time-Compiler reserviert zunächst immer wesentlich mehr Speicher, als eine Anwendung anfänglich benötigt. Sie können das mit einer einfachen .NET-Anwendung nachvollziehen, wenn Sie diese starten und sich danach im Taskmanager deren Speicherreservierung anschauen. Wenn Sie das Fenster der Anwendung dann einmal minimieren und wiederherstellen, sinkt die Speicherreservierung in der Regel rapide ab. Das liegt daran, dass Windows beim Minimieren einer Anwendung den Speicher der Anwendung ebenfalls minimiert, damit diese nicht unnütz Speicher in Anspruch nimmt. Wenn Sie mit einer .NETAnwendung arbeiten, wird der von Windows reservierte Speicher aber auch laufend an die Anforderungen angepasst. Lassen Sie sich von den teilweise sehr hohen Werten im Taskmanager aber nicht verwirren: Wie gesagt handelt es sich nicht um den benötigten, sondern lediglich um den reservierten (beim Betriebssystem vorgemerkten virtuellen) Speicher. Beim Erzeugen von Objekten füllt der Just-In-Time-Compiler den Heap von unten nach oben. Ein Zeiger definiert dabei die Adresse, an der das nächste Objekt angefügt werden kann. Wird ein neues Objekt erzeugt, wird es an dieser Adresse gespeichert und der Zeiger wird um die Größe des Objekts verschoben.
Der GC sucht nicht mehr referenzierte Objekte im Heap
Der Garbage Collector führt seine Arbeit nun immer dann aus, wenn ein neues Objekt erzeugt wird und der Speicher für das Objekt nicht ausreicht. Er geht außerdem immer dann, wenn das Programm nicht besonders beschäftigt ist, seiner Arbeit nach und führt eine Müllsammlung aus, wenn der freie Speicher der Anwendung zu klein zu werden droht. Er wird dazu per Voreinstellung4 in einem separaten Thread ausgeführt, der parallel neben dem Hauptthread der Anwendung (dem UI-Thread)
4
538
In der Konfiguration des .NET Framework oder einer Anwendung können Sie im Element configuration/runtime/gcConcurrent bestimmen, ob der GC in einem separaten Thread ausgeführt wird oder nicht (siehe msdn2.microsoft.com/en-us/library/yhwwzef8.aspx). Die Voreinstellung ist true (der GC wird in einem separaten Thread ausgeführt).
Umgang mit dem Garbage Collector
läuft. Immer dann, wenn der GC-Thread aufwacht, werden die anderen Threads der Anwendung für die Zeit, in der der GC arbeitet, angehalten, damit der Heap für diese Zeit einen definierten Zustand besitzt. Um die Objekte zu finden, die nicht mehr referenziert werden, geht der GC alle direkten Objekt-Referenzen (die so genannten Roots, das sind z. B. die statischen und lokalen Object-Variablen einer Klasse) des Programms durch. Diese Wurzel-Referenzen werden vom Just-In-Time-Compiler verwaltet und stehen dem GC zur Verfügung. Von den Wurzel-Referenzen aus geht der GC alle weiteren Referenzen durch. Er nutzt dabei Optimierungstechniken wie z. B., dass Objekte, die bereits markiert sind, nicht weiter durchlaufen werden. Die Adressen und die Größe der so gefundenen Objekte werden intern gespeichert. Nach dem Durchlaufen aller aktiven Objekte geht der GC den Heap von unten nach oben durch und vergleicht die Adressen der als referenziert erkannten Objekte mit den im Heap verwalteten. Er kann so relativ einfach die nicht mehr referenzierten Objekte erkennen. Die Adressen und die Größe dieser als Müll erkannten Objekte werden für die Freigabe zwischengespeichert.
1
2
3
Beim Freigeben wird der Speicherbereich von Objekten, die keine Finalisierer besitzen, direkt freigegeben. Objekte mit Finalisierern merkt sich der GC für die spätere Bearbeitung durch einen separaten Thread in einer Warteschlange, da der Aufruf der Finalisierer ansonsten zu viel Zeit in Anspruch nehmen würde. Der separate Thread ruft später die Finalisierer auf und gibt den Speicher der Objekte frei. Weil Finalisierer in einem separaten Thread aufgerufen werden, können übrigens Probleme entstehen, wenn andere Threads gleichzeitig auf Ressourcen zugreifen, auf die ein Finalisierer ebenfalls zugreift. Deswegen sollte im Einzelfall der Zugriff auf eine Ressource in einem Finalisierer synchronisiert werden. In Kapitel 20 erfahren Sie mehr über das Thema »Synchronisierung«.
4
5 HALT
6
Nach dem Freigeben wird der Heap komprimiert, indem der GC versucht, die noch vorhandenen Objekte nach unten zu verschieben, sodass möglichst keine Lücken übrig bleiben. Nach oben sollte dann einiges an Platz frei werden, sodass neue Objekte problemlos erzeugt werden können. Zur Freigabe der Speicherbereiche der nicht mehr referenzierten Objekte verwenden der Just-In-Time-Compiler und der GC aus Optimierungsgründen ein GenerationenKonzept. Alle Objekte, die neu erzeugt werden, werden der Generation 0 (Gen0) zugeordnet. Wenn der GC eine Müllsammlung ausführt, werden zunächst nur die Objekte dieser ersten Generation berücksichtigt. Reicht der freigewordene Speicherplatz aus, ist die Müllsammlung damit beendet. Die Objekte der ersten Generation, die die Müllsammlung überlebt haben, weil sie noch referenziert werden, werden der zweiten Generation (Gen1) zugeordnet. Reicht der freigewordene Speicherplatz nach der Müllsammlung der Generation 0 nicht aus, werden auch die nicht mehr referenzierten Objekte der zweiten Generation freigegeben. Alle Objekte der zweiten Generation, die die Müllsammlung überleben, werden der Generation 2 (Gen2) zugeordnet. Generation 2 ist die letzte der derzeit verwendeten Generationen. Reicht der freie Speicherplatz nach der Freigabe der zweiten Generation immer noch nicht aus, werden auch die nicht mehr referenzierten Objekte der dritten Generation freigegeben. Für den Fall, dass der freie Speicherplatz nun für ein neues Objekt immer noch nicht ausreicht, wird eine OutOfMemoryException generiert.
7
Der GC verwendet ein GenerationenKonzept
8 9
10
11
539
Grundlegende Programmiertechniken
Das Generationen-Konzept folgt den folgenden Annahmen und optimiert damit die Performance des GC: ■ ■ ■
■
Je neuer ein Objekt ist, desto kürzer wird seine Lebenszeit wahrscheinlich sein, Je älter ein Objekt ist, desto wahrscheinlicher ist, dass es länger lebt, Neuere Objekte tendieren dazu, mit anderen neueren Objekten in Beziehung zu stehen und gemeinsam mit diesen benutzt zu werden. eine Freigabe eines neuen Objekts führt also häufig zu der Freigabe einiger weiterer Objekte, Das Komprimieren eines Teils des Heap ist schneller als das Komprimieren des gesamten Heap.
Das Generationen-Konzept hat sich in der Praxis (nicht nur bei .NET) bewährt.
8.12.2 Vorkehrungen zur Optimierung des Speicherverbrauchs Zur Optimierung des Speicheverbrauchs einer .NET-Anwendung gibt es eigentlich nur eine Regel (die an anderer Stelle bereits genannt wurde): Rufen Sie immer dann, wenn ein Objekt eine Dispose-Methode besitzt, diese auf, wenn das Objekt nicht mehr benötigt wird. HALT
Idealerweise erzeugen Sie Objekte, die IDisposable implementieren, in einem using-Block, der dafür sorgt, dass Dispose unter allen Umständen automatisch aufgerufen wird. Dies ist deswegen wichtig, weil der Garbage Collector zwar Objekte freigeben kann, aber nicht die von diesen verwendeten externen Ressourcen. Wenn Sie z. B. mit vielen Bildern (in Form von Bitmap-Objekten) arbeiten und diese nicht freigeben, werden die für die Bilder verwendeten Windows-Systemressourcen vom GC nicht freigegeben und es resultiert nach einiger Zeit eine OutOfMemoryException.
8.12.3 Manuelles Aufrufen des GC Die können den Garbage Collector über die Methode Collect der Klasse GC auffordern, eine Müllsammlung zu initiieren: GC.Collect();
Wenn Sie nichts übergeben, sorgt Collect für eine Müllsammlung in allen Generationen. Sie können aber auch den Index der Generation übergeben, die behandelt werden soll. Eine Müllsammlung in der Generation 0 wird wesentlich schneller ausgeführt als eine Müllsammlung in allen Generationen: GC.Collect(0);
In der Praxis würde ich aber immer alle Generationen behandeln, da der Sinn eines manuellen Aufrufs wohl sein sollte, möglichst viel Speicher freizugeben. Wenn Sie Collect aufrufen, sollten Sie danach über die Methode WaitForPendingFinalizers dafür sorgen, dass der Finialisierer-Aufruf-Thread seine Arbeit ausgeführt hat. Danach sollten Sie Collect noch einmal aufrufen: Listing 8.70: Eine komplette manuelle Müllsammlung GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
540
Umgang mit dem Garbage Collector
Ein manuelles Aufrufen des Garbage Collector ist in normalen Anwendungen nicht notwendig, weil dieser seine Arbeit im Hintergrund sehr zuverlässig ausführt (vorausgesetzt, Sie rufen bei Objekten, die eine Dispose-Methode besitzen, diese nach der Verwendung des Objekts auf). Es gibt aber auch Ausnahmen. Das kann z. B. ein Windows-Dienst sein, der nur selten arbeitet und in der Zwischenzeit wartet. Um den eventuell kostbaren Windows-Speicher freizugeben, könnte der Dienst nach getaner Arbeit den GC aufrufen.
Ein manuelles Aufrufen des GC ist nur in speziellen Fällen sinnvoll
1
2
3
4
5
6
7
8 9
10
11
541
Inhalt
9
Fehler debuggen, testen und protokollieren 1
Da Sie wahrscheinlich (genau wie ich) nicht immer und überall alle denkbaren Ausnahmen abfangen, treten beim Testen eines Programms in vielen Fällen unbehandelte oder unerwartete Ausnahmen auf. Außerdem enthalten Programme häufig logische Fehler, die zu einem Fehlverhalten der Anwendung führen. Diese Ausnahmen und Fehler müssen Sie dann debuggen. Deshalb geht dieses Kapitel im ersten Teil auf das Debuggen von Ausnahmen und logischen Fehlern ein. In diesem Zusammenhang ist auch das Debugging des .NET-Quellcodes interessant, das Microsoft über einen speziellen Symbolserver ermöglicht. Auch dieses für Lernzwecke interessante Debugging wird in diesem Kapitel behandelt.
2
3
4
Fehler sollten Sie in Ihren Anwendungen natürlich idealerweise vermeiden. Das können Sie durch intensives Testen erreichen. Die in Visual Studio ab der ProfessionalVersion (leider nicht in der Standard-Version und in den Express-Editionen) integrierten Unit-Tests können Ihnen dabei helfen. Ab Seite 564 erfahren Sie, wie Sie damit umgehen.
5
Da trotz allem beim Kunden immer noch Fehler auftreten können, sollten Sie eine Protokollierung vorsehen, die zumindest alle aufgetretenen unerwarteten Fehler in eine Datei schreibt der Ähnliches. Dazu eignet sich die in .NET eingebaute Protokollierung bedingt, aber die des externen Tools log4net hervorragend. Abschnitt »Protokollieren während der Ausführung einer Anwendung« zeigt, wie Sie beides grundlegend einsetzen.
6
In diesem Kapitel nenne ich eine Menge an Tastenkombinationen. Beachten Sie bitte, dass alle wichtigen Tastenkombinationen im Anhang, in der Datei Tastenkombinationen.pdf auf der Buch-DVD und auf meiner Website (an der Adresse www.juergenbayer.net/artikel/referenzen/VS-Tastenkombinationen.aspx) zu finden sind.
8
7
DISC
9 Fehler suchen und beseitigen (Debugging)
Visual Studio enthält einen sehr guten Debugger, den Sie nutzen können, um unerwartete Ausnahmen oder logische Fehler in Ihren Programmen zu finden und zu beseitigen. Dieser Debugger ist sehr mächtig und kann nicht nur .NET-Anwendungen, sondern auch »alte« Windows-Anwendungen und Scriptprogramme debuggen. Die folgenden Seiten zeigen, wie Sie mit dem Debugger grundsätzlich umgehen.
9.1.1
10
11
Voraussetzungen und Grundeinstellungen
Die wesentliche Voraussetzung für das Debuggen ist, dass neben den Assemblys, deren Programme im Debugger getestet werden sollen, eine Datei liegt, die Debuginformationen enthält. Diese Datei besitzt die Endung .pdb und wird von Visual Studio beim Kompilieren eines Projekts automatisch erzeugt. Ohne diese Informationen
Die PDB-Datei ist eine wesentliche Voraussetzung
543
Index
9.1
Fehler debuggen, testen und protokollieren
kann der Debugger aus dem erzeugten CIL-Code der Assembly nicht die passende Stelle im Quellcode des Programms identifizieren. Da Sie in den Projekteigenschaften gar nicht (mehr1) einstellen können, ob diese Datei erzeugt wird oder nicht, müssen Sie sich um diese Datei keine Sorgen machen. Da Visual Studio die Debuginformationsdatei auch für das Release erzeugt, können Sie sogar in diesem (über eine Protokollierung) Debuginformationen (wie z. B. die Zeilennummer) ausgeben. Sie können das Release aber nicht mit Visual Studio debuggen, weil in der erzeugten Assembly dazu Informationen fehlen. Sie sollten sich jedoch zur Angewohnheit machen, die Debuginformationsdatei mit Ihrer Anwendung auszuliefern, da Sie dann beim Protokollieren von Fehlern (Seite 575) auf weitere Informationen zurückgreifen können. In den Eigenschaften können Sie das Debuggen beeinflussen
In den Eigenschaften eines Projekts können Sie im Register DEBUGGEN einige Einstellungen vornehmen, die das Debuggen beeinflussen. Die wichtigste Einstellung ist die STARTAKTION. Hier können Sie festlegen, ob beim Starten des Projekts in Visual Studio (mit (F5) z. B.) das Projekt selbst, eine andere Anwendung oder der StandardBrowser des Systems mit einem angegebenen URL gestartet wird. Das Starten einer anderen Anwendung ist dann interessant, wenn die andere Anwendung Ihre Anwendung bzw. Klassenbibliothek verwendet. Wenn Sie z. B. ein Visual-StudioAdd-In entwickeln, schalten Sie die Option EXTERNES PROGRAMM STARTEN ein und geben den Pfad zum Visual-Studio-Programm (devenv.exe) an. Wenn alles richtig konfiguriert ist (was bei Add-Ins erfordert, dass diese z. B. in Form einer .addinDatei im Ordner Visual Studio 2008\Addins im Eigene-Dateien-Ordner bekannt gemacht werden), startet beim Starten der Projektmappe die separate Visual-StudioInstanz und lädt dabei das Add-In, das Sie gerade entwickeln. So können Sie das Add-In gleichzeitig testen und debuggen. Aber so weit müssen Sie zunächst nicht gehen (vor allen Dingen auch, weil die Entwicklung von Add-Ins nicht gerade einfach ist). Bei den weiteren Einstellungen im Register DEBUGGEN sind noch die folgenden interessant: ■ ■
■ ■
1
544
BEFEHLSZEILENARGUMENTE: Hier können Sie für Anwendungen, die mit Befehlszeilenargumenten arbeiten, Test-Befehlszeilenargumente eintragen. NICHT VERWALTETES CODEDEBUGGING AKTIVIEREN: Diese Einstellung, die per Voreinstellung ausgeschaltet ist, ermöglicht das Debuggen von nicht verwaltetem Code. Dies mag interessant sein, wenn Sie mit klassischen DLLs oder COMKomponenten arbeiten und diese Fehler produzieren. Sie debuggen dann aber in reinem Maschinencode, was nur für absolute Systemexperten einen Sinn ergibt. SQL SERVER DEBUGGING AKTIVIEREN: Mit dieser Option können Sie das Debugging von SQL-Server-Prozeduren ermöglichen. VISUAL STUDIO-HOSTPROZESS AKTIVIEREN: Diese per Voreinstellung eingeschaltete Option bewirkt, dass ausführbare Projekte (also .exe-Dateien) nicht als eigener Prozess ausgeführt werden, sondern über Visual Studio als Host. Visual Studio erzeugt neben der .exe-Datei für das Projekt eine weitere Datei, die denselben Basisnamen trägt, aber mit .vshost.exe endet. Diese Datei wird beim Starten mit (F5) gestartet und übernimmt die Integration der eigentlichen Anwendung in den Visual-Studio-Prozess. Die Verwendung von Visual Studio als Host führt zum einen dazu, dass auch bedingt vertrauenswürdiger Code (siehe in Kapitel 23) debuggt werden kann, was sonst ggf. nicht möglich wäre. Zum In früheren Visual-Studio-Versionen konnten Sie einstellen, ob die PDB-Datei erzeugt wird oder nicht.
Fehler suchen und beseitigen (Debugging)
anderen ermöglicht Visual Studio als Host die Auswertung von Ausdrücken zur Laufzeit, was beim Debugging sehr wichtig ist. Ein Abschalten dieser Option ist normalerweise nicht zu empfehlen, da Sie dann Nachteile beim Debuggen haben. In einigen speziellen Fällen macht der Visual-Studio-Prozess aber Probleme. Laut der Dokumentation können z. B. Aufrufe an bestimmte APIs durch den Hostprozess beeinflusst werden und falsche Ergebnisse zurückgeben. In solchen Fällen können Sie versuchen, die Probleme über ein Abschalten der Option VISUAL STUDIO-HOSTPROZESS AKTIVIEREN zu lösen.
9.1.2
1
Anhalten des Programms
Um den Debugger von Visual Studio nutzen zu können, muss das zu untersuchende Programm angehalten werden. Der Debugger hält eine Anwendung automatisch an, wenn eine unbehandelte Ausnahme eintritt. Sie können jedoch auch einstellen, dass das Programm auch bei behandelten Ausnahmen anhält. Für das Lokalisieren logischer Fehler, die nicht zu einer Ausnahme führen, können Sie zudem Haltepunkte setzen.
Das Anhalten ermöglicht das Debuggen
2
3
Unbehandelte Ausnahmen
4
Wenn Sie eine Anwendung in Visual Studio zum Debuggen gestartet haben (z. B. mit (F5)), hält der Debugger das Programm automatisch an, wenn eine unbehandelte Ausnahme eintritt, und markiert die fehlerhafte Anweisung (Abbildung 9.1).
5 Abbildung 9.1: Der Debugger hat ein Programm bei einer Ausnahme angehalten
6
7
8
9 10
11 Über das Ausnahme-Informationsfenster erhalten Sie Informationen zur Ausnahme. Die Hinweise zur Fehlerbehebung sind meist nur wenig hilfreich. Wichtiger ist allerdings der Link DETAILS ANZEIGEN, über den Sie nähere Informationen zur Ausnahme erhalten (Abbildung 9.2).
545
Fehler debuggen, testen und protokollieren
Abbildung 9.2: Detailinformationen zu einer aufgetretenen Ausnahme
INFO
Für Ausnahmen, die in ihrer Meldung einen sehr allgemeinen (und dann häufig nicht sehr informativen) Text verwalten, ist die Eigenschaft InnerException wichtig. Diese zur Exception-Klasse gehörende Eigenschaft referenziert die (jeweils) inneren Ausnahme, die die eigentliche Ursache war. Wenn Sie z. B. in einer WPF-Anwendung im Konstruktor eines Fensters programmieren und Ihr Code verursacht eine Ausnahme, wird eine XamlParseException mit einer nicht besonders aussagekräftigen Meldung geworfen. Erst die innerste Ausnahme weist auf die eigentliche Ursache hin (Abbildung 9.3).
Abbildung 9.3: Ausnahmedetails einer Ausnahme, die im Konstruktor eines Fensters in einer WPF-Anwendung aufgetreten ist
Für WPF-Anwendungen können Sie sich schon einmal merken, dass Sie im Konstruktor eines Fensters möglichst wenig programmieren sollten, besonders auch, weil der Debugger scheinbar nicht in der Lage ist, an einer Ausnahme im Konstruktor eines WPF-Fensters anzuhalten. Das Auslesen der InnerException-Eigenschaft ist aber auch für andere Situationen wichtig, in denen die äußere Ausnahme nichts über den eigentlichen Grund aussagt.
Anhalten »von Hand« (STRG) + (Pause)
unterbricht ein Programm
546
Wenn Sie ein Programm in Visual Studio zum Debuggen gestartet haben, können Sie dieses auch explizit anhalten, indem Sie in Visual Studio (Strg) + (Pause) betätigen. Das ist z. B. immer dann sinnvoll, wenn das Programm scheinbar nicht mehr reagiert, weil es z. B. im Hintergrund eine Endlosschleife ausführt (was mir natürlich niemals nicht passiert ☺). Mit (Strg) + (Pause) können Sie überprüfen, an welcher Stelle das Programm gerade ausgeführt wird. Wenn Sie mit (F10) schrittweise weitergehen, können Sie den weiteren Programmverlauf verfolgen und erkennen eventuelle Probleme meist recht schnell.
Fehler suchen und beseitigen (Debugging)
Haltepunkte Bei logischen Fehlern hält Visual Studio das Programm nur dann an, wenn der logische Fehler zu einer Ausnahme führt. In den meisten Fällen verursachen logische Fehler allerdings »lediglich« ein Fehlverhalten des Programms. Um solche Fehler zu finden, können Sie an der Stelle, an der Sie den Fehler vermuten, einen Haltepunkt setzen. Dazu klicken Sie entweder im Code-Editor auf die linke graue Spalte oder Sie setzen den Eingabecursor in die betreffende Zeile und betätigen (F9). Visual Studio markiert die Anweisung rot, um den Haltepunkt zu kennzeichnen.
Haltepunkte ermöglichen ein beliebiges Anhalten
1 Abbildung 9.4: Ein in Visual Studio gesetzter Haltepunkt
2
3
4
5
6
7 Wenn Sie das Programm dann ausführen, hält der Debugger an dieser Anweisung an.
8
Wo werden Haltepunkte gespeichert? Haltepunkte werden nicht in der Quellcodedatei gespeichert, sondern in der .suoDatei, die Visual Studio parallel zu der Projektmappe verwaltet. Wenn Sie diese Datei löschen, gehen also alle gesetzten Haltepunkte verloren (neben der Information, welche Fenster aktuell geöffnet sind). Gleichzeitig bedeutet dies aber auch, dass ein anderer Programmierer, der mit denselben Quellcodedateien arbeitet (z. B. über ein Quellcode-Verwaltungssystem) die von Ihnen gesetzten Haltepunkte nicht sieht (sofern die .suo-Datei nicht eingecheckt ist, was in der Praxis auch nicht erfolgt). Falls Sie dies erreichen wollen, können Sie statt eines normalen Haltepunkts Debugger.Break einsetzen, wie ich ab Seite 556 beschreibe.
9.1.3
Haltepunkte werden in der .suo-Datei gespeichert
10
11
Basis-Debugging
Wenn ein Programm auf eine der möglichen Arten angehalten wurde, können Sie mit (F10) oder (F11) schrittweise weitergehen. Mit (F10) überspringt der Debugger aufgerufene Methoden oder die Accessoren von Eigenschaften, die gelesen oder in die geschrieben wird. Mit (F11) springt der Debugger allerdings in diese hinein.
9
Mit (F10) und (F11) können Sie schrittweise weitergehen
547
Fehler debuggen, testen und protokollieren
Mit (ª) + (F11) wird die aktuelle Methode (bzw. bei Eigenschaften der aktuelle Accessor, aber der ist ja auch eigentlich eine Methode) zu Ende ausgeführt. Der Debugger verzweigt dann in die übergeordnete Methode, falls es eine solche gibt. Das ist recht hilfreich, wenn Sie gerade in einer untergeordneten Methode debuggen und in die übergeordnete (zurück-) springen wollen. Mit (F5) führen Sie das Programm ab der aktuellen Anweisung weiter aus. Falls noch ein Haltepunkt folgt, hält der Debugger natürlich dort wieder an. Mit (ª) + (F5) beenden Sie das Debuggen (und das Programm). Die Maus auf einem Bezeichner zeigt einen Tooltipp an
Die einfachste Art zu debuggen ist nun, den Mauscursor auf einen Bezeichner im Quellcode zu bewegen. Handelt es sich dabei um eine Variable, ein Feld oder Eigenschaft, zeigt der Debugger deren Wert in einem Tooltipp an. Alternativ können Sie auch den Eingabecursor im Quellcode auf eine Variable, ein Feld oder eine Eigenschaft setzen und (ª) + (F9) betätigen, um den Wert im Schnellüberwachungsfenster anzuzeigen. Sie können aber auch eines der vielen speziellen Debugfenster zur Auswertung von Werten und Ausdrücken verwenden, die ich nach dem folgenden Abschnitt beschreibe.
Visualisierer bzw. Schnellansichten Visualisierer zeigen Inhalte speziell formatiert an
Visual Studio besitzt einige so genannte Visualisierer (englisch: Visualizer), die in der Dokumentation als »Schnellansicht« bezeichnet werden (diesen Begriff halte ich allerdings für verfehlt, in der englischen Version wird der Begriff »Visualizer« verwendet). Visualisierer erlauben beim Debuggen die Anzeige von Ausdrucksergebnissen oder Variableninhalten in einer lesbareren Form. Der Text-Visualisierer zeigt z. B. einen String in einem separaten Fenster als Text (mit optionalem Umbruch) an. Visualisierer werden beim Debuggen an den verschiedenen Stellen angeboten, an denen Ausdrucksergebnisse (bzw. Variableninhalte) angezeigt werden, z. B. wenn Sie den Inhalt einer Variablen über ein Darüberfahren mit der Maus anzeigen lassen. Visual Studio zeigt einen Schalter mit einer Lupe an, wenn ein Visualisierer verfügbar ist (Abbildung 9.5).
Abbildung 9.5: Anzeige eines Variableninhalts in Visual Studio
Visual Studio stellt normalerweise einen passenden Visualisierer ein. Sie können aber auch auf den kleinen Pfeil neben dem Lupensymbol klicken, um einen der verfügbaren Visualisierer auszuwählen. Für Strings, die XML enthalten, können Sie so z. B. den XML-Visualisierer auswählen, um den String in einer strukturierten XMLForm anzeigen zu lassen. Abbildung 9.6 zeigt die Text-Schnellansicht an, die der Text-Visualisierer öffnet. Visual Studio besitzt per Voreinstellung bereits verschiedene Visualisierer, die je nach Kontext angeboten werden. Sie können jedoch auch spezielle Visualisierer nachinstallieren (und auch eigene programmieren), die Sie größtenteils im Internet finden.
548
Fehler suchen und beseitigen (Debugging)
Abbildung 9.6: Die Ansicht des Text-Visualisierers
1
2
3 Interessant sind z. B. der Regulator (ein Visualisierer zur Auswertung von regulären Ausdrücken, tools.osherove.com/CoolTools/Regulator/tabid/185/Default.aspx/ Articles/RegexKit.html), der ASP.NET Cache Visualizer von Brett Johnson (blog.bretts.net/?p=11) und der eigentlich zu Visual Studio gehörende LINQ to SQL Visualizer (blog.schelian.de/PermaLink,guid,3c527951-75e4-4e98-bb9c-aa217724 dde9.aspx).
4 REF
5
Die Installation von Visualisierern ist übrigens denkbar einfach: Kopieren Sie die Dateien des Visualisierers in den Ordner Visual Studio 2008\Visualizers im EigeneDateien-Ordner. Diesen Ordner müssen Sie u. U. (bei den Express-Editionen) zunächst anlegen. Falls Visualisierer zur Ausführung sehr viel Zeit benötigen und einen Timeout mit der Meldung »Das Timeout für die Funktionsauswertung wurde überschritten« produzieren, kann es sein, dass ein in den Debugging-Optionen eingestellter externer Symbolserver den Timeout verursacht. Näheres dazu finden Sie im Abschnitt »Debugging des .NET Framework-Quellcodes« ab Seite 560.
6
7 HALT
8
Bearbeiten und Fortfahren »Bearbeiten und Fortfahren« (Edit and Continue) ist ein nettes Feature des Debuggers, das es Ihnen für Windows-Anwendungen (leider nicht für Webanwendungen) erlaubt, während des Debuggens den Quellcode zu bearbeiten und das Programm mit dem geänderten Code weiter auszuführen. In vielen Fällen müssen Sie dazu zunächst zwar die aktuelle Anweisung mit (F10) ausführen (z. B. wenn der Debugger an einer Ausnahme angehalten hat), dafür können Sie danach den Quellcode fast beliebig ändern.
»Bearbeiten und Fortfahren« erlaubt das Editieren während der Ausführung
Abbildung 9.7 zeigt dies für die Ausnahme, die beim Versuch, die Datei C:\Debugging-Demo.txt in der Demoanwendung in Abbildung 9.1 (Seite 545) einzulesen, aufgetreten ist. Das Programm wurde nach dem Anhalten um die Überprüfung erweitert, ob die Datei existiert.
9 10
11
549
Fehler debuggen, testen und protokollieren
Abbildung 9.7: Ein angehaltenes Programm nach einer Veränderung
Mit (ª) + (STRG) + (F10) legen Sie eine neue Ausführungsposition fest
Wenn der neue Programmcode in Ordnung ist (also kompiliert werden kann), können Sie das Programm nach der Veränderung weiter ausführen. In der Regel müssen Sie dazu die aktuelle Ausführungsposition an eine andere Stelle (weiter oben) setzen. Dazu setzen Sie den Eingabecursor in die Zeile, die als nächste ausgeführt werden soll, und betätigen (ª) + (STRG) + (F10).
Abbildung 9.8: Ein angehaltenes Programm nach dem Umsetzen der aktuellen Anweisung
Danach können Sie das Programm mit (F10) oder (F11) schrittweise weiter ausführen, um Ihre Änderungen zu testen, oder Sie können einfach (F5) betätigen, um das Programm ab der aktuellen Position weiter auszuführen. Vergessen Sie aber nicht, zuvor zu speichern, damit Ihre Änderungen nicht bei einem eventuellen Absturz des Systems verloren gehen.
9.1.4
Die Debugging-Fenster
Visual Studio enthält eine Menge an Fenstern, die für das Debugging verwendet werden können. In diesem Abschnitt stelle ich die zunächst wichtigsten vor (in Kapitel 20 gehe ich noch auf die für das Debuggen von Threads wichtigen Fenster ein),
Das Schnellüberwachungsfenster Das Schnellüberwachungsfenster ermöglicht das Evaluieren beliebiger Ausdrücke
550
Das Schnellüberwachungsfenster zeigt einen Ausdruck (normalerweise eine Variable) an und ermöglicht eine sehr gute Übersicht, besonders über komplexe Objekte, die der Ausdruck ergibt bzw. die die Variable referenziert. Sie öffnen dieses Fenster für eine Variable bzw. ein Feld im Programmcode, indem Sie den Eingabecursor auf dessen Namen setzen und (ª) + (F9) betätigen. Steht der Eingabecursor nicht auf einem Namen, wird das Schnellüberwachungsfenster leer geöffnet, und Sie können einen beliebigen (natürlich im aktuellen Kontext gültigen) Ausdruck eingeben und auswerten.
Fehler suchen und beseitigen (Debugging)
Abbildung 9.9 zeigt das Schnellüberwachungsfenster bei der Anzeige eines BitmapSource-Objekts (das in einer WPF-Anwendung ein Bild repräsentiert). Abbildung 9.9: Das Schnellüberwachungsfenster bei der Anzeige eines (BitmapSource-) Objekts
1
2
3
4
5
6 Die Überwachungsfenster Visual Studio besitzt vier (gleichartige) Fenster, die der Überwachung von beliebigen Ausdrücken dienen. Sie erreichen diese Fenster in einem angehaltenen Programm über das Menü DEBUGGEN / FENSTER / ÜBERWACHEN oder über die Tastenkombinationen (STRG) + (ALT) + (W), (1) bis (STRG) + (ALT) + (W), (4). Das erste Überwachungsfenster wird in der Default-Fenstereinstellung von Visual Studio (in der Einstellung »Generelle Entwicklungseinstellungen«) bereits im unteren Bereich angezeigt. Dass vier Überwachungsfenster vorhanden sind, hat wohl den Grund, dass Sie damit in komplexen Debugging-Situationen Ihre Überwachungsausdrücke gruppieren können. In der Praxis reicht meist eines dieser Fenster aus.
Das Überwachungsfenster zeigt das Ergebnis von Ausdrücken an
8
9
Überwachungsfenster sind einfach: Entweder ziehen Sie zu überwachende Ausdrücke vom Quellcode in das Fenster, oder Sie geben neue Ausdrücke ein. Das Überwachungsfenster zeigt das Ergebnis von einfachen Ausdrücken beim Debuggen immer aktuell an. Komplexere Ausdrücke werden aber häufig nicht mit ihrem aktuellen Wert angezeigt und ausgegraut dargestellt. Bei solchen Ausdrücken erscheint in der WERT-Spalte rechts ein Schalter mit zwei grünen Pfeilen. Sie müssen diesen Schalter betätigen, um den Ausdruck auszuwerten.
7
10
11 HALT
Abbildung 9.10 zeigt Visual Studio beim Debuggen einer Anwendung, die ich gerade entwickle, während ich parallel dieses Buch schreibe (woran Sie erkennen, wie viel Stress ein Autor wirklich hat … OK, abgesehen von den zwei bis drei Windsurf- und
551
Fehler debuggen, testen und protokollieren
den ein bis zwei Snowboardurlauben pro Jahr …). Im Überwachungsfenster werden der Inhalt zweier Variablen und das Ergebnis eines Ausdrucks ausgegeben. Der Ausdruck ist ausgegraut und muss zunächst über den Pfeil-Schalter wieder aktiviert werden. Abbildung 9.10: Visual Studio beim Debuggen einer komplexeren Anwendung mit Überwachungsfenster
Ansonsten ist zum Überwachungsfenster nicht viel zu sagen (außer vielleicht, dass es eines der wichtigsten Fenster beim Debuggen ist).
Das Direktfenster Das Direktfenster erlaubt die Auswertung von Ausdrücken, den Aufruf von Methoden und die Ausführung spezieller VS-Befehle
Das Direktfenster, das Sie über (STRG) + (ALT) + (I) (I = »Immediate« = Direkt) öffnen können, erlaubt in einem angehaltenen Programm die Eingabe beliebiger Ausdrücke (die im aktuellen Kontext gültig sind) und die Ausführung von speziellen Visual-Studio-Befehlen. So können Sie z. B. den Inhalt von Variablen im Direktfenster ausgeben, indem Sie einfach den Variablennamen schreiben und Ihre Eingabe mit (¢) bestätigen. Dabei unterstützt das Direktfenster auch IntelliSense (das Sie allerdings über (STRG) + (____) explizit aufrufen müssen). Das Direktfenster ist aber weniger für die Ausgabe von Variablenwerten als mehr für komplexere Ausdrücke vorgesehen, die Sie vielleicht in einer fehlerhaften Form im Programm verwenden und korrigieren wollen, aber erst noch testen müssen. Ich setze das Direktfenster immer dann ein, wenn ein komplexer Ausdruck im Programmcode nicht das ergibt, was ich wollte. Im Direktfenster verändere ich den Ausdruck so lange, bis das Ergebnis stimmt und tausche den Ausdruck im Programmcode dann gegen die korrigierte Version aus. Sie können auch den Wert von Variablen, Feldern und Eigenschaften im Direktfenster neu definieren, was ich aber nicht empfehlen würde. Falls Ihr Programmcode den Wert von Variablen, Feldern oder Eigenschaften nicht oder falsch setzt, gehen Sie besser so vor, dass Sie den Programmcode entsprechend ändern, mit (ª) + (STRG) + (F10) an eine Anweisung vor der Initialisierung der Variablen, Felder oder Eigenschaften springen und den Programmcode von da aus weiter ausführen. Damit vermeiden Sie, dass Sie die Korrektur des Programmcodes vergessen.
552
Fehler suchen und beseitigen (Debugging)
Im Direktfenster können Sie auch Methoden ausführen, was für den Fall interessant ist, wenn Sie Methoden ohne viel Aufwand testen wollen. Schließlich können Sie noch Visual-Studio-Befehle ausführen. Dazu setzen Sie ein > vor den auszuführenden Befehl. Sie können die in Visual Studio auch für Makros und Add-Ins verfügbaren Befehle verwenden. Diese werden in der Visual-StudioDokumentation unter dem Thema »Visual Studio-Befehle und -Schalter« beschrieben (ENTWICKLUNGSTOOLS UND -SPRACHEN / VISUAL STUDIO / ANWENDUNGSENTWICKLUNG IN VISUAL STUDIO / REFERENZ / VISUAL STUDIO-BEFEHLE UND -SCHALTER).
1
Der Befehl Debug.QuickWatchq zeigt z. B. das Schnellüberwachungsfenster an. Wenn Sie dem Befehl einen Ausdruck übergeben, wird dieser direkt in das Schnellüberwachungsfenster übertragen.
2
Statt der Befehle können Sie auch vordefinierte Befehlsaliase verwenden. Diese werden im Unterthema »Vordefinierte Visual Studio-Befehlsaliase« beschrieben. Der Alias »??« steht z. B. für den Befehl Debug.QuickWatchq.
3
Der Befehl > ?? i
4
gibt z. B. den Inhalt der Variablen i im Schnellüberwachungsfenster aus. Sie können über den Befehl Tools.Alias sogar Aliase für eigene Befehle erzeugen. Tools.Alias ohne Argumente zeigt übrigens alle vorhandenen Befehlsaliase an.
5
Das Befehlsfenster Das Befehlsfenster ((STRG) + (ALT) + (A)) hat eine ähnliche Funktion wie das Direktfenster und ist ein Überbleibsel aus alten Tagen. Der Unterschied zum Direktfenster ist, dass das Befehlsfenster grundsätzlich nur die vordefinierten Visual-StudioBefehle ausführt. Wenn Sie das Ergebnis eines Ausdrucks ausgeben wollen, müssen Sie den Befehl Debug.Print bzw. dessen Alias »?« vor den Ausdruck setzen. Natürlich können Sie wie im Direktfenster auch alle anderen Visual-Studio-Befehle ausführen.
Das Befehlsfenster ist der Vorläufer des Direktfensters
7
Das Auto- und das Lokalfenster Das Autofenster (im Menü DEBUGGEN / FENSTER / AUTO) zeigt beim Debuggen automatisch die Werte aller Variablen an, die in der aktuellen Anweisung verwendet werden. Das Lokalfenster (DEBUGGEN / FENSTER / LOKAL) zeigt die Werte von Variablen an, die in der aktuellen Methode deklariert sind (also lokal gelten).
6
Das Auto- und das Lokalfenster zeigen lokale Daten automatisch an
8
9
Die Aufrufliste In der Aufrufliste ((STRG) + (ALT) + (C)) können Sie alle Aufrufe zurückverfolgen, die zur aktuellen Debugposition geführt haben. Der letzte Aufruf steht immer oben. Über einen Doppelklick auf einem der Aufrufe in der Liste gelangen Sie zu der entsprechenden Stelle im Quellcode. Die Verwendung der Aufrufliste ist sinnvoll, wenn eine Ausnahme aufgetreten ist, deren Ursache ggf. irgendwo im Aufrufpfad liegen könnte.
Die Aufrufliste enthält den Aufruf-Stack
10
11
Das Haltepunkte-Fenster Das Haltepunkte-Fenster zeigt alle im Projekt gesetzten Haltepunkte an. Über dieses Fenster können Sie Haltepunkte löschen und neue Haltepunkte setzen. Außerdem erlaubt dieses Fenster die Definition von Bedingungen für Haltepunkte.
553
Fehler debuggen, testen und protokollieren
9.1.5 Bedingte Haltepunkte halten nur an, wenn eine Bedingung erfüllt ist
Bedingte Haltepunkte
Für schwieriger zu lokalisierende Fehler können Sie bedingte Haltepunkte definieren. Diese speziellen Haltepunkte besitzen eine Bedingung, die wahr werden muss, damit der Debugger anhält. Neben der Bedingung können Sie noch festlegen, wie oft der Haltepunkt (inkl. Bedingung) angesprochen wird, damit er aktiv wird, und Sie können spezielle Filter setzen. Bedingte Haltepunkte sind enorm hilfreich, wenn im Programm ein logischer Fehler auftritt, der durch einen fehlerhaften Wert in einer Variablen, einem Feld oder einer Eigenschaft verursacht wird, der in einem anderen Programmteil geschrieben wurde. Durch einen bedingten Haltepunkt können Sie das Programm anhalten lassen, wenn der fehlerhafte Wert geschrieben wird. Damit können Sie den Übeltäter (ggf. über die Aufrufliste) in vielen Fällen bereits lokalisieren. Das einzige Problem dabei ist, dass Sie den Haltepunkt an einer Anweisung setzen müssen. Sie müssen also ziemlich genau wissen, wo der Fehler auftritt. Eine Lösung für dieses Problem zeige ich im Abschnitt »Debuggen des fehlerhaften Setzens von Eigenschaften und Feldern« auf Seite 555. Ein anderer Einsatz von bedingten Haltepunkten ist die Verarbeitung von eingelesenen Daten für den Fall, dass bei einem bestimmten Datensatz ein logischer Fehler auftritt. Wenn Sie die Haltepunkt-Bedingung mit einer Zähl-Variablen definieren, können Sie das Einlesen der unproblematischen Datensätze umgehen und beim Fehler verursachenden Datensatz anhalten. Um einen bedingten Haltepunkt zu erzeugen, setzen Sie zunächst einen normalen. Im Kontextmenü des Haltepunkts (Rechtsklick auf dem Haltepunkt) finden Sie den Befehl HALTEPUNKT / BEDINGUNG, über den Sie die Bedingung definieren können. Abbildung 9.11 zeigt dies für eine Methode, der ein Dateiname übergeben wird und bei deren Aufruf das Argument fileName in einigen Fällen mit null belegt ist.
Abbildung 9.11: Definition eines bedingten Haltepunktes in einer Methode
INFO
554
Dieses Beispiel dient nur der Demonstration von bedingten Haltepunkten. In der Praxis sollten Sie Referenztyp-Argumente, die nicht null sein dürfen, auf null abfragen und in diesem Fall eine ArgumentNullException (mit einer sprechenden Meldung) werfen.
Fehler suchen und beseitigen (Debugging)
Über die Option HAT SICH GEÄNDERT können Sie Haltepunkte setzen, die immer dann zum Anhalten führen, wenn das Ergebnis des Bedingungsausdrucks sich geändert hat. Damit können Sie erkennen, in welchen Situationen der Wert einer Variable, eines Feldes oder einer Eigenschaft im Programm geändert wird.
9.1.6
Haltepunkte mit Trefferanzahl
Der Befehl TREFFERANZAHL im Kontextmenü-Eintrag HALTEPUNKT öffnet die Einstellung der für das Anhalten notwendigen Anzahl der Treffer. Diese Einstellung ist in der Praxis zwar nur selten notwendig, hilft aber ggf. beim Debuggen komplexer Probleme. So können Sie z. B. das im vorhergehenden Abschnitt angesprochene Datensatz-Verarbeitungs-Problem lösen, indem Sie als Trefferanzahl die logische Nummer des Datensatzes angeben, der den Fehler verursacht. In vielen Programmen kommt es auch vor, dass eine Methode, die eigentlich nur einmal aufgerufen werden soll, fälschlicherweise mehrfach aufgerufen wird. Setzen Sie in einer solchen Methode einen (ggf. auch bedingten) Haltepunkt und setzen Sie die Trefferanzahl auf 2 (für das hier beschriebene Beispiel). Beim zweiten Aufruf der Methode hält das Programm an und Sie erfahren über die Aufrufliste, woher der Aufruf kommt.
Haltepunkte können auch erst bei einer angegebenen Anzahl Treffer aktiv werden
1
2
3
Abbildung 9.12: Der Dialog zur Einstellung der Trefferanzahl für einen Haltepunkt
4
5
6
9.1.7
Weitere Features von Haltepunkten
7
Neben bedingten Haltepunkten und Haltepunkten mit Trefferanzahl können Sie für einen Haltepunkt noch Filter definieren (HALTEPUNKT / FILTER) und festlegen, was passiert, wenn der Haltepunkt erreicht wird (HALTEPUNKT / BEI TREFFER). Obwohl dies ggf. in speziellen Fällen interessant sein mag (so können Sie beim Eintreten des Haltepunkts z. B. ein Makro ausführen), gehe ich auf diese in der Praxis selten genutzten Features aus Platzgründen nicht ein.
9.1.8
8
9
Debuggen des fehlerhaften Setzens von Eigenschaften und Feldern
Wenn Sie bedingte Haltepunkte einsetzen, um das fehlerhafte Setzen von Feldern oder Eigenschaften zu debuggen, haben Sie in der Praxis häufig das Problem, dass Sie wissen müssen, in welchem Programmteil der Fehler auftritt. Sie müssen ja schließlich einen Haltepunkt setzen. Leider erlaubt Visual Studio nicht das Setzen von bedingten Haltepunkten, ohne dass diese eine Position im Quellcode besitzen. Ein solches Feature wäre sehr hilfreich für den Fall, dass ein Feld oder eine Eigenschaft in Ihrem Programm während der Ausführung mit einem ungültigen Wert belegt wird, Sie aber absolut nicht wissen, wo das passiert (was in der Praxis – wenigstens in meinen Anwendungen – leider relativ häufig auftritt). Dieses Problem können Sie über einen kleinen Trick lösen:
Das fehlerhafte Setzen von Feldern kann mit einem Trick debuggt werden
555
10
11
Fehler debuggen, testen und protokollieren
TIPP
Wenn eine Eigenschaft oder ein Feld an einem unbekannten Ort auf einen ungültigen Wert gesetzt wird, können Sie für Eigenschaften einfach im set-Accessor einen Haltepunkt setzen und eine passende Bedingung für value angeben (value == Wert, value higherDate) { throw new ArgumentException("Das kleinere Datum darf " + "nicht größer sein als das größere"); }
1
2
// Basis-Alter als Differenz zwischen den Jahren ermitteln int age = higherDate.Year - lowerDate.Year;
3
// Wenn der Monat des heutigen Datums kleiner oder wenn der Monat // gleich und der Tag kleiner ist, muss ein Jahr abgezogen werden if ((higherDate.Month
Tracing-Schalter bestimmen, welche Nachrichten protokolliert werden
Der im source-Element angegebene Tracing-Schalter bestimmt, welche Nachrichten protokolliert werden. Das Attribut switchType definiert den Typ des Tracing-Schalters und ist normalerweise vom Typ System.Diagnostics.SourceSwitch. In diesem Fall können Sie dieses Attribut auch weglassen. Tracing-Schalter definieren Sie separat im Element switches. Über add fügen Sie neue Schalter hinzu, die Sie über das name-Attribut benennen. Im value-Attribut geben Sie einen Wert der TraceLevel-Aufzählung an, der bestimmt, für welchen Level von Nachrichten die Tracing-Quelle gilt. Diese Aufzählung besitzt die folgenden Werte: ■ ■
580
Off: Ausgabe keiner Nachrichten. Error: Ausgabe der Meldungen vom Typ Critical und Error.
Protokollieren während der Ausführung einer Anwendung
■ ■ ■
Warning: Ausgabe der Meldungen vom Typ Critical, Error und Warning. Information: Ausgabe der Meldungen vom Typ Critical, Error, Warning und Information. Verbose: Ausgabe aller Meldungstypen.
Die Nachrichten vom Typ Start, Stop, Suspend, Resume und Transfer werden hierbei nicht berücksichtigt, da diese scheinbar lediglich in WCF Verwendung finden.
1
INFO
Für die erste funktionsfähige Konfiguration fehlt nun nur noch der Tracing-Schalter, der im source-Element angegeben war. Das folgende Beispiel definiert einen Schalter, der alle Nachrichten zu den Listenern durchlässt:
2
Listing 9.16: Konfiguration eines Tracing-Schalters
3
4
Abbildung 9.25 zeigt das Ergebnis des Beispielprogramms (Listing 9.13, Seite 578) mit der bisherigen Konfiguration. Abbildung 9.25: Das Ergebnis des Beispielprogramms
5
6 Sie können nun mehrere Tracing-Schalter definieren, die Sie dann in den sourceElementen beliebig angeben können, um die Protokollierung zu steuern. So können Sie eine Anwendung z. B. mit Tracing-Schaltern für Fehler-Nachrichten, für Fehler und Warnungen und für alle Nachrichten ausliefern und den Schalter für FehlerNachrichten zunächst in den source-Elementen angeben.
Über TracingSchalter steuern Sie die Ausgabe der Nachrichten
7
8
Listing 9.17: Konfiguration mehrerer Tracing-Schalter
9 10
Die Anwendung können Sie dann z. B. mit dem Tracing-Schalter Errors im switchName-Attribut der source-Elemente ausliefern, damit die Protokolle nur Fehler enthalten. Treten beim Kunden Fehler auf, kann dieser die Protokollierung ggf. über einen der anderen Tracing-Schalter erweitern um z. B. auch Warnungen und DebugNachrichten in das Protokoll zu schreiben.
11
581
Fehler debuggen, testen und protokollieren
INFO
Der Grund, warum Sie den Tracing-Level nicht direkt im source-Element angeben können, sondern zunächst einen Tracing-Schalter definieren müssen, ist, dass Sie auch eigene Tracing-Schalter-Klassen implementieren können (die von System. Diagnostics.Switch abgeleitet sind), die das Tracing auf eine individuell programmierte Art ein- und abschalten können.
Konfiguration der Listener Als Listener können Sie eine der im .NET Framework bereits enthaltenen Klassen oder eine eigene Listener-Klasse angeben (die von TraceListener abgeleitet sein muss). Das .NET Framework enthält die folgenden vordefinierten Listener-Klassen: ■ ■ ■ ■ ■
■ ■
Microsoft.VisualBasic.Logging.FileLogTraceListener: Eigentlich für Visual Basic vorgesehener Trace-Listener, der in eine Datei protokolliert. System.Diagnostics.TextWriterTraceListener: Protokolliert in eine Datei. System.Diagnostics.DefaultTraceListener: Protokolliert in Standardausgabe, die beim Debuggen in Visual Studio im Ausgabefenster erscheint. System.Diagnostics.EventLogTraceListener: Protokolliert in das WindowsEreignisprotokoll. System.Diagnostics.Eventing.EventProviderTraceListener: Protokolliert in das ETW-Subsystem. ETW steht für »Event Tracing for Windows«, ein relativ unbekanntes Subsystem unter Windows, das eine Performance-Analyse von Anwendungen erlaubt, die (im Vergleich zu Performance-Countern) sehr wenig Overhead benötigt. System.Web.IisTraceListener: Leitet die Protokollnachrichten an die IIS-7Infrastruktur weiter. System.Web.WebPageTraceListener: Diese Listener-Klassen kann in Webanwendungen verwendet werden und führt dazu, dass das Protokoll so geschrieben wird, dass es über eine spezielle URL separat im Web angezeigt werden kann oder an die aufgerufenen Seiten unten angehängt wird.
Die einzelnen Listener-Klassen müssen natürlich individuell konfiguriert werden. Die Initialisierungsdaten (das, was die Listener-Klasse am Argument des Konstruktors erwartet, der genau ein Argument besitzt) übergeben Sie am Attribut initializeData. Dem TextWriterTraceListener muss z. B. der Dateiname übergeben werden: Listing 9.18: Anfügen eines TextWriterTraceListeners
1
2
3
Listener-Filter Jeder Listener empfängt alle Nachrichten, die der für die Tracing-Quelle aktive Tracing-Schalter durchlässt. Diese Nachrichten können in einem Listener zusätzlich noch gefiltert werden. Dazu geben Sie im filter-Element unterhalb des add-Elements einen Filter an. Sie können dazu die Klasse System.Diagnostics.EventTypeFilter verwenden, die die Nachrichten nach deren Ebene (wie bei den TracingSchaltern) filtert. Den Typ des Filters geben Sie im type-Attribut an, im initializeData-Attribut geben Sie die Initialisierungsdaten an. So können Sie z. B. einen Filter definieren, der nur Fehler-Nachrichten durchlässt:
Listener können die empfangenen Einträge filtern
4
5
Listing 9.19: Anfügen eines Listeners mit einem Filter, der nur Fehler-Nachrichten durchlässt
6
... ...
7
8
Leider scheint es keine Möglichkeit zu geben, Filter für einzelne ProtokolleintragsEbenen oder einen Bereich von Protokolleintrags-Ebenen anzugeben (wie es bei log4net der Fall ist). Die Angabe einer Protokolleintrags-Ebene bezieht sich immer auf alle Einträge dieser und höherer Ebenen. Ein mit Error initialisierter EventTypeFilter lässt z. B. Protokolleinträge der Ebene Error und Fatal durch. Eigenartig ist allerdings, dass die korrespondierende Eigenschaft EventType vom Typ SourceLevels ist. SourceLevels ist eine Flags-Aufzählung, die Werte können also miteinander kombiniert werden. In der Konfiguration kombinieren Sie mehrere Werte, indem Sie diese durch Kommata trennen. Was macht es aber für einen Sinn, Werte miteinander zu kombinieren, die implizit Werte höherer Ebenen mit einbeziehen? Sehr eigenartig …
9 10
11
So wäre es z. B. sinnvoll, eine Protokolldatei mit Fehlern, eine weitere mit Warnungen und eine dritte mit den restlichen Nachrichten zu erzeugen. Wenn Sie aber z. B. für einen Listener einen Filter mit der Nachrichtenebene Warning angeben, verarbeitet dieser Listener auch Fehler-Nachrichten.
583
Fehler debuggen, testen und protokollieren
Sie können auch eigene Filter definieren, die von der Klasse TraceFilter abgeleitet sein müssen. Möglicherweise können Sie damit eine praxisgerechte Protokollierung implementieren.
Gemeinsame Listener Wenn Sie einen Listener für mehrere Tracing-Quellen angeben wollen, können Sie diesen in dem Element sharedListeners definieren. Einen gemeinsamen Listener fügen Sie im listeners-Element eines source-Elements an, indem Sie einfach dessen Namen angeben: Listing 9.20: Verwendung eines gemeinsamen Listeners
584
Protokollieren während der Ausführung einer Anwendung
9.3.2
Flexibles, konfigurierbares Protokollieren mit log4net
Das hervorragende (etwas komplexe) Open-Source-Tool log4net ermöglicht eine extrem flexible Protokollierung. log4net ist in der Entwicklerszene etabliert und wird von vielen Entwicklern und Firmen als Standard-Protokollierungstool eingesetzt. Die Vorteile von log4net gegenüber der TraceSource-Klasse habe ich bereits im Abschnitt »Protokollieren mit TraceSource« (Seite 575) dargelegt. Hier geht es nun um eine grundlegende (!) Einführung in die Arbeit mit log4net.
log4net ermöglicht ein extrem flexibles Protokollieren
1
2
Download und Dokumentation 3
log4net finden Sie an der Adresse logging.apache.org/log4net. Die Version 1.2.10 finden Sie auch auf der Buch-DVD im Ordner Komponenten. Der Download enthält neben Assemblys für verschiedene Systeme auch die Dokumentation, den Quellcode und Beispiele. Die einzige Assembly, die Sie in Ihren Projekten referenzieren müssen, ist log4net.dll, die Sie im Ordner bin/net/2.0/release finden. Beachten Sie bei der Verwendung die Lizenz (www.apache.org/licenses/LICENSE-2.0.html).
4
Die Dokumentation finden Sie im Ordner doc oder im Internet. Interessant sind das MANUAL, das auf einer log4net-Kurzeinführung basiert, die CONFIG EXAMPLES und die SDK REFERENCE.
5
Die grundlegende Arbeitsweise von log4net
6
log4net arbeitet mit so genannten Loggern. Ein Logger ist ein Objekt, das die ILogSchnittstelle implementiert. Einen Logger erzeugen Sie normalerweise einmal pro Struktur oder Klasse über die statische GetLogger-Methode der LogManager-Klasse. Dieser Methode können Sie den Typ der Klasse bzw. Struktur übergeben. Der Logger trägt damit den vollen Namen des Typs, in dem protokolliert wird (inklusive Namensraum). Sie können jedoch auch beliebige Namen für Logger vergeben, wenn Sie ein eigenes logisches System von Loggern aufbauen wollen.
Für das Protokollieren werden Logger verwendet
Logger sind in log4net hierarchisch organisiert. Ein Logger ist ein Vorfahre eines anderen, wenn sein Name, gefolgt von einem Punkt, der Anfang des Namens des anderen Loggers ist. Der Logger »Kompendium.Samples« wäre z. B. der direkte Vorfahre des Loggers »Kompendium.Samples.log4net«. Das entspricht genau dem Namensraumkonzept in .NET. Hinzu kommt, dass der gemeinsame Vorfahre aller Logger immer der so genannte Wurzel-Logger (Root logger) ist. Diese Hierarchie macht später Sinn, wenn Sie die einzelnen Logger konfigurieren. Wenn Sie z. B. nur den Wurzel-Logger konfigurieren (was in der Praxis meistens ausreicht), konfigurieren Sie damit automatisch auch alle im Programm verwendeten Logger. Sie können aber auch einzelne Logger oder deren (logische) Vorfahren individuell konfigurieren, um deren Nachrichten separat zu verarbeiten. Wenn Sie meiner Empfehlung folgen und Ihre Logger immer mit dem Typ erzeugen, in dem Sie programmieren, hilft dies u. U. bei der Suche nach Fehlern über das Protokoll, wenn Sie wissen, wo (also in welchem .NET-Namensraum oder in welchem Typ) der Fehler zu suchen ist.
Der Wurzel-Logger ist der BasisVorfahre aller Logger
7
8
9 10
11
585
Fehler debuggen, testen und protokollieren
Methoden eines Loggers schreiben Protokolleinträge
Über verschiedene Methoden des Loggers können Sie nun protokollieren. Die ErrorMethode protokolliert z. B. einen Fehler, Warn eine Warnung. An den zur Verfügung stehenden Methoden erkennen Sie, dass log4net Protokolleinträge in einzelne Ebenen einteilt: ■ ■ ■ ■ ■
In der Konfiguration bestimmen Sie Appender als Ziel der Protokolleinträge
Fatal: Fatale Fehler, die dazu führen, dass die Anwendung nicht mehr weiter ausgeführt werden kann Error: Fehler, die allerdings nicht fatal sind (die Anwendung also weiter ausgeführt werden kann) Warn: Warnungen Info: Informationen Debug: Protokolleinträge, die beim Debugging hilfreich sind
Um die Protokolleinträge nun in ein Protokoll zu schreiben, muss log4net konfiguriert werden. Normalerweise erfolgt dies ähnlich der Konfiguration der Protokollierung über TraceSource in der Anwendungskonfigurationsdatei. Sie können bei log4net aber auch eine separate XML-Datei zur Konfiguration verwenden oder die Konfiguration im Programmcode vornehmen. In der Konfiguration definieren Sie zunächst normalerweise den Wurzel-Logger (Root logger), der die Nachrichten aller Logger erhält. Darüber hinaus können Sie Ihre Logger separat konfigurieren, was aber wie gesagt nur zur speziellen Fehlersuche sinnvoll ist. Jeden konfigurierten Logger versehen Sie mit einem oder mehreren Appendern. Ein Appender ist ein Objekt, das die Protokolleinträge des Loggers entgegennimmt und weitergibt. log4net besitzt eine große Anzahl an Appendern, die z. B. in eine Datei schreiben oder eine E-Mail versenden. Bei der Konfiguration (Seite 589 ) komme ich auf Appender zurück.
Protokollieren mit log4net Das Protokollieren erfolgt über Methoden der ILog-Schnittstelle
Zum Protokollieren benötigen Sie idealerweise in jeder Struktur und jeder Klasse Ihrer Klassenbibliotheken und Anwendungen ein Objekt, das die ILog-Schnittstelle implementiert. Dieses erzeugen Sie (mit einem kleinen Trick) am besten so, dass Sie das Objekt auch in statischen Methoden verwenden und die Deklaration per Copy & Paste auch in andere Typen übernehmen können: private static log4net.ILog logger = log4net.LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().ReflectedType);
Wie gesagt können Sie der GetLogger-Methode auch einen String als Namen des Loggers übergeben. Damit können Sie eine eigene Logger-Hierarchie aufbauen. So können Sie z. B. in mehreren Klassen, die etwas mit der Verwaltung von Daten in einer Datenbank zu tun haben, einen Logger mit Namen »Database« erzeugen. Ich halte mich aber lieber an das bewährte Namensraumkonzept. Dann können Sie die Methoden des Loggers verwenden, um zu protokollieren (Tabelle 9.5).
586
Protokollieren während der Ausführung einer Anwendung
Methode
Beschreibung
void Debug(object message [, Exception t ])
schreibt eine Debug-, Info-, Warnungs-, Fehler- bzw. Fataler-FehlerNachricht. Am zweiten Argument können Sie eine Exception übergeben, deren Informationen dann in das Protokoll aufgenommen werden (wenn Sie im Layout %exception angeben, siehe Seite 592).
void Info(object message [, Exception t ])
Tabelle 9.5: Die Methoden der ILog-Schnittstelle
void Warn(object message [, Exception t ])
1
void Error(object message [, Exception t ]) void Fatal(object message [, Exception t ]) void DebugFormat( [IFormatProvider provider,] string format, params object[] args)
2 schreibt eine Nachricht, die Platzhalter mit Formatangaben (wie bei String.Format) enthalten kann.
3
void InfoFormat( [IFormatProvider provider,] string format, params object[] args)
4
void WarnFormat( [IFormatProvider provider,] string format, params object[] args)
5
void ErrorFormat( [IFormatProvider provider,] string format, params object[] args)
6
void FatalFormat( [IFormatProvider provider,] string format, params object[] args)
7
8 Listing 9.21: Protokollieren verschiedener Nachrichtenebenen // Einige Protokoll-Einträge schreiben logger.Fatal("Fataler-Fehler-Eintrag"); logger.Error("Fehler-Eintrag"); logger.Warn("Warnungs-Eintrag"); logger.Info("Info-Eintrag"); logger.Debug("Debug-Eintrag");
9 10
// Eine Ausnahme protokollieren try { int number1 = 1; int number2 = 1; int result = number1 / number2; } catch (Exception ex) { logger.Error("Fehler beim Dividieren", ex); }
11
587
Fehler debuggen, testen und protokollieren
Über Eigenschaften können Sie abfragen, ob Protokollebenen aktiviert sind Tabelle 9.6: Die Eigenschaften der ILog-Schnittstelle
In der Konfiguration können Sie das Protokollieren für einzelne Nachrichtenebenen aus Performancegründen abschalten. Deshalb können Sie über die Eigenschaften der ILog-Schnittstelle abfragen, ob die entsprechende Ebene aktiviert ist (Tabelle 9.6).
Eigenschaft
Beschreibung
bool IsDebugEnabled
Diese Eigenschaften geben true zurück, wenn die entsprechende Nachrichtenebene für den Logger aktiviert ist.
bool IsInfoEnabled bool IsWarnEnabled bool IsErrorEnabled bool IsFatalEnabled
INFO
Protokollieren können Sie überall
Die Methoden zum Protokollieren überprüfen selbst, ob die entsprechende Ebene aktiviert ist. Im negativen Fall kehren diese sofort zurück. Das sollte eine gute Performance ergeben, wenn Protokolleintrag-Ebenen deaktiviert sind. Die Verwendung der Eigenschaften in Tabelle 9.6 macht deswegen eigentlich nur dann Sinn, wenn das Zusammenstellen der auszugebenden Nachricht aufwändig ist und selbst einiges an Zeit benötigt. Protokollieren können Sie überall, auch in Klassenbibliotheken und auch, wenn log4net noch gar nicht konfiguriert ist. Die Konfiguration erfolgt normalerweise ausschließlich in einer Anwendung. So kann es z. B. sein, dass Sie eine Klassenbibliothek entwickeln, die in einer Anwendung eingesetzt werden soll, die ein anderer Entwickler programmiert. In den Typen Ihrer Klassenbibliotheken protokollieren Sie auf die gezeigte Weise zumindest alle unerwarteten Ausnahmen (über die Erroroder Fatal-Methode) und alles weitere, was protokolliert werden soll (z. B. den Start und das Ende spezieller Methoden). In der Praxis sieht das in etwa folgendermaßen aus: Listing 9.22: Protokollieren mit log4net in einer praxisorientierten Form public class Demo { /* Logger mit den Namen des aktuellen Typs */ private static log4net.ILog logger = log4net.LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().ReflectedType); /* Demo-Methode */ public void DemoMethod() { // Protokollieren, dass die Methode gestartet wurde logger.Info("Start"); #if DEBUG // Eine Performancemessung starten System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); #endif try { // Die Implementierung der Methode fehlt in diesem Beispiel
588
Protokollieren während der Ausführung einer Anwendung
} catch (Exception ex) { // Ausnahme protokollieren logger.Error("Fehler beim ...", ex); // Ausnahme weiterwerfen oder auswerten }
1
#if DEBUG // Die Performancemessung beenden und das Ergebnis protokollieren stopwatch.Stop(); double seconds = stopwatch.ElapsedTicks / (double)System.Diagnostics.Stopwatch.Frequency; logger.Debug("Performance-Messung: " + seconds + " Sekunden"); #endif
2
// Protokollieren, dass die Methode beendet wurde logger.Info("Ende");
3
} }
Dieses Beispiel erfordert wohl etwas Erläuterung ☺.
4
Zu Anfang der Methode wird mit einer Info-Nachricht protokolliert, dass die Methode gestartet wurde. Ähnliches geschieht am Ende der Methode. Damit kann im Protokoll später nachvollzogen werden, wann welche Methode aufgerufen und wann diese beendet wurde. Dies sollten Sie natürlich nur in Sonderfällen einsetzen, wenn Ihr Programm z. B. nicht das macht, was es eigentlich machen sollte.
5
Dann erzeugt das Programm eine Instanz der Klasse System.Diagnostics.Stopwatch, die für eine Zeitmessung benötigt wird, und startet diese Stoppuhr über die Start-Methode. Das Ganze erfolgt in einer bedingten Kompilierung, die nur aktiv ist, wenn die DEBUG-Konstante definiert ist (was ja per Voreinstellung nur in der DebugKonfiguration der Fall ist). Damit wird die Ausführung dieses evtl. zeitaufwändigen Codes für die Release-Konfiguration vermieden.
6
7
Im darauf folgenden try-catch-Block, der die eigentliche Implementierung der Methode enthält, wird im catch-Block der Fehler protokolliert. Schließlich werden vor dem Schreiben der Info-Nachricht des Endes der Methode die Stoppuhr beendet, die Zeit ausgewertet und das Ergebnis als Debug-Nachricht geschrieben. Damit der Code nicht in das Release übernommen wird, sind die entsprechenden Anweisungen wieder in einen #if-Block eingebettet. Das Ermitteln der genauen Zeit ist etwas trickreich, da die Stopwatch-Klasse selbst als kleinste normale Zeiteinheit leider nur Millisekunden zurückgibt. Die Eigenschaft ElapsedTicks enthält aber die einzelnen Ticks (die von der Mainboard-Grundfrequenz abhängen). Werden diese durch die Mainboard-Grundfrequenz geteilt (die über die statische Frequency-Eigenschaft gelesen werden können), resultiert eine wesentlich genauere Zeitangabe.
8
9 10
Konfigurieren von log4net
11
Die Konfiguration ist das eigentlich Komplexe an log4net, denn hier haben Sie eine Vielzahl an Möglichkeiten, die Protokollierung zu beeinflussen. Ich gehe hier auf die wesentlichen Dinge ein und zeige die Konfiguration an einigen Beispielen. Damit Sie die komplexe Konfiguration verstehen, ein Tipp:
589
Fehler debuggen, testen und protokollieren
TIPP
Die Konfiguration bezieht sich immer auf log4net-Klassen. In der Dokumentation der Klassen finden Sie die wichtigen Eigenschaften, die Sie in der Konfiguration angeben. Diese Eigenschaften geben Sie als XML-Unterelement an. Dabei schreiben Sie den ersten Buchstaben klein. Handelt es sich um eine einfache Eigenschaft (wie z. B. einen String), geben Sie deren Wert über das value-Attribut an. Handelt es sich um eine Objekt-Eigenschaft, geben Sie meist im Attribut type den tatsächlich zu verwendenden Typ an. Dessen zu konfigurierende Eigenschaften tragen Sie dann als Unterelement ein. Ein Beispiel verdeutlicht dies wahrscheinlich: Dem log4net-Element können Sie beliebig viele appender-Elemente unterordnen (die ich gleich noch näher erläutere). Dabei müssen Sie den Typ der tatsächlich zu verwendenden Klasse angeben:
Die Klasse TraceAppender besitzt die wichtige Eigenschaft Layout, die vom Typ ILayout ist. Diese Eigenschaft wird über das layout-Element definiert. Die Klasse log4net.Layout.PatternLayout implementiert die ILayout-Schnittstelle. Sie kann deshalb im type-Attribut angegeben werden:
PatternLayout besitzt die wichtige Eigenschaft ConversionPattern, die in der Konfiguration über ein conversionPattern-Element dargestellt wird. Da es sich beim Wert dieser Eigenschaft um einen einfachen String handelt, wird dieser in dem valueAttribut übergeben:
Die Konfiguration erfolgt meist in der Anwendungskonfigurationsdatei
Die Konfiguration können Sie wie bereits gesagt in der Anwendungskonfigurationsdatei, in einer separaten XML-Datei oder im Programm vornehmen. Ich bevorzuge die Konfiguration in der Anwendungskonfigurationsdatei, da diese genau dafür vorgesehen ist. Wie bei TraceSource fügen Sie dem Projekt zunächst eine neue Anwendungskonfigurationsdatei hinzu, indem Sie im Kontextmenü des Projekteintrags im Projektmappen-Explorer den Befehl HINZUFÜGEN / NEUES ELEMENT und dann den Eintrag ANWENDUNGSKONFIGURATIONSDATEI wählen. Übernehmen Sie den voreingestellten Namen der Datei (app.config). Visual Studio kopiert diese Datei beim Kompilieren in den Ordner der Anwendung und benennt sie so um, dass diese dem Namen der Anwendung mit der zusätzlichen Endung .config entspricht. Im configuration-Element müssen Sie zunächst eine neue Sektion für log4net angeben. Das dazu notwendige configSections-Element muss immer ganz oben in der Konfigurationsdatei stehen. Listing 9.23: Konfiguration der log4net-Sektion
Dann beginnen Sie die log4net-Sektion, die die komplette log4net-Konfiguration enthält:
Die Appender
1
Als Erstes konfigurieren Sie die Appender, die Sie verwenden wollen. log4net bringt im Namensraum log4net.Appender eine Vielzahl an vordefinierten Appendern mit. Die wichtigsten zeigt Tabelle 9.7.
2 Appender
Beschreibung
AdoNetAppender
protokolliert in eine Datenbank.
Tabelle 9.7: Die wichtigsten log4net-Appender
3
AspNetTraceAppender protokolliert in ASP.NET-Webanwendungen in das ASP.NET-Trace-Protokoll, das über eine spezielle URL separat angezeigt werden kann oder an die aufgerufenen Seiten unten angehängt wird. ColoredConsoleAppender ConsoleAppender
Diese Appender protokollieren an die Konsole.
EventLogAppender
protokolliert in das Windows-Ereignisprotokoll.
FileAppender
protokolliert in eine Datei.
NetSendAppender
sendet die Protokolleinträge als Netzwerk-Nachrichten.
4
5
6
RollingFileAppender protokolliert in mehrere Dateien. log4net erzeugt dabei automatisch eine neue Datei, wenn die Datei eine angegebene Größe erreicht hat oder wenn ein angegebener Zeitbereich (z. B. ein Tag) verstrichen ist. Die alte Datei wird dann umbenannt (mit einem Index oder mit dem Datum hinter der Endung) und eine neue, leere zum weiteren Protokollieren erzeugt. SmtpAppender
sendet die Protokolleinträge an einen SMTP-Server.
SmtpPickupDirAppender
schreibt die Protokolleinträge in das Verzeichnis, das im IIS als E-Mail-Pickup-Verzeichnis definiert ist. Alle in diesem Verzeichnis abgelegten E-Mails werden vom IIS automatisch regelmäßig versendet, wenn dieser entsprechend konfiguriert ist.
TraceAppender
protokolliert in das Trace-System, sodass die Nachrichten als Ausgabe in Visual Studio angezeigt werden.
UdpAppender
sendet die Protokolleinträge über das UDP-Protokoll an eine angegebene IPAdresse und einen angegebenen IP-Port. Damit können Sie im Bedarfsfall Protokolleinträge z. B. über das Internet von Ihren Kunden empfangen und über ein separates Tool (das auf den angegebenen Port hört) auswerten. Dazu können Sie z. B. das Programm Log4Net Viewer von Devintelligence verwenden, das Sie an der Adresse devintelligence.com/log4netviewer downloaden können. Eine Demo dieser Technik finden Sie in den Beispielen dieses Abschnitts.
7
8
9 10
11
Darüber hinaus besitzt log4net noch weitere spezielle Appender, die ich nicht näher erläutern kann: AnsiColorTerminalAppender, DebugAppender, LocalSyslogAppender, MemoryAppender, OutputDebugStringAppender, RemoteSyslogAppender,
591
Fehler debuggen, testen und protokollieren
RemotingAppender, TelnetAppender und TextWriterAppender. Lesen Sie ggf. in der Dokumentation des Appender-Namensraums nach (doc/release/sdk/log4net.Appender.html). Ich kann aus Platzgründen nicht auf die teilweise komplexe Konfiguration der Appender und auf die verschiedenen Appender eingehen. Die log4net-Dokumentation enthält aber im Bereich CONFIG EXAMPLES sehr gute Beispiele zur Konfiguration der verschiedenen Appender. Ich zeige hier lediglich, wie Sie einen RollingFileAppender so konfigurieren, dass dieser für jeden Tag eine neue Protokolldatei erzeugt, und außerdem, wie Sie den ConsoleAppender und den TraceAppender konfigurieren.
Das Layout Wenn Sie einen Appender konfigurieren, geben Sie u. a. (im layout-Element) das Layout der Protokolleinträge an. Das Layout ist eine komplexe Angelegenheit (wie vieles bei log4net). Sie verwenden dazu die Typen aus dem Namensraum log4net.Layout. Die Klasse PatternLayout ist die am meisten verwendete. Diese Klasse erlaubt die Angabe eines Musters für das Layout. Interessant ist daneben auch die Klasse XmlLayout, die die Protokolleinträge als XML-Elemente formatiert. Für die PatternLayout-Klasse stehen Ihnen einige Muster zur Verfügung. Tabelle 9.8 beschreibt die Muster, die keine Performancenachteile haben. Tabelle 9.8: Die log4net-Muster für das PatternLayout ohne Performance-Nachteile
Muster
Beschreibung
%a oder
Name der Anwendungsdomäne
%appdomain %c oder
Der Name des Loggers
%logger %C oder
Der Name des Typs, in dem protokolliert wird
%class %d oder %date
Das aktuelle Datum. In geschweiften Klammern können Sie das Format übergeben, wozu Sie die in .NET üblichen Format-Zeichen verwenden. %date{dd.MM.yyyy HH:mm:ss} gibt das Datum z. B. im deutschen Format (inkl. Zeit) aus. Die Voreinstellung ist das Format yyyy-MM-dd HH:mm:ss,fff.
%exception
Eine Ausnahme, die mit der Protokollnachricht gesendet wurde
%m oder
Die Nachricht
%message %n oder
erzeugt einen Zeilenumbruch.
%newline %p oder
Die Ebene des Protokolleintrags
%level %P{Name} oder %property{Name}
592
gibt den Inhalt einer speziell definierten, Thread-spezifischen Eigenschaft aus. Name ist der Name der Eigenschaft. Eigenschaften fügen Sie im Programm über die Auflistung log4net.ThreadContext.Properties hinzu, indem Sie den Namen der Eigenschaft am Indexer angeben und den Wert schreiben. Damit können Sie auf einfache Weise zusätzliche Informationen definieren, die Sie in das Layout integrieren können. Auf der Buch-DVD finden Sie ein Beispiel dazu.
Protokollieren während der Ausführung einer Anwendung
Muster
Beschreibung
%t oder
Die Anzahl der Millisekunden seit dem Start der Anwendung
%timestamp %T oder
Der Name oder die ID des Thread, der die Protokollnachricht gesendet hat
Tabelle 9.8: Die log4net-Muster für das PatternLayout ohne Performance-Nachteile (Forts.)
%thread %utcdate
Das aktuelle Datum im UTC-Format
%x oder
Der »Nested Diagnostic Context« (NDC), der mit dem Thread assoziiert ist, der die Protokollnachricht gesendet hat. Ein NDC ist ein spezielles log4net-Feature, das in Multithreading-Anwendungen hilfreich sein kann. Über die Methode log4net.NDC.Push können Sie eine Information auf einen Thread-internen Stack ablegen. Über log4net.NDC.Pop können Sie die letzte Information wieder entfernen. Wenn Sie zwischenzeitlich protokollieren, werden die im Stack vorhandenen NDC-Informationen an der Stelle ausgegeben, an der Sie im Muster %x oder %ndc angegeben haben. Sie sollte am Ende des Thread die Methode log4net.NDC.Remove aufrufen, um den vom NDC belegten Speicher freizugeben. Auf der Buch-DVD finden Sie ein Beispiel dazu. Weitere Informationen über NDCs finden Sie in der Dokumentation des log4net zugrunde liegenden log4j (logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/NDC.html) und an der Adresse www.onjava.com/pub/a/onjava/2002/08/07/log4j.html?page=3.
%ndc
1
2
3
4
5
In Tabelle 9.9 finden Sie Muster, deren Ausführung viel Zeit in Anspruch nimmt. Diese Muster sollten Sie nur für Appender einsetzen, die ausschließlich Fehler-Nachrichten protokollieren.
6 Muster
Beschreibung
%F oder
Der Name der Quellcodedatei, in der der Protokolleintrag geschrieben wurde
%file %l oder %location
Der Name der aufrufenden Methode inkl. Informationen dazu (Dateiname, Zeilennummer)
%L oder
Die Nummer der Zeile, in der die Protokollnachricht geschrieben wurde
Tabelle 9.9: Die log4net-Muster für das PatternLayout mit einer langsamen Ausführung
7
8
%line %M oder
Der Name der Methode, in der die Protokollnachricht geschrieben wurde
9
%method %type
%u oder
Der voll qualifizierte Name des Typs, in dem die Protokollnachricht geschrieben wurde. Sie können nur Teile des Namens ausgeben, indem Sie deren Anzahl in geschweiften Klammern anhängen. %type{1} führt z. B. dazu, dass nur der Typname (ohne Namensraum) ausgegeben wird.
%identity
Der Loginname des aktuellen Benutzers, wenn dieser in .NET Membership eingeloggt ist (z. B. in ASP.NET-Webanwendungen).
%w oder
Die Windows-Identität (der Windows-Loginname) des aktuellen Benutzers
10
11
%username
Hinter dem Prozentzeichen können Sie noch eine Mindestbreite angeben. Eine positive Zahl bedeutet, dass die Information nach links mit Leerzeichen aufgefüllt wird,
593
Fehler debuggen, testen und protokollieren
bei einer negativen Zahl wird nach rechts aufgefüllt. %20logger gibt z. B. den Namen des Loggers mit 20 Zeichen aus, wobei fehlende Zeichen links mit Leerzeichen aufgefüllt werden. Mit .Anzahl können Sie festlegen, dass die Information links abgeschnitten wird, wenn sie länger ist als Anzahl Zeichen. Sie können beides auch kombinieren: %20.20logger.
Die Konfiguration der Appender Zur Konfiguration der Appender geben Sie im log4net-Element einzelne appenderElemente an. Im Attribut name vergeben Sie einen eindeutigen Namen, den Sie später, bei der Konfiguration der Logger, angeben. Das Attribut type nimmt den vollen Namen des Typs des Appenders auf. Im Unterelement layout definieren Sie das Layout. Das folgende Beispiel demonstriert dies an einem TraceAppender und einem ConsoleAppender: Listing 9.24: Deklaration eines TraceAppenders und eines ConsoleAppenders
Das Ergebnis des Beispielprogramms in Listing 9.21 auf Seite 587 ist für den ConsoleAppender das in Abbildung 9.26 dargestellte. Abbildung 9.26: Das Ergebnis des Beispielprogramms
Andere Appender wie der RollingFileAppender sind komplizierter zu konfigurieren. Ich kann wie gesagt aus Platzgründen nicht näher darauf eingehen. Das Beispiel im folgenden Abschnitt demonstriert die Konfiguration eines RollingFileAppender, der jede Minute eine neue Datei erzeugt.
594
Protokollieren während der Ausführung einer Anwendung
Filter Über Filter können Sie einen Appender auf bestimmte Nachrichten flexibel einschränken. Dazu stehen Ihnen im Namensraum log4net.Filter einige Klassen zur Verfügung (Tabelle 9.10). Tabelle 9.10: Die Filter von log4net
Filter
Bedeutung
DenyAllFilter
ignoriert alle Protokolleinträge.
LevelMatchFilter
lässt nur Protokolleinträge durch, die der in der LevelToMatch-Eigenschaft angegebenen Ebene entsprechen, wenn AcceptOnMatch true ist (Voreinstellung). Ist AcceptOnMatch false, werden die der in der LevelToMatch-Eigenschaft angegebenen Ebene entsprechenden Protokolleinträge verworfen.
2
LevelRangeFilter
lässt nur Protokolleinträge durch, die in dem Bereich liegen, der durch LevelMin und LevelMax festgelegt ist, wenn AcceptOnMatch true ist (Voreinstellung). Ist AcceptOnMatch false, werden die entsprechenden Protokolleinträge verworfen.
3
LoggerMatchFilter
lässt nur Protokolleinträge durch, deren Logger den Namen trägt, der in der LoggerToMatch-Eigenschaft angegeben ist, wenn AcceptOnMatch true ist (Voreinstellung). Ist AcceptOnMatch false, werden die entsprechenden Protokolleinträge verworfen.
4
NdcFilter
lässt nur Protokolleinträge durch, deren NDC einen String enthält, der dem in StringToMatch oder RegexToMatch angegebenen entspricht, wenn AcceptOnMatch true ist (Voreinstellung). Ist AcceptOnMatch false, werden die entsprechenden Protokolleinträge verworfen.
5
PropertyFilter
lässt nur Protokolleinträge durch, die eine Eigenschaft mit dem in Key angegebenen Schlüssel und dem in StringToMatch oder RegexToMatch angegebenen Wert enthalten, wenn AcceptOnMatch true ist (Voreinstellung). Ist AcceptOnMatch false, werden die entsprechenden Protokolleinträge verworfen.
6
StringMatchFilter
lässt nur Protokolleinträge durch, deren Nachricht einen String enthält, der dem in StringToMatch oder RegexToMatch angegebenen entspricht, wenn AcceptOnMatch true ist (Voreinstellung). Ist AcceptOnMatch false, werden die entsprechenden Protokolleinträge verworfen.
1
7
8
Das folgende Beispiel deklariert einen RollingFileAppender, dessen Filter so eingestellt wird, dass nur Nachrichten der Ebene Warn bis Fatal protokolliert werden. Beachten Sie bitte die Kommentare, die einiges erläutern.
9
Listing 9.25: Filtern in einem Appender am Beispiel eines RollingFileAppenders
10
Die Konfiguration der Logger Normalerweise wird nur der Wurzel-Logger konfiguriert
Wenn Sie die Appender konfiguriert haben, sollten Sie zumindest den Wurzel-Logger angeben, der für alle Logger steht. Diesen deklarieren Sie im root-Element. Das Unterelement level definiert die Nachrichtenebene, ab der der Logger protokolliert. Hier können Sie für eine an Kunden auszuliefernde Anwendung z. B. zunächst WARN angeben, damit Debug- und Info-Nachrichten nicht protokolliert werden. Für die Fehlersuche beim Kunden können Sie diesen den Level dann z. B. auf DEBUG vermindern lassen. Danach geben Sie in einzelnen appender-ref-Elementen die Appender an, an die der Logger seine Protokollnachrichten weitergeben soll. Damit ist die Konfiguration auch schon beendet, wenn Sie nur den Wurzel-Logger konfigurieren wollen: Listing 9.26: Konfiguration des Wurzel-Loggers
INFO
Wie bereits gesagt können Sie aber auch noch weitere Logger konfigurieren, wenn Sie z. B. die Nachrichten bestimmter Strukturen, Klassen oder Namensräume separat behandeln wollen. Ich gehe hier nicht weiter darauf ein. Sie konfigurieren diese Logger ähnlich dem Wurzel-Logger über logger-Elemente, in deren Attribut name Sie den Logger-Namen angeben. In den Beispielen dieses Abschnitts finden Sie eine Demonstration dieser Technik.
Aktivieren der Konfiguration Wenn die Konfiguration erfolgt ist, müssen Sie diese nur noch aktivieren, was beim Start des Programms über die Methode Configure der Klasse log4net.Config.XmlConfigurator erfolgt:
596
Protokollieren während der Ausführung einer Anwendung
Listing 9.27: Aktivieren der log4net-Konfiguration static void Main(string[] args) { // Konfigurieren von log4net log4net.Config.XmlConfigurator.Configure(); ...
Nun sollte die Anwendung Protokolleinträge schreiben. Wenn nicht, hilft in der Regel das Debugging der log4net-Konfiguration.
1
Debugging der log4net-Konfiguration 2
Die Konfiguration von log4net ist sehr fehlerträchtig. Wenn Ihre Anwendung keine Protokolle erzeugt, können Sie das interne Debugging von log4net einschalten. Dazu tragen Sie in der Konfiguration der Anwendung unterhalb des configSections-Elements (das immer zuerst angegeben sein muss) die folgende Einstellung ein:
3
Listing 9.28: Aktivierung des internen log4net-Debuggings
4
Jetzt starten Sie die Anwendung über Visual Studio. Im Ausgabefenster (und bei Konsolenanwendungen an der Konsole) finden Sie nun eine Menge an log4netNachrichten, die bei der Fehlersuche behilflich sein können. Viel Spaß dabei ☺. Und viel Glück beim Konfigurieren.
5
6
7
8
9 10
11
597
Inhalt
10
Arbeiten mit Dateien, Ordnern und Streams 1
Der Namensraum System.IO enthält eine Vielzahl an Typen zur Arbeit mit dem Dateisystem. Mit diesen Typen können Sie u. a. Dateien und Ordner erzeugen, kopieren, verschieben und überprüfen, ob Dateien bzw. Ordner existieren. Dieses Kapitel geht zunächst auf die Arbeit mit dem Dateisystem ein, z. B. um einen Ordner zu erzeugen, falls dieser nicht existiert.
2
3
Danach behandelt das Kapitel das wichtige Stream-Konzept, das nicht nur bei der Arbeit mit Dateien eine Rolle spielt. Auch beim Versenden oder Übergeben von (binären) Daten an andere Systeme (z. B. über das Internet) oder an Objekte werden häufig Streams eingesetzt, die ein konsistentes Programmiermodell über verschiedene Speicher- bzw. Übertragungsarten besitzen. In diesem Zusammenhang ist das Lesen und Schreiben binärer und von Textdateien ein weiteres wichtiges Thema, das natürlich auch behandelt wird.
4
5
Das Kapitel schließt ab mit der Behandlung der Arbeit mit isoliertem Speicher, die für Anwendungen wichtig ist, die unter eingeschränkten Zugriffsrechten ausgeführt werden. Das Kapitel konzentriert sich auf die Typen des Namensraums System.IO. Unter .NET können Sie zudem noch auf verschiedene andere Arten mehr oder weniger direkt mit dem Dateisystem arbeiten. So können Sie z. B. über LINQ to SQL oder ADO.NET mit Datenbanken arbeiten (Kapitel 19) oder WMI (Windows Messaging Instrumentation) nutzen, um Informationen zum Dateisystem einzulesen (WMI wird in diesem Buch nicht behandelt).
6
7
INFO
8
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
Die Klassen FileInfo und File zur Arbeit mit Dateien Die Klassen DirectoryInfo und Directory zur Arbeit mit Ordnern Pfadinformationen über die Klasse Path Ermitteln spezieller Systempfade Informationen zu Laufwerken über die Klasse DriveInfo Überwachen des Dateisystems Grundlagen des Stream-Konzepts Dateien binär lesen und schreiben Textdateien lesen und schreiben Daten komprimieren und dekomprimieren Richtlinien zum Speichern von Daten im Dateisystem Mit isoliertem Speicher arbeiten
9
10 11
599
Index
■
Arbeiten mit Dateien, Ordnern und Streams
In diesem Kapitel wird nicht behandelt: ■ ■
■ ■ ■ ■
Die Arbeit mit der Zugriffskontroll-Liste (ACL) von Dateien und Ordnern zum Steuern der Zugriffsberechtigungen. Verwendung einer FileStream-Instanz mit der Möglichkeit, dass andere Prozesse auf dieselbe Datei zugreifen können, und mit weiteren Optionen wie der Möglichkeit, die Größe des internen Puffers zu bestimmen. Die Klassen aus dem Namensraum System.IO.Pipes zum Datenaustausch zwischen Anwendungen über Windows-Pipes. Die Klasse System.Net.Sockets.NetworkStream zum Datenaustausch über ein Netzwerk. Die Klasse System.IO.BufferedStream zum Hinzufügen eines Puffers für ansonsten ungepufferte Streams (wie NetworkStream-Instanzen). Die Klasse System.Security.Cryptography.CryptoStream zur Verwaltung verschlüsselter Daten. Verschlüsselung wird allerdings in Kapitel 23 behandelt.
10.1
Mit dem Dateisystem arbeiten
Im Namensraum System.IO finden Sie einige Klassen, die Datei- und Ordneroperationen wie das Kopieren, Verschieben, Umbenennen und das Lesen und Setzen von Attributen erlauben: ■ ■ ■ ■
Die Klassen File und Directory bieten statische Methoden zur Arbeit mit Dateien (Erstellen, Kopieren, Verschieben, Auflisten etc.). Die Klassen FileInfo und DirectoryInfo bieten ähnliche Möglichkeiten wie File und Directory, müssen aber instanziert werden. Die Path-Klasse ist mit ihren statischen Methoden für die Arbeit mit Verzeichnispfaden vorgesehen. Die Klasse DriveInfo liefert Informationen zu einem Laufwerk.
Zusätzlich dazu können Sie die Pfade von Systemordnern über die EnvironmentKlasse aus dem Namensraum System ermitteln. Schließlich (aber nicht endlich ☺) können Sie über die Klasse FileSystemWatcher auf Änderungen im Dateisystem reagieren.
10.1.1 Über File und FileInfo können Sie mit Dateien arbeiten
Dateioperationen über File und FileInfo
Die Klassen File und FileInfo ermöglichen das Kopieren, Verschieben, Umbenennen von Dateien, das Lesen und Setzen von Dateiattributen, das Überprüfen, ob eine Datei existiert, das Erstellen neuer Dateien (als Stream), das Anfügen von Texten an Dateien und weitere Dateioperationen. Beide Klassen unterscheiden sich dadurch, dass File ausschließlich statische Elemente besitzt. Diese Klasse ist damit für allgemeine Dateiaufgaben vorgesehen. FileInfo muss hingegen instanziert werden und repräsentiert genau eine Datei. Damit eignet sich FileInfo als Datei-Referenz für Methodenargumente, Felder oder für Eigenschaften. FileInfo besitzt außerdem einige Eigenschaften, über die Sie weitere Informationen zu einer Datei abfragen können. Tabelle 10.1 beschreibt die wichtigsten Methoden der File-Klasse für die »normale« Arbeit mit Dateien.
600
Mit dem Dateisystem arbeiten
Methode
Beschreibung
void Copy( string sourceFileName, string destFileName [, bool overwrite ])
kopiert eine Datei. overwrite gibt an, ob existierende Dateien überschrieben werden sollen. Ist dieses Argument nicht oder mit false angegeben und die Zieldatei existiert bereits, wird eine IOException geworfen.
void Delete( string path)
löscht die angegebene Datei. Ist die Datei nicht vorhanden, wird keine Ausnahme geworfen.
bool Exists( string path)
überprüft, ob die angegebene Datei existiert.
FileAttributes GetAttributes( string path)
liefert die Attribute der Datei.
DateTime GetCreationTime( string path)
liefert das Erstelldatum der Datei als lokales bzw. als UTC-Datum.
Tabelle 10.1: Die wichtigsten Methoden der File-Klasse für die »normale« Arbeit mit Dateien
1
2
3
4
DateTime GetCreationTimeUtc( string path) DateTime GetLastAccessTime( string path)
liefert das Datum des letzten Zugriffs auf die angegebene Datei als lokales bzw. als UTC-Datum.
5
DateTime GetLastAccessTimeUtc( string path) DateTime GetLastWriteTime( string path)
6 liefert das Datum des letzten Schreibens in die angegebene Datei als lokales bzw. als UTC-Datum.
7
DateTime GetLastWriteTimeUtc( string path)
8
void Move( string sourceFileName, string destFileName)
verschiebt eine Datei oder benennt die Datei um (Umbenennen ist das Verschieben einer Datei in denselben Ordner, aber mit einem anderen Namen).
void SetAttributes( string path, FileAttributes fileAttributes)
setzt die Attribute der angegebenen Datei.
9
10
Alle in Tabelle 10.1 angegebenen Methoden werfen eine FileNotFoundException, wenn die Quelldatei nicht existiert. Delete, Copy und Move werfen ebenfalls eine Ausnahme, wenn der Benutzer keine ausreichenden Rechte besitzt oder das Schreibgeschützt-Attribut der Quell- bzw. Zieldatei gesetzt ist. Sie können über GetAttributes abfragen, ob dieses Attribut gesetzt ist. Über SetAttributes können Sie das Attribut ggf. entfernen (z. B. nachdem Sie den Benutzer gefragt haben). Move wirft ebenfalls eine IOException, wenn die Zieldatei bereits existiert. Sie sollten beim Verschieben also zuvor fragen, ob das Ziel existiert, den Benutzer fragen, ob er das Ziel überschreiben will, die Zieldatei löschen und dann erst verschieben.
11
601
Arbeiten mit Dateien, Ordnern und Streams
Neben den in Tabelle 10.1 angegebenen Methoden besitzt die File-Klasse noch einige Methoden zum Schreiben und Lesen von Dateien wie AppendAllText, Open, OpenText und ReadAllText, Methoden zum Setzen der Dateidaten wie SetCreationTime, die Methoden Decrypt und Encrypt zum Ver- und Entschlüsseln und die Methode GetAccessControl und SetAccessControl zum Zugriff auf die Zugriffskontroll-Liste (ACL). Auf diese Methoden gehe ich hier aus Platzgründen nicht ein. Das Lesen und Schreiben von Dateien wird separat behandelt (Seite 630 und Seite 634). FileInfo muss instanziert werden und bietet ähnliche Möglichkeiten wie File
Die FileInfo-Klasse bietet etwas weniger Methoden, dafür aber einige Eigenschaften mit Informationen zu einer Datei. Da ihre Elemente nicht statisch sind, müssen Sie eine Instanz dieser Klasse erzeugen. Am Konstruktor übergeben Sie den Dateinamen. Die angegebene Datei muss nicht existieren. So können Sie z. B. den Dateinamen einer (noch) nicht existierenden Datei übergeben und für diese Teilinformationen wie den Namen ohne Pfad oder die Dateierweiterung auslesen. Wenn Sie allerdings Methoden und Eigenschaften verwenden, die sich auf eine existierende Datei beziehen, resultiert eine Ausnahme. Die Methoden für die grundsätzliche Dateiarbeit sind nahezu identisch zu denen der File-Klasse, mit dem Unterschied, dass natürlich der Pfad zur Datei nicht angegeben wird. Tabelle 10.2 zeigt die wichtigsten.
Tabelle 10.2: Die wichtigsten Methoden der FileInfo-Klasse für die »normale« Arbeit mit Dateien
Methode
Beschreibung
void CopyTo( string destFileName [, bool overwrite ])
kopiert die Datei. overwrite gibt an, ob existierende Dateien überschrieben werden sollen. Ist dieses Argument nicht oder mit false angegeben und die Zieldatei existiert bereits, wird eine IOException geworfen.
void Delete()
löscht die Datei. Ist die Datei nicht vorhanden, wird keine Ausnahme geworfen.
void MoveTo( verschiebt die Datei oder nennt die Datei um. string destFileName) void Refresh()
aktualisiert den Zustand des FileInfo-Objekts.
Neben diesen Methoden bietet auch FileInfo Methoden zum Lesen und Schreiben (AppendText, Create, CreateText, Open, OpenRead, OpenText, OpenWrite und Replace), die Methoden GetAccessControl und SetAccessControl zum Zugriff auf die Zugriffskontroll-Liste und die Methoden Decrypt und Encrypt zum Ver- und Entschlüsseln. Auf diese Methoden gehe ich nicht weiter ein. Da FileInfo eine Datei repräsentiert, können Sie über einige Felder und Eigenschaften Informationen zu dieser Datei auslesen (Tabelle 10.3). Tabelle 10.3: Die Felder und Eigenschaften der FileInfo-Klasse
602
Feld / Eigenschaft
Beschreibung
string FullPath
liefert den vollen Pfad der Datei.
string OriginalPath
liefert den am Konstruktor ursprünglich angegebenen Pfad. Dabei kann es sich auch um einen relativen Pfad handeln.
FileAttributes Attributes
liest oder schreibt die Dateiattribute.
Mit dem Dateisystem arbeiten
Feld / Eigenschaft
Beschreibung
DateTime CreationTime
liefert das Erstelldatum der Datei als lokales bzw. als UTC-Datum.
DateTime CreationTimeUtc DirectoryInfo Directory
liefert eine DirectoryInfo-Instanz mit Informationen zu dem Ordner, in dem die Datei gespeichert ist.
string DirectoryName
gibt den vollen Pfad des Ordners zurück, in dem die Datei gespeichert ist.
bool Exists
gibt zurück, ob die Datei existiert.
string Extension
liefert die Dateierweiterung (inkl. Punkt).
string FullName
liefert den vollen Pfad der Datei.
bool IsReadOnly
gibt an, ob die Datei schreibgeschützt ist. Diese Eigenschaft können Sie auch setzen.
DateTime LastAccessTime( string path)
liefert das Datum des letzten Zugriffs auf die Datei als lokales bzw. als UTCDatum.
Tabelle 10.3: Die Felder und Eigenschaften der FileInfo-Klasse (Forts.)
1
2
3
4
DateTime LastAccessTimeUtc( string path) DateTime LastWriteTime( string path)
5 liefert das Datum des letzten Schreibens in die Datei als lokales bzw. als UTCDatum.
6
DateTime LastWriteTimeUtc( string path)
7
long Length
gibt die Größe der Datei in Byte zurück.
string Name
gibt den Dateinamen (ohne Verzeichnispfad) zurück.
8
Dateiinformationen auslesen Das folgende Beispiel zeigt, wie Sie Dateiinformationen über die FileInfo-Klasse auslesen. Alternativ können Sie auch die entsprechenden Methoden der File-Klasse verwenden. Wenn Sie mehr als eine Information benötigen, ist FileInfo performanter. Listing 10.1:
9
Ermitteln von Dateiinformationen
10
// Den Dateinamen ermitteln string fileName = "C:\\Demo.txt"; // Dateiinformationen auslesen FileInfo fileInfo = new FileInfo(fileName); Console.WriteLine("Name: " + fileInfo.Name); Console.WriteLine("Pfad: " + fileInfo.FullName); Console.WriteLine("Erweiterung: " + fileInfo.Extension); Console.WriteLine("Pfad des Ordners: " + fileInfo.DirectoryName); Console.WriteLine("Ist schreibgeschützt: " + fileInfo.IsReadOnly); if (fileInfo.Exists) { Console.WriteLine("Größe: {0} Byte", fileInfo.Length);
11
603
Arbeiten mit Dateien, Ordnern und Streams
Console.WriteLine("Erstelldatum: " + fileInfo.CreationTime); Console.WriteLine("Datum des letzten Schreibens: " + fileInfo.LastWriteTime); Console.WriteLine("Datum des letzten Zugriffs: " + fileInfo.LastAccessTime); }
INFO
Beim Ermitteln der Dateiinformationen sollten Sie beachten, dass die Eigenschaften Attributes, Length, CreationTime, CreationTimeUtc, LastWriteTime, LastWriteTimeUtc, LastAccessTime und LastAccessTimeUtc nur dann zur Verfügung stehen, wenn die Datei existiert. Eigenartigerweise können Sie IsReadOnly auch für nicht existierende Dateien aufrufen (was true ergibt). Bei nicht existierenden Dateien liefert die LengthEigenschaft eine FileNotFoundException. Die Eigenschaften Attributes und die Datumseigenschaften können bei nicht existierenden Dateien zwar ausgelesen werden. Attributes liefert aber -1, die Datumseigenschaften liefern (wenigstens unter Windows XP) das Datum 1.1.1601 01:00:00 (warum auch immer …). Beim Ermitteln der Informationen ist es also sinnvoll, mit Exists zu prüfen, ob die Datei existiert. Außerdem muss der Benutzer, unter dem die Anwendung ausgeführt wird, über Leserechte verfügen.
Dateiattribute auslesen und setzen Dateiattribute können Sie über die GetAttributes-Methode der File-Klasse ermitteln und über SetAttributes setzen. Alternativ können Sie eine FileInfo-Instanz erzeugen und deren Attributes-Eigenschaft verwenden. In jedem Fall arbeiten Sie mit einem Wert der FileAttributes-Aufzählung. Die wichtigsten der FileAttributes-Werte beschreibt die folgende Aufzählung: ■ ■ ■ ■ ■
ReadOnly: Die Datei ist schreibgeschützt. Hidden: Die Datei ist versteckt. System: Die Datei ist eine Systemdatei. Directory: Es handelt sich um ein Verzeichnis (bei Directory und DirectoryInfo). Normal: Es handelt sich um eine »normale« Datei, für die keine weiteren Attribute festgelegt wurden. Dieses Attribut ist nur gültig, wenn kein anderes Attribut gesetzt ist.
FileAttributes ist eine Flags-Aufzählung. Es kann also sein, dass mehrere Werte kombiniert angegeben sind. Bei der Abfrage und beim Setzen müssen Sie die berücksichtigen. Zudem muss das Benutzerkonto, unter dem die Anwendung ausgeführt wird, über ausreichende Rechte verfügen. Außerdem muss die Datei existieren und darf beim Setzen nicht durch eine andere Anwendung gesperrt sein. Im negativen Fall resultiert eine Ausnahme. Welche Ausnahmen erzeugt werden können, lesen Sie bitte in der Dokumentation der GetAttributes-Methode nach. Das folgende Beispiel zeigt, wie Sie ein Attribut explizit setzen und ein anderes explizit entfernen. Dieses Beispiel verwendet zur Demonstration die FileInfo-Klasse: Listing 10.2:
Explizites Setzen und Entfernen von Dateiattributen
// Den Dateinamen ermitteln string fileName = "C:\\Demo.txt"; // FileInfo-Instanz erzeugen FileInfo fileInfo = new FileInfo(fileName);
604
Mit dem Dateisystem arbeiten
// Das Schreibgeschützt-Attribut explizit setzen fileInfo.Attributes |= FileAttributes.ReadOnly; // Das Versteckt-Attribut explizit entfernen fileInfo.Attributes &= ~FileAttributes.Hidden;
Beachten Sie bitte die kleinen Feinheiten dieses Beispiels: Beim Setzen bewirkt das bitweise Oder, dass das Attribut gesetzt wird, auch wenn es bereits gesetzt ist. Beim Entfernen bewirkt das bitweise Und mit dem negierten Wert des Attributs (!), dass das Attribut entfernt wird, unabhängig davon, ob es gesetzt ist oder nicht. Alternativ könnten Sie beim Entfernen auch ^= einsetzen, was ein Umschalten des Attributs bewirkt, aber dann müssten Sie zuvor abfragen, ob das Attribut gesetzt ist.
1
INFO
2
Das folgende Beispiel zeigt, dieses Mal über die File-Klasse, wie Sie die wichtigen Attribute einer Datei abfragen: Listing 10.3:
3
Abfragen der wichtigen Dateiattribute
// Den Dateinamen ermitteln string fileName = "C:\\Demo.txt");
4 // Die Dateiattribute über die File-Klasse ermitteln FileAttributes fileAttributes = File.GetAttributes(fileName); // Die wichtigen Attribute abfragen if ((fileAttributes & FileAttributes.Hidden) > 0) { Console.WriteLine("Die Datei ist versteckt"); } if ((fileAttributes & FileAttributes.Normal) > 0) { Console.WriteLine("Die Datei besitzt keine Attribute außer Normal"); } if ((fileAttributes & FileAttributes.ReadOnly) > 0) { Console.WriteLine("Die Datei ist schreibgeschützt"); } if ((fileAttributes & FileAttributes.System) > 0) { Console.WriteLine("Die Datei ist eine System-Datei"); }
5
6
7
8
Dateien kopieren Dateien können Sie über die Copy-Methode der File- oder die CopyTo-Methode der FileInfo-Klasse kopieren. Wenn Sie am Argument overwrite true übergeben, wird eine bereits vorhandene Datei gelöscht, ansonsten werfen diese Methoden eine IOException wenn die Zieldatei bereits existiert. Der Benutzer muss außerdem über ausreichende Rechte verfügen, die Zieldatei darf nicht schreibgeschützt und nicht durch eine andere Anwendung gesperrt sein. Daneben gibt es noch einige andere Fälle, die eine Ausnahme hervorrufen können.
Über File.Copy und FileInfo. CopyTo können Sie Dateien kopieren
9
10 11
Listing 10.4 zeigt das Kopieren einer Datei mit Überschreiben und Abfangen der möglichen Ausnahmen: Listing 10.4: Kopieren einer Datei mit Überschreiben und Abfangen der möglichen Ausnahmen // Die Dateinamen ermitteln string fileName = "C:\\Demo.txt"); string destFileName = "C:\\Demo-Kopie.txt");
605
Arbeiten mit Dateien, Ordnern und Streams
try { File.Copy(fileName, destFileName, true); Console.WriteLine(); Console.WriteLine("{0} erfolgreich nach {1} kopiert", fileName, destFileName); } catch (ArgumentNullException) { Console.WriteLine("Die Pfadangabe der Quell- oder Zieldatei " + "ist null"); } catch (ArgumentException) { Console.WriteLine("Die Pfadangabe der Quell- oder Zieldatei " + "ist leer oder enthält ungültige Zeichen"); } catch (FileNotFoundException) { Console.WriteLine("Die Datei '{0}' wurde nicht gefunden", fileName); } catch (DirectoryNotFoundException ex) { Console.WriteLine("Quell- oder Zielverzeichnis nicht gefunden: " + ex.Message); } catch (PathTooLongException) { Console.WriteLine("Der Quell- oder/und der Zielpfad sind zu lang"); } catch (NotSupportedException) { Console.WriteLine("Die Pfadangabe der Quell- oder Zieldatei " + "ist ungültig"); } catch (UnauthorizedAccessException) { Console.WriteLine("Sie haben keinen Zugriff auf die " + "existierende Zieldatei"); } catch (IOException ex) { Console.WriteLine("Die Datei kann nicht kopiert werden: " + ex.Message); }
Dateien verschieben und umbenennen Über File.Move und FileInfo. MoveTo können Sie Dateien verschieben
Verschieben oder Umbenennen können Sie Dateien mit der Move-Methode der Fileoder der MoveTo-Methode der FileInfo-Klasse. Das Verschieben und Umbenennen einer Datei entspricht im Wesentlichen dem Kopieren, nur dass die Quelldatei hier implizit gelöscht wird. Außerdem können Sie beim Verschieben nicht explizit überschreiben, sondern sollten vorher abfragen, ob die Zieldatei existiert, und diese ggf. löschen. Das Umbenennen ist übrigens ein Verschieben in denselben Ordner. Listing 10.5 zeigt das Verschieben einer Datei. Auf das Abfangen der möglichen Ausnahmen verzichte ich in diesem Beispiel, da diese denen beim Kopieren entsprechen: Listing 10.5:
Verschieben (bzw. hier: Umbenennen) einer Datei
// Die Dateinamen ermitteln string fileName = "C:\\Demo.txt"); string destFileName = "C:\\Demo-Kopie.txt");
606
Mit dem Dateisystem arbeiten
if (File.Exists(destFileName) == false) { File.Move(fileName, destFileName); } else { Console.WriteLine("Die Zieldatei '{0}' existiert bereits", destFileName); }
1
Dateien löschen Dateien löschen Sie über die Delete-Methode der File- oder der FileInfo-Klasse. Das Löschen ist nur dann möglich, wenn der Benutzer, unter dem die Anwendung ausgeführt wird, über ausreichende Rechte verfügt, die Datei nicht schreibgeschützt ist und nicht gerade von einer anderen Anwendung gesperrt ist. Beim Löschen sind prinzipiell dieselben Ausnahmen möglich wie beim Kopieren (mit Ausnahme der FileNotFoundException, die nicht geworfen wird, wenn die zu löschende Datei nicht existiert). Ich verzichte in dem folgenden Beispiel deshalb auf das Abfangen, sondern setze lediglich das Schreibgeschützt-Attribut zurück (falls dieses gesetzt ist):
File.Delete und FileInfo.Delete löschen eine Datei
2
3
Listing 10.6: Löschen einer Datei
4
// Den Dateinamen ermitteln string fileName = "C:\\Demo.txt"); if (File.Exists(fileName)) { // Das evtl. gesetzte Schreibgeschützt-Attribut entfernen FileAttributes attributes = File.GetAttributes(fileName); attributes &= ~FileAttributes.ReadOnly; File.SetAttributes(fileName, attributes);
5
6
// Die Datei löschen File.Delete(fileName); }
10.1.2
7
Ordneroperationen
Die Klassen Directory und DirectoryInfo erlauben einige Operationen auf Ordnerebene. Ähnlich wie bei File und FileInfo besitzt Directory statische Methoden und muss DirectoryInfo instanziert werden. Darüber hinaus können Sie über die GetFolderPath-Methode der Environment-Klasse die Pfade von Systemordnern ermitteln.
Directory und DirectoryInfo erlauben Operationen auf Ordnerebene
Die Klasse Directory
8
9
Tabelle 10.4 beschreibt zunächst die wichtigsten Methoden der Klasse Directory. Methode
Beschreibung
DirectoryInfo CreateDirectory( string path [, DirectorySecurity directorySecurity ])
erstellt einen Ordner. Dabei werden ggf. alle notwendigen Unterordner ebenfalls erzeugt. Am Argument directorySecurity können Sie ein DirectorySecurityObjekt mit der Zugriffskontroll-Liste (ACL) des Ordners übergeben, falls der Zugriff ein anderer als der vom System vorgegebene Standard sein soll.
void Delete( string path [, bool recursive ])
löscht einen Ordner. Wenn Sie recursive nicht oder mit false angeben, wird der Ordner nicht gelöscht, wenn er Inhalt besitzt, und es resultiert eine IOException. Geben Sie an diesem Argument true an, wird der Ordner inklusive seines kompletten Inhalts auch dann gelöscht, wenn er Dateien oder Unterordner enthält.
Tabelle 10.4: Die wichtigsten Methoden der Directory-Klasse
10 11
607
Arbeiten mit Dateien, Ordnern und Streams
Tabelle 10.4: Die wichtigsten Methoden der Directory-Klasse (Forts.)
Methode
Beschreibung
bool Exists( string path)
gibt an, ob der angegebene Ordner existiert.
string[] GetDirectories( string path [, string searchPattern [, SearchOption searchOption ]])
liefert ein Array mit den Pfaden von Unterverzeichnissen. Am Argument searchPattern können Sie ein Muster für die Suche (mit den DOS-Wildcards * und ?)
string[] GetFiles( string path, [, string searchPattern [, SearchOption searchOption ]])
liefert ähnlich GetDirectories ein Array mit den Pfaden von untergeordneten Dateien. Leider erzeugt auch GetFiles beim rekursiven Suchen beim internen Zugriff auf Ordner, auf die der aktuelle Benutzer keinen Zugriff hat, eine UnauthorizedAccessException und ist somit nur zur nicht rekursiven Suche zu gebrauchen. Die rekursive Suche zeige ich auf Seite 611.
void Move( string sourceDirName, string destDirName)
verschiebt einen Ordner.
DirectoryInfo GetParent( string path)
liefert eine DirectoryInfo-Instanz für den übergeordneten Ordner oder null, wenn der Ordner keinen solchen besitzt.
übergeben. Das letzte Argument schließlich definiert mit SearchOption.TopDirectoryOnly, dass nur die erste Ebene des Ordners durchsucht wird (Voreinstellung). Mit SearchOption.AllDirectories werden auch alle Unterordner durchsucht. GetDirectories bricht beim rekursiven Durchsuchen leider mit einer UnauthorizedAccessException ab, wenn der Zugriff auf einzelne Ordner (wie auf C:\System Volume Information) nicht möglich ist. Diese Methode ist also nur für das nicht rekursive Suchen zu gebrauchen. Auf Seite 611 zeige ich, wie Sie rekursiv suchen können.
Neben diesen Methoden besitzt die Directory-Klasse ähnlich der File-Klasse noch einige weitere zum Lesen und Schreiben der Ordner-Datumswerte, zum Lesen und Schreiben der Zugriffskontroll-Liste (ACL), zum Ermitteln der logischen Laufwerke des Systems (GetLogicalDrives) und zum Ermitteln der Dateisystemeinträge (GetFileSystemEntries). Eine Methode zum Kopieren eines Ordners fehlt übrigens (wie auch bei DirectoryInfo).
Die Klasse DirectoryInfo Die DirectoryInfo-Klasse besitzt ähnliche Methoden wie Directory und einige Felder und Eigenschaften. Die wichtigsten Methoden zeigt Tabelle 10.5, die wichtigsten Felder und Eigenschaften finden Sie in Tabelle 10.6. Tabelle 10.5: Die wichtigsten Methoden der DirectoryInfo-Klasse
Methode
Beschreibung
Create( [DirectorySecurity directorySecurity])
erstellt den Ordner inkl. aller ggf. notwendigen Unterordner. Am Argument directorySecurity können Sie ein DirectorySecurity-Objekt mit der Zugriffskontroll-Liste (ACL) des Ordners übergeben, falls der Zugriff ein anderer als der vom System vorgegebene Standard sein soll.
DirectoryInfo erstellt einen Unterordner. CreateSubdirectory( string path [, DirectorySecurity directorySecurity ])
608
Mit dem Dateisystem arbeiten
Methode
Beschreibung
void Delete( [bool recursive ])
löscht den Ordner. recursive hat dieselbe Bedeutung wie bei der DeleteMethode der Directory-Klasse.
void MoveTo( string destDirName)
verschiebt den Ordner.
string[] GetDirectories( [string searchPattern [, SearchOption searchOption ]])
liefert wie die gleichnamige Klasse der Directory-Klasse ein Array mit den Pfaden von Unterverzeichnissen. Beachten Sie, dass auch diese Methode Probleme mit Ordnern hat, auf die der aktuelle Besitzer keinen Zugriff hat.
string[] GetFiles( [string searchPattern [, SearchOption searchOption ]])
liefert ähnlich GetDirectories ein Array mit den Pfaden von untergeordneten Dateien (wieder mit Problemen bei Ordnern, auf die der aktuelle Benutzer keinen Zugriff hat).
Feld / Eigenschaft
Beschreibung
string FullPath
liefert den vollen Pfad des Ordners.
string OriginalPath
liefert den am Konstruktor ursprünglich übergebenen Pfad. Dabei kann es sich auch um einen relativen Pfad handeln.
FileAttributes Attributes
Tabelle 10.5: Die wichtigsten Methoden der DirectoryInfo-Klasse (Forts.)
1
2
3
Tabelle 10.6: Die Felder und Eigenschaften der DirectoryInfo-Klasse
4
5
liest oder schreibt die Dateiattribute des Ordners.
6
DateTime CreationTime liefert das Erstelldatum des Ordners als lokales bzw. als UTC-Datum. DateTime CreationTimeUtc bool Exists
gibt zurück, ob der Ordner existiert.
string Extension
liefert die Dateierweiterung des Ordnernamens (inkl. Punkt).
string FullName
liefert den vollen Pfad des Ordners.
DateTime LastAccessTime( string path)
liefert das Datum des letzten Zugriffs auf den Ordner als lokales bzw. als UTCDatum.
7
8
9
DateTime LastAccessTimeUtc( string path) DateTime LastWriteTime( string path)
10 liefert das Datum des letzten Schreibens in den Ordner als lokales bzw. als UTCDatum.
11
DateTime LastWriteTimeUtc( string path) string Name
gibt den Ordnernamen (ohne Verzeichnispfad) zurück.
609
Arbeiten mit Dateien, Ordnern und Streams
Tabelle 10.6: Die Felder und Eigenschaften der DirectoryInfo-Klasse (Forts.)
Feld / Eigenschaft
Beschreibung
DirectoryInfo Parent liefert den übergeordneten Ordner oder null, falls der Ordner keinen solchen besitzt. DirectoryInfo Root
liefert den Wurzel-Ordner.
Ordner erzeugen Create erzeugt einen Ordner inkl. Unterordnern
Ordner erzeugen Sie über die Create-Methode der Directory-Klasse oder einer DirectoryInfo-Instanz. Dabei können Sie auch Ordnerpfade angeben, die Unterordner enthalten, die noch gar nicht existieren. So können Sie z. B. den Ordner Kompendium\C#\Konfiguration im Ordner für temporäre Dateien erzeugen, auch wenn der Ordner Kompendium\C# noch gar nicht existiert: Listing 10.7:
Erzeugen eines Ordners
string folderName = Path.Combine(Path.GetTempPath(), @"Kompendium\C#\Konfiguration"); Directory.CreateDirectory(folderName);
INFO
Das Beispiel nutzt als kleinen Vorgriff die Combine-Methode der Path-Klasse, die das Zusammensetzen von Pfadangaben erleichtert, indem der notwendige Backslash nach Bedarf an den ersten Pfadteil angefügt wird. Create wirft keine Ausnahme, wenn der Ordner bereits existiert. In der Praxis kann es aber vorkommen, dass der Benutzer, unter dem die aktuelle Anwendung ausgeführt wird, nicht die zum Erstellen eines Ordners erforderlichen Rechte besitzt, dass der angegebene Pfad zu lang oder ungültig ist etc. Sie sollten deswegen prinzipiell dieselben Ausnahmen abfangen, die auch beim Kopieren von Dateien abgefangen werden sollten (außer die FileNotFoundException, siehe Seite 605).
Ordner verschieben Move verschiebt einen Ordner inkl. Inhalt
Verschieben können Sie einen Ordner über die Move-Methode der Directory-Klasse oder die MoveTo-Methode einer DirectoryInfo-Instanz. Beim Verschieben sollten Sie wieder alle Ausnahmen außer FileNotFoundException abfangen, die auch beim Kopieren von Dateien auftreten können (siehe Seite 605). Im Beispiel verzichte ich allerdings aus Platzgründen darauf: Listing 10.8: Verschieben eines Ordners string folderName = Path.Combine(Path.GetTempPath(), @"Kompendium\C#\Konfiguration"); string destFolderName = Path.Combine(Path.GetTempPath(), @"Kompendium\C#\conf"); Directory.Move(folderName, destFolderName);
Das Verschieben ist nur möglich, wenn der Zielordner noch nicht existiert, der aktuelle Benutzer über ausreichend Rechte verfügt, die Pfadangaben in Ordnung sind und der dem Zielordner direkt übergeordnete Ordner bereits existiert. Sie können z. B. nicht den Ordner C:\A\B in den Ordner C:\X\Y verschieben, wenn der Ordner C:\X noch nicht existiert. In einem solchen Fall müssten Sie den dem Zielordner übergeordneten Ordner zuvor erzeugen. Dazu können Sie für diesen zunächst eine DirectoryInfo-Instanz erzeugen. Über die Create-Methode der DirectoryInfoInstanz, die Parent zurückgibt, erzeugen Sie dann den übergeordneten Ordner:
610
Mit dem Dateisystem arbeiten
Listing 10.9: Verschieben eines Ordners in einen nicht existierenden übergeordneten Ordner string folderName = Path.Combine(Path.GetTempPath(), @"Kompendium\C#\Konfiguration"); string destFolderName = Path.Combine(Path.GetTempPath(), @"Kompendium\C#\conf"); DirectoryInfo directoryInfo = new DirectoryInfo(destFolderName); if (directoryInfo.Parent != null && directoryInfo.Parent.Exists == false) { // Der dem Zielordner übergeordnete Ordner existiert nicht: // Diesen anlegen directoryInfo.Parent.Create(); }
1
2
// Den Ordner verschieben Directory.Move(folderName, destFolderName);
3
Ordner löschen Ordner können Sie über die Delete-Methode der Directory-Klasse oder einer DirectoryInfo-Instanz löschen. Wenn Sie am Argument recursive true übergeben, werden auch Ordner gelöscht, die einen Inhalt besitzen. Dabei sollten Sie natürlich die üblichen Ausnahmen abfangen, worauf ich im Beispiel aber verzichte:
Delete löscht einen Ordner, bei Bedarf samt Inhalt
4
Listing 10.10: Löschen eines Ordners samt Inhalt
5 string folderName = Path.Combine(Path.GetTempPath(), "Kompendium"); Directory.Delete(folderName, true);
Dateien und Unterordner suchen Das Suchen von Dateien und Ordnern können Sie über die Methoden GetFiles bzw. GetDirectories der Klasse Directory bzw. einer DirectoryInfo-Instanz programmieren. Am Argument searchPattern können Sie ein Suchmuster übergeben, das die DOS-Wildcards * (beliebig viele beliebige Zeichen) und ? (ein beliebiges Zeichen) enthalten kann. Wenn Sie das Argument searchOption nicht übergeben, wird nur im angegebenen Ordner direkt gesucht. Wollen Sie auch in Unterordnern suchen (was einer rekursiven Suche entspricht), könnten Sie theoretisch am Argument searchOption den Wert SearchOption.AllDirectories übergeben. Dummerweise brechen GetFiles und GetDirectories aber mit einer UnauthorizedAccessException ab, wenn der aktuelle Benutzer auf einen der durchsuchten Ordner keinen Zugriff besitzt. In diesem Fall erhalten Sie natürlich auch kein Ergebnis. Das ist z. B. für den Ordner System Volume Information eines Laufwerks unter Windows XP der Fall, wenn Sie ab dem Wurzelverzeichnis suchen.
6 Rekursiv suchen Sie besser selbst
7
8
9
10
Also sollten Sie die rekursive Suche lieber selbst implementieren. Listing 10.11 zeigt, wie Sie z. B. nach allen Textdateien im Eigene-Dateien-Ordner suchen. Der EigeneDateien-Ordner wird dabei über Environment.GetFolderPath ermittelt, wie ich es ab Seite 612 beschreibe:
11 Listing 10.11: Rekursives Suchen nach Dateien unter Berücksichtigung der Tatsache, dass der aktuelle Benutzer keinen Zugriff auf bestimmte Ordner besitzen kann /* Ermittelt alle Textdateien im Eigene-Dateien-Ordner */ private static void FindAllTextFilesInPersonalFolder() {
611
Arbeiten mit Dateien, Ordnern und Streams
DirectoryInfo personalFolder = new DirectoryInfo( Environment.GetFolderPath(Environment.SpecialFolder.Personal)); FindAllTextFiles(personalFolder); } /* Ermittelt alle Textdateien in einem Ordner und seinen Unterordnern */ private static void FindAllTextFiles(DirectoryInfo folder) { // Die Textdateien suchen, die direkt im Ordner liegen try { FileInfo[] textFiles = folder.GetFiles("*.txt"); foreach (var fileInfo in textFiles) { Console.WriteLine(fileInfo.FullName); } } catch (Exception ex) { Console.WriteLine("Der Ordner '{0}' kann nicht nach Dateien " + "durchsucht werden: {1}", folder.FullName, ex.Message); } // Alle Unterordner durchgehen und die Methode rekursiv aufrufen try { DirectoryInfo[] subFolders = folder.GetDirectories(); foreach (var directoryInfo in subFolders) { FindAllTextFiles(directoryInfo); } } catch (Exception ex) { Console.WriteLine("Der Ordner '{0}' kann nicht nach Ordnern " + "durchsucht werden: {1}", folder.FullName, ex.Message); } }
10.1.3 Die EnvironmentKlasse liefert einige SystemOrdnerpfade
Ermitteln der Pfade von Systemordnern
Den Windows-Systemordner erhalten Sie recht einfach über die Eigenschaft SystemDirectory der Klasse System.Environment: string systemDirectory = Environment.SystemDirectory;
Den Pfad spezieller Ordner wie z. B. den des Programmordners können Sie über die Methode GetFolderPath der Environment-Klasse ermitteln, der Sie eine Konstante der Aufzählung SpecialFolder übergeben. Diese Aufzählung besitzt eine Vielzahl an Werten. Die wichtigsten sind: ■
612
ApplicationData, LocalApplicationData, CommonApplicationData: Ergibt den Pfad zu einem der Ordner, die für Programmdaten verwendet werden. In einem solchen Ordner legen Programme Daten ab, die während der Ausführung erzeugt werden und die wieder verwendet werden sollen (z. B. Konfigurationsdaten). Dabei werden Ordner für »wandernde« Benutzer (roaming user), nicht wandernde Benutzer und für alle Benutzer unterschieden. Ein »wandernder« Benutzer ist ein Benutzer, der auf verschiedenen Systemen arbeitet und dessen Daten auf einem Netzwerklaufwerk verwaltet werden. Bei jeder An- und Abmeldung werden diese Daten mit dem lokalen Verzeichnis für Roaming-Daten (unter XP normalerweise der Ordner C:\Dokumente und Einstellungen\\Anwendungsdaten, unter Vista normalerweise C:\Users\\AppData\Roaming)
Mit dem Dateisystem arbeiten
■ ■
■ ■ ■ ■ ■ ■ ■ ■ ■
synchronisiert. Das muss natürlich in der Windows-Konfiguration entsprechend eingestellt sein. Den Ordner für wandernde Benutzer erreichen Sie über ApplicationData. Diesen Ordner sollten Sie immer dann verwenden, wenn Ihre Anwendung Daten speichern muss (laut den Windows-Richtlinien sollte eine Anwendung ihre Daten immer in diesem oder den anderen AnwendungsdatenOrdnern verwalten). Den Ordner für nicht wandernde Benutzer, den Sie über LocalApplicationData abfragen können, sollten Sie für benutzerspezifische Anwendungsdaten verwenden, die bei wandernden Benutzern explizit nicht auch auf anderen Systemen zur Verfügung stehen sollen. Normalerweise handelt es sich dabei unter XP um den Ordner C:\Dokumente und Einstellungen\\Lokale Einstellungen\Anwendungsdaten und unter Vista um den Ordner C:\Users\\AppData\Local. Der Wert CommonApplicationData ergibt schließlich den Ordner, der für alle Benutzer verwendet wird (unter XP normalerweise C:\Dokumente und Einstellungen\All Users\Anwendungsdaten, unter Vista normalerweise C:\ProgramData). Programs: Der Ordner, der als Programme-Ordner im Startmenü dargestellt wird. Personal oder MyDocuments: Der Eigene-Dateien-Ordner. Personal und MyDocuments besitzen denselben int-Wert (5), stehen also definitiv für denselben Ordner. Warum zwei Aufzählungswerte für den Eigene-Dateien-Ordner existieren, ist vollkommen unklar. Möglicherweise wollte Microsoft den in .NET 1.1 ausschließlich vorhandenen Wert Personal eindeutiger benennen und musste den alten Wert aus Kompatibilitätsgründen ebenfalls beibehalten. MyMusic: Der Ordner für eigene Musik. MyPictures: Der Ordner »Eigene Bilder«. Recent: Der Ordner, der Verknüpfungen zu den vom Benutzer zuletzt verwendeten Dokumenten enthält. StartMenu: Der Startmenü-Ordner. Startup: Der Ordner, der Verknüpfungen und Dateien enthält, die beim Windows-Start automatisch ausgeführt werden. Templates: Der Ordner, der für Dokumentvorlagen verwendet wird. System: Der Windows-Systemordner (normalerweise C:\Windows\System32) DesktopDirectory: Der Ordner, der den Desktop enthält. ProgramFiles: Der Ordner, der die installierten Programme enthält.
1
2
3
4
5
6
7
8
9
Den normalen Ordner für benutzerspezifische Anwendungsdaten können Sie z. B. folgendermaßen auslesen: string applicationDataFolder = Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData);
10
Der Windows-Ordner (normalerweise C:\Windows) kann übrigens unter .NET direkt nicht ermittelt werden. Dazu müssen Sie eine Windows-API-Funktion verwenden. Im Beispiel zu diesem Abschnitt zeige ich, wie das geht.
11
Den Ordner für temporäre Dateien erhalten Sie ebenfalls nicht über die GetFolderPath-Methode. Dazu rufen Sie die GetTempPath-Methode der Path-Klasse auf: string tempFolder = Path.GetTempPath();
613
Arbeiten mit Dateien, Ordnern und Streams
Den Ordner der aktuellen Anwendung erhalten Sie über einen kleinen Trick (der ein wenig Reflektion verwendet): string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location);
10.1.4 Path erleichtert die Arbeit mit Pfaden
Pfade über die Path-Klasse bearbeiten
Die Path-Klasse liefert mit ihren statischen Methoden Informations- und Bearbeitungs-Features für Datei- und Ordnerpfade. Die in meinen Augen wichtigste Methode ist Combine, die zwei Teilpfade so zusammensetzt, dass zwischen ihnen genau ein Backslash steht. Tabelle 10.7 fasst die Methoden der Path-Klasse zusammen.
Tabelle 10.7: Die Methoden der Path-Klasse
614
Methode
Beschreibung
string ChangeExtension( string path, string extension)
gibt einen Pfad zurück, dessen Erweiterung auf die angegebene neue Erweiterung geändert ist. Die Erweiterung wird mit dem Punkt angegeben.
string Combine( string path1, string path2)
kombiniert zwei Pfadteile so, dass dazwischen ein Backslash steht (auch wenn der linke mit einem solchen abgeschlossen ist oder/und der rechte mit einem solchen beginnt).
string GetDirectoryName( string path)
liefert die Ordnerangaben des übergebenen Pfads.
string GetExtension( string path)
liefert die Erweiterung des übergebenen Pfads (inkl. Punkt) oder String.Empty, wenn der Pfad keine Erweiterung besitzt.
string GetFileName( string path)
liefert den letzten Teil des übergebenen Pfads inkl. Erweiterung. Bei Dateipfaden ist das der Dateiname, bei Ordnerpfaden der Name des obersten Ordners.
string GetFileNameWithoutExtension( string path)
liefert den letzten Teil des übergebenen Pfads ohne Erweiterung. Bei Dateipfaden ist das der Dateiname, bei Ordnerpfaden der Name des obersten Ordners.
string GetFullPath( string path)
liefert den absoluten Pfad für den ggf. relativ angegebenen Pfad. Für den Pfad C:\Windows\System32\..\..\Programme wird z. B. der Pfad C:\Programme zurückgegeben. Beginnt der übergebene Pfad nicht mit einem Wurzelordner, wird der gerade aktuelle Ordner als Basis verwendet. Welcher das ist, ist schwierig zu bestimmen, da dieses DOS-Überbleibsel von Windows verwaltet wird. Über Directory.GetCurrentDirectory können Sie das aktuelle Verzeichnis auslesen und über Directory.SetCurrentDirectory setzen. Ich würde aber empfehlen, immer mit absoluten Pfadangaben zu arbeiten.
char[] GetInvalidFileNameChars()
liefert (unzuverlässig) Zeichen zurück, die in Dateinamen nicht zulässig sind. Laut der Dokumentation ist nicht gewährleistet, dass die Rückgabe alle ungültigen Zeichen enthält.
Mit dem Dateisystem arbeiten
Methode
Beschreibung
char[] GetInvalidPathChars()
liefert ähnlich GetInvalidFileNameChars unzuverlässig Zeichen zurück, die in Pfadnamen nicht gültig sind.
string GetPathRoot( string path)
liefert den Wurzelanteil des übergebenen Pfads.
string GetRandomFileName()
gibt einen zufälligen Dateinamen zurück.
string GetTempFileName()
gibt einen eindeutigen temporären Dateinamen (inkl. Pfad zum Temp-Ordner) zurück.
string GetTempPath()
gibt den Pfad zum Ordner für temporäre Dateien zurück.
bool HasExtension( string path)
ermittelt, ob der übergebene Pfad eine Erweiterung besitzt.
bool IsPathRooted( string path)
gibt true zurück, wenn der übergebene Pfad ein absoluter ist, und false, wenn es sich um einen relativen Pfad handelt.
Tabelle 10.7: Die Methoden der Path-Klasse (Forts.)
1
2
3
4 Listing 10.12 zeigt die Anwendung der wichtigsten Methoden. Listing 10.12: Anwendung der wichtigsten Methoden der Path-Klasse
5
// Den Pfad des Temp-Ordners ermitteln string tempPath = Path.GetTempPath(); Console.WriteLine("Temp-Ordner: " + tempPath); Console.WriteLine();
6
// Einen temporären Dateinamen ermitteln string tempFileName = Path.GetTempFileName(); Console.WriteLine("Temporärer Dateiname: " + tempFileName); Console.WriteLine();
7
// Einen Pfad zusammensetzen string path = Path.Combine(tempPath, "Demo.txt"); Console.WriteLine("Zusammengesetzter Pfad: " + path); Console.WriteLine();
8
// Die Erweiterung austauschen path = Path.ChangeExtension(path, ".doc"); Console.WriteLine("Pfad mit geänderter Erweiterung: " + path); Console.WriteLine();
9
// Den Datei- bzw. Ordnernamen auslesen string fileName = Path.GetFileName(path); Console.WriteLine("Dateiname: " + fileName); Console.WriteLine();
10
// Die Erweiterung auslesen string extension = Path.GetExtension(path); Console.WriteLine("Erweiterung: " + extension); Console.WriteLine();
11
// Einen absoluten Pfad ermitteln path = @"C:\Windows\System32\..\..\Programme"; Console.WriteLine("Absoluter Pfad für den Pfad '{0}':", path); string absolutePath = Path.GetFullPath(path); Console.WriteLine(absolutePath); Console.WriteLine();
615
Arbeiten mit Dateien, Ordnern und Streams
Abbildung 10.1 zeigt das Ergebnis des Programms. Abbildung 10.1: Das Ergebnis des Beispielprogramms
10.1.5 DriveInfo liefert Informationen zu Laufwerken
Laufwerkinformationen über die Klasse DriveInfo
Die Klasse DriveInfo liefert einige Informationen zu einem logischen Laufwerk. Wenn Sie eine DriveInfo-Instanz erzeugen, übergeben Sie am Konstruktor den Laufwerknamen. Über die statische GetDrives-Methode können Sie auch alle logischen Laufwerke des aktuellen Systems ermitteln. Tabelle 10.8 fasst die Eigenschaften dieser Klasse zusammen.
Tabelle 10.8: Die Eigenschaften der DriveInfo-Klasse
Eigenschaft
Beschreibung
string Name
gibt den Namen des Laufwerks zurück (z. B. »c:« für die C:-Partition des Systems).
string VolumeLabel
Diese Eigenschaft gibt die Bezeichnung des Laufwerks zurück, so wie diese z. B. im Windows Explorer angezeigt wird.
string DriveFormat
gibt den Namen des Dateisystems zurück.
bool IsReady
gibt an, ob das Laufwerk verfügbar ist.
DriveType DriveType gibt den Typ des Laufwerks an. Die zurückgegebene Aufzählung DriveType besteht aus den folgenden Werten: Unknown (unbekannter Typ), NoRootDirectory (Laufwerk ohne Stammverzeichnis, wahrscheinlich ein Bandlaufwerk), Removable (Wechseldatenträger), Fixed (Festplatten-Partition), Network (Netzwerklaufwerk), CDRom (optischer Datenträger) und Ram (Speicher-Laufwerk). long TotalSize
gibt die Gesamtgröße des Laufwerks in Byte zurück.
long TotalFreeSpace gibt den gesamten freien Speicher in Byte zurück (auch den, der nicht verfügbar ist). long AvailableFreeSpace
gibt den verfügbaren freien Speicherplatz für den Benutzer zurück, unter dessen Konto die Anwendung ausgeführt wird. Diese Angabe kann kleiner ausfallen als die des insgesamt freien Speicherplatzes, da es möglich sein kann, dass den einzelnen Benutzern auf dem angegebenen Laufwerk Speicherplatz-Quoten zugeteilt wurden.
DirectoryInfo Root- gibt ein DirectoryInfo-Objekt zurück, das das Wurzelverzeichnis des LaufDirectory werks enthält (sofern das Laufwerk über ein Wurzelverzeichnis verfügt).
616
Mit dem Dateisystem arbeiten
Die Eigenschaften DriveFormat, TotalSize, TotalFreeSpace, AvailableFreeSpace und VolumeLabel sind nur dann verfügbar, wenn das Laufwerk bereit ist. Diese Eigenschaften erzeugen eine IOException, wenn sie bei einem nicht bereiten Laufwerk abgefragt werden (wie z. B. bei einem DVD-Laufwerk ohne CD/DVD). Sie sollten also vor deren Abfrage ermitteln, ob IsReady true ergibt.
HALT
1
Außerdem sollten Sie beachten, dass logische Laufwerke, die mit subst1 zugewiesen sind, als normale Laufwerke ausgegeben werden. INFO
Wenn Sie eine Instanz der DriveInfo-Klasse erzeugen, übergeben Sie am Konstruktor den Namen des Laufwerks (normalerweise der Laufwerkbuchstabe). Das Laufwerk muss natürlich existieren, damit die Informationen gelesen werden können, ansonsten erhalten Sie eine ArgumentException.
Am Konstruktor übergeben Sie den Namen des Laufwerks
2
3
Listing 10.13 demonstriert das Auslesen der Laufwerkinformationen für die C:-Partition eines Systems:
4
Listing 10.13: Auslesen von Informationen zu einem Laufwerk // Ein DriveInfo-Objekt für das Laufwerk C: ermitteln DriveInfo driveInfo = new DriveInfo("c");
5
// Informationen zu dem Laufwerk ausgeben Console.WriteLine("Name: {0}", driveInfo.Name); if (driveInfo.IsReady) { Console.WriteLine("Label: {0}", driveInfo.VolumeLabel); Console.WriteLine("Dateisystem: {0}", driveInfo.DriveFormat); }
6
Console.Write("Laufwerktyp: "); switch (driveInfo.DriveType) { case DriveType.CDRom: Console.WriteLine("Optischer Datenträger"); break; case DriveType.Fixed: Console.WriteLine("Festplatte"); break; case DriveType.Network: Console.WriteLine("Netzwerklaufwerk"); break; case DriveType.NoRootDirectory: Console.WriteLine("Laufwerk ohne Stammverzeichnis " + "(wahrscheinlich ein Bandlaufwerk)"); break; case DriveType.Ram: Console.WriteLine("Speicher-Laufwerk"); break; case DriveType.Removable: Console.WriteLine("Wechseldatenträger"); break; case DriveType.Unknown: Console.WriteLine("Unbekannter Typ"); break; } 1
7
8
9
10 11
Der gute alte DOS-Befehl subst ermöglicht die Erstellung von logischen Laufwerken auf der Basis eines Ordners
617
Arbeiten mit Dateien, Ordnern und Streams
Console.WriteLine("Ist verfügbar: {0}", driveInfo.IsReady); Console.WriteLine("Wurzelverzeichnis: {0}", driveInfo.RootDirectory.FullName); if (driveInfo.IsReady) { Console.WriteLine("Gesamtgröße: {0} Byte", driveInfo.TotalSize); Console.WriteLine("Freier Speicher: {0} Byte", driveInfo.TotalFreeSpace); Console.WriteLine("Für den akt. Benutzer verfügbarer freier " + "Speicher: {0} Byte", driveInfo.AvailableFreeSpace); }
GetDrives liefert alle logischen Laufwerke
Über die statische GetDrives-Methode können Sie die logischen Laufwerke des Systems abfragen. Diese Methode gibt ein Array mit DriveInfo-Objekten für die einzelnen Laufwerke zurück. Listing 10.14 zeigt, wie Sie diese Methode einsetzen. Listing 10.14: Durchgehen aller logischen Laufwerke eines Systems foreach (var logicalDrive in DriveInfo.GetDrives()) { Console.Write(logicalDrive.Name + ": "); if (logicalDrive.IsReady) { Console.WriteLine(logicalDrive.DriveType + ": " + logicalDrive.AvailableFreeSpace + " Byte frei"); } else { Console.WriteLine("Nicht bereit"); } }
10.1.6
Überwachen des Dateisystems
Über eine Instanz der Klasse FileSystemWatcher aus dem Namensraum System.IO können Sie einen Ordner daraufhin überwachen, ob in ihm Dateien oder Ordner erzeugt, geändert oder gelöscht werden. Beim Erzeugen übergeben Sie der FileSystemWatcher-Instanz den zu überwachenden Ordner am ersten Argument (oder Sie definieren diesen über die PathEigenschaft). Die Eigenschaft IncludeSubdirectories gibt an, ob auch alle Unterverzeichnisse überwacht werden sollen. Am zweiten Argument des Konstruktors (oder in der Eigenschaft Filter) können Sie einen Filter übergeben, der die überwachten Elemente einschränkt. Hier geben Sie normale Dateimuster an, wie z. B. *.txt für alle Dateien mit der Endung .txt. Leider können Sie nicht mehrere Dateimuster kombinieren. Wenn Sie z. B. alle Bilddateien überwachen wollen, müssen Sie auf den Filter verzichten und die Dateien in den Ereignissen der FileSystemWatcherKlasse an Hand der Endung selbst filtern. FileSystemWatcher ist übrigens eine Komponente, die auf ein Windows.Forms-Formular (leider nicht auf ein WPF-Fenster) gezogen werden kann. Das erleichtert das Erzeugen und das Zuweisen der Ereignisse. Zur Überwachung stehen fünf Ereignisse zur Verfügung, die in Tabelle 10.9 beschrieben werden.
618
Mit dem Dateisystem arbeiten
Ereignis
Bedeutung
Changed
Eine Datei oder ein Ordner wurde geändert
Created
Eine Datei oder ein Ordner wurde erzeugt. Dieses Ereignis wird auch aufgerufen, wenn eine Datei oder ein Ordner in den überwachten Pfad kopiert oder verschoben wird.
Deleted
Eine Datei oder ein Ordner wurde gelöscht
Renamed
Eine Datei oder ein Ordner wurde umbenannt
Error
wird aufgerufen, wenn bei der Überwachung ein Fehler auftritt. Dies ist z. B. dann der Fall, wenn zu viele Ereignisse einen Überlauf des internen Puffers bewirken. Sie können die Puffergröße über die Eigenschaft InternalBufferSize verändern, allerdings sollten Sie den Puffer möglichst klein halten, da der Puffer Systemspeicher nutzt, der nicht auf die Festplatte ausgelagert werden kann. Um Pufferüberläufe zu vermeiden, sollten Sie ggf. über die NotifyFilter-Eigenschaft einen Filter setzen, um unwichtige Ereignisse auszufiltern.
Tabelle 10.9: Die Überwachungsereignisse der FileSystemWatcherKomponente
1
2
3
Der angegebene Ordner wird nur dann überwacht, wenn die Eigenschaft EnableRaisingEvents true ist. Die Voreinstellung dieser Eigenschaft ist false.
4 INFO
Über die Eigenschaft FullPath des den Ereignissen übergebenen EreignisargumentObjekts erhalten Sie den vollen Pfad der Datei bzw. des Ordners. Die Eigenschaft Name liefert lediglich den Datei- oder Ordnernamen. Im Ereignis Renamed liefern FullPath den neuen Pfad und Name den neuen Namen. Über die zusätzlichen Eigenschaften OldFullPath und OldName erhalten Sie in diesem Ereignis den alten Pfad und den alten Namen. Leider bietet das Ereignis Created, das auch aufgerufen wird, wenn Dateien oder Ordner in den überwachten Ordner kopiert oder verschoben werden, nicht die Möglichkeit, herauszufinden, woher die verschobene bzw. kopierte Datei kam.
5
6
Die Methode WaitForChanged erlaubt darüber hinaus das Warten auf eine bestimmte Änderung, optional mit einem angegebenen Timeout.
7
Das folgende Beispiel nutzt die Ereignisse einer FileSystemWatcher-Instanz zur Überwachung aller möglichen Änderungen des Ordners C:\ und schreibt eine entsprechende Meldung an die Konsole. Das Beispiel nutzt die Tatsache, dass die Signatur des Delegaten der Ereignisse Created, Changed und Deleted gleich ist und dass am Argument e.ChangeType die Art der Änderung übergeben wird.
8
9
Listing 10.15: Konsolenanwendungs-Beispiel zum Überwachen des Dateisystems static void Main(string[] args) { // FileSystemWatcher zum Überwachen des Ordners C:\ erzeugen FileSystemWatcher fileSystemWatcher = new FileSystemWatcher("C:\\");
10
// Die Ereignisse zuweisen fileSystemWatcher.Created += new FileSystemEventHandler( fileSystemWatcher_CreatedOrChangedOrDeleted); fileSystemWatcher.Changed += new FileSystemEventHandler( fileSystemWatcher_CreatedOrChangedOrDeleted); fileSystemWatcher.Deleted += new FileSystemEventHandler( fileSystemWatcher_CreatedOrChangedOrDeleted); fileSystemWatcher.Renamed += new RenamedEventHandler( fileSystemWatcher_Renamed); fileSystemWatcher.Error += new ErrorEventHandler( fileSystemWatcher_Error);
11
619
Arbeiten mit Dateien, Ordnern und Streams
// Die Ereignisüberwachung einschalten fileSystemWatcher.EnableRaisingEvents = true; Console.WriteLine("Der Ordner C:\\ wird überwacht ..."); Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine(); } static void fileSystemWatcher_CreatedOrChangedOrDeleted(object sender, FileSystemEventArgs e) { switch (e.ChangeType) { case WatcherChangeTypes.Changed: Console.WriteLine(e.FullPath + " wurde geändert"); break; case WatcherChangeTypes.Created: Console.WriteLine(e.FullPath + " wurde erstellt"); break; case WatcherChangeTypes.Deleted: Console.WriteLine(e.FullPath + " wurde gelöscht"); break; } } static void fileSystemWatcher_Renamed(object sender, RenamedEventArgs e) { Console.WriteLine(e.OldFullPath + " wurde umbenannt in " + e.FullPath); } static void fileSystemWatcher_Error(object sender, ErrorEventArgs e) { Console.WriteLine("Fehler beim Überwachen des Dateisystems: " + e.GetException().Message); }
10.2
Einführung in die Arbeit mit Streams
Streams sind ein wichtiges Konzept, das bei der Arbeit mit Daten immer wieder zum Einsatz kommt. Wenn Sie z. B. Dateien binär lesen und schreiben, arbeiten Sie mit einer FileStream-Instanz, beim Verschlüsseln von Daten arbeiten Sie mit einer GZipStream-Instanz, beim Versenden von Daten über ein Netzwerk mit einem NetworkStream-Objekt.
10.2.1 Streams verwalten einen Strom von Bytes Abbildung 10.2: Das Stream-Konzept
620
Das Stream-Konzept
Ein Stream ist ein Strom von Daten. Sie können sich einen Stream vorstellen wie eine Datenleitung, die von einer Quelle zu einem Ziel führt. Die Quelle speist Daten(bytes) in den Stream, das Ziel liest diese aus und verarbeitet sie.
Einführung in die Arbeit mit Streams
Zum Schreiben und Lesen verwaltet die Stream-Klasse einen Positionszeiger, der angibt, ab welcher Position geschrieben bzw. gelesen werden soll. Bei Streams, die gleichzeitig ein Lesen und Schreiben erlauben, muss der Zeiger beim Schreiben und beim Lesen positioniert werden, damit an die richtige Stelle geschrieben und von der richtigen Stelle gelesen wird. Das kommt jedoch relativ selten vor (ggf. in Multithreading-Anwendungen, bei denen ein Thread schreibt und ein anderer liest).
Ein Stream verwaltet eine Position
1
Ein entsprechendes Beispiel finden Sie in Form des Projekts MemoryStream-ThreadDemo neben den Beispielen zu diesem Abschnitt auf der Buch-DVD. DISC
In den meisten Fällen wird ein Stream von der Quelle zunächst komplett gefüllt, bevor er vom Ziel ausgelesen wird. Spezielle Streams wie FileStream-Instanzen werden auch gar nicht gefüllt, sondern simulieren das Stream-Konzept lediglich (bei FileStream über den direkten Zugriff auf die entsprechende Datei). Der Positionszeiger kann bei einigen Streams (wie FileStream-Instanzen) auch an eine andere Position gesetzt werden (über die Position-Eigenschaft). Das ist besonders wichtig bei Streams, die an einer Stelle im Programm geschrieben und an einer anderen Stelle gelesen werden. Vor dem Lesen muss die Position dann nämlich zurückgesetzt werden, da die Position ansonsten am Ende des Stream steht und das Lesen keine Daten ergibt. Über das Zurücksetzen der Position können Sie einen Stream auch mehrfach lesen. Andere Streams lassen das Setzen der Position aber nicht zu und können demnach nur einmal gelesen werden.
2
Der Positionszeiger kann (bzw. muss manchmal) bei vielen Streams auch umgesetzt werden
10.2.2
6 Die Quelle und das Ziel sind unerheblich
9
Die Basisklasse Stream stellt wie gesagt Elemente zur Verfügung, über die die Daten des Stream bearbeitet werden können. Tabelle 10.10 stellt zunächst die Methoden vor, Tabelle 10.11 dann die Eigenschaften. Beschreibung
Int Read( byte[] buffer, int offset, int count )
liest ab der in offset angegebenen Position die in count angegebene Anzahl Bytes in das übergebene Byte-Array. Die aktuelle Leseposition im Stream wird um die gelesene Anzahl Zeichen erhöht. offset wird normalerweise mit 0 angegeben, damit ab der aktuellen Position gelesen wird. Das Byte-Array wird normalerweise so groß definiert, wie der Stream Daten enthält (was Sie über die Length-Eigenschaft ermitteln können). count entspricht normalerweise der Größe des Byte-Arrays. Bei größeren Streams können Sie auch in einzelnen Blöcken lesen. Die Rückgabe der Methode gibt die gelesene Anzahl Bytes an. Wird 0 zurückgegeben, ist der Stream leer.
7
8
Die Methoden und Eigenschaften der Basisklasse Stream
Methode
4
5
Streams, bei denen die Quelle und das Ziel nicht in derselben Anwendung liegen, verwalten natürlich prinzipiell zwei Positionszeiger. Das ist z. B. bei einem NetworkStream der Fall, der in einer Anwendung geschrieben und in einer anderen Anwendung gelesen wird. Bei der Arbeit mit Streams sind die Quelle und das Ziel vollkommen unerheblich. Eine Anwendung arbeitet immer mit denselben Methoden der Basisklasse Stream, um Daten in den Stream zu schreiben. Und auch wenn das Programm das Ziel ist (wie beim Lesen von Daten aus einer Datei), ist die Art des Stream eigentlich unerheblich, da auch zum Lesen die Elemente der Basisklasse Stream verwendet werden. So ist es z. B. möglich, Methoden zu entwickeln, die einen beliebigen Stream übergeben bekommen und diesen verarbeiten.
3
10 Tabelle 10.10: Die wichtigen Methoden der Stream-Klasse
11
621
Arbeiten mit Dateien, Ordnern und Streams
Tabelle 10.10: Die wichtigen Methoden der Stream-Klasse (Forts.)
Methode
Beschreibung
int ReadByte()
liest ein Byte aus dem Stream und erhöht den Positionszeiger um 1. Ist kein Byte verfügbar, wird -1 zurückgegeben.
long Seek( legt bei einem Stream, der das Setzen der Position erlaubt, die Leseposition fest. Das long offset, Argument origin gibt an, ob der in offset angegebene Wert vom Anfang des Stream SeekOrigin origin) (SeekOrigin.Begin), von der aktuellen Position (SeekOrigin.Current) oder vom Ende (SeekOrigin.End) gilt.
Tabelle 10.11: Die Eigenschaften der Stream-Klasse
622
void Write( byte[] buffer, int offset, int count )
schreibt die in buffer angegebenen Bytes in den Stream. offset gibt an, ab welcher Position Zeichen aus dem übergebenen Byte-Array gelesen werden. Hier wird in der Regel 0 übergeben. count gibt die Anzahl der Bytes an, die geschrieben werden sollen. An diesem Argument wird normalerweise die Länge des Byte-Arrays übergeben.
void WriteByte( byte value )
schreibt ein Byte in den Stream.
void Flush()
sorgt dafür, dass ein ggf. verwendeter interner Puffer geleert und die Stream-Daten damit tatsächlich in das Ziel geschrieben werden. Näheres dazu finden Sie im Abschnitt »Das richtige Schließen und der interne Puffer« (Seite 625).
void Close()
schließt den Stream und gibt alle vom Stream verwendeten Ressourcen frei.
void Dispose()
ruft Close auf.
Eigenschaft
Beschreibung
bool CanRead
gibt an, ob der Stream gelesen werden kann. FileStreams-Instanzen, die nur zum Schreiben geöffnet wurden, können z. B. nicht gelesen werden.
bool CanSeek
gibt an, ob der Lese-Positionszeiger im Stream geändert werden kann.
bool CanTimeout
gibt an, ob der Stream einen Timeout erzeugt, wenn eine Schreib- oder Leseoperation zu viel Zeit benötigt. Dies kann z. B. bei einem NetworkStream der Fall sein.
bool CanWrite
gibt an, ob der Stream beschrieben werden kann. FileStreams-Instanzen, die nur zum Lesen geöffnet wurden, können z. B. nicht beschrieben werden.
long Length
gibt die Anzahl der Bytes im Stream zurück.
long Position
gibt Zugriff auf die Leseposition im Stream. Bei Streams, die keine Veränderung der Position erlauben, erzeugt Position beim Schreiben eine NotSupportedException. Sie sollten vor dem Schreiben ggf. CanSeek abfragen.
int ReadTimeout
definiert bei Streams, die einen Timeout verwenden, den Timeout in Millisekunden, der beim Lesen verwendet wird. Bei Streams, die keinen Timeout verwenden, resultiert beim Zugriff eine InvalidOperationException.
int WriteTimeout
definiert bei Streams, die einen Timeout verwenden, den Timeout in Millisekunden, der beim Schreiben verwendet wird. Bei Streams, die keinen Timeout verwenden, resultiert beim Zugriff eine InvalidOperationException.
Einführung in die Arbeit mit Streams
10.2.3 MemoryStream: Ein Beispiel für das Stream-Konzept Die grundsätzliche Arbeit mit Streams kann am besten mit einem MemoryStream demonstriert werden. Ein solcher verwaltet die Stream-Daten im Arbeitsspeicher (und wird für so einige Tricks benötigt). Ein MemoryStream unterstützt das Schreiben und das Lesen. Beim Schreiben wird die Position um die geschriebenen Bytes erhöht. Deshalb muss die Position zum Lesen wieder zurückgesetzt werden:
Ein MemoryStream demonstriert die grundsätzliche Arbeit mit Streams
1
Listing 10.16: Ein MemoryStream als Demo für die Arbeit mit Streams
2
// MemoryStream als Demo für das Stream-Konzept erzeugen using (MemoryStream memoryStream = new MemoryStream()) { if (memoryStream.CanWrite) { // Daten in den Stream schreiben Byte[] data = new Byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; memoryStream.Write(data, 0, data.Length);
3
// Nach dem Schreiben steht die Position auf dem letzten Byte // und muss zum Lesen wieder zurückgesetzt werden if (memoryStream.CanSeek) { memoryStream.Position = 0; }
4
5
} if (memoryStream.CanRead) { // Daten aus dem Stream lesen Byte[] data = new Byte[memoryStream.Length]; int count = memoryStream.Read(data, 0, data.Length); for (int i = 0; i < count; i++) { Console.WriteLine(data[i]); } }
6
7
}
8
10.2.4 Die Standard-Streams Die folgende Auflistung fasst die wichtigen Standard-Stream-Klassen des .NET Framework zusammen: ■ ■ ■
■ ■
9
System.IO.FileStream: Wird zum binären Lesen und Schreiben von Dateien verwendet. System.IO.MemoryStream: Stream, der komplett im Speicher verwaltet wird. Wird (von mir ☺) für einige spezielle Tricks eingesetzt. System.Net.Sockets.NetworkStream: Wird (sehr selten) zum Übertragen von Daten über das Netzwerk (über Stream-Sockets) verwendet. In modernen Anwendungen werden Daten eher über Webdienste oder über WCF übertragen. System.IO.IsolatedStorage.IsolatedStorageFileStream: Wird zum Speichern von Daten in isolierten Speicher verwendet (siehe Seite 642). System.IO.Pipes.PipeStream: Die von dieser in .NET 3.5 neuen, abstrakten Klasse abgeleiteten Klassen werden dazu verwendet, Daten über eine WindowsPipe zwischen zwei Anwendungen auf demselben System auszutauschen. Pipes sind eine Standard-Windows-Technologie zur Kommunikation von Anwendungen.
10 11
623
Arbeiten mit Dateien, Ordnern und Streams
10.2.5 Stream-Adapter und Dekorator-Streams Stream-Adapter erleichtern die Arbeit mit Streams
Streams haben die Einschränkung, dass sie mit binären Daten arbeiten. Hier kommen Stream-Adapter ins Spiel. Stream-Adapter sind spezielle Klassen, die an Streams andocken und eine leichtere Arbeit mit den Daten ermöglichen. Die wichtigsten sind: ■ ■ ■ ■ ■ ■
System.IO.StreamReader: Ermöglicht das Lesen der Stream-Daten als String. System.IO.StreamWriter: Ermöglicht das Schreiben der Stream-Daten als String. System.IO.BinaryReader: Ermöglicht das Lesen von einfachen Typen wie int, double und char aus einem Stream. System.IO.BinaryWriter: Ermöglicht das Schreiben von einfachen Typen in einen Stream. System.Xml.XmlReader: Ermöglicht das schnelle, vorwärtsgerichtete Lesen von XML-Daten. System.Xml.XmlWriter: Ermöglicht das schnelle, vorwärtsgerichtete Schreiben von XML-Daten.
Den Konstruktoren der Stream-Adapter-Klassen können Sie in der Regel einen Stream mit den zu bearbeitenden Daten übergeben. Bei XmlReader und XmlWriter müssen Sie allerdings die statische Create-Methode aufrufen, um eine Instanz zu erzeugen, wobei Sie aber u. a. auch einen Stream übergeben können. Alternativ erlauben die Konstruktoren bzw. Create in einigen Fällen auch noch die Angabe von Dateinamen und anderen Reader- und Writer-Instanzen. Intern erzeugen die Stream-Adapter-Klassen aber auch dann einen Stream zum Lesen bzw. Schreiben der Daten. Dekorator-Streams setzen auf Streams auf und bieten zusätzliche Funktionalität
Dekorator-Streams arbeiten nach dem Prinzip des Dekorator-Programmiermusters (falls Ihnen das etwas sagt). Sie setzen auf anderen Streams auf und »dekorieren« diese mit zusätzlichen Features. Ein CryptoStream kann z. B. die Daten eines Stream verschlüsseln und wieder entschlüsseln. Die folgende Auflistung beschreibt die wichtigen Dekorator-Streams des .NET Framework: ■ ■ ■ ■
System.IO.Compression.DeflateStream: packt und entpackt Stream-Daten nach dem Deflate-Algorithmus. System.IO.Compression.GZipStream: packt und entpackt Stream-Daten nach dem GZIP-Algorithmus. System.Security.Cryptography.CryptoStream: ermöglicht das Ver- und Entschlüsseln von Stream-Daten. System.IO.BufferedStream: Bietet einen gepufferten Zugriff auf die Daten eines Streams.
Die Dekorator-Stream-Klassen erwarten am Konstruktor den Stream, der dekoriert werden soll. Das Besondere (aber eigentlich auch Logische) an Dekorator-Streams ist, dass Sie auch mehrere zusammenketten können. So können Sie z. B. die Daten eines Stream erst über einen CryptoStream verschlüsseln und dann über einen GZipStream komprimieren. Abbildung 10.3 zeigt das Prinzip von Stream-Adaptern und Dekorator-Streams. Close schließt immer auch die adaptierten oder dekorierten Streams
624
Bei der Arbeit mit Stream-Adaptern und Dekorator-Streams müssen Sie beachten, dass deren Close-Methode immer auch den zugrunde liegenden Stream schließt. Wenn Sie z. B. einen StreamWriter verwenden, der mit einem GZipStream initialisiert ist, der einen FileStream dekoriert und Sie schließen den StreamWriter, dann schließt dieser den GZipStream und der GZipStream schließlich den FileStream.
Einführung in die Arbeit mit Streams
Abbildung 10.3: Stream-Adapter, Dekorator-Streams und Streams
1
2 Stream-Adapter und Dekorator-Streams zeige ich am Beispiel des Packens einer Textdatei, die über einen StreamWriter erzeugt wird. Ich gehe allerdings auf das Packen, Entpacken und das (komplexe) Ver- und Entschlüsseln hier nicht weiter ein. Das Komprimieren und Dekomprimieren von Daten wird ab Seite 636 behandelt.
3
4
Listing 10.17: Anwenden einer Dekorator-Stream- und einer Stream-Adapter-Klasse // Den Anwendungsordner ermitteln string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location);
5
// Dateiname ermitteln: Achtung: Das Schreiben von Dateien im // Anwendungsordner wiederspricht den Windows-Anwendungsdesign// Konventionen von Microsoft! string fileName = Path.Combine(appPath, "Demo.gzip"); FileStream fileStream = new FileStream(fileName, FileMode.Create);
6
// Einen GZip-Stream erzeugen GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Compress);
7
// Über einen Streamwriter einen Text gepackt schreiben StreamWriter streamWriter = new StreamWriter(gzipStream); streamWriter.WriteLine("Windsurfen ist der coolste Sport :-)");
8
// Den StreamWriter und damit implizit die Streams schließen streamWriter.Close();
9
// Zum Test die gepackte Datei wieder einlesen fileStream = new FileStream(fileName, FileMode.Open); gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); StreamReader streamReader = new StreamReader(gzipStream); Console.WriteLine(streamReader.ReadToEnd()); streamReader.Close();
10
10.2.6 Das richtige Schließen und der interne Puffer Das Schließen eines Stream kann in besonderen Situationen recht problematisch sein. Deswegen sollten Sie wissen, wie Streams geschlossen werden und welche Probleme auftreten können. Einige Streams, die ihre Daten intern puffern, können zu weiteren Problemen führen. Auch damit sollten Sie in der Praxis sicher umgehen können.
11
625
Arbeiten mit Dateien, Ordnern und Streams
Schließen von Streams Streams sollten immer geschlossen werden
Streams sollten wie alle Ressourcen, die geöffnet werden, immer geschlossen werden. Dazu können Sie die Close-Methode aufrufen. Sie können jedoch auch die Dispose-Methode aufrufen, denn Dispose macht bei Streams (und Stream-Adaptern) nichts anderes, als Close aufzurufen. Die Verwendung einer Stream-, Dekorator-Stream- oder/und Stream-Adapter-Instanz in einem using-Block ist also grundsätzlich keine schlechte Idee: Listing 10.18: Verwenden eines Stream, eines Dekorator-Stream und eines Stream-Adapters in je einem using-Block string fileName = "C:\\Demo.zip"; using (FileStream fileStream = new FileStream(fileName, FileMode.Create)) { using (GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) { using (StreamWriter streamWriter = new StreamWriter(fileStream)) { streamWriter.WriteLine("Das ist ein Test"); } } }
Dass die äußeren using-Blöcke Dispose der äußeren Streams erneut aufrufen, ist übrigens kein Problem: Bei bereits geschlossenen Streams führen weder Dispose noch Close zu einer Ausnahme. Was Sie sich jedoch merken sollten, ist:
HALT
Schließen Sie niemals einen Stream, der von einem Dekorator-Stream oder einem Stream-Adapter verwendet wird, bevor der Dekorator-Stream bzw. Stream-Adapter geschlossen wurde oder Sie zumindest dessen Flush-Methode aufgerufen haben. Ansonsten kann es sein, dass Daten, die noch im internen Puffer liegen, verloren gehen. Wenn Sie nach den Schließen des Stream noch mit dem Stream-Adapter oder Dekorator-Stream arbeiten, erhalten Sie sogar eine ArgumentException mit einer Meldung wie »Der Stream war nicht schreibbar«. Wenn Sie using-Blöcke verwenden, tritt dieses Problem prinzipiell nicht auf (es sei denn, Close oder Dispose werden explizit aufgerufen).
Stream-Adapter und DekoratorStreams rufen beim Schließen implizit Close auf
Bei der Arbeit mit Dekorator-Streams und Stream-Adaptern müssen Sie zudem beachten, dass deren Close-Methode immer auch den dekorierten bzw. adaptierten Stream schließt. Das geschieht einfach dadurch, dass Close die Dispose-Methode der StreamKlasse aufruft. Dieses Verhalten sollten Sie immer im Auge behalten. Beim Zugriff auf einen geschlossenen Stream erhalten Sie ansonsten eine ObjectDisposedException: Listing 10.19: Beim Zugriff auf einen geschlossenen Stream wird eine ObjectDisposedException geworfen string fileName = "C:\\Demo.zip"; using (FileStream fileStream = new FileStream(fileName, FileMode.Create)) { using (GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) { using (StreamWriter streamWriter = new StreamWriter(fileStream))
626
Einführung in die Arbeit mit Streams
{ streamWriter.WriteLine("Das ist ein Test"); } gzipStream.WriteByte(42); // ObjectDisposedException } }
Das Problem können Sie nur so lösen, dass Sie den Stream-Adapter bzw. DekoratorStream explizit nicht schließen. Vor der Weiterverarbeitung sollten Sie dann aber Flush aufrufen, um den internen Puffer zu leeren:
1
Listing 10.20: Lösen des Problems, dass ein Stream-Adapter beim Schließen den adaptierten Stream schließt
2
string fileName = "C:\\Demo.zip"; using (FileStream fileStream = new FileStream(fileName, FileMode.Create)) { using (GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) { StreamWriter streamWriter = new StreamWriter(fileStream); streamWriter.WriteLine("Das ist ein Test"); streamWriter.Flush(); gzipStream.WriteByte(42); // OK } }
3
4
5
Umgang mit dem internen Puffer Einige Streams puffern ihre Daten intern, bevor diese blockweise in das Ziel geschrieben werden. Dies bedeutet, dass Daten, die in den Stream geschrieben werden, nicht immer sofort auch in das Ziel geschrieben werden. Ein gutes Beispiel dafür ist die FileStream-Klasse, die mit dem Schreiben jeweils wartet, bis der interne Puffer (dessen Größe 4 KB ist) voll ist.
Flush leert den Puffer für Streams, die einen solchen verwenden
Die Stream-Adapter-Klassen puffern ihre Daten ebenfalls in einem internen Puffer. Die StreamWriter-Klasse verwendet z. B. einen Puffer mit einer Defaultgröße von 1024 Zeichen (oder 3075 Byte).
6
7
Die Größe des Puffers können Sie in der Regel am Argument bufferSize des Konstruktors bestimmen. Ein größerer Puffer führt u. U. zu einem beschleunigten Schreiben, da große Blöcke auf einmal geschrieben werden können.
8
Bei gepufferten Streams und den Stream-Adapter-Klassen sorgt die Flush-Methode dafür, dass der Puffer explizit geleert wird. Normalerweise wird Flush (oder die Methode, die Flush intern aufruft) von der Close-Methode implizit aufgerufen. Wenn Sie einen Stream oder einen Stream-Adapter also ordnungsgemäß schließen, sollten keine Daten verloren gehen.
9
10
2
Da allerdings in Stream selbst Flush nicht von Close aufgerufen wird , liegt es an der konkreten Stream-Klasse, dass Flush in Close aufgerufen wird. Und ich habe (mit nicht zum .NET Framework gehörenden Klassen) schon erlebt, dass dies nicht der Fall ist. In diesem Fall musste ich Flush explizit vor Close aufrufen. Sie sollten also für Klassen, die nicht zum .NET Framework gehören, überprüfen, ob Flush notwendig ist und von Close aufgerufen wird. 2
HALT
11
Dass Stream Flush nicht in Close aufruft, habe ich einmal über den .NET Reflector herausgefunden, zum anderen über die Implementierung einer von Stream abgeleiteten Testklasse, bei der ich in der überschriebenen Flush-Methode einen Haltepunkt gesetzt und im Testprogramm die Close-Methode aufgerufen habe.
627
Arbeiten mit Dateien, Ordnern und Streams
In bestimmten Situationen muss Flush explizit aufgerufen werden
Ein expliziter Aufruf von Flush ist immer dann notwendig, wenn Sie die Daten eines Stream oder Stream-Adapters verarbeiten wollen, bevor dieser geschlossen ist. Das kann z. B. der Fall sein, wenn Sie einen StreamWriter verwenden, um Daten in einen MemoryStream zu schreiben: Listing 10.21: Beispiel für die Notwendigkeit eines expliziten Flush using (MemoryStream memoryStream = new MemoryStream()) { using (StreamWriter streamWriter = new StreamWriter(memoryStream)) { streamWriter.WriteLine("Das ist ein Test"); // Hier ist der MemoryStream noch leer" Console.WriteLine("MemoryStream-Länge vor Flush: " + memoryStream.Length); // 0 streamWriter.Flush(); // Den Puffer des StreamWriter leeren // MemoryStream verarbeiten Console.WriteLine("MemoryStream-Länge nach Flush: " + memoryStream.Length); // 18 } }
10.2.7 StringWriter und StringReader basieren auf TextWriter bzw. TextReader
StringWriter und StringReader
Die Klassen StringWriter und StringReader haben mit Streams eigentlich nichts zu tun. Kennen sollten Sie diese Klassen aber trotzdem, denn sie haben einen Vorteil, der in der Praxis häufig eingesetzt wird. Sie sind nämlich von denselben Basisklassen abgeleitet wie die Klassen StreamWriter bzw. StreamReader. Die gemeinsame Klasse der Schreib-Klassen ist TextReader, die der Lese-Klassen ist TextWriter. Referenzen von diesen Typen tauchen im .NET Framework an vielen Stellen auf. Wenn es sich um Methodenargumente, Felder oder Eigenschaften handelt, können Sie einer TextReader-Referenz dann eine StreamReader-Instanz übergeben, die die Textdaten aus einem Stream liest, oder Sie übergeben eine StringReader-Instanz. Der Vorteil ist nun, dass Sie die StringReader-Instanz beim Erzeugen mit einem String initialisieren können. Damit können Sie mit Objekten, die einen TextReader erwarten, ohne viel Aufwand einfache Strings verwenden. Ähnliches gilt für die StringWriter-Klasse, über deren ToString-Methode Sie den von einem Objekt oder einer Methode geschriebenen String auslesen können. Ein Beispiel für die sinnvolle Verwendung ist die XmlTextReader-Klasse, über die Sie XML-Dokumente effizient lesen (und parsen) können. Der Konstruktor dieser Klasse (und die Create-Methode der Basisklasse XmlReader) erlauben keinen String als Quelle, aber einen TextReader. Wenn Sie die zu lesenden XML-Daten in einem String vorliegen haben, müssen Sie diese über einen StringReader übergeben.
INFO
Das folgende Beispiel greift dem Kapitel 18 vor, das die Arbeit mit XML-Dokumenten als Thema hat. In diesem Kapitel werden u. a. die Klassen XmlTextReader und XmlTextWriter ausführlicher behandelt. Ich brauchte hier aber ein Beispiel für den sinnvollen Einsatz der Klassen StringReader und StringWriter. Listing 10.22 zeigt, wie Sie über einen StringReader einen String, der XML-Daten enthält, nach bestimmten XML-Elementen durchsuchen können (ohne näher auf das relativ komplexe Parsen einzugehen):
628
Einführung in die Arbeit mit Streams
Listing 10.22: Beispiel für die Verwendung einer StringReader-Instanz als Möglichkeit, einen String an Objekte zu übergeben, die einen TextReader erwarten // Zur Demo einen XML-String zusammenstellen string xml = "" + "" + "" + "";
1
// Den XML-String über einen XmlTextReader parsen using (XmlTextReader xmlTextReader = new XmlTextReader( new StringReader(xml))) { while (xmlTextReader.Read()) { if (xmlTextReader.Name == "car") { xmlTextReader.MoveToNextAttribute(); Console.WriteLine("Hersteller: " + xmlTextReader.Value); xmlTextReader.MoveToNextAttribute(); Console.WriteLine("Modell: " + xmlTextReader.Value); } } }
2
3
4
Listing 10.23 zeigt, wie Sie einen XML-String über einen StringWriter erzeugen können (ohne auf das relativ komplexe Erzeugen der XML-Elemente und -Attribute näher einzugehen):
5
Listing 10.23: Beispiel für die Verwendung einer StringWriter-Instanz als Möglichkeit, einen String von Objekten zu erhalten, die mit einem TextWriter arbeiten
6
// StringWriter erzeugen, in die der String geschrieben werden soll StringWriter stringWriter = new StringWriter(); // Über einen XmlTextWriter ein XML-Dokument erzeugen using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter)) { xmlTextWriter.Formatting = Formatting.Indented; xmlTextWriter.Indentation = 3; xmlTextWriter.WriteStartDocument(); xmlTextWriter.WriteStartElement("cars"); xmlTextWriter.WriteStartElement("car"); xmlTextWriter.WriteAttributeString("make", "Ford"); xmlTextWriter.WriteAttributeString("model", "Puma"); xmlTextWriter.WriteEndElement(); // car-Element xmlTextWriter.WriteStartElement("car"); xmlTextWriter.WriteAttributeString("make", "Citroen"); xmlTextWriter.WriteAttributeString("model", "C1"); xmlTextWriter.WriteEndElement(); // car-Element xmlTextWriter.WriteEndElement(); // cars-Element }
7
8
9
10
// Den XML-String aus dem StringWriter auslesen und ausgeben xml = stringWriter.ToString(); Console.WriteLine(); Console.WriteLine(xml);
11
Abbildung 10.4 zeigt das Ergebnis des gesamten Beispiels.
629
Arbeiten mit Dateien, Ordnern und Streams
Abbildung 10.4: Das Beispielprogramm in Aktion
10.3 FileStream erlaubt das binäre Lesen und Schreiben
Dateien binär über eine FileStreamInstanz lesen und schreiben
Die Klasse FileStream erlaubt das binäre Lesen und Schreiben von Dateien. Diese Klasse wird sehr selten dazu genutzt, eigene Daten in Dateien zu schreiben bzw. daraus zu lesen. In vielen Fällen arbeiten Methoden und/oder Objekte aber mit Streams, aus denen diese Daten lesen oder in die sie Daten schreiben. An diesen Stream-Referenzen können Sie dann eine FileStream-Instanz übergeben, um zu erreichen, dass die zu verarbeitenden Daten aus einer Datei gelesen bzw. in eine Datei geschrieben werden. Ein Beispiel dafür ist die Serialize-Methode der BinaryFormatter-Klasse, über die Sie den Inhalt beliebiger Objekte serialisieren können. Das Serialisieren wird in Kapitel 17 behandelt. Daneben arbeiten auch die Stream-Adapter-Klassen mit FileStream-Instanzen, wenn die gelesenen oder geschriebenen Daten in einer Datei verwaltet werden sollen. Wenn Sie diesen eine FileStream-Instanz (an Stelle eines Dateinamens) übergeben, besitzen Sie erweiterte Möglichkeiten, wie z. B. die Steuerung des gemeinsamen Zugriffs auf eine Datei durch mehrere Prozesse. Beim Erzeugen einer FileStream-Instanz können Sie an den Argumenten des mehrfach überladenen Konstruktors verschiedene Informationen übergeben. Die zusammengefasste Syntax der wichtigen Überladungen ist die folgende: FileStream(string path, FileMode mode [, FileAccess access [, FileShare share [, int bufferSize [, FileOptions options]]]])
Die Argumente beschreibt die folgende Auflistung: ■ ■
630
path: Der komplette Dateiname (Dateipfad) mode: Gibt an, wie die Datei geöffnet oder ob sie erstellt werden soll. Die folgenden Werte sind möglich: ■ FileMode.CreateNew: Erstellen einer neuen Datei. Falls bereits eine gleichnamige Datei existiert, wird eine IOException ausgelöst. ■ FileMode.Create: Erstellen einer neuen Datei, wobei eine bereits vorhandene ggf. überschrieben wird. ■ FileMode.Open: Öffnen einer Datei (die vorhanden sein muss). Die Art des Öffnens wird über das access-Argument bestimmt. ■ FileMode.OpenOrCreate: Öffnen einer Datei. Ist diese noch nicht vorhanden, wird automatisch eine neue erzeugt.
Dateien binär über eine FileStream-Instanz lesen und schreiben
FileMode.Truncate: Öffnen einer Datei (die vorhanden sein muss). Nach dem Öffnen wird diese abgeschnitten, sodass sie leer ist (Null-Byte-Größe). ■ FileMode.Append: Öffnen einer Datei zum Anfügen. Die Datei muss nicht existieren. Kann nur ohne access-Argument oder mit FileAccess.Write an diesem Argument verwendet werden. access: Bestimmt mit den Werten der FileAccess-Aufzählung, welcher Zugriff auf die Datei möglich ist: ■ FileAccess.Read: Die Datei kann gelesen werden. ■ FileAccess.Write: Die Datei kann geschrieben werden. ■ FileAccess.ReadWrite: Die Datei kann gelesen und geschrieben werden. share: Gibt mit den Werten der FileShare-Aufzählung an, ob und welcher gleichzeitige Zugriff auf die Datei möglich ist. Die für die Praxis wichtigen Werte dieser Aufzählung sind: ■ FileShare.None: Die Datei kann nicht noch einmal durch den aktuellen oder einen anderen Prozess geöffnet werden, nachdem diese geöffnet wurde. ■ FileShare.Read: Die Datei kann ein weiteres Mal geöffnet werden, allerdings nur zum Lesen. Wenn Sie einem anderen Programmteil oder Prozess das gleichzeitige Lesen ermöglichen wollen, denken Sie beim Schreiben daran, ggf. Flush aufzurufen, um den Puffer zu leeren, damit die geschriebenen Daten auch direkt zur Verfügung stehen. ■ FileShare.Write: Die Datei kann ein weiteres Mal geöffnet werden, allerdings nur zum Schreiben. ■ FileShare.ReadWrite: Die Datei kann ein weiteres Mal zum Lesen und Schreiben geöffnet werden. ■ FileShare.Delete: Die Datei kann während des Lesens oder Schreibens von einem anderen Prozess (oder vom selben Prozess) gelöscht werden. Wird die Datei gelöscht, wird beim Lesen oder Schreiben keine Ausnahme geworfen. Delete kann auch mit den anderen Konstanten kombiniert werden, sodass z. B. ein gleichzeitiges Lesen und Löschen möglich ist. bufferSize: Gibt die Größe des intern verwendeten Puffers an. Die Voreinstellung ist 4 KB. options: An diesem Argument können Sie spezifische Optionen mit den Werten der FileOptions-Aufzählung definieren. Die wichtigsten sind: ■ FileOptions.WriteThrough: Gibt an, dass das Betriebssystem einen ggf. zwischengelagerten Cache umgehen und direkt in die Datei schreiben oder aus dieser lesen soll. ■ FileOptions.RandomAccess, FileOptions.SequentialScan: Mit diesen Optionen teilen Sie dem Betriebssystem mit, dass wahlfrei (RandomAccess) bzw. sequentiell (SequentialScan) auf die Datei zugegriffen werden soll. Dieses kann die Information nutzen, um die Zwischenspeicherung der Datei zu optimieren. U. U. können Sie damit die Performance beim Dateizugriff erhöhen. ■ FileOptions.Encrypted: Gibt an, dass die Datei mit Betriebssystem-Features verschlüsselt ist. Windows ermöglicht das Verschlüsseln von Dateien über das aktuelle Benutzerkonto. Dabei müssen Sie aber höllisch aufpassen: Bei einer Neueinrichtung des Systems können verschlüsselte Dateien prinzipiell nicht mehr entschlüsselt werden. Das gilt auch dann, wenn Sie ein neues Benutzerkonto mit demselben Namen und demselben Passwort einrichten und darüber versuchen, die Dateien zu entschlüsseln. ■
■
■
■ ■
1
2
3
4
5
6
7
8
9
10 11
631
Arbeiten mit Dateien, Ordnern und Streams
Um zu zeigen, wie eine FileStream-Instanz arbeitet, erzeugt das folgende Beispiel zunächst eine solche zum Anfügen und schreibt Daten in den Stream. Für die Daten verwende ich einen String, der über die GetBytes-Methode einer Encoding-Instanz in ein Byte-Array konvertiert wird. Die resultierende Datei ist damit eine Textdatei im UTF-8-Format. Auf die Encoding-Klasse gehe ich übrigens im Abschnitt »Textdateien lesen und schreiben« (Seite 634) ein. Listing 10.24: Schreiben von (Text-)Daten in einen FileStream // Den Dateinamen ermitteln string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Demo.txt"); // FileStream zum Anfügen erzeugen FileStream fileStream = new FileStream(fileName, FileMode.Append); // Die zu schreibenden Bytes ermitteln string demoText = "Snowboarden ist der coolste Sport (im Winter)"; byte[] data = Encoding.UTF8.GetBytes(demoText); // Die Daten in den Stream schreiben fileStream.Write(data, 0, data.Length);
INFO
Bei der Arbeit mit einer FileStream-Instanz können verschiedene Ausnahmen erzeugt werden, die in der jeweiligen Dokumentation des Konstruktors bzw. der verwendeten Methode beschrieben werden. Diese Ausnahmen sollten Sie natürlich in der Praxis abfangen. Listing 10.25 liest die erzeugte Datei ein, konvertiert die gelesenen Bytes wieder in einen String und zeigt diesen an: Listing 10.25: Lesen einer Datei // Den Dateinamen ermitteln string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Demo.txt"); // FileStream zum Lesen erzeugen FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read); // Den Inhalt des Stream lesen byte[] data = new byte[fileStream.Length]; fileStream.Read(data, 0, data.Length); // Den Stream schließen fileStream.Close(); // Die gelesenen Bytes wieder in einen String umwandeln // und ausgeben string fileContent = Encoding.UTF8.GetString(data); Console.WriteLine(fileContent);
INFO
632
Das Beispiel arbeitet mit einer Textdatei, weil es damit einfacher ist, die Daten zu visualisieren. FileStream wird allerdings normalerweise für Textdateien nicht verwendet. Das Schreiben und Lesen von binären Daten ist jedoch ein wenig zu abstrakt für ein Beispiel, denke ich. Nebenbei haben Sie in diesem Beispiel erfahren, wie Sie einen String in ein Byte-Array und ein Byte-Array in einen String konvertieren ☺.
Spezielles binäres Schreiben und Lesen über BinaryWriter und BinaryReader
Textdateien schreiben Sie normalerweise über einen StreamWriter und Sie lesen diese über einen StreamReader. Näheres dazu finden Sie ab Seite 634.
10.4
Spezielles binäres Schreiben und Lesen über BinaryWriter und BinaryReader
Nur der Vollständigkeit halber zeigt Listing 10.26 die Verwendung der Klassen BinaryWriter und BinaryReader, über die Sie einfache Typen in einem Stream verwalten können. Ich denke, dass diese Art der Verwaltung von Daten in modernen Anwendungen kaum noch angewendet wird, da Datenbanken (Kapitel 19), XMLDateien (Kapitel 18), Serialisierung (Kapitel 17) und die Möglichkeiten der Anwendungskonfiguration (Kapitel 15) wesentlich bessere Möglichkeiten bieten.
1
2
Listing 10.26: Schreiben und Lesen von binären Daten über eine BinaryWriter- bzw. BinaryReaderInstanz
3
// Den Dateinamen ermitteln string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Demo.dat");
4
// Datei-Stream erzeugen FileStream fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write);
5
// Einige Daten über einen BinaryWriter schreiben BinaryWriter binaryWriter = new BinaryWriter(fileStream); double doubleValue = 1.234; int intValue = 999; bool boolValue = true; binaryWriter.Write(doubleValue); binaryWriter.Write(intValue); binaryWriter.Write(boolValue);
6
7
// Den BinaryWriter und damit den Stream schließen binaryWriter.Close(); // Die Datei über einen BinaryReader einlesen fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read); BinaryReader binaryReader = new BinaryReader(fileStream);
8
// Die Daten des Stream einlesen doubleValue = binaryReader.ReadDouble(); intValue = binaryReader.ReadInt32(); boolValue = binaryReader.ReadBoolean();
9
// Den BinaryReader und damit den Stream schließen binaryReader.Close();
10
Bei der Verwendung dieser Klassen müssen Sie aufpassen, dass Sie genau die Daten auslesen, die Sie auch geschrieben haben. Ansonsten erhalten Sie entweder falsche Werte oder – beim Lesen über den Stream hinaus – eine EndOfStreamException.
11 HALT
633
Arbeiten mit Dateien, Ordnern und Streams
10.5
Textdateien lesen und schreiben
Textdateien können Sie über eine FileStream-Instanz direkt bearbeiten, wie ich es im Beispiel des Abschnitts »Dateien binär über eine FileStream-Instanz lesen und schreiben« (Seite 630) gezeigt habe. Dieses Beispiel zeigt auch, dass bei der Verwaltung von Textdateien die Codierung wichtig ist. Normalerweise wird zum Schreiben von Textdateien aber eine StreamWriter- und zum Lesen eine StreamReader-Instanz verwendet. Am Konstruktor erwarten diese entweder einen Stream oder einen Dateinamen. Wenn Sie einen Dateinamen übergeben, wird intern ein FileStream erzeugt, der die Daten verwaltet. Neben dem Dateinamen bzw. Stream können Sie noch die Codierung übergeben und eine Information, ob an bereits vorhandene Dateien angefügt werden soll. Die Überladungen des Konstruktors der StreamWriter-Klasse sehen zusammengefasst folgendermaßen aus: StreamWriter(Stream stream [, Encoding encoding [, int bufferSize]]) StreamWriter(string path [, bool append [, Encoding encoding [, int bufferSize]]]))
Das Argument append bestimmt, ob an die Datei angefügt wird. Bei der Variante mit Stream entfällt dieses Argument, da an einen Stream immer angefügt wird (bei FileStream können Sie bestimmen, ob an eine Datei angefügt wird). encoding bestimmt die in der Textdatei verwendete Codierung. bufferSize schließlich gibt die Größe des (von FileStream) intern verwendeten Puffers an (Default: 4 KB). StreamReader besitzt ähnliche Konstruktoren: StreamReader(Stream stream [, Encoding encoding [, bool detectEncodingFromByteOrderMarks [, int bufferSize]]]) StreamReader(string path [, Encoding encoding [, bool detectEncodingFromByteOrderMarks [, int bufferSize]]]) StreamReader(string path, bool detectEncodingFromByteOrderMarks) StreamReader(Stream stream, bool detectEncodingFromByteOrderMarks)
Die Codierung Bei Textdateien ist die Codierung wichtig
Bei Textdateien spielt die Codierung eine große Rolle. Auf das Codierungs-Prinzip bin ich bereits in Kapitel 1 eingegangen. Die für Textdateien normalerweise verwendete Codierung ist UTF-8, da diese zum einen die Unicode-Vorteile besitzt und zum anderen möglichst wenig Speicher benötigt. Die Codierung geben Sie in Form einer Instanz der Klasse System.Text.Encoding an. Diese Klasse besitzt einige statische Eigenschaften, die vordefinierte CodierungsObjekte zurückgeben: ■ ■ ■ ■ ■
634
ASCII: 7-Bit-ASCII-Code BigEndianUnicode: UTF-16-Codierung in der Big-Endian-Byte-Reihenfolge Default: Die Standard-Codierung des Betriebssystems (unter Windows ist das normalerweise entweder Windows-1252 oder ISO-8859-1). Unicode: UTF-16-Codierung UTF32: UTF-32-Codierung
Textdateien lesen und schreiben
■ ■
UTF7: UTF-7-Codierung UTF8: UTF-8-Codierung
Für andere Codierungen können Sie die statische GetEncoding-Methode aufrufen, der Sie den Namen der Codierung übergeben. So können Sie z. B. eine EncodingInstanz für die 8-Bit-Codierung ISO-8859-1 erzeugen (die noch häufig im Internet verwendet wird):
1
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
Beim Lesen von Textdateien müssen Sie die Codierung kennen. Mit einer falschen Codierung resultieren Texte mit falschen Zeichen. Die meisten Codierungen bieten keine Möglichkeit, deren Art zu erkennen. In Unicode-Codierungen besteht aber die Möglichkeit, eine Markierung mit den Textdaten zu verwalten, die als Byte Order Mark (BOM) bezeichnet wird. Diese »Byte-Reihenfolge-Markierung« besteht aus einigen Zeichen, die am Anfang der Datei angegeben sind.
Unicode bietet das BOM zur Anzeige der Codierung
2
3
Wenn Sie Unicode-Dateien lesen, können Sie am Argument detectEncodingFromByteOrderMarks angeben, ob die Codierung anhand des BOM bestimmt werden soll. Da die meisten Dateien eine solche Markierung aber nicht besitzen, sollten Sie detectEncodingFromByteOrderMarks nur dann auf true setzen, wenn Sie die Codierung nicht kennen. In diesem Fall ist aber nicht gewährleistet, dass die Textdaten korrekt eingelesen werden.
4
5
Textdateien schreiben StreamWriter besitzt einige Überladungen der Write-Methode und der WriteLineMethode zum Schreiben verschiedener Typen. Diese Methoden arbeiten ähnlich denen der Console-Klasse. WriteLine erzeugt nach dem Schreiben einen Zeilenumbruch.
6
StreamReader besitzt die folgenden Methoden zum Lesen:
7
■ ■ ■ ■
Read: Liest einzelne Zeichen. ReadBlock: Liest Zeichen in ein Array. ReadLine: Liest eine Zeile in einen String. Gibt null zurück, wenn der Stream keine Daten mehr enthält. ReadToEnd: Liest ab der aktuellen Position den Rest des Stream in einen String.
8
Listing 10.27 zeigt, wie Sie eine Textdatei im UTF-8-Format so schreiben, dass eine evtl. vorhandene Datei überschrieben wird:
9
Listing 10.27: Schreiben einer Textdatei über einen StreamWriter
10
// Den Dateinamen ermitteln string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Demo.txt");
11
// StreamWriter zum Schreiben im UTF-8-Format erzeugen StreamWriter streamWriter = new StreamWriter(fileName, false, Encoding.UTF8); // Einige Strings schreiben streamWriter.Write("Das ");
635
Arbeiten mit Dateien, Ordnern und Streams
streamWriter.Write("ist "); streamWriter.WriteLine("eine Demo "); streamWriter.WriteLine("zum Schreiben und Lesen "); streamWriter.WriteLine("von Textdateien."); // StreamWriter und damit auch den Stream schließen streamWriter.Close();
INFO
Beim Erzeugen der StreamWriter-Instanz und beim Schreiben können verschiedene Ausnahmen generiert werden, die in der jeweiligen Dokumentation des Konstruktors bzw. der Methode beschrieben werden. Diese Ausnahmen sollten Sie natürlich in der Praxis abfangen. Das folgende Beispiel zeigt, wie Sie eine Textdatei im UTF-8-Format zeilenweise lesen: Listing 10.28: Zeilenweises Lesen einer Textdatei über einen StreamReader // Den Dateinamen ermitteln string appPath = Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Demo.txt"); // StreamReader zum Lesen im UTF-8-Format erzeugen StreamReader streamReader = new StreamReader(fileName, Encoding.UTF8); // Die Datei zeilenweise einlesen string row; while ((row = streamReader.ReadLine()) != null) { Console.WriteLine(row); } // StreamReader und damit auch den Stream schließen streamWriter.Close();
Wie beim Schreiben sollten Sie auch beim Lesen Ausnahmen vom Typ OutOfMemoryException und IOException abfangen. Interessant ist neben der ReadLine-Methode auch die Methode ReadToEnd, über die Sie eine Textdatei in einem Rutsch komplett einlesen können. INFO
10.6
Daten komprimieren und komprimierte Daten entpacken
Das .NET Framework enthält einige Klassen, mit denen Sie die in einem Stream gespeicherten Daten komprimieren und komprimierte Daten entpacken können: ■
■
636
System.IO.Compression.GZipStream: Verwendet den GZIP-Algorithmus. Dieser Algorithmus ist im Vergleich zu Deflate besser geeignet, wenn die Daten in einer Datei gespeichert werden sollen, weil der GZIP-Algorithmus (in Form von Dateien mit der Endung .gz) weiter verbreitet ist. System.IO.Compression.DeflateStream: Verwendet den Deflate-Algorithmus, der auf demselben Komprimier-Algorithmus basiert wie GZIP. Deflate ist angeblich besser geeignet, wenn die Daten über ein Netzwerk versendet werden.
Daten komprimieren und komprimierte Daten entpacken
■
System.IO.Packaging.ZipPackage: Verwendet den ZIP-Algorithmus und ermöglicht erweiterte Features wie das Speichern mehrerer logischer Dateien.
GZipStream und DeflateStream sind relativ einfache Klassen, die lediglich die Daten eines Stream nach dem jeweiligen Algorithmus komprimieren bzw. dekomprimieren. Beim Erstellen einer Instanz geben Sie am ersten Argument des Konstruktors den Stream an. Am zweiten Argument geben Sie mit CompressionMode.Compress an, dass komprimiert werden soll, und mit CompressionMode.Decompress, dass Sie dekomprimieren wollen. Wenn Sie komprimieren, schreiben Sie in den Stream, beim Dekomprimieren lesen Sie daraus. Das ist schon alles zu diesen Klassen. Weitere Features wie die Einstellung des Komprimierungsgrads oder das Archivieren mehrerer logischer Dateien in einem komprimierten Stream bieten GZipStream und DeflateStream nicht.
GZipStream und DeflateStream verarbeiten einen Stream
Die Klasse ZipPackage (aus dem Namensraum System.IO.Packaging, der die Referenzierung der Assembly Windows.Base.dll erfordert) arbeitet etwas anders. Eine ZipPackage-Instanz repräsentiert ein ZIP-Archiv. Ein solches enthält eine oder mehrere logische Dateien, die in Form von ZipPackagePart-Objekten dargestellt werden. Jedes ZipPackagePart-Objekt besitzt einen logische URI (Uniform Resource Identifier) und einen MIME-Typ. »URI« und »MIME« sind Begriffe, die im Internet Verwendung finden. Ein URI ist der logische Name einer Ressource, über den diese identifiziert werden kann. Eine E-Mail-Adresse ist z. B. ein URI, ein Dateiname ebenfalls. Ein MIME-Typ gibt die Art von Daten an. Die MIME-Typen sind standardisiert. text/plain steht z. B. für einfachen Text.
ZipPackage packt nach dem ZIPAlgorithmus und ermöglicht logische Dateien
1
2
4
5
Die GetStream-Methode eines ZipPackagePart-Objekts liefert einen Stream, über den Sie Daten lesen oder schreiben können. Die Eigenschaft CompressionOption, deren Wert Sie beim Erzeugen eines ZipPackagePart-Objekts übergeben, definiert den Komprimierungsgrad. Die ZipPackage-Klasse unterstützt laut eigenen Tests (siehe den Performancetest unten) und der Dokumentation (siehe msdn2.microsoft.com/de-de/library/system.io. packaging.package.createpartcore.aspx) allerdings nur die Komprimierungsoptionen NotCompressed und Normal. Bei allen anderen Werten wird implizit eine normale Kompression verwendet (besser wäre wohl gewesen, wenn in diesem Fall eine NotSupportedException geworfen werden würde … Dann hätte ich auf jeden Fall nicht so lange nach der Ursache dafür suchen müssen, dass die komprimierten Daten immer gleich groß sind).
3
6
7 HALT
8
9
Damit Sie eine Idee von der Performance dieser Klassen erhalten, habe ich einen Performancetest implementiert, den Sie in dem Ordner der Beispiele zu diesem Kapitel finden. Tabelle 10.10 stellt das Ergebnis auf meinem Rechner (AMD 64, 4400+) dar. Der Performancetest integriert auch die Bibliothek #ziblib (www.icsharpcode.net/ OpenSource/SharpZipLib), die ich hier nicht bespreche, die aber eine bessere Kompression bietet. Für #ziplib habe ich jeweils die minimale, eine mittlere und die maximal mögliche Kompression gewählt. Gemessen wurde nur die Zeit, die das Komprimieren benötigt, nicht die Zeit, die das Speichern der jeweiligen Dateien in Anspruch nahm.
10 11
637
Arbeiten mit Dateien, Ordnern und Streams
Tabelle 10.12: Ein Vergleich der Komprimierung der Klassen DeflateStream, GZipStream und ZipPackage und der Bibliothek #ziblib
Klasse
Komprimierung einer Textdatei mit 100 KB Größe
Komprimierung einer Bitmap-Datei mit 900 KB Größe und vielen Farben
Komprimierung einer Bitmap-Datei mit 1001 KB Größe und wenigen Farben
Größe
Zeit
Größe
Zeit
Größe
Zeit
DeflateStream
44,80 KB
12,89 ms
1042,9 KB
125,23 ms
49,55 KB
36,85 ms
GZipStream
44,81 KB
14,31 ms
1042,9 KB
130,70 ms
49,56 KB
46,96 ms
ZipPackage mit 45,34 KB normaler Kompression
21,69 ms
1043,0 KB
133,76 ms
56,37 KB
51,57 ms
#ziplib mit minimaler Kompression (1 von 9)
28,71 KB
49,75 ms
706,06 KB
141,79 ms
27,53 KB
32,29 ms
#ziplib mit mittlerer 29,76 KB Kompression (4 von 9)
13,24 ms
706,89 KB
152,04 ms
27,71 KB
32,05 ms
#ziplib mit maximaler Kompression
32,40 ms
719,65 KB
257,83 ms
22,64 KB
955,0 ms
25,29 KB
Wie Sie dem Testergebnis entnehmen können, sind DeflateStream und GZipStream nahezu identisch. ZipPackage ist zwar langsamer und erzeugt u. U. etwas größere Archive, dafür bietet diese Klasse die Möglichkeit, mehrere logische Dateien in einem Archiv abzulegen und für diese den Dateinamen und den Typ festzulegen.
INFO
#ziplib ist mit der mittleren Kompression aber der absolute Gewinner. Mit dem eher geringen Kompressionsgrad 4 benötigt diese Klasse etwa so viel Zeit wie die schnellen Klassen DeflateStream und GZipStream, erzeugt aber kleinere Archive. Interessant ist, dass die maximale Kompression zwar (teilweise wesentlich) mehr Zeit benötigt, die Archive aber nicht wesentlich kleiner werden. Bei der Bilddatei mit vielen Farben wird das Archiv sogar größer. Weiterhin interessant ist, dass das Komprimieren der Textdaten in der mittleren Kompression wesentlich schneller ist als in der niedrigen. Eine mittlere Kompression scheint also keine schlechte Wahl zu sein.
Daten komprimieren Das Komprimieren mit DeflateStream und GZipStream ist einfach, da diese Klassen Dekorator-Streams implementieren. Das folgende Beispiel komprimiert über die GZipStream-Klasse einen String in einen FileStream: Listing 10.29: Komprimieren mit der GZipStream-Klasse string demoText = "Das ist ein Test-String"; string gzipFileName = "C:\\Demo.gzip"; using (FileStream fileStream = new FileStream(gzipFileName, FileMode.Create)) { using (GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) {
638
Daten komprimieren und komprimierte Daten entpacken
using (StreamWriter streamWriter = new StreamWriter(gzipStream)) { streamWriter.Write(demoText); } } }
Um mit der ZipPackage-Klasse arbeiten zu können, müssen Sie zunächst eine ZipPackage-Instanz erzeugen. Dazu verwenden Sie die statische Open-Methode, die ZipPackage von der Basisklasse Package geerbt hat. Dieser Methode können Sie einen Dateinamen oder einen Stream übergeben. Wenn Sie einen Dateinamen übergeben, wird implizit ein FileStream erzeugt. Am zweiten Argument definieren Sie mit den Werten der FileMode-Aufzählung, was mit dem Stream geschehen soll. Wenn Sie Daten komprimieren wollen, übergeben Sie hier FileMode.Create (wenn Sie den Stream komplett neu erzeugen wollen) oder FileMode.Append (wenn Sie an vorhandene Daten anfügen wollen). Dieses Argument müssen Sie auch dann angeben, wenn Sie am ersten Argument eine FileStream-Instanz übergeben haben, die bereits entsprechend initialisiert.
ZipPackage kann Streams oder Dateien bearbeiten
1
2
3
Da die Open-Methode vererbt wurde, gibt diese eine Package-Referenz zurück, die Sie nach ZipPackage casten müssen. Beachten Sie, dass Sie zur Verwendung der ZipPackage-Klasse die Assembly Windows.Base.dll referenzieren müssen.
4
Listing 10.30: Erzeugen eines ZipPackage-Objekts
5
string demoText = "Das ist ein Test-String"; // FileStream zum Speichern der komprimierten Daten erzeugen string zipFileName = "C:\\Demo.zip"; using (FileStream fileStream = new FileStream(zipFileName, FileMode.Create)) { // Ein ZipPackage erzeugen ZipPackage zipPackage = (ZipPackage)Package.Open(fileStream, FileMode.Create);
6
7
Dann erzeugen Sie für jede logische Datei im Archiv über die CreatePart-Methode der ZipPackage-Instanz ein ZipPackagePart-Objekt. Dabei übergeben Sie am ersten Argument eine Instanz der Klasse Uri, die den URI der logischen Datei darstellt. Für ZIP-Archive verwenden Sie die folgende Form des Uri-Konstruktors:
8
new Uri("/Dateiname", UriKind.Relative)
Ein solcher URI repräsentiert einen relativen Dateinamen. Der Dateiname wird deswegen auch mit einem Schrägstrich (in URIs äquivalent zu dem Backslash) begonnen. Den eigentlichen Dateinamen können Sie beliebig definieren. Sie können sogar eine Ordner-Struktur einsetzen (indem Sie die einzelnen Ordner durch Schrägstriche voneinander trennen), damit diese beim späteren Entpacken berücksichtigt werden kann (wie es in ZIP-Archiven üblich ist).
9
10
Am zweiten Argument der CreatePart-Methode übergeben Sie den MIME-Typ der Daten (sofern diese einen solchen besitzen). Über den MIME-Typ kann beim Entpacken der Typ der Daten ermittelt werden. Den MIME-Typen können Sie über einen der standardisierten Bezeichner (wie z. B. text/plain für eine einfache Textdatei) angeben. Die Bezeichner der wichtigsten Standard-MIME-Typen erhalten Sie über die Klasse System.Net.Mime.MediaTypeNames.
11
639
Arbeiten mit Dateien, Ordnern und Streams
Am dritten Argument können Sie den Komprimierungsgrad bestimmen. Wie gesagt unterstützt die ZipPackagePart-Klasse nur die Werte CompressionOption.NotCompressed und CompressionOption.Normal. Die letztere ist die Voreinstellung. Da auch CreatePart von Package geerbt wurde, müssen Sie die Rückgabe nach ZipPackagePart casten. Listing 10.31: Erzeugen eines ZipPackagePart-Objekts ZipPackagePart zipPackagePart = (ZipPackagePart)zipPackage.CreatePart( new Uri("/Demo.txt", UriKind.Relative), MediaTypeNames.Text.Plain, CompressionOption.Normal);
Für das Erzeugen eines ZipPackagePart-Objekts ermitteln Sie dann über die GetStream-Methode den Stream und schreiben die zu komprimierenden Daten in diesen: Listing 10.32: Schreiben von Daten in das ZipPackagePart-Objekt // Den Stream des ZipPackagePart holen Stream zipStream = zipPackagePart.GetStream(); // Den Text in den Stream schreiben using (StreamWriter streamWriter = new StreamWriter(zipStream)) { streamWriter.Write(demoText); } }
Wenn Sie mehrere logische Dateien in dem Archiv speichern wollen, wiederholen Sie das Erzeugen von ZipPackagePart-Objekten. Schließlich müssen Sie die ZipPackage-Instanz (die leider IDisposable nicht implementiert) noch schließen: Listing 10.33: Schließen der ZipPackagePart-Instanz (mit schließender Klammer für den using-Block, in dem der FileStream erzeugt wurde ) zipPackage.Close(); }
Dekomprimieren von Daten Das Dekomprimieren komprimierter Daten ist mit DeflateStream und GZipStream genauso einfach wie das Komprimieren. Das folgende Beispiel liest die Datei C:\Demo.gz ein, dekomprimiert den Inhalt und gibt diesen als String einer Konsole aus: Listing 10.34: Dekomprimieren mit der GZipStream-Klasse // Die GZIP-Datei einlesen string gzipFileName = "C:\\Demo.gz"; using (FileStream fileStream = new FileStream(gzipFileName, FileMode.Open, FileAccess.Read)) { // Die Daten über einen GZipStream dekomprimieren using (GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Decompress)) { using (StreamReader streamReader = new StreamReader(gzipStream)) {
640
Richtlinien zum Speichern von Daten im Dateisystem
string text = streamReader.ToString(); Console.WriteLine("GZIP dekomprimiert: " + demoText); } } }
Beim Dekomprimieren von ZIP-Archiven über eine ZipPackage-Instanz müssen Sie natürlich berücksichtigen, dass diese einzelne logische Dateien beinhalten kann. Das folgende Beispiel nutzt dieses Mal die Variante der Open-Methode, der ein Dateiname übergeben werden kann, um die Archiv-Daten direkt aus einer Datei zu lesen. Über die PartExists-Methode wird ermittelt, ob die erwartete logische Datei existiert. Dabei übergeben Sie den URI der Datei (der natürlich identisch zum URI der gespeicherten Datei sein muss). Über GetPart können Sie die logische Datei dann auslesen und deren Stream auswerten:
1
2
Listing 10.35: Entpacken ZIP-komprimierter Daten mit ZipPackage
3
// Die ZIP-Datei einlesen string zipFileName = "C:\\Demo.zip"; ZipPackage zipPackage = (ZipPackage)ZipPackage.Open(zipFileName, FileMode.Open, FileAccess.Read);
4 // Überprüfen, ob die erwartete logische Datei existiert Uri packagePartUri = new Uri("/Demo.txt", UriKind.Relative); if (zipPackage.PartExists(packagePartUri)) { // Die logische Datei auslesen ZipPackagePart zipPackagePart = (ZipPackagePart)zipPackage.GetPart(packagePartUri);
5
// Den Stream auslesen Stream packagePartStream = zipPackagePart.GetStream(); using (StreamReader streamReader = new StreamReader(packagePartStream)) { string text = streamReader.ReadToEnd(); Console.WriteLine("ZIP dekomprimiert: " + text); }
6
7
} else { Console.WriteLine("Die erwartete logische Datei '" + packagePartUri.AbsolutePath + "' existiert nicht im Archiv"); }
Alternativ können Sie über die GetParts-Methode alle vorhandenen logischen Dateien auslesen und auswerten. Bei der Auswertung helfen der URI der Datei und der MIMETyp, die Sie aus den Eigenschaften Uri und ContentType auslesen können. Das überlasse ich aber jetzt ganz Ihnen ☺.
10.7
8
9 INFO
10
Richtlinien zum Speichern von Daten im Dateisystem
11
Wenn Sie Daten im Dateisystem speichern (nicht in einem Datenbanksystem), sollten Sie eine wichtige Regel berücksichtigen: ■
Speichern Sie die Daten in einem der Ordner, die für Anwendungsdaten vorgesehen sind. Diese erhalten Sie über die GetFolderPath-Methode der EnvironmentKlasse, indem Sie die Konstanten Environment.SpecialFolder.Application-
641
Arbeiten mit Dateien, Ordnern und Streams
■
INFO
Data, Environment.SpecialFolder.LocalApplicationData oder Environment.SpecialFolder. CommonApplicationData übergeben, oder überlassen Sie dem Anwender die Auswahl des Ordners. Speichern Sie den ausgewählten Ordner in der Anwendungskonfiguration (siehe Kapitel 15), damit Sie diesen wieder verwenden können.
Wenn Sie einen der Ordner verwenden, die für Anwendungsdaten vorgesehen sind, erzeugen Sie in diesem eine Unterordner-Struktur in der folgenden Form: Firmenname\Anwendungsname. In diesem speichern Sie die Daten der Anwendung. Damit folgen Sie dem allgemein verwendeten Standard. Wenn Sie keine Firma besitzen oder für keine solche arbeiten, setzen Sie einfach Ihren eigenen Namen ein ☺. Sie sollten Daten vor allen Dingen niemals im Anwendungsordner oder in einem der Windows-Systemordner speichern. Die Wahrscheinlichkeit, dass der Benutzer, unter dem die Anwendung ausgeführt wird, Schreibrechte in diesen Ordnern besitzt, ist relativ gering. Außerdem könnte es natürlich sein, dass Sie beim Speichern in Systemordnern das System beschädigen. Im Wesentlichen folgen Sie damit auch den Richtlinien zur Entwicklung von Windows-Anwendungen von Microsoft. Leider kann ich Ihnen für die aktuelle Version keinen direkten Link anbieten, da diese auf der Microsoft-Website scheinbar sehr gut versteckt sind. Das aktuelle »Application Compatibility Cookbook« finden Sie an der folgenden Adresse: msdn2.microsoft.com/en-us/library/aa480152.aspx. Auf der deutschen Seite »Windows Vista-Anwendungskompatibilität« sind ebenfalls Informationen zu finden: msdn2.microsoft.com/de-de/windowsvista/aa904987.aspx.
10.8
Arbeiten mit isoliertem Speicher
Isolierter Speicher ist ein spezielles Konzept von .NET, das einen speziellen Ordner im Dateisystem verwendet, um die Daten von Anwendungen und/oder Benutzern isoliert zu speichern. Damit können Sie zum einen die Daten von Anwendungen und Benutzern so gegen andere Daten isolieren, dass diese nicht versehentlich verändert werden können. Zum anderen ist es auf gesicherten Systemen eher wahrscheinlich, dass eine Anwendung in den Ordner für isolierten Speicher schreiben kann als in irgendeinen anderen Ordner. Der erste Punkt ist ein für die Praxis eher nebensächlicher. Durch die Isolation von Anwendungsdaten verhindern Sie prinzipiell, dass andere Anwendungen die Daten einer Anwendung lesen oder verändern können. Wirklich sicher ist dies nicht, da eine Anwendung immer noch direkt auf den Ordner für isolierte Speicherung zugreifen kann. Das muss dann aber (in böser Absicht) bewusst programmiert werden. Ohne isolierte Speicherung können Sie prinzipiell auch eine Trennung von Anwendungsdaten erreichen, indem Sie diese in einem der dafür vorgesehenen Ordner speichern und als Unterordner die Struktur verwenden, die ich im vorhergehenden Abschnitt vorgeschlagen habe. Isolierter Speicher kann meist auch mit eingeschränkten CASRechten verwendet werden
642
Der zweite Punkt ist in meinen Augen der wirkliche Grund für den Einsatz isolierten Speichers. Auf gesicherten Systemen kann es nämlich sein, dass die Codezugriffssicherheit von .NET (CAS) für Assemblys so festgelegt ist, dass diese nicht in einen anderen als den Ordner für isolierten Speicher schreiben dürfen. Das kann besonders für Anwendungen der Fall sein, die per Click Once mehr oder weniger automatisch
Arbeiten mit isoliertem Speicher
über einen Klick auf einen Link auf einer Webseite oder in einer E-Mail installiert werden. Näheres dazu finden Sie in Kapitel 16. Codezugriffssicherheit wird in Kapitel 23 grundlegend behandelt. Im Wesentlichen kann ein Administrator damit Assemblys, die bestimmten Bedingungen entsprechen, spezielle Rechte vergeben oder entziehen. Per Voreinstellung gelten die folgenden Regeln: ■
■
■
1
Assemblys, die über das Internet in den Speicher geladen werden (über einen URL), besitzen z. B. nur sehr eingeschränkte Rechte. Diese Assemblys dürfen z. B. keine Dateien lesen und schreiben (auch nicht im isolierten Speicher). Assemblys, die über das Internet in den Speicher geladen werden (z. B. über einen UNC-Pfad), besitzen ebenfalls eingeschränkte Rechte, allerdings nicht so restriktiv wie die der Internet-Zone. Dateien schreiben dürfen solche Assemblys aber auch nicht. Assemblys, die lokal ausgeführt werden, besitzen alle Rechte, die auch der Benutzer besitzt, der die entsprechende Anwendung ausführt.
2
3
Auf gesicherten Systemen ist es nun eher wahrscheinlich, dass eine Anwendung Daten im Ordner für isolierte Speicherung verwalten kann als in irgendeinem anderen Ordner. Ich habe z. B. einmal ein Seminar für die deutsche Bundeswehr gehalten (obwohl ich Pazifist bin). Auf deren Systemen war CAS so eingeschränkt, dass nur speziell zertifizierte Assemblys alle normalen Rechte (des aktuellen Benutzers) besaßen. Alle anderen Assemblys wurden nur mit minimalen Rechten ausgeführt. Auf diesen Systemen war die Benutzung isolierten Speichers die einzige Möglichkeit, mit nicht zertifizierten Assemblys Daten in Dateien zu verwalten. Auf einem System, dessen Codezugriffssicherheit nicht geändert wurde, macht die Verwendung isolierten Speichers keinen Sinn. Da lokal ausgeführte Assemblys alle Rechte besitzen, können diese ihre Daten in einem der für Anwendungsdaten vorgesehenen Ordner verwalten. Lediglich auf Systemen, die vom Administrator so eingeschränkt sind, dass alle oder bestimmte Assemblys nicht in diese Ordner schreiben dürfen, ist es u. U. sinnvoll, isolierten Speicher zu verwenden. Die Codezugriffssicherheit muss dann aber so eingestellt werden, dass dies auch erlaubt ist.
4
5
6 INFO
7
8
10.8.1
Grundlagen von isoliertem Speicher
Isolierter Speicher ist ein Ordner im Dateisystem, auf den Sie über die Klasse IsolatedStorageFile (aus dem Namensraum System.IO.IsolatedStorage) zugreifen können. Anders als der Name vermuten lässt, repräsentiert diese Klasse einen ganzen Ordner und nicht nur eine einzige Datei. Der Speicherort der isolierten Daten ist ein Ordner mit Namen IsolatedStorage in einem der Ordner für lokale Anwendungsdaten (der auf einem System noch nicht existieren muss und bei Bedarf angelegt wird). Dabei werden Speicherorte für lokale, wandernde und alle Benutzer unterschieden. Beim Erstellen einer IsolatedStorageFile-Instanz geben Sie an, welchen Isolationstyp Sie wünschen. Abhängig davon werden die Daten in den Ordnern gespeichert, die die Konstanten LocalApplicationData, ApplicationData bzw. CommonApplicationData der SpecialFolder-Aufzählung mit der GetFolderPathMethode der Environment-Klasse ergeben (siehe Seite 612). Im IsolatedStorage-Ordner werden die isolierten Daten allerdings in einem Gewirr von Unterordnern gespeichert, sodass es prinzipiell nur der Anwendung, die die Daten geschrieben hat, möglich ist, diese wieder auszulesen.
Isolierter Speicher ist ein spezieller, isolierter Ordner im Dateisystem
9
10 11
643
Arbeiten mit Dateien, Ordnern und Streams
INFO
Dieses Gewirr ist neben der komplexen Anwendung von isoliertem Speicher ggf. auch der Grund dafür, isolierte Speicherung nicht einzusetzen. Wenn Sie z. B. Konfigurationsdaten im isolierten Speicher verwalten, ist es dem Benutzer nahezu unmöglich, diese zu finden, falls die Daten einmal von Hand angepasst werden müssen. Auch das Sichern wichtiger Daten ist mit isolierter Speicherung nahezu unmöglich (es sei denn, der gesamte jeweilige IsolatedStorage-Ordner wird gesichert).
Isolationstypen Isolierter Speicher unterscheidet einige Isolationstypen
Isolierter Speicher wird in Isolationstypen unterschieden, die Sie festlegen, wenn Sie eine IsolatedStorageFile-Instanz erzeugen. Zum Erzeugen können Sie die statische GetStore-Methode verwenden, der Sie eine Kombination der Werte der IsolatedStorageScope-Aufzählung übergeben. Deren Werte sind die folgenden: ■ ■ ■
■ ■
■ ■
None: Es wird keine isolierte Speicherung verwendet. User: Der Gültigkeitsbereich des isolierten Speichers wird anhand der Benutzeridentität festgelegt. Domain: Der Gültigkeitsbereich wird anhand der Identität der Anwendungsdomäne festgelegt. Anwendungsdomänen sind Domänen von Anwendungen, die zusammengefasst sind. Anwendungsdomänen werden in Kapitel 22 behandelt. Assembly: Der Gültigkeitsbereich der isolierten Speicherung wird durch die aktuelle Assembly festgelegt. Roaming: Der isolierte Speicher kann an einem roamingfähigen Speicherort im Dateisystem abgelegt werden, sofern das Roaming der Benutzerdaten im zugrunde liegenden Betriebssystem aktiviert ist. Machine: Der Gültigkeitsbereich der isolierten Speicherung wird durch den Computer festgelegt. Application: Der Gültigkeitsbereich der isolierten Speicherung wird durch die Anwendung festgelegt.
Sie können die Werte kombinieren, sodass Sie z. B. eine Trennung verschiedener Anwendungen und in dieser eine Trennung nach den einzelnen Benutzern erreichen. Einfacher ist jedoch die Verwendung der Methoden, die vordefinierte IsolatedStorageFile-Instanzen zurückgeben: ■
■
■
■
644
GetMachineStoreForApplication: Liefert isolierten Speicher, der mit der aktuellen Anwendung verknüpft ist. Alle Assemblys der Anwendung verwenden dann denselben isolierten Speicher. Entspricht IsolatedStorageScope.Application | IsolatedStorageScope.Machine. GetMachineStoreForAssembly: Liefert isolierten Speicher, der mit der aktuellen Assembly verknüpft ist. Entspricht IsolatedStorageScope.Assembly | IsolatedStorageScope.Machine. GetMachineStoreForDomain: Liefert isolierten Speicher, der mit der aktuellen Assembly verknüpft ist, aber für die aktuelle Anwendungsdomäne gilt. Entspricht der Kombination von IsolatedStorageScope.Assembly, IsolatedStorageScope.Domain und IsolatedStorageScope.Machine. GetUserStoreForApplication: Liefert benutzerbezogenen isolierten Speicher, der mit der aktuellen Anwendung verknüpft ist. Entspricht IsolatedStorageScope.Application | IsolatedStorageScope.User.
Arbeiten mit isoliertem Speicher
■
■
GetUserStoreForAssembly: Liefert benutzerbezogenen isolierten Speicher, der mit der aktuellen Assembly verknüpft ist. Entspricht IsolatedStorageScope.Assembly | IsolatedStorageScope.User. GetUserStoreForDomain: Liefert benutzerbezogenen isolierten Speicher, der mit der aktuellen Assembly verknüpft ist, aber für die aktuelle Anwendungsdomäne gilt. Entspricht der Kombination von IsolatedStorageScope.Assembly, IsolatedStorageScope.Domain und IsolatedStorageScope.User.
Die hier beschriebenen Methoden bieten nicht die Möglichkeit, isolierten Speicher für wandernde Benutzer (roaming user) festzulegen. Wenn Sie diese benötigen, müssen Sie die GetStore-Methode mit dem zusätzlichen Wert IsolatedStorageScope.Roaming verwenden. In meinen Versuchen erhielt ich für alle Methoden außer denen, die die Domäne einbezogen, beim Erzeugen einer IsolatedStorageFile-Instanz eine IsolatedStorageException mit der Meldung »Die Anwendungsidentität des Aufrufers kann nicht bestimmt werden«. Laut den wenigen Informationen, die dazu im Internet zu finden sind, muss die Anwendung bzw. Assembly für Isolationstypen, die IsolatedStorageScope.Domain nicht integrieren, einen starken Namen besitzen. Das habe ich natürlich ausprobiert. Die Ausnahme erhielt ich aber trotzdem, sowohl für Konsolenals auch für Windows.Forms-Anwendungen. Na dann …
1
INFO
2
3 HALT
4
5
Werden Daten auf Assembly- oder Anwendungsebene gespeichert, wird der starke Name zur Identifikation verwendet, falls die Anwendung bzw. Assembly einen solchen besitzt. Besitzt diese keinen starken Namen, wird der Pfad zur Assembly verwendet. Dies bedeutet aber, dass die Daten im isolierten Speicher verloren gehen, wenn die Assembly bzw. Anwendung an einen anderen Ort verschoben wird. Ein starker Name ist also eine wichtige Voraussetzung für das fehlerfreie Arbeiten mit isoliertem Speicher. Wie Sie einen solchen vergeben, zeigt Kapitel 22.
6
7
10.8.2 Schreiben und Lesen von isoliertem Speicher Zum Schreiben und Lesen von isoliertem Speicher benötigen Sie zunächst eine IsolatedStorageFile-Instanz:
8
Listing 10.36: Erzeugen einer IsolatedStorageFile-Instanz, die die Daten so verwaltet, dass diese von allen Benutzern und allen Assemblys der Anwendungsdomäne verwendet werden kann
9
using (IsolatedStorageFile isolatedStorageFile = IsolatedStorageFile.GetMachineStoreForDomain()) {
10
Daten können Sie dann über eine IsolatedStorageFileStream-Instanz schreiben, der Sie an den ersten Argumenten ähnliche Informationen übergeben, wie sie auch an den Konstruktor der FileStream-Klasse übergeben werden. Das Argument path wird hier allerdings mit einem relativen Dateinamen belegt. Am letzten Argument übergeben Sie die Referenz auf die IsolatedStorageFile- Instanz:
11
Listing 10.37: Schreiben einer Datei in den isolierten Speicher using (IsolatedStorageFileStream writeStream = new IsolatedStorageFileStream("Demo.txt", FileMode.Create, isolatedStorageFile)) {
645
Arbeiten mit Dateien, Ordnern und Streams
using (StreamWriter streamWriter = new StreamWriter(writeStream)) { streamWriter.Write("Das ist ein Test-Text"); } }
Wenn Sie einen relativen Dateinamen mit Ordner angeben, muss der entsprechende Ordner existieren. Sie sollten diesen vor dem Schreiben über die CreateDirectoryMethode erzeugen. CreateDirectory erzeugt keine Ausnahme, wenn der Ordner bereits existiert: Listing 10.38: Schreiben einer Datei in einen Unterordner des isolierten Speichers // Unterordner erzeugen isolatedStorageFile.CreateDirectory("Daten"); // Eine Datei im Unterordner erzeugen using (IsolatedStorageFileStream writeStream = new IsolatedStorageFileStream("Daten\\Demo.txt", FileMode.Create, isolatedStorageFile)) { using (StreamWriter streamWriter = new StreamWriter(writeStream)) { streamWriter.Write("Das ist ein Test-Text"); } }
Das Lesen ist ähnlich einfach, wenn Sie den Dateinamen kennen: Listing 10.39: Lesen einer Datei aus isoliertem Speicher using (IsolatedStorageFileStream readStream = new IsolatedStorageFileStream("Daten\\Demo.txt", FileMode.Open, FileAccess.Read, isolatedStorageFile)) { using (StreamReader streamReader = new StreamReader(readStream)) { Console.WriteLine(streamReader.ReadToEnd()); } }
Problematisch ist nur das Überprüfen, ob die zu lesende Datei auch vorhanden ist. Dabei hilft der folgende Abschnitt.
10.8.3 Auflisten der Dateien in einem isolierten Speicher Über die Methode GetFileNames erhalten Sie die (relativen) Namen der Dateien im isolierten Speicher. Dieser Methode übergeben Sie ein Dateimuster, das die DOSWildcards * und ? beinhalten kann. Das Muster »*.*« steht für alle Dateien. Das Muster bezieht sich auf genau einen Ordner. Wenn Sie die Dateien eines Unterordners auflisten wollen, müssen Sie dessen relativen Pfad voranstellen: Listing 10.40: Auflisten der Dateien im Unterordner 'Daten' des isolierten Speichers foreach (var fileName in isolatedStorageFile.GetFileNames("Daten\\*.*")) { Console.WriteLine(fileName); }
646
Arbeiten mit isoliertem Speicher
Die Methode GetDirectoryNames liefert die Unterordner eines Ordners. Auch dieser Methode übergeben Sie ein Suchmuster, ggf. mit vorangestelltem Ordner: Listing 10.41: Auflisten der Ordner im Wurzelordner des isolierten Speichers foreach (var directoryName in isolatedStorageFile.GetDirectoryNames("*.*")) { Console.WriteLine(directoryName); }
1
Damit können Sie relativ komfortabel im isolierten Speicher suchen. Eine rekursive Suche in einer Ordnerstruktur wird aber sehr schwierig, da Sie nur mit einem einzigen logischen Ordner arbeiten.
2
10.8.4 Löschen von Ordnern und Dateien 3
Schließlich können Sie über die Methoden DeleteDirectory und DeleteFile Ordner und Dateien auch löschen. DeleteDirectory ist allerdings nicht in der Lage, rekursiv zu löschen. Sie müssen also zuvor den gesamten Inhalt eines Ordners löschen, bevor Sie diesen löschen können:
4
Listing 10.42: Löschen eines Ordners und einer Datei und abschließende Klammer des ersten usingBlocks des Beispiels
5
// Ordner löschen isolatedStorageFile.DeleteFile("Daten\\Demo.txt"); isolatedStorageFile.DeleteDirectory("Daten"); // Datei löschen isolatedStorageFile.DeleteFile("Demo.txt");
6
}
Dieses schwierige Löschen von Ordnern sollte Grund genug sein, keine komplexen Ordnerstrukturen aufzubauen.
7
Bleibt mir nur noch, Ihnen viel »Spaß« beim Erkunden der weiteren Features der isolierten Speicherung zu wünschen (wie z. B. das Auflisten der Speicher für den aktuellen Benutzer oder die Maschine), die ich hier nicht besprechen kann.
8
9
10 11
647
Inhalt
11
LINQ 1
LINQ (Language Integrated Query) ist ein neues Feature in .NET 3.5 und C# 3.0, das es ermöglicht, typsichere, SQL-ähnliche Abfragen auf Auflistungen und anderen Datenquellen auszuführen. LINQ ermöglicht es zunächst, alle Auflistungen abzufragen, die IEnumerable implementieren. Über Erweiterungen wie LINQ to SQL und LINQ to XML (die in der Regel nutzen, dass LINQ auch für IQueryable implementiert ist) können Sie sogar Datenquellen wie Datenbanken und XML-Dokumente sehr komfortabel abfragen. Und damit ist LINQ noch nicht am Ende, denn prinzipiell können LINQ-Erweiterungen für alle möglichen Datenquellen implementiert werden.
2 NEU
3
4
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
Grundlegendes zu LINQ Die Besonderheit der »aufgeschobenen Ausführung« LINQ-Erweiterungsmethoden und die C#-Abfrageausdruck-Syntax Abfragen mit Einschränkung des Ergebnisses Sortierte Abfragen Projektionen Gruppierungen Verknüpfen von Sequenzen Die LINQ-Erweiterungsmethoden Komplexe Abfragen Tipps und Tricks
5
6
7
8
Neu in .NET 3.5 und C# 3.0: Alles in diesem Kapitel ☺.
Grundlegendes 10
LINQ besteht aus mehreren Teilbereichen: ■ ■ ■ ■
Die Klassen des Namensraums System.Linq liefern die Basisfunktionalität. System.Linq ist übrigens Teil der Assembly System.Core.dll. C# 3.0 bietet die Möglichkeit mit speziellen Schlüsselwörtern LINQ-Abfragen ähnlich SQL direkt im Programmcode unterzubringen. LINQ to SQL (Namensraum System.Data.Linq) erweitert LINQ um die Abfrage von Datenbanksystemen. LINQ to XML (Namensraum System.Xml.Linq) erweitert LINQ um die Möglichkeit, XML-Dokumente abzufragen.
11
649
Index
11.1
9
LINQ
■
■
■ ■
LINQ to LDAP ermöglicht die Abfragen von Objekten in einem Netzwerk über LDAP (Lightweight Directory Access Protocol). Informationen dazu finden Sie an der Adresse www.hookedonlinq.com/LINQ2LDAP.ashx. LINQ to DataSets bietet LINQ-Abfragefunktionalität für die (im Vergleich zu LINQ to SQL und dem ADO.NET Entity Framework veralteten) DataSetObjekte, über die ADO.NET Datenbanken im Arbeitsspeicher verwaltet. LINQ to Entities bietet LINQ-Abfragen bei der Verwendung des ADO.NET Entity Framework. Spezielle Erweiterungen häufig externer Entwickler (wie LINQ to Flickr, siehe www.codeplex.com/LINQFlickr) ermöglichen den Zugriff auf spezielle Daten.
Dieses Kapitel konzentriert sich auf die ersten beiden Punkte. LINQ to SQL wird in Kapitel 19 besprochen, LINQ to XML in Kapitel 18. Die anderen LINQ-Erweiterungen behandelt dieses Buch nicht. System. Linq enthält die Basisklassen
Die Klassen, die Sie im Namensraum System.Linq finden, sind die Basisklassen von LINQ. Die wichtigsten dieser Klassen sind Enumerable und Queryable. Bei beiden handelt es sich um Klassen, die fast ausschließlich Erweiterungsmethoden beinhalten. Enumerable erweitert die IEnumerable-Schnittstelle, Queryable die Schnittstelle IQueryable. TSource ist der Typ, der von der Auflistung bzw. der Datenklasse (die IQueryable implementiert) verwaltet wird. Die LINQ-Erweiterungsmethoden beschreibe ich ab Seite 663. IEnumerable wird von den meisten Auflistungen implementiert. Die in Enumerable enthaltenen Methoden stehen damit automatisch für die meisten Auflistungen zur Verfügung, wenn der Namensraum System.Linq in eine Quellcodedatei eingebunden ist. IQueryable (und deren nicht generische Basisschnittstelle IQueryable) steht für Objekte, die keine Auflistung sind, aber die Abfrage ihrer Daten zulassen. Die in IQueryable definierten Elemente werden von den Erweiterungsmethoden der Queryable-Klasse verwendet, um die Daten zu ermitteln. Objekte, die IQueryable implementieren, stellen damit ihre Daten dynamisch zur Verfügung. Viele LINQ-Erweiterungen wie LINQ to SQL arbeiten nach diesem Prinzip, indem deren Klassen IQueryable implementieren und damit über LINQ abfragbar sind. Wenn Sie eine eigene Datenklasse implementieren, deren Daten über LINQ abfragbar sein sollen, müssen Sie also »nur« IQueryable implementieren (was aber nicht trivial ist). Im .NET Framework wird diese Schnittstelle nach meinen Recherchen zurzeit nur von der Table-Klasse aus dem Namensraum System.Data.Linq implementiert. IQueryable ist übrigens von IEnumerable abgeleitet. Objekte, die IQueryable implementieren, können deshalb auch mit foreach durchlaufen werden. Enumerable und Queryable enthalten nach außen nahezu identische Erweiterungsmethoden. Über die Methode Where können Sie z. B. Daten abfragen, die einer Bedingung entsprechen: var result = personList.Where(person => person.City == "Dublin");
Das Ergebnis ist in diesem Fall entweder eine Auflistung, die IEnumerable implementiert, oder ein Objekt, das IQueryable implementiert, je nach dem, welche Objekte die Auflistung verwaltet.
650
Grundlegendes
LINQ to SQL basiert in der Form auf LINQ, dass die Table-Klasse, die eine Tabelle aus einer Datenbank repräsentiert, die IQueryable-Schnittstelle implementiert. Bei der Abfrage von Daten über die LINQ-Erweiterungsmethoden werden also intern Methoden (und Eigenschaften) verwendet, die in LINQ-to-SQL-Klassen definiert sind. LINQ to SQL wird in Kapitel 19 behandelt.
LINQ to SQL und LINQ to XML basieren auf LINQ
LINQ to XML basiert auf Klassen wie XDocument, die ein XML-Dokument im Speicher darstellen. Die Elemente dieser Klassen, die eine Liste von XML-Elementen zurückgeben (wie die Descendants-Methode, die untergeordnete Elemente ergibt), geben Objekte zurück, die IEnumerable implementieren und wieder über LINQ abgefragt werden können. Das Kapitel 18 geht auf LINQ to XML ein. LINQ-Erweiterungsmethoden sind generisch. Der Typ der Objekte, die die Auflistung oder Datenklasse verwaltet, auf der die Erweiterungsmethode angewendet wird, wird über den Typparameter TSource angegeben. Die Syntax einer LINQMethode sieht deswegen häufig recht »wild« aus. Selbst IntelliSense zeigt den Typparameter TSource an (Abbildung 11.1).
1
LINQ-Methoden sind generisch
2
3 Abbildung 11.1: IntelliSense zeigt die komplexe Syntax einer LINQ-Methode an
4
5
In der Regel können Sie LINQ-Erweiterungsmethoden aufrufen, ohne den Typparameter anzugeben. C# erkennt den Typparameter implizit über einen Typrückschluss. Im Beispiel, das eine Auflistung vom Typ List verwendet, könnten Sie also products.First(…) aufrufen an Stelle von products.First(…).
11.1.1
6 INFO
7
Grundlegende Abfragen über die LINQErweiterungsmethoden
LINQ-Erweiterungsmethoden sind prinzipiell einfach anzuwenden. Das einzig Komplexe an diesen Methoden ist, dass sie häufig mit einer der Varianten des FuncDelegaten arbeiten. Bei der Where-Methode bestimmt dieses Argument z. B. die Bedingung, die erfüllt sein muss, damit ein Objekt in das Ergebnis übernommen wird.
8 Viele LINQMethoden arbeiten mit dem Func -Delegaten
An einem solchen Argument übergeben Sie normalerweise einen passenden Lambda-Ausdruck. Alternativ können Sie in der Regel eine anonyme Methode oder eine Instanz des Delegaten übergeben, der eine normale Methode referenziert. Dies ist z. B. bei LINQ to XML in einigen Situationen sinnvoll. Normalerweise ist aber die Übergabe eines Lambda-Ausdrucks wesentlich einfacher. Bei LINQ to SQL (und anderen LINQ-Erweiterungen, die LINQ-Abfragen vor der Ausführung interpretieren) ist allerdings nur die Übergabe eines Lambda-Ausdrucks (oder die eines äquivalenten Ausdrucksbaums, siehe Kapitel 6) möglich.
9
10
11
651
LINQ
EXKURS
INFO
Zur Erinnerung: Func steht für eine Methode, die den am letzten Typparameter angegebenen Typ zurückgibt. Die optional davor stehenden Typparameter (bis zu vier) definieren Argumente der Methode. Func steht z. B. für eine Methode, der ein int-Wert übergeben wird und die einen booleschen Wert zurückgibt.
Die an einem Delegat-Argument übergebene Methode bzw. der übergebene LambdaAusdruck wird grundsätzlich für jedes Element der Objektmenge (bzw. Sequenz) aufgerufen, die abgefragt wird. LINQ ist damit ein sehr schönes Beispiel für die Funktionale Programmierung. Als Beispiel verwende ich die oben angegebene erste Überladung der WhereMethode. Func steht hier für eine Methode oder einen Lambda-Ausdruck, dem eine Instanz des Typen übergeben wird, der in dem Objekt verwaltet wird, auf dem die LINQ-Erweiterungsmethode angewendet wird. Die Methode gibt einen booleschen Wert zurück, der aussagt, ob die Bedingung erfüllt ist oder nicht. So können Sie eine Liste von Namen z. B. so eingeschränkt ermitteln, dass diese nur Namen enthält, die mit »Hickey« enden: Listing 11.1:
Abfragen einer Liste mit LINQ
// Eine Liste mit Namen erzeugen string[] names = {"Randy Hickey", "Earl Hickey", "Joy Turner", "Darnell Turner"}; // Die Liste abfragen var hickeys1 = names.Where(name => name.EndsWith("Hickey")); foreach (var name in hickeys1) { Console.WriteLine(name); }
Der hier übergebene Lambda-Ausdruck entspricht dem Delegaten Func und kann deswegen an dem Argument eingesetzt werden. Die Where-Methode geht alle Elemente der Objektmenge durch, auf der sie aufgerufen wurde, ruft für jedes Element den Lambda-Ausdruck (bzw. die alternativ übergebene Methode) auf, wertet die Rückgabe aus und übernimmt das Element nur dann in das Ergebnis, wenn die Rückgabe true ist. Das Ergebnis dieses Beispiels ist: Randy Hickey Earl Hickey
Lesen Sie ggf. in Kapitel 5 und 6 nach, wenn Sie hier Verständnisprobleme haben. Im Besonderen sollten Sie mit Lambda-Ausdrücken sicher umgehen können.
11.1.2 LINQ-Erweiterungsmethoden geben entweder eine Sequenz zurück oder Einzeldaten
652
Sequenzen und Einzeldaten
LINQ-Erweiterungsmethoden werden immer auf einer Menge von Objekten oder Werten angewendet. Eine solche Menge wird von Microsoft als »Sequenz« bezeichnet. Viele LINQ-Erweiterungsmethoden geben eine Sequenz zurück. Dazu gehören z. B. die Methoden Where und OrderBy. Diese Methoden geben ein Objekt zurück, das entweder die IEnumerable- oder die IQueryable-Schnittstelle imp-
Grundlegendes
lementiert. Da IQueryable von IEnumerable abgeleitet ist, können Sie das zurückgegebene Objekt in beiden Fällen mit foreach durchlaufen. Andere LINQ-Erweiterungsmethoden geben einfache Werte (oder Objekte) zurück. Ein Beispiel ist die Methode Min, die das kleinste Element einer Sequenz zurückgibt.
11.1.3
Die Aufrufkette 1
LINQ-Erweiterungsmethoden, die eine Sequenz zurückgeben, können Sie (natürlich) miteinander verketten: Da diese ein Objekt zurückgeben, das entweder IEnumerable oder IQueryable implementiert, können Sie auf diesem Objekt wieder LINQ-Erweiterungsmethoden anwenden. So können Sie z. B. eine Namensliste zuerst einschränken und dann sortieren: Listing 11.2:
2
Verketten von LINQ-Erweiterungsmethoden
3
var hickeys2 = names.Where(name => name.EndsWith("Hickey")) .OrderBy(name => name); foreach (var name in hickeys2) { Console.WriteLine(name); }
4
Das Ergebnis dieses Beispiels ist: Earl Hickey Randy Hickey
Dabei sollten Sie natürlich eine logisch korrekte Reihenfolge einhalten. In dem obigen Beispiel wäre es ineffizienter, wenn die Daten erst sortiert und danach eingeschränkt würden. Mit einer logisch korrekten Reihenfolge optimieren Sie (besonders bei LINQ to SQL) die Performance.
11.1.4
5
6
INFO
Die Originaldaten
7
LINQ-Methoden ändern die Sequenz, auf der sie angewendet werden, nicht, sondern geben immer entweder eine neue Sequenz zurück oder einen berechneten Einzelwert. Wenn Sie z. B. mit OrderBy die Elemente einer Sequenz in einer sortierten Form ermitteln, wird die originale Sequenz nicht geändert. Dieses Verhalten entspricht dem Paradigma der funktionalen Programmierung, auf dem LINQ basiert, und im Übrigen auch dem .NET-Standard, verdient aber wenigstens eine Erwähnung.
LINQ ändert die Originaldaten nicht
Ein weiteres (ebenfalls eigentlich vorauszusetzendes) Verhalten der LINQ-Methoden ist, dass diese immer die natürliche Sortierung der Sequenz verwenden, auf der sie angewendet werden. Dieses Verhalten ist wichtig bei Methoden wie First und Last, die das erste bzw. letzte Element einer Sequenz liefern. Wenn Sie eine bestimmte Sortierung benötigen, müssen Sie die Sequenz vor der weiteren Abfrage über OrderBy (oder die Abfrageausdruck-Klausel orderby) explizit sortieren.
LINQ verwendet die OriginalSortierung
11.1.5
9
10
11
C#-Abfrageausdrücke
Wenn Sie mit C# arbeiten, müssen Sie zur Arbeit mit LINQ nicht unbedingt die LINQErweiterungsmethoden direkt aufrufen. Stattdessen können Sie mit speziellen Abfrageausdrücken arbeiten, die C# zur Arbeit mit LINQ zur Verfügung stellt. Die Abfrage der Namen aus dem obigen Beispiel kann z. B. auch folgendermaßen geschrieben werden:
8
C# erlaubt SQLähnliche Abfrageausdrücke
653
LINQ
Listing 11.3:
Abfragen einer Liste mit der C#-LINQ-Syntax
string[] names = {"Randy Hickey", "Earl Hickey", "Joy Turner", "Darnell Turner"}; var hickeys3 = from name in names where name.EndsWith("Hickey") orderby name select name; foreach (var name in hickeys3) { Console.WriteLine(name); }
Im Wesentlichen unterscheidet sich der Quellcode hier prinzipiell nur dadurch, dass er eine zusätzliche Projektion (über select) enthält. Das liegt daran, dass C# eine solche erzwingt. Wenn Sie statt einfacher Daten komplexe Objekte abfragen, können Sie über select die Daten so ermitteln, dass nur Teile davon im Ergebnis erscheinen. Dieses Feature beschreibe ich ab Seite 669.
INFO
Fall Sie SQL kennen und sich darüber wundern, dass select am Ende steht (in SQL steht SELECT immer vorne): select kann nur am Ende stehen, denn logischerweise müssen zunächst die Daten abgefragt werden, bevor diese projiziert werden können. Einfache Abfrageausdrücke basieren auf dem folgenden Schema: var Ergebnis = from Iterationsvariable in Sequenz [where Einschränkung] [orderby Sortierung [{ascending | descending}]] select Projektion
Da gerade bei Verwendung von Abfrageausdrücken in der Regel nicht bekannt ist, welchen Typ die zurückgegebene Sequenz besitzt, und weil Projektionen erlauben, anonyme Typen zurückzugeben, wird die Ergebnisvariable prinzipiell immer mit var deklariert. from bestimmt die abgefragte Sequenz und legt eine Iterationsvariable fest
Mit dem folgenden from geben Sie zum einen die Sequenz an, die Sie auswerten wollen. Zum anderen geben Sie den Namen einer Variablen an, die von C# implizit erzeugt wird und die Sie im weiteren Verlauf der Abfrage nutzen können, um auf das jeweilige Element der abgefragten Sequenz zuzugreifen. Diese Variable wird als Iterationsvariable bezeichnet. Der Compiler setzt die Iterationsvariable als diejenige ein, die beim Aufruf der zu den Klauseln des Abfrageausdrucks äquivalenten Erweiterungsmethoden am linken Teil des Lambda-Ausdrucks übergeben wird. Um dies ein wenig klarer zu machen, zeigt Listing 11.4, was der C#-Compiler aus dem Quellcode in Listing 11.3 prinzipiell erzeugt. Listing 11.4:
Prinzipieller Code, den der C#-Compiler aus dem Beispiel-Abfrageausdruck erzeugt
IOrderedEnumerable hickeys2 = names .Where(name => name.EndsWith("Hickey")) .OrderBy(name => name);
Die Iterationsvariable wird ggf. mehrfach eingesetzt
654
Wie Sie sehen, handelt es sich beim erzeugten CIL-Code um die ganz normale Verwendung von Erweiterungsmethoden. Die in dem Abfrageausdruck angegebene Variable name taucht hier wieder auf, nämlich jeweils in dem Lambda-Ausdruck, der Where und OrderBy übergeben wird. Es handelt sich also in Wirklichkeit nicht um eine Variable, sondern um mehrere!
Grundlegendes
Die Teilausdrücke, die mit where und orderby beginnen, sind optional. Über where schränken Sie das Ergebnis ein, über orderby können Sie sortieren. Der Ausdruck endet schließlich entweder mit einer Projektion über select oder einer Gruppierung über group. Projektionen werden ab Seite 669 behandelt, Gruppierungen ab Seite 671. Für einfache Ausdrücke geben Sie für die Projektion einfach die Variable an, die Sie in der from-Klausel angegeben haben. Damit wählen Sie das gesamte Objekt bzw. den gesamten Wert aus.
1
Das komplette Schema von Abfrageausdrücken ist etwas komplexer: var Ergebnis = from Iterationsvariable in Sequenz [...] [join Innerer Bezeichner in Innerer Ausdruck on Äußerer Schlüssel equals InnererSchlüssel [into Variable Abfrage auf die Variable]] [...] [let Bezeichner = Ausdruck] [...] [where Einschränkung] [let Bezeichner = Ausdruck] [...] [orderby Sortierung [{ascending | descending}]] {select Projektion | group Ausdruck by Ausdruck} [into Variable Abfrage auf die Variable]
2
3
4
5
Die erweiterten Möglichkeiten mit into, let und zusätzlichen from-Klauseln beschreibe ich ab Seite 703. REF
Abfrageausdrücke repräsentieren nicht alle LINQ-Erweiterungsmethoden. Unterstützt werden lediglich die folgenden Methoden: Where, Select, SelectMany, OrderBy, OrderByDescending ThenBy, ThenByDescending, Group, Join, GroupJoin. Für alle weiteren Methoden müssen Sie auf die entsprechende Erweiterungsmethode direkt zugreifen. Das ist aber nicht weiter problematisch, weil Sie auch, wie ich im folgenden Abschnitt zeige, Abfrageausdrücke und Erweiterungsmethoden mischen können. Bei der Behandlung der Erweiterungsmethoden im folgenden Abschnitt zeige ich für die einzelnen Erweiterungsmethoden die ggf. auch mögliche Abfrageausdruck-Syntax.
6 INFO
7
8
ä
11.1.6
9
Gemischte Verwendung von Abfrageausdrücken und Erweiterungsmethoden
Für den Fall, dass eine der Erweiterungsmethoden kein Äquivalent in der Syntax der Abfrageausdrücke besitzt, können Sie beide Varianten der Abfrage miteinander mischen. Dabei können Sie die Tatsache nutzen, dass ein Abfrageausdruck immer eine Sequenz zurückgibt. Wenn Sie diese klammern, können Sie auf der ErgebnisSequenz Erweiterungsmethoden ausführen. So können Sie z. B. aus der Menge der Namen den ersten heraussuchen, der mit »Hickey« endet, indem Sie auf der abgefragten Menge die First-Methode anwenden:
Sie können Abfrageausdrücke mit Erweiterungsmethoden mischen
10
11
655
LINQ
Listing 11.5:
Gemischte Verwendung von Abfrageausdrücken und Erweiterungsmethoden in der ersten Variante
var firstHickey = (from name in names where name.EndsWith("Hickey") orderby name select name).First(); Console.WriteLine("Der erste Hickey: " + firstHickey);
Umgekehrt können Sie aber auch mischen, z. B. indem Sie in einem Abfrageausdruck eine Erweiterungsmethode aufrufen. Das macht aber erst dann Sinn, wenn ein Element der abgefragten Sequenz eine Untersequenz verwaltet. Angenommen, eine Auflistung von Autoren verwaltet Objekte, deren Eigenschaft Books eine Auflistung der Bücher des Autoren referenziert, können Sie so z. B. alle Autoren mit mehr als drei Büchern abfragen: Listing 11.6:
Gemischte Verwendung von Abfrageausdrücken und Erweiterungsmethoden in der zweiten Variante
var authorsWithMoreThen3Books = from author in authors where author.Books.Count() > 3 select author;
INFO
Falls Sie sich fragen, warum Count eine Erweiterungsmethode ist: Count ist eine Erweiterungsmethode, die für den Fall existiert, dass eine abgefragte Sequenz keine Auflistung ist, die ICollection implementiert und die deshalb eine eigene CountEigenschaft besitzt.
11.1.7
Wann Abfrageausdrücke und wann Erweiterungsmethoden?
In welchen Fällen Sie Abfrageausdrücke oder Erweiterungsmethoden einsetzen, bleibt ganz Ihnen überlassen. Für die Methoden, die kein Äquivalent in Abfrageausdrücken besitzen (wie Min, Max, Average), bleibt Ihnen allerdings keine Wahl (außer dass Sie diese in der gemischten Form einsetzen können). In einigen Fällen ist die Verwendung eines Abfrageausdrucks einfacher, in anderen der direkte Aufruf der Erweiterungsmethoden. ■
■ ■
656
Abfrageausdrücke sind einfacher: ■ wenn Sie mit dem let-Schlüsselwort (siehe Seite 705) Teilergebnisse in Variablen schreiben und in der Abfrage weiter auswerten, ■ wenn Sie verknüpfen oder gruppieren und das Ergebnis mit into (siehe Seite 703) in eine Variable schreiben, die Sie im weiteren Verlauf auswerten, und wenn Sie mehrfach verknüpfen (siehe Seite 682). Für alle einfachen Abfragen, die lediglich mit Where, OrderBy und/oder Select arbeiten, sind beide Varianten gleichwertig. Abfragen, die die folgenden Erweiterungsmethoden verwenden, können nicht als Abfrageausdruck angegeben werden: Aggregate, All, Any, AsEnumerable, Average, Cast, Concat, Contains, Count, DefaultIfEmpty, Distinct, ElementAt, ElementAtOrDefault, Except, First, FirstOrDefault, Intersect, Last, LastOrDefault, LongCount, Max, Min, OfType, Reverse, SequenceEqual, Single, SingleOrDefault, Skip, SkipWhile, Sum, Take, TakeWhile, ToArray, ToDictionary, ToList, ToLookup und Union.
Grundlegendes
11.1.8
Aufgeschobene Ausführung
Aufgeschobene Ausführung (Deferred execution) ist ein wichtiges Konzept in LINQ. Danach werden die meisten Erweiterungsmethoden noch nicht sofort dann ausgeführt, wenn sie aufgerufen werden, sondern erst, wenn über das Ergebnis iteriert oder das Ergebnis in anderer Weise ausgewertet wird.
Viele LINQ-Erweiterungsmethoden werden nicht sofort ausgeführt
Das folgende Beispiel macht dieses Feature klar. Es sortiert eine Liste von Zahlen. Der Originalliste wird nach dem Aufruf der OrderBy-Methode eine Zahl hinzugefügt. Danach wird das Ergebnis der Abfrage abgefragt: Listing 11.7:
1
Beispiel für eine aufgeschobene Ausführung
2
// Beispiel-Liste erzeugen List numbers1= new List { 3, 2 }; // Die Liste sortieren var orderedNumbers1 = numbers1.OrderBy(number => number);
3
// Eine Zahl hinzufügen numbers1Add(1);
4
// Das Ergebnis der Abfrage ausgeben foreach (var number in orderedNumbers1) { Console.WriteLine(number); }
5
Das Ergebnis dieses Programms ist: 1 2 3
6
Die nach dem Aufruf von OrderBy hinzugefügte Zahl ist in der sortierten Liste enthalten. Das funktioniert sogar, wenn Sie danach noch einmal eine Zahl hinzufügen und das Ergebnis ausgeben (ohne neu zu sortieren):
7
Listing 11.8:
Auch weitere Änderungen der Original-Sequenz werden bei der Auswertung berücksichtigt
8
// Eine weitere Zahl hinzufügen numbers1.Add(0); // Das Ergebnis der Abfrage noch einmal ausgeben foreach (var number in orderedNumbers1) { Console.WriteLine(number); }
9
Das Ergebnis ist:
10
0 1 2 3
11
Auch dieses Mal wurde OrderBy aufgeschoben und implizit erneut aufgerufen. Die meisten Standard-Erweiterungsmethoden verwenden eine aufgeschobene Ausführung. Ausnahmen sind: ■
Methoden, die ein einzelnes Element oder einen skalaren Wert zurückgeben (wie Min, Max, Average), und
657
LINQ
■
die Methoden ToArray, ToList, ToDictionary und ToLookup.
Diese Methoden führen zu einer sofortigen Ausführung der Abfrage. Aufgeschobene Ausführung funktioniert auch dann, wenn nicht iteriert, sondern die Ergebnis-Sequenz direkt ausgewertet wird: Listing 11.9:
Aufgeschobene Ausführung ohne Iteration
List numbers2 = new List { 1, 2 }; var orderedNumbers2 = numbers2.OrderBy(number => number); Console.WriteLine(); int count = orderedNumbers2.Count(); Console.WriteLine(count); // 2 numbers2.Add(3); count = orderedNumbers2.Count(); Console.WriteLine(count); // 3
Wenn Sie in Delegaten bzw. Lambda-Ausdrücken, die Sie LINQ-Erweiterungsmethoden übergeben, Variablen, Felder oder Eigenschaften verwenden, wird auch eine Änderung derselben bei einer erneuten Iteration oder Auswertung berücksichtigt. Das ist eigentlich logisch, verdient aber trotzdem eine Erwähnung: Listing 11.10: Aufgeschobene Ausführung berücksichtigt auch Änderungen in Variablen (und Feldern und Eigenschaften) List numbers3 = new List { 1, 2 }; int factor = 2; var factorizedNumbers = numbers3.Select(number => number * factor); foreach (var number in factorizedNumbers) { Console.WriteLine(number); // 2, 4 } factor = 3; Console.WriteLine(); foreach (var number in factorizedNumbers) { Console.Write(number); // 3, 6 }
EXKURS
Die Funktionsweise der aufgeschobenen Ausführung kann am Beispiel der WhereMethode der Enumerable-Klasse erläutert werden. Wenn Sie diese aufrufen, erhalten Sie zunächst lediglich ein Objekt zurück, das die IEnumerable-Schnittstelle implementiert. Dieses Objekt besitzt je eine private Referenz auf die Basis-Sequenz und auf den Prädikat-Delegaten (die von Where übergeben wurden). Wie Sie in Kapitel 7 erfahren haben, bietet IEnumerable die Methode GetEnumerator. Wenn Sie GetEnumerator (implizit über foreach) aufrufen, erzeugt diese Methode ein Enumerator-Objekt, dem die Referenzen auf die Basis-Sequenz und den Prädikat-Delegaten übergeben werden. Über dieses Enumerator-Objekt können Sie die Ergebnis-Sequenz durchgehen (was foreach implizit macht). Wenn Sie nun die Ergebnis-Sequenz mit foreach durchgehen, rufen Sie in Wirklichkeit die Methode MoveNext des Enumerator-Objekts auf und fragen Sie innerhalb der Schleife die Eigenschaft Current ab. MoveNext positioniert auf das nächste Element. Um dieses zu ermitteln, verwaltet das Enumerator-Objekt den Index des aktuellen Objekts in der Basis-Sequenz, geht die Basis-Sequenz bis zum aktuellen Objekt durch und prüft dann für die nächsten Elemente, ob das Prädikat erfüllt ist,
658
Grundlegendes
indem der Prädikat-Delegat mit dem jeweils nächsten Element als Argument aufgerufen wird. Gibt der Delegat true zurück, schreibt MoveNext das Element in ein privates Feld, das das aktuelle Element des Enumerators verwaltet, und gibt true zurück. Der Index des aktuellen Elements der Basis-Sequenz wird dann natürlich entsprechend hochgesetzt. Das mag zwar kompliziert erscheinen. Was hier aber wichtig ist, ist, dass Sie erkennen, dass Sie in Wirklichkeit mit einem separaten Enumerator-Objekt arbeiten, wenn Sie das Ergebnis einer LINQ-Abfrage, die eine Sequenz ergibt, auswerten. Deswegen erfolgt die Ausführung der Abfrage erst bei der Auswertung der Ergebnis-Sequenz. Und das ist eigentlich ganz einfach ☺.
1
2
Dieses Verhalten können Sie prinzipiell über eine selbst implementierte WhereMethode nachempfinden. Um dem Original nahe zu kommen, bräuchten Sie eine Klasse, die den Enumerator implementiert (und deshalb von IEnumerator abgeleitet ist). Diese Klasse benötigt Referenzen auf die Quell-Sequenz und die Prädikat-Methode, die am Konstruktor übergeben werden. Zudem bräuchten Sie eine Klasse, die IEnumerable implementiert und die in der GetEnumeratorMethode eine Instanz der Enumerator-Klasse zurückgibt. In Where schließlich geben Sie eine Instanz der Enumerable-Klasse zurück. Ich verzichte auf eine Darstellung dieser Klassen, weil diese zu viel Platz in Anspruch nehmen würde. Auf der Buch-DVD finden Sie im Ordner dieses Abschnitts allerdings das Projekt »Nachbau der aufgeschobenen Ausführung von Where«, das die Where-Methode auf die dargestellte Weise implementiert.
3
4
5
Über yield können Sie das Ganze wesentlich einfacher implementieren:
6
public static IEnumerable Where( this IEnumerable source, Func predicate) { foreach (var item in source) { if (predicate(item) == true) { yield return item; } } }
7
8
Denken Sie aber daran, dass yield in Wirklichkeit zu einem Code führt, der dem oben beschriebenen ähnlich ist! Die aufgeschobene Ausführung kann zu unerwarteten Ergebnissen führen, besonders wenn äußere Variablen, Felder oder Eigenschaften involviert sind. Das folgende (etwas konstruierte) Beispiel soll das Problem demonstrieren: Listing 11.11: Demonstration eines möglichen Problems bei aufgeschobener Ausführung
9 Aufgeschobene Ausführung kann zu unerwarteten Ergebnissen führen
10
11
// Erzeugen einer Auflistung mit Zahlen IEnumerable numbers4 = new List{ 1, 2, 3 }; // In einer Schleife wird die Zahl-Liste jeweils mit Where so // eingeschränkt, dass die Test-Zahl (theoretisch) entfernt wird for (int testNumber = 1; testNumber number != testNumber); } // Ausgeben der Ergebnis-Liste foreach (var number in numbers4) { Console.WriteLine(number); // 1, 2, 3 }
Das Ergebnis des Programms ist: 1 2
Das sollte eigentlich nicht der Fall sein, weil die zwei Where-Aufrufe in der Schleife ja auf Zahlen einschränken sollten, die ungleich 1 und 2 sind. Das Problem ist aber die aufgeschobene Ausführung aller Where-Aufrufe, die erst unter der Schleife beim Durchgehen eintritt und die dann mit dem Wert 3 für die Variable testNumber arbeitet (nach einer for-Schleife ist die Zählvariable um den Wert erhöht, der in der forSchleife angegeben ist). Deswegen wird nur die Zahl 3 entfernt, die ja eigentlich gar nicht entfernt werden sollte. Dieses Problem können Sie lösen, indem Sie innerhalb der Schleife den Wert der äußeren Variablen in eine temporäre Variable schreiben: Listing 11.12: Eine Möglichkeit, das Problem der aufgeschobenen Ausführung zu lösen, wenn eine äußere Variable im Spiel ist IEnumerable numbers5 = new List { 1, 2, 3 }; for (int testNumber = 1; testNumber number != i); } foreach (var number in numbers5) { Console.WriteLine(number); // 3 }
HALT
Das Verhalten bei der aufgeschobenen Ausführung sollten Sie immer im Auge behalten. Wenn Sie nicht wollen, dass ein nachträgliches Ändern der Basis-Sequenz oder einer äußeren Variable bei einem weiteren Auswerten von abgefragten Daten berücksichtigt wird, sollten Sie die abgefragte Sequenz über ToArray, ToList oder eine der anderen To-Methoden in ein Array oder eine Auflistung konvertieren und mit dieser weiterarbeiten.
11.1.9 LINQ-Abfragen werden von LINQ direkt oder von einer LINQ-Erweiterung ausgeführt
660
Lokale und interpretierte Abfragen
LINQ-Abfragen werden entweder lokal, von LINQ direkt, ausgeführt oder interpretiert und von LINQ-Erweiterungen ausgeführt. Lokale Abfragen sind alle, die über die Methoden der Enumerable-Klasse laufen, also alle Abfragen auf Sequenzen, die IEnumerable implementieren (und nicht IQueryable). Implementiert die abgefragte Sequenz aber IQueryable, werden stattdessen die Methoden der Queryable-Klasse aufgerufen. Das ist z. B. der Fall bei der Klasse System. Data.Linq.Table, die in LINQ to SQL eine Datenbanktabelle (oder eine Sicht) repräsentiert. Die IQueryable-Methoden führen die Abfrage nicht lokal aus, sondern interpretieren sie, geben die interpretierte Abfrage an die zugrunde lie-
Grundlegendes
gende Datenquelle weiter und werten das Ergebnis aus. Im Fall von LINQ to SQL wird eine Abfrage z. B. in SQL konvertiert und auf der Datenbank ausgeführt. Bei der Interpretation der Abfrage nutzen die entsprechenden Methoden der LINQErweiterung (die IQueryable implementiert) die Möglichkeit, einen LambdaAusdruck in einen Ausdrucksbaum (in Form einer Instanz einer von Expression abgeleiteten Klasse) zu konvertieren. Auf welche Weise das prinzipiell geschieht, habe ich bereits in Kapitel 6 beschrieben.
1
Daraus folgt, dass die LINQ-Erweiterung nicht unbedingt alle LINQ-Erweiterungsmethoden oder Abfragetypen (z. B. verschiedene Verknüpfungen) unterstützen muss. Das ist bei LINQ to SQL z. B. in den Limitationen von SQL begründet. LINQ to SQL unterstützt z. B. nicht die String-Methode StartsWith in Prädikaten und nicht die LINQ-Erweiterungsmethoden ElementAt und ElementAtOrDefault (weil diese sich auf einen Index beziehen und dieser in einer Datenbank nicht zur Verfügung steht).
2
3
Interpretierte und lokale Abfragen kombinieren Sie können interpretierte und lokale Abfragen auch miteinander kombinieren. Dazu fragen Sie üblicherweise die interpretierte Abfrage als innere ab und fügen außen eine lokale an. Notwendig ist dies erst dann, wenn die LINQ-Erweiterung eine der Erweiterungsmethoden oder eine Prädikat-Einschränkung nicht oder nicht so unterstützt, wie LINQ dies (für IEnumerable) macht. Das ist z. B. der Fall für Prädikate, die reguläre Ausdrücke verwenden. Wenn Sie ein solches Prädikat in LINQ to SQL direkt verwenden, erhalten Sie eine NotSupportedException mit einer Meldung wie »Für den Abfrageoperator "Where" wurde eine nicht unterstützte Überladung verwendet«.
4
5
6
Konvertieren Sie in solchen Fällen die Abfrage nach IEnumerable, um auf dem konvertierten Ergebnis eine weitere lokale Abfrage ausführen zu können. So können Sie z. B. das Problem mit den regulären Ausdrücken lösen:
7
Listing 11.13: Gemischt interpretierte und lokale Abfrage var productsWithMoreThanOneWords = ((IEnumerable)(dataContext.Products)) .Where( // Ab hier ist die Abfrage lokal product => Regex.Matches(product.ProductName, @"\w\b").Count > 1) .OrderBy(product => product.ProductName);
8
9
foreach (var product in productsWithMoreThanOneWords) { Console.WriteLine(product.ProductName); }
10 Ich gehe auf LINQ to SQL hier nicht weiter ein. Näheres dazu finden Sie in Kapitel 19. Der Wechsel in den lokalen Kontext geschieht auch automatisch, wenn Sie auf einem Abfrage-Ergebnis eine eigene LINQ-Erweiterungsmethode ausführen, die Sie für IEnumerable implementiert haben. Ab dieser Methode ist die Abfrage dann lokal.
REF
11
661
LINQ
11.1.10 Performance-Überlegungen Die Performance selbst implementierter Abfragen kann besser sein
Die Performance von LINQ-Abfragen ist zunächst in der Regel nicht so gut wie die von selbst implementierten Abfragen. Das liegt daran, dass LINQ zum einen Overhead produziert und zum anderen häufig mit Methoden arbeitet, die während der Iteration über die Sequenz aufgerufen werden. In besonders performance-kritischen Anwendungen sollten Sie deshalb überprüfen, ob eine »klassische« Programmierung ggf. eine schnellere Ausführung bewirkt. Vergleichen Sie dazu auch den Performance-Vergleich für das Suchen in Kapitel 7, bei dem LINQ nicht besonders gut abgeschnitten hat. LINQ bietet aber eine Vielfalt an Möglichkeiten, die die ggf. etwas schlechtere Performance vergessen lässt. Und bei LINQ to SQL und LINQ to XML ist der im Vergleich zur klassischen Programmierung sehr geringe Mehrverbrauch an Zeit nicht relevant, besonders wenn Sie sich den erheblich höheren Aufwand der klassischen Programmierung anschauen.
INFO
Sie können LINQ-Abfragen aber auch optimieren. So sollten Sie z. B. die Erweiterungsmethoden in einer logisch richtigen Reihenfolge aufrufen (z. B. erst einschränken und dann sortieren). Außerdem sollten Sie die aufgeschobene Ausführung beachten: Wenn Sie das Ergebnis einer Abfrage in einer Schleife mehrfach auswerten, wird die Abfrage dann mehrfach ausgeführt, wenn Sie eine der LINQ-Erweiterungsmethoden verwenden, die eine aufgeschobene Ausführung bewirken (und das sind ja die meisten!). Dieses Problem können Sie lösen, indem Sie das Ergebnis der Abfrage vor der Auswertung über ToArray in ein Array oder über ToList in eine List-Auflistung konvertieren. Interessant ist, dass meine Performance-Tests (die Sie neben einigen Beispielen dieses Kapitels finden) ergeben haben, dass LINQ-Abfragen mit Abfrageausdrücken in der Regel schneller ausgeführt werden als solche, die mit den Erweiterungsmethoden direkt implementiert wurden. Etwas hinderlich bei der Implementierung der Performance-Tests war die Tatsache, dass (zumindest beim Gruppieren) eine erste Abfrage immer mehr Zeit benötigte als eine zweite, unabhängig davon, ob die erste mit Erweiterungsmethoden oder einem Abfrageausdruck ausgeführt wurde. In meinen Performancetests habe ich deswegen vor jeder gemessenen Abfrage eine identische Abfrage ausgeführt, um vergleichbare Werte zu erhalten. Tabelle 11.1 stellt das Ergebnis für das Abfragen von 1000 Instanzen einer Klasse mit mehreren Feldern dar. Dieses Ergebnis ist aber mit etwas Vorsicht zu genießen (und natürlich ohne Gewähr). Sie sollten bei performancekritischen Anwendungen auf jeden Fall selbst vergleichen.
Tabelle 11.1: Ergebnis meiner PerformanceMessungen für verschiedene LINQAbfragen auf meinem (noch alten) Rechner mit AMD 64, 4400+ Prozessor
662
Abfrage
Zeitbedarf für den Zeitbedarf für Aufruf mit Erwei- den Aufruf mit terungsmethoden einem Abfrageausdruck
Zeitbedarf für eine klassische Implementierung
Abfragen der Daten nach einem Kriterium
0,1752 ms
0,1590 ms
0,0173 ms
Sortieren der Daten
0,4163 ms
0,3819 ms
0,9750 ms
Gruppierung ohne Sortierung
0,1352 ms
0,1224 ms
0,1900 ms
Die LINQ-Erweiterungsmethoden
Abfrage
Zeitbedarf für den Zeitbedarf für Aufruf mit Erwei- den Aufruf mit terungsmethoden einem Abfrageausdruck
Zeitbedarf für eine klassische Implementierung
Gruppierung mit Sortierung der Gruppen
0,2151 ms
0,2478 ms
Nicht gemessen
Gruppierung mit Sortierung der Gruppen inkl. Inhalt
0,7878 ms
0,6797 ms
Nicht gemessen
Tabelle 11.1: Ergebnis meiner PerformanceMessungen für verschiedene LINQAbfragen auf meinem (noch alten) Rechner mit AMD 64, 4400+ Prozessor (Forts.)
1
Wie Sie der Tabelle entnehmen können, scheint es sich nur beim Abfragen von Daten nach einem Kriterium wirklich zu lohnen, klassisch zu programmieren. Das Sortieren und Gruppieren benötigt in der klassischen Variante sogar wesentlich mehr Zeit. Dabei sollten Sie aber beachten, dass die klassische Variante von mir zwar nach bestem Wissen möglichst optimal gestaltet wurde, aber u. U. noch optimiert werden kann.
2
11.2
4
3
Die LINQ-Erweiterungsmethoden
Die Klassen Enumerable und Queryable stellen eine Vielzahl an Erweiterungsmethoden zur Abfrage von Daten zur Verfügung. Diese Methoden können in mehrere Kategorien eingeteilt werden: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
5
Basismethoden, die Sie bei der Arbeit mit LINQ immer wieder benötigen (Where, OrderBy, Select) Methoden für Gruppierungen Methoden für das Verknüpfen von Sequenzen Methoden für das Ermitteln einzelner Elemente Aggregat-Methoden, die über eine Menge von Objekten Berechnungen ausführen Methoden zur Ermittlung, ob Elemente in einer Sequenz enthalten sind Spezielle Filter-Methoden Methoden zum Konvertieren von Sequenzen Methoden zum Erzeugen von Sequenzen Methoden für Mengenoperationen
6
7
8
9
Im Folgenden stelle ich diese Methoden vor und zeige, wie Sie damit arbeiten. Zur praxisnahen Demonstration der einzelnen Erweiterungsmethoden arbeiten die Beispiele der folgenden Abschnitte mit je einer Auflistung von Instanzen einer Klasse Category und Product. Das mag Ihnen jetzt zwar komplex erscheinen. Ich denke aber, dass LINQ mit praxisgerechten Daten wesentlich einfacher zu verstehen ist als mit einfachen Daten (wie String-Arrays). Damit Sie mit den Beispielen besser zurechtkommen, beschreibe ich hier zunächst die Klassen und die in den Beispielen verwendeten Daten. Was Sie zu den Erweiterungsmethoden wissen sollten ist, dass diese (wie unter .NET allgemein üblich) die Sequenz, auf der sie aufgerufen werden, nie verändern. Sie geben lediglich eine ermittelte neue Sequenz oder ein einzelnes Objekt (bzw. einen einzelnen Wert) zurück.
10
11 INFO
663
LINQ
DISC
Auf der Buch-DVD finden Sie im Beispiele-Ordner zu diesem Kapitel die PDF-Datei Beispieldaten.pdf, die die hier dargestellten Informationen enthält. Diese können Sie ggf. ausdrucken, wenn Sie im weiteren Verlauf des Buchs eine Übersicht über die Beispieldaten benötigen. Die Klassen Category und Product sind folgendermaßen deklariert: /* Repräsentiert eine Artikelkategorie */ public class Category { /* Die ID der Kategorie */ public int ID; /* Der Name der Kategorie */ public string Name; } /* Repräsentiert einen Artikel */ public class Product { /* Die ID des Artikels */ public int ID; /* Der Name des Artikels */ public string Name; /* Der Preis */ public decimal Price; /* Die ID der Kategorie */ public int? CategoryID; }
Die Beziehung zwischen einem Product- und einem Category-Objekt wird über die Felder CategoryId in Product und ID in Category aufgebaut. CategoryId ist ein Nullable, damit einem Artikel auch keine Kategorie zugewiesen werden kann.
INFO
Die Beziehung zwischen einem Artikel und seiner Kategorie wird bewusst über einen Integer-ID-Wert definiert, wie es in normalen (relationalen, nicht objektorientierten) Datenbanken der Fall ist. In einem Objektmodell werden Beziehungen normalerweise nicht so dargestellt. Korrekt wäre, wenn Product eine Category-Referenz verwalten würde. In LINQ to SQL werden Beziehungen zwischen Datenbanktabellen auch so abgebildet. Ich brauchte aber Beispieldaten zur Demonstration des Verknüpfens von Sequenzen in LINQ (siehe Seite 679). Und verknüpft werden müssen nur Sequenzen, die nicht von sich aus bereits verknüpft sind. In den Beispielen rufe ich die statischen Methoden GetCategories und/oder GetProducts auf, die eine Liste von Category- bzw. Product-Objekten liefern. Diese Methoden sind folgendermaßen deklariert: private static List GetCategories() { ... } private static List GetProducts() { ... }
664
Die LINQ-Erweiterungsmethoden
Die Implementierung der Methoden stelle ich hier aus Platzgründen – und weil diese nicht relevant ist – nicht dar. In den Beispielprojekten auf der Buch-DVD lesen diese Methoden je eine Textdatei ein. Diese Dateien enthalten die in Tabelle 11.2 und Tabelle 11.3 dargestellten Daten. ID
Name
1
Fachbücher
2
Normale Bücher
3
Filme
4
Zeitschriften
Tabelle 11.2: Die BeispielKategorien
1
2
ID
Name
CategoryID
Price
1
Per Anhalter durch die Galaxis
2
9,95
2
Per Anhalter durch die Galaxis (DVD)
3
28,99
3
Das C# 2008 Kompendium
1
59,99
4
Das C# 2008 Codebook
1
99,95
5
Die wilde Geschichte vom Wassertrinker
2
12,90
6
2001: Odyssee im Weltraum (DVD)
3
9,95
7
2010 – Das Jahr, in dem wir Kontakt aufnehmen (DVD)
3
9,95
8
Programmieren lernen
1
24,95
9
Programmieren lernen (E-Book)
10
Fool on the Hill
Tabelle 11.3: Die Beispiel-Artikel
3
4
5
6
7
15,95 2
9,95
8 Die Kategorie 4 kommt bewusst nicht in den Artikeln vor. Genauso bewusst besitzt der Artikel 9 keine Kategorie. Diese Daten benötige ich für die Demonstration von Verknüpfungen.
9
Abbildung 11.2 stellt schließlich die Beziehung zwischen den beiden Klassen dar. Abbildung 11.2: Die Beziehung zwischen den Beispiel-Klassen
10
11
665
LINQ
11.2.1
Grundlagen zu der in den folgenden Abschnitten verwendeten Syntaxbeschreibung der LINQErweiterungsmethoden
Um bei der Beschreibung der einzelnen Methoden nicht immer deren komplexe Syntax angeben zu müssen (die im Buch wohl eher verwirrt als hilfreich ist), habe ich die Syntaxbeschreibung der Methoden wenn möglich vereinfacht (nicht zuletzt, weil mein Fachlektor die originale Syntax sehr unübersichtlich fand): ■
LINQ-Erweiterungsmethoden geben immer entweder eine Sequenz oder ein einzelnes Element zurück. Eine Sequenz ist entweder vom Typ IEnumerable oder IEnumerable. Die erste Sequenz enthält die Typen, die in der Originalsequenz verwaltet werden, die zweite mit TResult enthält neue Typen. Die Select-Methode ermöglicht z. B. eine Projektion und gibt eine Sequenz von neuen Typen zurück. Ich verwende für IEnumerable den Begriff Quelltyp-Sequenz und für IEnumerable den Begriff Ergebnistyp-Sequenz. Wird nur ein einzelnes Element zurückgegeben, verwende ich den Begriff Quelltyp-Element, wenn es sich um ein Element vom Typ TSource handelt, und den Begriff Ergebnistyp-Element, wenn es sich um ein Objekt vom Typ TResult handelt.
■
■
■
■
Da die LINQ-Methoden Erweiterungsmethoden sind, die entweder immer IQueryable oder IEnumerable erweitern, verzichte ich in meiner Beschreibung der Methoden auf das entsprechende, immer gleich deklarierte this-Argument. Viele LINQ-Erweiterungsmethoden arbeiten mit einem Prädikat in der Form Func oder Func. Dem ersten Prädikat wird ein Element der Sequenz übergeben, dem zweiten zusätzlich der Index des Elements (bezogen auf die Sequenz). Beide geben true zurück, wenn die durch das Prädikat definierte Bedingung erfüllt ist, und false, wenn nicht. Da Prädikate immer gleich sind, verwende ich dafür den Begriff predicate, der so auch als Name der entsprechenden Argumente verwendet wird. Einige Erweiterungsmethoden arbeiten mit einem Delegaten der Form Func, der einen Schlüssel zurückgibt, nach dem z. B. sortiert werden soll. OrderBy müssen Sie z. B. einen solchen Delegaten (bzw. LambdaAusdruck) übergeben, der den Schlüssel zurückgibt, nach dem sortiert wird. Ich benutze den auch für die entsprechenden Argumente verwendeten Begriff keySelector. Einige Erweiterungsmethoden besitzen ein Argument in Form der IComparerSchnittstelle, über das ein Vergleich erfolgen soll. Ich nenne dieses Argument (wie im Original) einfach comparer.
Statt der folgenden originalen Deklaration der Where-Methode: IEnumerable Where( this IEnumerable source, Func predicate) IEnumerable Where( this IEnumerable source, Func predicate)
verwende ich z. B. die etwas übersichtlichere folgende Form: Quelltyp-Sequenz Where(predicate)
Danken Sie meinem Fachlektor dafür ☺.
666
Die LINQ-Erweiterungsmethoden
11.2.2
Einschränken mit Where
Where ergibt eine Sequenz, die nach der übergebenen Bedingung eingeschränkt ist. Where ist prinzipiell folgendermaßen deklariert: Quelltyp-Sequenz Where(predicate)
Am Argument predicate übergeben Sie die Bedingung. Die Rückgabe des Prädikats sagt aus, ob das am ersten Argument übergebene Objekt in das Ergebnis übernommen wird. Dem int-Argument der zweiten Variante wird der Index des Objekts übergeben (bezogen auf die Auflistung).
1
So können Sie z. B. alle Produkte abfragen, deren Preis größer ist als 25 Euro:
2
Listing 11.14: Abfragen von Daten einer Auflistung mit Where // Beispiel-Artikel einlesen List products = GetProducts();
3
// Artikel nach deren Preis abfragen var filteredProducts1 = products.Where( product => product.Price >= 25M); foreach (var product in filteredProducts1) { Console.WriteLine(product.Name + ": " + product.Price); }
4
5
Sie können natürlich alle möglichen Bedingungen angeben. Die Bedingung des Prädikats (die Sie dem rechten Teil des Lambda-Ausdrucks übergeben, wenn Sie einen solchen verwenden), muss lediglich einen booleschen Wert ergeben. Das folgende Beispiel fragt alle Artikel der Kategorie 1 mit einem Preis größer 25 Euro ab:
6
Listing 11.15: Abfragen von Daten einer Auflistung mit Where mit einer komplexen Bedingung var filteredProducts2 = products.Where( product => product.CategoryID == 1 && product.Price > 25M); foreach (var product in filteredProducts2) { Console.WriteLine(product.Name + ": " + product.Price); }
7
8
Wenn Sie die C#-Abfrageausdruck-Syntax verwenden, schränken Sie über das where-Schlüsselwort ein:
9 Listing 11.16: Abfragen von Daten einer Auflistung mit where var filteredProducts3 = from product in products where product.CategoryID == 1 && product.Price > 25M select product; foreach (var product in filteredProducts3) { Console.WriteLine(product.Name + ": " + product.Price); }
10
11
667
LINQ
11.2.3
Sortieren mit OrderBy, OrderByDescending, ThenBy und ThenByDescending
Die Methoden OrderBy und OrderByDescending erlauben ein aufsteigendes bzw. absteigendes Sortieren. Die Deklaration der einzelnen Überladungen ist prinzipiell die folgende: Quelltyp-Sequenz OrderBy(keySelector [, comparer])
keyselector bestimmt die Sortierung
Am Argument keyselector übergeben Sie einen Lambda-Ausdruck (oder einen Delegaten), der den Wert zurückgibt, nach dem sortiert werden soll. Der hier zurückgegebene Typ muss IComparable implementieren, wenn Sie das Argument comparer nicht übergeben. Für die einfachen .NET-Typen ist das ja bereits der Fall. Am Argument comparer können Sie ein Objekt übergeben, das den Vergleich der Werte bzw. Objekte übernimmt, nach denen sortiert wird. Damit können Sie auch nach Objekten sortieren, die IComparable nicht implementieren, oder Sie können spezielle Sortierungen erreichen. Das folgende Beispiel sortiert die Artikel aufsteigend nach deren Namen: Listing 11.17: Einfaches aufsteigendes Sortieren // Beispiel-Artikel einlesen List products = GetProducts(); // Produkte nach deren Namen sortieren var orderedProducts1 = products.OrderBy(product => product.Name); foreach (var product in orderedProducts1) { Console.WriteLine(product.Name + ": " + product.Price); }
Wenn Sie Abfrageausdrücke verwenden, sortieren Sie über das orderby-Schlüsselwort: orderby Sortierfeld-Liste [[descending | ascending}]
Eine absteigende Sortierung erreichen Sie, indem Sie hinter der Sortierfeld-Liste descending angeben: Listing 11.18: (Absteigendes) Sortieren über einen Abfrageausdruck var orderedProducts2 = from product in products orderby product.Name descending select product; foreach (var product in orderedProducts2) { Console.WriteLine(product.Name + ": " + product.Price); }
Für eine aufsteigende Sortierung können Sie ascending angeben. ascending ist allerdings Voreinstellung und muss deswegen nicht angegeben werden. Wie bereits gesagt können Sie LINQ-Erweiterungsmethoden auch kombinieren. So können Sie z. B. erst einschränken und dann (absteigend nach dem Preis) sortieren:
668
Die LINQ-Erweiterungsmethoden
Listing 11.19: Absteigendes Sortieren in Kombination mit einer Einschränkung // Mit Erweiterungsmethoden var orderedProducts3 = products .Where(product => product.CategoryID == 1) .OrderByDescending(product => product.Price); ...
1
// Mit einem Abfrageausdruck var orderedProducts4 = from product in products where product.CategoryID == 1 orderby product.Price descending select product;
2
...
Die Methoden ThenBy und ThenByDescending können Sie nach OrderBy bzw. OrderByDescending aufrufen, um eine Untersortierung zu erreichen. Die Deklaration ist prinzipiell die folgende:
Über ThenBy und ThenByDescending erreichen Sie eine Untersortierung
3
Quelltyp-Sequenz ThenBy(keySelector [, comparer])
4
Das folgende Beispiel sortiert Artikel mit einem Preis größer 10 Euro nach deren Kategorie und deren Namen: Listing 11.20: Sortieren nach mehreren Kriterien
5
// Produkte nach mehreren Kriterien sortieren var orderedProducts5 = products .Where(product => product.Price > 10M) .OrderBy(product => product.CategoryID) .ThenBy(product => product.Name); foreach (var product in orderedProducts3) { Console.WriteLine(product.Name + ": " + product.Price); }
6
7
In Abfrageausdrücken geben Sie die Untersortierung kommagetrennt hinter den Haupt-Sortierfeldern an:
8
Listing 11.21: Sortieren nach mehreren Kriterien in einem Abfrageausdruck var orderedProducts6 = from product in products where product.Price > 10M orderby product.CategoryID, product.Name select product;
11.2.4
9
Projektionen mit Select
10
Wenn in der abgefragten Sequenz komplexe Objekte verwaltet werden, macht es in vielen Fällen Sinn, nicht alle Eigenschaften dieser Objekte abzufragen. Dies gilt besonders, wenn Sie mit LINQ-Erweiterungen wie LINQ to SQL arbeiten, da diese dann nicht alle Daten von der Datenquelle abrufen müssen und deswegen effizienter arbeiten. Eine Projektion hilft in diesen Fällen, die Daten so abzufragen, wie diese benötigt werden. Aber auch beim Abfragen einfacher Daten kann eine Projektion Sinn machen. So können Sie z. B. die in einer String-Auflistung gespeicherten Zeichenketten so abfragen, dass diese in Großschreibung ausgegeben werden.
11
669
LINQ
Eine einfache Projektion erreichen Sie über die Methode Select, die prinzipiell folgendermaßen aufgebaut ist: Ergebnistyp-Sequenz Select(selector)
Projizieren können Sie in jeden verfügbaren Typen, auch in anonyme
TResult ist der Typ, der in der zurückgegebenen Sequenz verwaltet wird. Dabei kann es sich um jeden verfügbaren Typen handeln, auch um einen anonymen. Eine Projektion projiziert die Daten eines Elements der Sequenz in ein anderes Element, dessen Typ auch ein vollkommen anderer sein kann. Da Sie mit LINQ eigentlich immer mit Ergebnis-Variablen arbeiten, die mit var deklariert sind und weil C# Typen per Typrückschluss erkennt, müssen Sie den Ergebnistyp in der Regel nicht angeben. Der Lambda-Ausdruck (oder der Delegat), den Sie am selector-Argument übergeben, übernimmt die Projektion. Dieser Selektor gibt den Ergebnis-Typ zurück. Am ersten Argument wird dem Selektor das jeweilige Element der Sequenz übergeben. Daraus konstruieren Sie das Ergebnis-Element. In der zweiten Überladung von Select erhält der Selektor am int-Argument den Index des Elements übergeben, das projiziert werden soll. Das folgende Beispiel projiziert eine String-Liste in eine Liste, deren Strings in Großschreibung umgewandelt wurden: Listing 11.22: Eine einfache Projektion string[] names = { "Zaphod Beeblebrox", "Ford Prefect", "Tricia McMillan", "Arthur Dent" }; var upperNames = names.Select(name => name.ToUpper()); foreach (var name in upperNames) { Console.WriteLine(name); }
Sie können aber natürlich auch Sequenzen von komplexen Objekten projiziert ausgeben. Listing 11.23 projiziert die Beispiel-Produktliste so, dass nur die in den Brutto-Wert projizierten Preise der Produkte der Kategorie »Fachbücher« (ID: 1) ausgegeben werden: Listing 11.23: Projektion auf einer Liste mit Objekten (mit vorheriger Einschränkung der Objekte) // Beispiel-Artikel einlesen List products = GetProducts(); const decimal vat = .19M; var grossPrices = products .Where(product => product.CategoryID == 1) .Select(product => product.Price * (1 + vat)); foreach (var grossPrice in grossPrices) { Console.WriteLine(grossPrice); }
Eine Projektion in einen anonymen Typen gibt Ihnen alle Freiheiten
670
Da der Lambda-Ausdruck bzw. die Methode, die die Projektion vornimmt, alle Typen zurückgeben kann, können Sie nicht nur einfache Typen zurückgeben, sondern auch komplexe. Und hier kommen anonyme Typen ins Spiel. So können Sie die Daten der in einer Sequenz verwalteten Elemente in jegliche Form projizieren. Das folgende Beispiel zeigt dies an der Projektion der Daten der Produkte der Kategorie »Normale Bücher« in Objekte, deren anonymer Typ den Produktnamen, die Kategorie und den Brutto-Preis verwaltet:
Die LINQ-Erweiterungsmethoden
Listing 11.24: Projektion in einen anonymen Typen // Beispiel-Artikel und -Kategorien einlesen List products = GetProducts(); List categories = GetCategories(); // Die Kategorie »Normale Bücher« ermitteln Category normalBooksCategory = null; foreach (Category category in categories) { if (category.Name == "Normale Bücher") { normalBooksCategory = category; break; } }
1
2
// Die Artikel der Kategorie ermitteln var productsOfCategoryNormalBooks1 = products .Where(product => product.CategoryID == normalBooksCategory.ID) .Select(product => new { ProductName = product.Name, Category = normalBooksCategory, GrossPrice = product.Price * (1 + vat) }); foreach (var productOfCategoryNormalBooks in productsOfCategoryNormalBooks1 ) { Console.WriteLine(productOfCategoryNormalBooks.Category.Name + ": " + productOfCategoryNormalBooks.ProductName + ": " + productOfCategoryNormalBooks.GrossPrice); }
3
4
5
In Abfrageausdrücken ist all das ebenfalls möglich. Dazu verwenden Sie die selectKlausel, der Sie rechts die Projektion übergeben. Das vorhergehende Beispiel sieht dann so aus:
6
Listing 11.25: Projektion in einem Abfrageausdruck
7
var productsOfCategoryNormalBooks2 = from product in products where product.CategoryID == normalBooksCategory.ID select new { ProductName = product.Name, Category = normalBooksCategory, GrossPrice = product.Price * (1 + vat) }; foreach (var productOfCategoryNormalBooks in productsOfCategoryNormalBooks2 ) { Console.WriteLine(productOfCategoryNormalBooks.Category.Name + ": " + productOfCategoryNormalBooks.ProductName + ": " + productOfCategoryNormalBooks.GrossPrice); }
11.2.5
8
9
10
11
Gruppierungen
LINQ unterstützt über die GroupBy-Methode auch das Gruppieren von Sequenzen. Bei einer Gruppierung werden Teilmengen nach einem oder mehreren Schlüsseln zusammengefasst und können dann gruppenweise abgerufen werden. Bei den in diesem Kapitel für die Beispiele verwendeten Produkten wäre beispielsweise eine Gruppierung nach der Kategorie denkbar.
GroupBy ermöglicht das Gruppieren
671
LINQ
GroupBy ist prinzipiell folgendermaßen deklariert: Gruppierungs-Sequenz GroupBy(keySelector [, comparer]) Gruppierungs-Sequenz GroupBy(keySelector, elementSelector [, comparer]) Ergebnistyp-Sequenz
GroupBy(keySelector, resultSelector [, comparer])
Ergebnistyp-Sequenz GroupBy(keySelector, elementSelector, resultSelector [, comparer]
Tabelle 11.4 erläutert zunächst die Argumente der einzelnen Varianten. Tabelle 11.4: Die Argumente der GroupBy-Methode (außer dem thisArgument)
Argument
Beschreibung
keySelector
An diesem Argument (dem »Schlüssel-Selektor«) übergeben Sie einen LambdaAusdruck (oder einen Delegaten), der den Schlüssel zurückgibt, nach dem gruppiert werden soll. Der Schlüssel-Selektor erhält ein Objekt der Sequenz übergeben und gibt einen Wert zurück, der den Schlüssel darstellen soll.
elementSelector
Über einen Lambda-Ausdruck (oder einen Delegaten), den Sie an diesem Argument übergeben, können Sie die einzelnen Objekte der Sequenz vor dem Gruppieren transformieren. Der »Element-Selektor« erhält dazu das jeweilige Objekt übergeben und gibt einen Wert oder ein Objekt zurück, das die Transformation darstellt.
resultSelector
An diesem Argument können Sie einen Lambda-Ausdruck (oder einen Delegaten) übergeben, der für jede Gruppe ein Ergebnis berechnet. Diesem »Ergebnis-Selektor« werden für jeden Gruppe der Schlüssel der Gruppe und eine Auflistung übergeben, die die Objekte enthält, die zu der Gruppe gehören. Der Ergebnis-Selektor gibt das berechnete Ergebnis zurück. Die GroupBy-Varianten mit resultSelector-Argument geben lediglich eine Auflistung der berechneten Werte zurück. Mit diesen Varianten können Sie also Berechnungen über Gruppen ausführen und das Ergebnis auswerten.
comparer
Das comparer-Argument übernimmt wie unter .NET üblich (siehe bei Contains, Seite 694) einen speziellen Vergleich des Wertes, nach dem gruppiert wird.
Die unterschiedliche Rückgabe der einzelnen Varianten wird im Folgenden deutlich.
Einfaches Gruppieren Die ersten beiden Überladungen der GroupBy-Methode geben ein Objekt zurück, das die Schnittstelle IEnumerable implementiert. Es handelt sich dabei also um eine Auflistung von Objekten, die die Schnittstelle IGrouping implementieren. TKey ist dabei der Typ des Schlüssels, nach dem gruppiert wird, TSource der Typ der in der Sequenz verwalteten Objekte. Ein Objekt, das diese Schnittstelle implementiert, liefert in der Eigenschaft Key den Schlüssel und implementiert selbst wieder IEnumerable, erlaubt also das Durchgehen der in der Gruppe verwalteten Objekte über foreach. Wow. Ein Beispiel macht das Ganze wahrscheinlich leichter verständlich. In Listing 11.26 werden die Beispiel-Produkte nach der Kategorie-ID gruppiert. Listing 11.26: Einfaches Gruppieren // Beispiel-Artikel einlesen List products = GetProducts(); // Die Produkte nach der Kategorie gruppieren
672
Die LINQ-Erweiterungsmethoden
var groupedProducts1 = products.GroupBy( product => product.CategoryID); // Die einzelnen Gruppen durchgehen und abfragen foreach (var grouping in groupedProducts1) { Console.WriteLine(grouping.Key != null ? "Kategorie " + grouping.Key : "Keine Kategorie");
1
foreach (var product in grouping) { Console.WriteLine(product.Name); } Console.WriteLine(); }
2
Abbildung 11.3 zeigt das Ergebnis des Programms. Der Artikel »Programmieren lernen (E-Book)«, dem keine Kategorie zugewiesen wurde, wurde ebenfalls gruppiert. Der Schlüssel dieser Gruppe ist in diesem Fall null.
3 Abbildung 11.3: Die gruppierten Artikel
4
5
6
7 Das Ergebnis ist übrigens (wie Sie in Abbildung 11.3 sehen) unsortiert. Im folgenden Abschnitt zeige ich, wie Sie Gruppierungen sortieren.
8
In C#-Abfrageausdrücken verwenden Sie das group-Schlüsselwort: Listing 11.27: Gruppieren mit einem Abfrageausdruck
9
var groupedProducts2 = from product in products group product by product.CategoryID;
10
Sortieren der Gruppierung Eine Gruppierung ist normalerweise nicht sortiert. Im vorhergehenden Beispiel werden die Gruppen z. B. in der Reihenfolge 2, 3, 1, Keine Kategorie ausgegeben. Sie können die Gruppen allerdings über die OrderBy-Methode sortieren. Wenn Sie diese hinter GroupBy hängen, erhält der keySelector-Lambda-Ausdruck (oder -Delegat) (der OrderBy übergeben wird) ein IGrouping-Objekt übergeben und Sie können nach der Eigenschaft Key sortieren:
Sortieren müssen Sie separat
11
673
LINQ
Listing 11.28: Sortieren einer Gruppierung // Beispiel-Artikel einlesen List products = GetProducts(); // Gruppieren var groupedProducts1 = products .GroupBy(product => product.CategoryID) .OrderBy(grouping => grouping.Key);
In einem Abfrageausdruck sieht das Ganze etwas anders aus. Da group normalerweise den Ausdruck beendet, müssen Sie dessen Ergebnis über into in eine Variable schreiben. into, das ab Seite 703 näher beschrieben wird, führt dazu, dass das bisherige Ergebnis über die Variable referenziert wird, die rechts von into angegeben ist. Über diese Variable können Sie anschließend mit dem Ergebnis weiterarbeiten. Da es sich dann wieder um einen neuen Abfrageausdruck handelt, gelten für diesen alle Regeln eines normalen Abfrageausdrucks. Sie müssen den Ausdruck z. B. über select (oder wieder über group) abschließen: Listing 11.29: Sortieren einer Gruppierung in einem Abfrageausdruck und Ausgabe der Gruppen var groupedProducts2 = from product in products group product by product.CategoryID into grouping orderby grouping.Key select grouping; foreach (var grouping in groupedProducts2) { Console.WriteLine(grouping.Key != null ? "Kategorie " + grouping.Key : "Keine Kategorie"); foreach (var product in grouping) { Console.WriteLine(product.Name); } Console.WriteLine(); }
Die Ausgabe des Programms zeigt Abbildung 11.4. Abbildung 11.4: Sortierte Gruppierung
Wie Sie an Kategorie 2 sehen, sind die Objekte innerhalb der einzelnen Gruppen nicht sortiert. Um dies zu erreichen, können Sie diese vor dem Gruppieren sortieren:
674
Die LINQ-Erweiterungsmethoden
Listing 11.30: Sortieren von Gruppen inklusive deren Inhalt // Mit Erweiterungsmethoden var groupedProducts3 = products .OrderBy(product => product.Name) .GroupBy(product => product.CategoryID) .OrderBy(grouping => grouping.Key); // Mit einem Abfrageausdruck var groupedProducts4 = from product in products orderby product.Name group product by product.CategoryID into grouping orderby grouping.Key select grouping;
Das Sortieren vor dem Gruppieren kann aber ggf. inperformant sein, besonders wenn Sie die Gruppen, wie im nächsten Abschnitt beschrieben, einschränken. Deswegen sollten Sie ggf. überprüfen, ob ein Sortieren nach dem Gruppieren performanter ist:
1
2
3
INFO
Listing 11.31: Sortieren des Gruppeninhalts nach dem Gruppieren
4
var groupedProducts5 = from product in products group product by product.CategoryID into grouping orderby grouping.Key select grouping;
5
foreach (var grouping in groupedProducts5) { Console.WriteLine(grouping.Key != null ? "Kategorie " + grouping.Key : "Keine Kategorie"); var sortedProducts = grouping.OrderBy(product => product.Name); foreach (var product in sortedProducts) { Console.WriteLine(product.Name); } Console.WriteLine(); }
6
7
Einschränken der Gruppierung
8
Das Einschränken einer Gruppierung auf diejenigen Gruppen, die eine bestimmte Bedingung erfüllen, ist (zumindest in SQL) eine wichtige Praxis-Technik. So könnten Sie z. B. bei den Beispiel-Produkten nur die Gruppen in das Ergebnis übernehmen, die mehr als 10 Produkte beinhalten. Dazu hängen Sie die Where-Methode an die Gruppierung. Dem dieser Methode übergebenen Prädikat wird von LINQ dann ein IGrouping-Objekt übergeben. Da IGrouping von IEnumerable abgeleitet ist, können Sie auf diesem Objekt wieder alle LINQ-Erweiterungsmethoden anwenden. Damit können Sie z. B. auch die Anzahl der Objekte in der Gruppe ermitteln und darauf einschränken:
9
10
11
Listing 11.32: Einschränken einer Gruppierung // Beispiel-Artikel einlesen List products = GetProducts(); // Die Artikel nach der Kategorie gruppieren und dabei auf Gruppen // einschränken, die mehr als zwei Artikel beinhalten
675
LINQ
var groupedProducts1 = products .GroupBy(product => product.CategoryID) .Where(grouping => grouping.Count() > 2);
In einem Abfrageausdruck hängen Sie where ebenfalls hinter die Gruppierung. Da group einen Abfrageausdruck normalerweise beendet, müssen Sie das jeweilige Teilergebnis wieder über into in eine Variable schreiben, die Sie dann im weiteren Verlauf verwenden: Listing 11.33: Einschränken einer Gruppierung in einem Abfrageausdruck var groupedProducts2 = from product in products group product by product.CategoryID into grouping where grouping.Count() > 2 select grouping;
Transformation der gruppierten Objekte elementSelector erlaubt eine Transformation der Objekte
Über das optionale Argument elementSelector können Sie die in der Sequenz verwalteten Elemente nach dem Gruppieren transformieren. Der Lambda-Ausdruck (bzw. Delegat), den Sie an diesem Argument übergeben, erhält am ersten Argument das jeweilige Element übergeben. Die Methode gibt einen transformierten Wert (bzw. ein transformiertes Objekt) zurück. Diese Methode macht besonders dann Sinn, wenn Sie mit LINQ to SQL Datenbankdaten bearbeiten und erreichen wollen, dass die von LINQ to SQL generierte SQL-Anweisung nur die Felder beinhaltet, die Sie benötigen. In unserem Beispiel transformiere ich die Product-Objekte in einen String, der den Namen des Produkts beinhaltet: Listing 11.34: Gruppierung mit Transformation // Beispiel-Artikel einlesen List products = GetProducts(); // Die Artikel nach der Kategorie gruppieren, aber so transformieren, // dass nur der Artikelname zurückgegeben wird var groupedProductNames = products.GroupBy( product => product.CategoryID, product => product.Name); // Die einzelnen Gruppen durchgehen und abfragen foreach (var grouping in groupedProductNames) { Console.WriteLine(grouping.Key != null ? "Kategorie " + grouping.Key : "Keine Kategorie"); foreach (var productName in grouping) { Console.WriteLine(productName); } Console.WriteLine(); }
Gruppen-Ergebnisse berechnen Die Varianten der GroupBy-Methode mit dem resultSelector-Argument erlauben die Übergabe eines Lambda-Ausdrucks (bzw. Delegaten), dem für jede Gruppe der Gruppen-Schlüssel und eine Auflistung der in der Gruppe enthaltenen Objekte übergeben werden. Der »Ergebnis-Selektor« kann dann über diese Objekte ein Ergebnis
676
Die LINQ-Erweiterungsmethoden
berechnen und dieses zurückgeben. Als Beispiel gruppiert Listing 11.35 die in diesem Kapitel verwendeten Produkte so, dass für jede Gruppe der Durchschnitt der Produktpreise berechnet wird: Listing 11.35: Ermitteln eines Gruppen-Ergebnisses // Beispiel-Artikel einlesen List products = GetProducts();
1
// Mit Gruppen-Ergebnis gruppieren var groupedProducts1 = products .GroupBy(product => product.CategoryID, (key, groupProducts) => groupProducts.Average( product => product.Price));
2
Die GroupBy-Überladungen mit resultSelector-Argument geben eine einfache Sequenz zurück, die TResult-Objekte verwaltet. TResult ist der Typ, den der resultSelector-Ausdruck bzw. -Delegat zurückgibt. Über die zurückgegebene Sequenz können Sie dann also die Gruppen-Ergebnisse auswerten. Damit Sie wenigstens den Gruppen-Schlüssel zur Verfügung haben, sollten Sie in dem Ergebnis-Selektor einen (anonymen) Typen zurückgeben, der den Gruppen-Schlüssel beinhaltet:
3
4
Listing 11.36: Ermitteln eines Gruppen-Ergebnisses mit anonymem Typ und Auswertung der Ergebnisse var groupResults2 = products .GroupBy(product => product.CategoryID, (key, groupProducts) => new { Key = key, AveragePrice = groupProducts.Average( product => product.Price) });
5
// Die einzelnen Gruppen-Ergebnisse durchgehen und abfragen foreach (var groupResult in groupResults2) { Console.WriteLine((groupResult.Key != null ? "Kategorie " + groupResult.Key : "Keine Kategorie") + ": " + groupResult.AveragePrice); }
6
7
Das Ergebnis dieses Programms zeigt Abbildung 11.5. Abbildung 11.5: Das GruppenErgebnis-Beispielprogramm in Aktion
8
9
Wie das Ganze möglichst performant mit einem Abfrageausdruck möglich ist, ist mir ehrlich gesagt nicht ganz klar. Der folgende Abfrageausdruck ist der in Listing 11.36 verwendeten Gruppierung äquivalent, aber wahrscheinlich nicht sehr schnell (weil erst gruppiert und dann projiziert wird):
10
11
Listing 11.37: Gruppenberechnung mit einem Abfrageausdruck var groupResults3 = from product in products group product by product.CategoryID into grouping select new { Key = grouping.Key, AveragePrice = grouping.Average(product => product.Price) };
677
LINQ
INFO
Wenn Sie Gruppen normal auswerten wollen, aber Gruppen-Ergebnisse benötigen, können Sie auf den IGrouping-Instanzen, die Sie durchgehen, LINQ-Erweiterungsmethoden ausführen. IGrouping ist ja schließlich von IEnumerable abgeleitet.
Nach mehreren Schlüsseln gruppieren Ein anonymer Typ als Schlüssel erlaubt das Gruppieren nach mehreren Schlüsselwerten
Wenn Sie nach zusammengesetzten Schlüsseln gruppieren wollen, müssen Sie als Schlüssel einen anonymen Typen verwenden, der die Werte der eigentlichen Schlüssel beinhaltet. So können Sie die Northwind-Produkte z. B. nach der Kategorie und danach gruppieren, ob der Preis kleiner/gleich oder größer als 10 Euro ist: Listing 11.38: Gruppieren nach einem zusammengesetzten Schlüssel // Beispiel-Artikel einlesen List products = GetProducts(); // Nach der Kategorie und danach gruppieren, ob der Preis // kleiner/gleich oder größer als 10 Euro ist var groupedProducts1 = products.GroupBy( product => new {CategoryID = product.CategoryID, PriceAbove10Euro = product.Price > 10}); // Auswerten der Gruppierung foreach (var productGroup in groupedProducts1) { string category = productGroup.Key.CategoryID != null ? productGroup.Key.CategoryID.ToString() : "Keine"; Console.WriteLine("Kategorie {0}, Preis über 10 Euro: {1}", category, productGroup.Key.PriceAbove10Euro); foreach (var product in productGroup) { Console.WriteLine(product.Name + ": " + product.Price); } Console.WriteLine(); }
Das (unsortierte) Ergebnis zeigt Abbildung 11.6. Abbildung 11.6: Das Ergebnis des Beispiel-Programms
678
Die LINQ-Erweiterungsmethoden
11.2.6
Verknüpfen von Sequenzen
LINQ ermöglicht über verschiedene Methoden das Verknüpfen von Sequenzen zu einer Ergebnis-Sequenz. Tabelle 11.5 stellt diese Methoden zunächst vor. Methode
Beschreibung
Join
verknüpft die Elemente zweier Sequenzen über einen Schlüssel, den Sie der Methode übergeben. Erzeugt eine flache Sequenz.
GroupJoin
verknüpft die Elemente zweier Sequenzen ähnlich Join über einen Schlüssel, erzeugt aber eine hierarchische Sequenz.
SelectMany
Diese Methode ermöglicht flexible Verknüpfungen zweier Sequenzen, die über eine Methode gesteuert werden, die SelectMany übergeben wird. Ich beschreibe SelectMany erst im Abschnitt »Komplexe Abfragen mit into, let und mehreren from-Klauseln« (Seite 703).
Tabelle 11.5: Die LINQ-Methoden zum Verknüpfen von Sequenzen
1
2
3
Innere Verknüpfungen (Inner Joins) über Join
4
Join verknüpft zwei Sequenzen, die über einen Schlüsselwert logisch miteinander verbunden sind, zu einer Ergebnis-Sequenz. Dies ist z. B. in unseren Beispiel-Daten der Fall: Product-Objekte sind auf der logischen Ebene über das Feld CategoryID mit je einem Category-Objekt verknüpft (über dessen Feld ID).
5 Abbildung 11.7: Das Objektmodell der Beispiel-Daten
6
7
8 Wie bereits eingangs dieses Kapitels gesagt ist diese nur logisch vorhandene Verknüpfung für die Praxis eher unhandlich. In »richtigen« Objektmodellen sollten auch richtige Referenzen verwendet werden. Ein Product-Objekt sollte seine Kategorie über eine Category-Referenz referenzieren und nicht über einen ID-Wert. Das Problem bei einer nur auf der logischen Ebene vorhandenen Verknüpfung ist, dass diese separat aufgelöst werden muss. OK, genau dafür ist die Join-Methode da. Das war auch der Grund dafür, die Verbindung zwischen Product und Category nur lose zu implementieren: Damit kann ich Join (und die anderen Methoden zum Verknüpfen und Verketten) demonstrieren.
INFO
9
10
11
Join ist folgendermaßen deklariert: IEnumerable Join( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector [, IEqualityComparer comparer])
679
LINQ
Die Argumente sind die folgenden: ■ ■ ■
■
■
■
INFO
outer: Die (äußere bzw. linke) Sequenz, auf der Join angewendet wird. inner: Die (innere bzw. rechte) Sequenz, mit der die äußere Sequenz verknüpft werden soll. outerKeySelector: Der an diesem Argument übergebene Lambda-Ausdruck bzw. Delegat stellt den Schlüssel für die Verknüpfung in der äußeren Sequenz zur Verfügung. innerKeySelector: Der an diesem Argument übergebene Lambda-Ausdruck bzw. Delegat stellt den Schlüssel für die Verknüpfung in der inneren Sequenz zur Verfügung. resultSelector: Der an diesem Argument übergebene Lambda-Ausdruck bzw. Delegat gibt einen Wert oder ein (anonymes) Objekt zurück, das in die ErgebnisSequenz aufgenommen wird. Üblicherweise wird hier ein anonymer Typ erzeugt, dessen Felder aus Daten der äußeren und der inneren Sequenz zusammengestellt werden. Der Methode werden das jeweilige Objekt der äußeren und der inneren Sequenz übergeben. comparer: An diesem optionalen Argument können Sie ein Objekt übergeben, das optional den Vergleich der Schlüsselwerte übernimmt.
Die von Microsoft verwendeten Bezeichner für die Sequenzen, die auf eine äußere und eine innere Sequenz hindeuten, sind verwirrend, besonders im Hinblick auf die Verwendung von inneren und äußeren Verknüpfungen (Inner Joins und Outer Joins). Das »innere« und »äußere« bezieht sich bei einer Verknüpfung nämlich nicht auf die Sequenzen, sondern auf die Art der Verknüpfung. Besser wäre gewesen, analog zu SQL die äußere Sequenz als linke und die innere als rechte zu bezeichnen. Dann würden auch Begriffe wie »Left Outer Join« passen (bei dem alle Elemente der linken und nur die passenden der rechten übernommen werden). Das folgende Beispiel verknüpft die Produkte mit den Kategorien. Als Ergebnis wird ein anonymer Typ erzeugt, der den Produktnamen, eine Referenz auf die Kategorie und den Preis enthält: Listing 11.39: Verknüpfen zweier Sequenzen über Join // Beispiel-Artikel und -Kategorien einlesen List products = GetProducts(); List categories = GetCategories(); // Die Artikel mit den Kategorien var products1 = products.Join( categories, product => product.CategoryID, category => category.ID, (product, category) => new { product.Name, Category = category, product.Price });
verknüpfen // // // //
Innere bzw. rechte Sequenz outerKeySelector innerKeySelector resultSelector
foreach (var product in products1) { Console.WriteLine("Artikel: {0}, Kategorie: {1}", product.Name, product.Category.Name); }
680
Die LINQ-Erweiterungsmethoden
Das Ergebnis dieses Programms zeigt Abbildung 11.8. Abbildung 11.8: Die über Join verknüpften Daten
1
Wie Sie in Abbildung 11.8 sehen, erzeugt Join (und das Abfrageausdruck-Äquivalent join) eine so genannte innere Verknüpfung (Inner Join). Dabei werden nur die Elemente der Sequenzen miteinander verknüpft, bei denen die Schlüsselwerte jeweils in der anderen Sequenz gefunden werden. Existieren in einer der beiden Sequenzen Elemente mit einem Schlüsselwert, der nicht in der anderen Sequenz vorkommt, werden diese nicht in das Ergebnis übernommen.
Join erzeugt in der Grundform einen Inner Join
3
Deshalb tauchen im Ergebnis weder der Artikel »Programmieren lernen (E-Book)« auf (der keine Kategorie besitzt) noch die Kategorie »Zeitschriften« (der kein Artikel zugewiesen ist). Vergleichen Sie dazu ggf. die auf Seite 665 dargestellten Beispieldaten Wenn Sie wollen, dass auch Elemente übernommen werden, die einen Schlüsselwert aufweisen, der nicht in der anderen Sequenz vorkommt, müssen Sie eine linksgerichtete Verknüpfung (Left Outer Join) verwenden. Diese beschreibe ich auf Seite 684.
2
4
5 INFO
6
Wollen Sie zum Verknüpfen zweier Sequenzen einen Abfrageausdruck verwenden, setzen Sie die join-Klausel ein. Das Schema ist das Folgende: join Variable in RechterSequenz on SchlüsselwertDerLinkenSequenz equals SchlüsselWertderRechtenSequenz
7
Über die angegebene Variable greifen Sie später auf das jeweilige Element der rechten Sequenz zu. Das equals-Schlüsselwort ist für den Vergleich das einzig Mögliche. Es führt zu einem Gleichheitsvergleich der Schlüssel. Andere Verknüpfungen, wie Ungleichheits-Verknüpfungen oder Kreuz-Verknüpfungen, müssen Sie anders implementieren (siehe im Abschnitt »Einige Tipps und Tricks« ab Seite 714). Microsoft hat das equals-Schlüsselwort übrigens deswegen an Stelle des Vergleichs-Operators (= =) eingesetzt, um deutlich zu machen, dass neben equals nichts anderes möglich ist.
8
9
Listing 11.40: Join in einem Abfrageausdruck
10
var products2 = from product in products join category in categories on product.CategoryID equals category.ID select new { product.Name, Category = category, product.Price };
11
681
LINQ
Mehrfache Verknüpfungen Wenn Sie mehr als eine Verknüpfung benötigen, rufen Sie Join einfach auf dem Ergebnis der vorhergehenden Join-Operation auf. Mit einem Abfrageausdruck ist das sehr einfach und logisch. So könnte es sein, dass ein Artikel noch einen Lieferanten (Supplier) referenziert: Listing 11.41: Verknüpfung über drei Sequenzen mit einem Abfrageausdruck // Beispiel-Daten einlesen List products = GetProducts(); List categories = GetCategories(); List suppliers = GetSuppliers(); // Die Artikel mit den Kategorien und den Lieferanten verknüpfen var productList1 = from product in products join category in categories on product.CategoryID equals category.ID join supplier in suppliers on product.SupplierID equals supplier.ID select new { product.Name, Category = category.Name, Supplier = supplier.Name }; // Die verknüpften Daten ausgeben foreach (var product in productList1) { Console.WriteLine(product.Name); Console.WriteLine(" Kategorie: {0}", product.Category); Console.WriteLine(" Lieferant: {0}", product.Supplier); Console.WriteLine(); }
Wollen Sie stattdessen die Join-Erweiterungsmethode verwenden, müssen Sie lediglich beachten, dass Sie den Verknüpfungsschlüssel für die äußere Verknüpfung mit in das Ergebnis der inneren Verknüpfung übernehmen: Listing 11.42: Verknüpfung über drei Sequenzen mit der Join-Erweiterungsmethode // Die Artikel mit den Kategorien und den Lieferanten verknüpfen var productList2 = products.Join(categories, product => product.CategoryID, category => category.ID, (product, category) => new { product.Name, product.SupplierID, // Übernahme des Schlüssels Category = category.Name }).Join(suppliers, // Äußere Verknüpfung product => product.SupplierID, supplier => supplier.ID, (product, supplier) => new { product.Name, product.Category, Supplier = supplier.Name });
682
Die LINQ-Erweiterungsmethoden
Verknüpfungen mit mehreren Schlüsselwerten Wenn die in den zu verknüpfenden Sequenzen gespeicherten Objekte mit Schlüsseln arbeiten, die aus mehreren Feldern oder Eigenschaften bestehen, müssen Sie die Schlüssel zu einem anonymen Typen zusammenfassen und diesen für die Verknüpfung verwenden. Am einfachsten ist dies in einem Abfrageausdruck: var Ergebnis = from x in Sequenz1 join y in Sequenz2 on new {JoinFeld1 = x.Feld1, JoinFeld2 = x.Feld2 } equals new {JoinFeld1 = y.Feld1, JoinFeld2 = y.Feld2 } select ...
1
Gruppen-Verknüpfungen
2
Die Erweiterungsmethode GroupJoin verknüpft zwei Sequenzen ähnlich Join über einen Schlüssel. Im Gegensatz zu Join erzeugt GroupJoin aber keine flache Ergebnis-Sequenz, sondern eine hierarchische. Mit GroupJoin können Sie z. B. die Kategorien abfragen und die Artikel, die diesen zugeordnet sind, über eine ProductAuflistung durchgehen, die als Feld zur Verfügung steht.
3
Gruppen-Verknüpfungen werden in Abfrageausdrücken erzeugt, wenn Sie das Ergebnis einer Verknüpfung mit into (siehe ab Seite 703) in eine Variable schreiben und mit dieser eine Projektion ausführen.
4
Listing 11.43: Eine Gruppen-Verknüpfung in einem Abfrageausdruck
5
var categoryList = from category in categories join product in products on category.ID equals product.CategoryID into categoryProducts select new { category.Name, Products = categoryProducts };
6
Das Schlüsselwort into führt dazu, dass der rechte Teil der Verknüpfung (also im Beispiel die Artikel, die einer Kategorie zugeordnet sind) in die Variable geschrieben werden, die rechts von into angegeben ist. Diese Variable kann dann in der (bei der Verwendung von into notwendigen) folgenden Projektion eingesetzt werden. Im Beispiel wird ein anonymer Typ erzeugt, dessen Felder den Kategorie-Namen und die Artikel-Auflistung verwalten.
7
8
Die Abfrage der Daten ist dann relativ einfach: Listing 11.44: Abfrage von mit GroupJoin verknüpften Frequenzen
9
foreach (var category in categoryList) { Console.WriteLine(category.Name);
10
// Die der Kategorie zugeordneten Artikel durchgehen foreach (var product in category.Products) { Console.WriteLine(" " + product.Name); } Console.WriteLine();
11
}
Abbildung 11.9 zeigt das Ergebnis.
683
LINQ
Abbildung 11.9: Das Ergebnis der GruppenVerknüpfung
INFO
Interessant ist hier, dass die Kategorie »Zeitschriften«, der keine Artikel zugeordnet sind, im Ergebnis erscheint. Damit handelt es sich prinzipiell schon um eine linksgerichtete Verknüpfung. Mit einer normalen Gruppen-Verknüpfung funktioniert dies aber nur, wenn die Master-Sequenz (in unserem Beispiel die Kategorien) auf der linken (äußeren) Seite stehen. Im umgekehrten Fall (Detail-Sequenz links bzw. außen und Master-Sequenz rechts bzw. innen) müssen Sie die Gruppen-Verknüpfung anpassen, wie ich es im nächsten Abschnitt zeige. Eine Gruppen-Verknüpfung basiert auf der GroupJoin-Erweiterungsmethode, die prinzipiell folgendermaßen deklariert ist: IEnumerable GroupJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector [, IEqualityComparer comparer])
Ich verzichte an dieser Stelle allerdings auf ein Beispiel, das diese Erweiterungsmethode direkt einsetzt, weil ich denke, dass Gruppen- Verknüpfungen über Abfrageausdrücke wesentlich einfacher anzuwenden sind.
Linksgerichtete Verknüpfungen Eine linksgerichtete Verknüpfung (Left Outer Join) übernimmt alle Elemente der linken (äußeren) Sequenz, unabhängig davon, ob in der rechten (inneren) Sequenz Elemente gefunden werden, deren Verknüpfungsschlüssel gleich ist. Von der rechten Sequenz werden aber nur die Elemente übernommen, deren Verknüpfungsschlüssel auch in der linken Sequenz gefunden werden. Damit können Sie in den Beispieldaten z. B. alle Artikel abfragen, unabhängig davon, ob deren Kategorie auch existiert. Diese Art der Verknüpfung ist besonders wichtig für Datenbankabfragen für den Fall, dass ein Schlüsselfeld in einer Detailtabelle auch Nullwerte zulässt (wie in unserer Product-Klasse). Nur über eine linksgerichtete Verknüpfung können Sie auch solche Objekte abfragen, die im Schlüsselfeld null (oder einen Wert, der in der »referenzierten« anderen Sequenz nicht vorkommt) speichern.
684
Die LINQ-Erweiterungsmethoden
In Datenbanken ist üblicherweise per »Referentieller Integrität« festgelegt, dass ein in einer Detailtabelle angegebener Schlüsselwert in der Mastertabelle vorkommen muss. Ungültige Schlüsselwerte sind deshalb normalerweise nicht möglich. Eine Sonderform der referentiellen Integrität erlaubt allerdings auch Nullwerte in Schlüsselfeldern von Detailtabellen.
INFO
Eine linksgerichtete Verknüpfung erreichen Sie automatisch mit einer Gruppen-Verknüpfung, wenn die Master-Sequenz auf der linken Seite steht. Im vorherigen Abschnitt finden Sie ein Beispiel dazu.
1
Befindet sich die Master-Sequenz aber auf der rechten Seite, müssen Sie die GruppenVerknüpfung anpassen. Dazu geben Sie bei der weiteren Abfrage des Ergebnisses der Verknüpfung in der from-Klausel nicht einfach nur die Verknüpfungsergebnis-Variable an, sondern rufen auf dieser die Methode DefaultIfEmpty auf. Diese Methode gibt das Default-Element zurück, das ihr übergeben wurde, wenn die Variable leer ist. DefaultIfEmpty übergeben Sie in der Regel eine Instanz des Typs, der in der MasterSequenz verwaltet wird, allerdings natürlich mit leeren Werten. Damit können Sie dann in der Projektion auf alle Elemente des Typs zugreifen.
2
3
4
So können Sie z. B. alle Artikel abfragen, auch solche, die keine Kategorie besitzen: Listing 11.45: Linksgerichtete Verknüpfung für den Fall, dass die Master-Sequenz rechts steht
5
// Alle Artikel mit den Kategorien verknüpfen (Left Outer Join) var productList = from product in products join category in categories on product.CategoryID equals category.ID into productCategory from category in productCategory.DefaultIfEmpty( new Category { ID = 0, Name = "Keine" }) select new { product.Name, Category = category.Name };
6
7
// Das Ergebnis ausgeben foreach (var product in productList) { Console.WriteLine(product.Name + ", Kategorie: " + product.Category); }
8
Abbildung 11.10 zeigt das Ergebnis dieses Programms. Der Artikel »Programmieren lernen (E-Book)«, dem keine Kategorie zugeordnet ist, wird wegen der linksgerichteten Verknüpfung auch im Ergebnis ausgegeben.
9
Abbildung 11.10: Das Ergebnis der linksgerichteten Verknüpfung
10
11
685
LINQ
INFO
LINQ to SQL unterstützt DefaultIfEmpty nur in der Überladung ohne Argument. Der Grund dafür ist, dass LINQ to SQL SQL-Code erzeugt und deshalb mit einem übergebenen Default-Objekt nichts anfangen kann. Das Beispiel würde mit DefaultIfEmpty ohne Argumente in LINQ to SQL genauso funktionieren. LINQ to SQL würde im SQLCode einen LEFT OUTER JOIN unterbringen.
Spezifische Verknüpfungen Wenn Sie mit einer normalen oder einer Gruppen-Verknüpfung nicht auskommen, haben Sie noch weitere Möglichkeiten, die größtenteils mit SelectMany zusammenhängen. Diese Methode ist u. a. in der Lage, ein Kreuzprodukt zweier Sequenzen zu erzeugen. Bei einem solchen werden alle Elemente der einen mit allen Elementen der anderen Sequenz kombiniert. Solche spezifischen Verknüpfungen werden ab Seite 703 behandelt.
11.2.7
Methoden zur Ermittlung einzelner Elemente
Nach den stürmischen Verknüpfungen wird es nun wird ein wenig ruhiger (Gott sei Dank). LINQ bietet mit den in Tabelle 11.6 beschriebenen Methoden einige, die einzelne Elemente aus einer Sequenz herausfiltern. Diese Methoden besitzen kein Äquivalent in der Abfrageausdruck-Syntax. Tabelle 11.6: Die LINQ-Methoden zur Ermittlung einzelner Objekte
Methode
Beschreibung
First, FirstOrDefault
liefert das erste Element der Sequenz. First und FirstOrDefault werden auch von LINQ to SQL unterstützt, FirstOrDefault allerdings nur in der Überladung ohne Prädikat-Argument.
Last, LastOrDefault
Diese Methoden arbeiten ähnlich First und FirstOrDefault, liefern aber das letzte Element der Sequenz. Diese Methoden werden von LINQ to SQL nicht unterstützt.
Single, SingleOrDefault
liefert in der jeweils ersten Überladung (ohne Prädikat) ein Element, wenn die Sequenz genau ein Element beinhaltet. In der zweiten Überladung, der Sie ein Prädikat übergeben können, liefern diese Methoden das Element, das zum Prädikat passt.
ElementAt, ElementAtOrDefault
liefert das Element, das an einem übergebenen Index verwaltet wird. Diese Methoden werden in LINQ to SQL nicht unterstützt.
Die Varianten der Methoden zur Ermittlung einzelner Objekte, deren Name mit »OrDefault« endet, erzeugen keine Ausnahme, wenn die Abfrage kein Element ergibt. Diese Methoden geben stattdessen den Defaultwert des in der Sequenz verwalteten Typs zurück. Bei Referenztypen handelt es sich dabei um den Wert null, bei Werttypen um ein »leeres« Objekt. Die Methoden ohne »OrDefault« im Namen erzeugen in dem Fall, dass kein Element ermittelt werden kann, eine InvalidOperationException mit der Meldung »Die Sequenz enthält kein übereinstimmendes Element«.
686
Die LINQ-Erweiterungsmethoden
LINQ to SQL unterstützt die »OrDefault«-Methoden nicht. Beim Aufruf erhalten Sie eine NotSupportedException. INFO
Wenn Sie für eine Sequenz, die Referenztypen verwaltet, ermitteln wollen, ob ein Element gefunden wurde, ist die »OrDefault«-Variante besser geeignet. Über einen Vergleich auf null können Sie sehr einfach überprüfen, ob ein Element gefunden wurde. Verwaltet die Sequenz allerdings Werttypen, ist die Variante besser geeignet, die kein Default-Objekt zurückgibt. Würden Sie auf einer Werttyp-Sequenz die »OrDefault«Variante verwenden, könnten Sie nicht unterscheiden, ob ein »leeres« Objekt zurückgegeben wurde, das in der Sequenz verwaltet wird; oder ob dieses von der Methode erzeugt wurde.
TIPP
1
2
Das erste bzw. letzte Element einer Sequenz ermitteln Über die Methoden First, FirstOrDefault, Last und LastOrDefault können Sie das erste bzw. letzte Element einer Sequenz ermitteln. Die Syntax dieser Methoden sieht prinzipiell folgendermaßen aus: Quelltyp-Element Methodenname([predicate])
Über First, FirstOrDefault, Last und LastOrDefault ermitteln Sie das erste bzw. letzte Element
Die erste Überladung der jeweiligen Methode besitzt neben dem this-Argument kein weiteres und wird auf die gesamte Sequenz angewendet. Der zweiten Überladung können Sie am Argument predicate ein Prädikat übergeben, das die Auswahl der Elemente vornimmt, die berücksichtigt werden.
3
4
5
Sinn macht eine Anwendung dieser Methoden häufig nur, wenn die Sequenz vorher sortiert wurde. So können Sie in der Artikelliste z. B. nach dem Artikel mit dem kleinsten Preis suchen:
6
Listing 11.46: Suchen nach dem Artikel mit dem kleinsten Preis
7
// Beispiel-Artikel einlesen List products = GetProducts(); // Den Artikel mit dem kleinsten Preis ermitteln try { var cheapestProduct = products .OrderBy(product => product.Price).First();
8
9
Console.WriteLine("Der preiswerteste Artikel ist '{0}' mit {1} Euro", cheapestProduct.Name, cheapestProduct.Price); Console.WriteLine(); } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); }
Dieses Beispiel ist nicht ganz in Ordnung, da es auch vorkommen kann, dass mehrere Artikel denselben kleinsten Preis aufweisen. Dieses Problem können Sie über eine Unterabfrage lösen (Seite 712).
10
11 INFO
687
LINQ
TIPP
Um den try-Block zu vermeiden, könnten Sie vor der Abfrage über Count ermitteln, ob überhaupt Elemente in der Sequenz enthalten sind. Wenn die Sequenz Count nicht als Instanzeigenschaft zur Verfügung stellt, würde das aber bedeuten, dass zwei Abfragen auf der Sequenz ausgeführt werden. In diesem Fall wäre es ggf. sinnvoll, die Sequenz vor den Abfragen mit ToArray in ein Array oder mit ToList in eine ListAuflistung zu konvertieren. In performance-kritischen Anwendungen sollten Sie überprüfen, welche Variante das bessere Zeitverhalten bietet. -Auflistung zu konvertieren
Wenn Sie ein Prädikat übergeben, können Sie die Sequenz einschränken. So können Sie z. B. den letzten Artikel der Kategorie 1 ermitteln: Listing 11.47: Ermitteln des letzten Artikels der Kategorie 1 var lastProductOfCategory1 = products.LastOrDefault( product => product.CategoryID == 1); // Ermitteln, ob ein Artikel zurückgegeben wurde if (lastProductOfCategory1 != null) { Console.WriteLine("Der letzte Artikel der Kategorie 1 ist '{0}'", lastProductOfCategory1.Name); } else { Console.WriteLine("Die Kategorie 1 enthält keine Artikel"); }
In diesem Beispiel habe ich LastOrDefault verwendet, weil die Wahrscheinlichkeit, dass kein Artikel gefunden wird, relativ groß ist. INFO
Einzelne Objekte über Single und SingleOrDefault abfragen Single und SingleOrDefault erlauben das Ermitteln einzelner Elemente
Über Single und SingleOrDefault können Sie gezielt einzelne Elemente aus einer Sequenz abfragen. Diese Methoden sind prinzipiell folgendermaßen deklariert: Quelltyp-Element Single([predicate]) Quelltyp-Element SingleOrDefault([predicate])
Wie schon bei den anderen Methoden gibt die »OrDefault«-Variante ein DefaultObjekt zurück, wenn in der Liste kein Objekt gefunden wurde, das den Kriterien entspricht. Die andere Variante generiert in diesem Fall eine InvalidOperationException. Die Varianten ohne Prädikat geben nur dann ein Ergebnis zurück, wenn die Liste genau ein Element beinhaltet. In meinen Augen machen diese Varianten nicht besonders viel Sinn. Die Varianten mit Prädikat geben das Element zurück, das dem Prädikat entspricht.
HALT
688
Falls Single bzw. SingleOrDefault mehrere Objekte ermitteln, wird eine InvalidOperationException mit der Meldung »Die Sequenz enthält mehr als ein Element« oder »Die Sequenz enthält mehrere übereinstimmende Elemente« generiert. Diese sollten Sie also auf jeden Fall abfangen. Dumm ist nur, dass Sie beim Abfangen nicht ermitteln können, was die Ausnahme verursacht hat. Sie könnten zwar theoretisch die Nachricht der Ausnahme vergleichen, um zu ermitteln, ob die Abfrage mehr als ein Objekt ergab. Dies ist jedoch zum einen deswegen unsicher, da der Text dieser
Die LINQ-Erweiterungsmethoden
Meldung in zukünftigen Versionen des .NET Framework geändert werden kann. Außerdem würde diese Lösung nicht funktionieren, wenn die Anwendung unter einer anderen Kultur ausgeführt wird. Listing 11.48 fragt einen Artikel ab, dessen Name »Fool on the Hill« ist: Listing 11.48: Abfrage eines einzelnen Objekts
1
// Beispiel-Artikel einlesen List products = GetProducts(); // Den Artikel abfragen, dessen Name »Fool on the Hill« ist string productName = "Fool on the Hill"; try { var product = products.SingleOrDefault( p => p.Name.StartsWith(productName)); if (product != null) { // Gefunden Console.WriteLine(product.Name + ": " + product.Price); } else { // Nicht gefunden Console.WriteLine("Es wurde kein Artikel mit dem Namen '{0}' " + "gefunden", productName); } } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); }
2
3
4
5
6
Eine bessere Lösung des in der Praxis häufig auftretenden Problems, dass mehrere Objekte gefunden werden, die der definierten Bedingung entsprechen, ist, dass Sie vorher mit Count (siehe Seite 690) ermitteln, wie viele Objekte der Bedingung entsprechen:
7
Listing 11.49: Abfrage eines einzelnen Objekts mit vorheriger Ermittlung, wie viele Objekte der definierten Bedingung entsprechen
8
string productNamePrefix = "Per Anhalter durch die Galaxis"; // Bedingung vorformulieren Func predicate = p => p.Name.StartsWith(productNamePrefix); try { // Die Anzahl der Objekte ermitteln, die dem Prädikat entsprechen int count = products.Count(predicate); switch (count) { case 0: // Es existiert kein Objekt, das der Bedingung entspricht Console.WriteLine("Es wurde kein Artikel gefunden, dessen " + "Name mit '{0}' beginnt", productNamePrefix); break;
9
10
11
case 1: // Das Objekt abfragen var product = products.SingleOrDefault(predicate);
689
LINQ
// Zur Sicherheit noch einmal abfragen, // ob ein Objekt gefunden wurde if (product != null) { // Gefunden Console.WriteLine(product.Name + ": " + product.Price); } else { // Nicht gefunden Console.WriteLine("Es wurde kein Artikel gefunden, dessen " + "Name mit '{0}' beginnt", productNamePrefix); } break; default: // Zu viele Objekte entsprechen der Bedingung Console.WriteLine("Es wurden mehrere Artikel gefunden, " + "deren Name mit '{0}' beginnt", productNamePrefix); break; } } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); }
INFO
Die Abfrage auf product != null nach dem Einlesen des Objekts ist eigentlich unnötig. Ich vermeide damit aber eine NullReferenceException für den Fall, dass SingleOrDefault aus irgendwelchen Gründen dennoch null zurückgibt. NullReferenceException-Ausnahmen sind in einem laufenden Programm immer sehr unschön, weil diese nur sehr schlecht nachvollzogen werden können. Die zusätzliche Abfrage stellt sicher, dass das Programm unter allen Umständen funktioniert.
11.2.8
Aggregat-Methoden
LINQ enthält einige Methoden, die über eine Sequenz Berechnungen ausführen. Tabelle 11.7 fasst diese zunächst zusammen. Tabelle 11.7: Die LINQ-AggregatMethoden
690
Methode
Beschreibung
Average(…)
berechnet für Sequenzen, die numerische Werte verwalten, den Durchschnitt der Werte.
Min(…)
berechnet für Sequenzen, die numerische Werte verwalten oder deren Elemente IComparable() implementieren, den kleinsten Wert.
Max(…)
berechnet für Sequenzen, die numerische Werte verwalten oder deren Elemente IComparable() implementieren, den größten Wert.
Sum()
berechnet für Sequenzen, die numerische Werte verwalten, die Summe der gespeicherten Werte. Bei einer leeren Sequenz wird normalerweise der Wert 0 zurückgegeben. In LINQ to SQL resultiert allerdings in diesem Fall null. Außerdem werden in LINQ to SQL integerSummierungen nicht als long-Ergebnis zurückgegeben und ergeben folglich ggf. einen Überlauf.
Count
ermittelt die Anzahl der Elemente, optional die Anzahl der Elemente, die einem Prädikat entsprechen. Die Rückgabe ist ein int-Wert. Count ist also auf Sequenzen mit 2.147.483.647 Elementen beschränkt. Für größere Sequenzen verwenden Sie LongCount.
Die LINQ-Erweiterungsmethoden
Methode
Beschreibung
LongCount
ermittelt die Anzahl der Elemente (optional mit einem Prädikat) einer Sequenz als longWert.
Aggregate
ermöglicht ein spezielles Aggregieren einer Sequenz.
Tabelle 11.7: Die LINQ-AggregatMethoden
1
Den Mittelwert, den Minimalwert, den Maximalwert und die Summe berechnen Die Methoden zur Berechnung des Mittel-, des Minimal-, des Maximalwerts und der Summe, Average, Min, Max und Sum, existieren in verschiedenen Überladungen. Min und Max können flexibel auf allen Sequenzen angewendet werden (deren Elemente IComparable() implementieren). Die entsprechenden Varianten dieser Methoden sind prinzipiell folgendermaßen deklariert:
Average, Min, Max und Sum besitzen mehrere Überladungen
2
3
Quelltyp-Element Min([selector]) Quelltyp-Element Max([selector])
Voraussetzung für diese Varianten ist, dass die in der Sequenz gespeicherten Elemente die IComparable- oder die IComparable-Schnittstelle implementieren.
4
Die jeweils erste hier dargestellte Überladung berechnet den Minimal- bzw. Maximalwert direkt über die in der Sequenz verwalteten Elemente.
5
In der zweiten hier dargestellten Überladung übergeben Sie am Argument selector einen Lambda-Ausdruck oder einen Delegaten, der eine Transformation der Objekte vornimmt. Der Wert, den der Selektor zurückgibt, wird von der Aggregat-Methode verwendet. Die Aggregat-Methode gibt dann einen entsprechenden Typen zurück. Voraussetzung ist, dass der zurückgegebene Typ (TResult) die IComparable- oder die IComparable-Schnittstelle implementiert.
6
Die Methoden Average und Sum existieren nur in Überladungen für Sequenzen, die numerische Werte verwalten, oder deren Transformationsmethode einen numerischen Wert zurückgibt.
7
Die double-Überladungen der Average-Methode sind z. B. prinzipiell folgendermaßen deklariert:
8
double Average([selector])
Die erste Variante kann auf einer Sequenz aufgerufen werden, die den entsprechenden numerischen Typen verwaltet. In der zweiten Variante, die auf beliebigen Sequenzen aufgerufen werden kann, wird ein numerischer Wert von dem am Argument selector übergebenen Lambda-Ausdruck bzw. Delegaten zurückgegeben.
9
10
Min und Max besitzen aus Performancegründen ebenfalls entsprechende Überladungen. Wenn Sie eine Aggregat-Methode also auf einer Sequenz anwenden, die einen numerischen Typen verwaltet, oder wenn Sie der Aggregat-Methode einen Transformations-Lambda-Ausdruck (oder -Delegaten1) übergeben, der einen numerischen Typen zurückgibt, wird implizit die dazu passende optimierte Überladung verwendet.
1
11
OK. ich werde auch langsam müde, immer darauf hinzuweisen, dass Sie neben einem Lambda-Ausdruck auch einen Delegaten übergeben können. Aber das ist nun einmal so.
691
LINQ
Im folgenden Beispiel, das in einer Auflistung von double-Werten den kleinsten Wert, den größten Wert, den Mittelwert und die Summe ermittelt, wird demnach implizit die double-Variante der jeweiligen Erweiterungsmethode verwendet: Listing 11.50: Anwendungen von Aggregat-Methoden ohne Transformation // Liste von double-Werten erzeugen double[] numbers = { 2.5, 1.3, 0.2 }; // Minimal-, Maximal-, Mittelwert und Summe berechnen double minNumber = numbers.Min(); double maxNumber = numbers.Max(); double averageNumber = numbers.Average(); double numberSum = numbers.Sum();
Über das Argument selector können Sie die Objekte transformieren
Über die optionale Transformationsmethode können Sie die in der Sequenz verwalteten Objekte transformieren. Wenn Sie hier einen Lambda-Ausdruck übergeben, ist der linke Operand der Typ, der in der Sequenz verwaltet wird. Der rechte Operand ist ein Ausdruck, der bei Average und Sum einen numerischen Typen zurückgeben muss. Bei Min und Max können Sie zusätzlich dazu einen Typ zurückgeben, der IComparable oder IComparable implementiert. So können Sie z. B. in der Liste der Produkte den kleinsten, den größten und den mittleren Preis ermitteln: Listing 11.51: Anwendungen von Aggregat-Methoden mit Transformation // Beispiel-Artikel einlesen List products = GetProducts(); // Den kleinsten, größten und den mittleren Preis ermitteln decimal lowestPrice = products.Min(product => product.Price); decimal highestPrice = products.Max(product => product.Price); decimal averagePrice = products.Average( product => product.Price);
Über Count die Anzahl von Objekten ermitteln Über Count können Sie die Anzahl der Objekte ermitteln, die einer Bedingung entsprechen. Count existiert in zwei Überladungen: int Count() int Count(predicate)
Die Überladung ohne Prädikat konkurriert übrigens mit der entsprechenden Eigenschaft einer Auflistung, die ICollection implementiert. Der zweiten Überladung übergeben Sie ein Prädikat. Diese Überladung bezieht nur die Elemente mit ein, die der damit definierten Bedingung entsprechen. Das folgende Beispiel ermittelt die Anzahl der Produkte, die der Kategorie 1 angehören: Listing 11.52: Ermitteln der Anzahl von Objekten, die einer Bedingung entsprechen // Beispiel-Artikel einlesen List products = GetProducts(); // Ermitteln der Anzahl der Produkte der Kategorie 1 int categoryID = 1; int count = products.Count(
692
Die LINQ-Erweiterungsmethoden
product => product.CategoryID == 1); Console.WriteLine("Die Kategorie {0} enthält {1} Produkte", categoryID, count);
Spezielle Aggregationen über Aggregate Über die Aggregate-Methode können Sie eine beliebige Aggregation auf einer Sequenz ausführen. Diese Methode ist wie viele andere mehrfach überladen. Ich verwende hier die Original-Syntax, weil meine reduzierte Variante für Aggregate nicht ausreicht:
Aggregate erlaubt eine beliebige Aggregation
1
TSource Aggregate( this IEnumerable source, Func func)
2
TAccumulate Aggregate( this IEnumerable source, TAccumulate seed, Func func)
3
TResult Aggregate( this IEnumerable source, TAccumulate seed, Func func, Func resultSelector)
4
In der ersten (einfachsten) Überladung übergeben Sie am Argument func einen Lambda-Ausdruck oder Delegaten, der die Aggregation vornimmt. Dieser wird für jedes Element der Sequenz aufgerufen. Am ersten Argument übergibt Aggregate den bisher aggregierten Wert. Am zweiten wird das jeweilige Element der Sequenz übergeben. Der Lambda-Ausdruck oder Delegat sollte dann das um das jeweilige Element erweiterte Aggregat zurückgeben.
5
6
Eine sinnvolle Anwendung für diese Methode zu finden ist schwer, da die anderen Aggregat-Methoden, besonders mit einer Transformation, und einige der weiteren LINQ-Methoden bereits die normalen Anwendungsfälle abdecken. Weil mir kein sinnvolles Beispiel einfällt (und ich auch keines im Internet gefunden habe), das Aggregate unbedingt erfordert, aggregiert Listing 11.53 die in einem double-Array verwalteten Zahlen so, dass deren Wurzel miteinander multipliziert wird:
7
Listing 11.53: Benutzerdefinierte Aggregation
8
double[] numbers = { 2.5, 1.3, 0.2 }; double squareRootProduct = numbers.Aggregate( (currentResult, currentNumber) => currentResult *= Math.Sqrt(currentNumber));
9
Das Ergebnis dieses Beispiels ist übrigens 1,2747548783981963. In der zweiten Überladung können Sie am Argument seed einen Startwert übergeben, der in der ersten Iteration als Aggregat voreingestellt wird. Die dritte Überladung erlaubt zusätzlich am Argument resultSelector die Übergabe eines Lambda-Ausdrucks oder Delegaten, der den nach allen Iterationen berechneten Aggregat-Wert noch einmal transformiert. Auf diese Varianten gehe ich hier allerdings nicht ein.
10
11
693
LINQ
11.2.9
Methoden zur Ermittlung, ob Objekte in einer Sequenz enthalten sind
Die Methoden Contains, Any und All ermitteln, ob Objekte in der Sequenz enthalten sind. Tabelle 11.8: LINQ-Methoden zur Ermittlung, ob Objekte in einer Sequenz enthalten sind
Methode
Beschreibung
Contains
ermittelt, ob ein bestimmtes Objekt in der Sequenz verwaltet wird.
Any
ermittelt, ob mindestens eines der in der Sequenz verwalteten Objekte dem übergebenen Prädikat entspricht oder (in der Überladung ohne Argument) ob die Sequenz überhaupt Elemente enthält.
All
ermittelt, ob alle in der Sequenz verwalteten Objekte dem übergebenen Prädikat entsprechen.
Alle diese Methoden besitzen kein Äquivalent in C#-Abfrageausdrücken.
Über Contains ermitteln, ob ein Objekt in der Sequenz vorhanden ist Contains prüft, ob ein bestimmtes Element vorhanden ist
Die Methode Contains ermittelt, ob zumindest ein Objekt vorhanden ist, das dem übergebenen entspricht. Die Deklaration ist prinzipiell die folgende: bool Contains(value [, comparer])
value ist hier vom Typ TSource. Die Variante ohne comparer-Argument konkurriert mit der gleichnamigen Methode der ICollection-Schnittstelle. Abhängig von den übergebenen Argumenten ruft der Compiler vorwiegend die ICollection-Variante auf (sofern das Objekt, auf dem Sie Contains aufrufen, diese Schnittstelle implementiert). value ist eine Referenz auf das Objekt, mit dem Sie vergleichen wollen. Am Argument comparer können Sie ein Objekt übergeben, das den Vergleich übernimmt. Die Art des Vergleichs hängt von mehreren Faktoren ab: ■
Wenn comparer nicht übergeben wird und das übergebene Objekt implementiert nicht IEquatable und überschreibt nicht die von Object geerbte EqualsMethode, wird wie üblich bei Referenztypen die Referenz und bei Werttypen der Wert verglichen.
■
Wenn comparer nicht übergeben wird und das übergebene Objekt implementiert IEquatable und/oder überschreibt die von Object geerbte EqualsMethode, wird die Equals-Methode zum Vergleich der Objekte verwendet.
■
Wenn comparer übergeben wird, wird dieses Objekt für den Vergleich verwendet, unabhängig davon, ob das Objekt IEquatable implementiert und/oder Equals überschreibt.
Ich denke, Contains macht nur dann Sinn, wenn Sie ermitteln wollen, ob ein konkretes Objekt in der Liste vorkommt:
694
Die LINQ-Erweiterungsmethoden
Listing 11.54: Ermitteln, ob ein Objekt in einer Sequenz vorkommt // Beispiel-Artikel einlesen List products = GetProducts(); // Zur Demonstration: Einen Beispiel-Artikel abfragen, // der gesucht werden soll Product product = products.Single(p => p.Name == "Longlife Tofu");
1
// Ermitteln, ob ein Objekt in der Liste vorkommt if (products.Contains(product)) { Console.WriteLine("'{0}' existiert in der Liste", product.Name); } else { Console.WriteLine("'{0}' existiert nicht in der Liste", product.Name); }
2
3 Da Sie in der Regel nicht unbedingt wissen, ob der Typ, der in der Sequenz verwaltet wird, IEquatable implementiert oder Equals überschreibt, können Sie nicht mit Sicherheit sagen, ob die Sequenz tatsächlich dasselbe Objekt beinhaltet oder nur eines, das beim Vergleich als gleich erkannt wurde (und da ist er wieder, der Unterschied zwischen »dasselbe« und »das Gleiche« ☺).
HALT
4
Wollen Sie hingegen ermitteln, ob ein Objekt in der Liste vorkommt, das einer Bedingung entspricht, verwenden Sie vielleicht besser Count.
5
Über Any und All ermitteln, ob ein Objekt bzw. alle Objekte einer Sequenz einem Prädikat entsprechen
6
Über die Methode Any können Sie ermitteln, ob mindestens ein Objekt in einer Sequenz einer Bedingung entspricht, die Sie als Prädikat übergeben. Über All ermitteln Sie, ob alle Objekte dem übergebenen Prädikat entsprechen. Die Deklarationen dieser beiden Methoden sind folgende:
7
bool Any([predicate]) bool All(predicate)
8
Die Überladung der Any-Methode, der kein Prädikat übergeben wird, überprüft lediglich, ob die Sequenz überhaupt Elemente enthält. Diese Methode können Sie innerhalb einer Abfrage einsetzen, wenn Sie eine Sequenz zuvor mit Where eingeschränkt haben und überprüfen wollen, ob das Ergebnis nicht leer ist.
9
Der zweiten Any-Überladung und der All-Methode übergeben Sie am Argument predicate ein Prädikat, das die Bedingung definiert. So können Sie z. B. in der Liste der Produkte überprüfen, ob mindestens eines der Produkte zur Kategorie 1 gehört:
10
Listing 11.55: Ermitteln, ob mindestens ein Element einem Prädikat entspricht
11
// Beispiel-Artikel einlesen List products = GetProducts(); if (products.Any(product => product.CategoryID == 1)) { Console.WriteLine("Es existiert mindestens ein Artikel, " + "der der Kategorie 1 angehört"); } else
695
LINQ
{ Console.WriteLine("Es existiert kein Artikel, " + "der der Kategorie 1 angehört"); }
Listing 11.56 zeigt die Anwendung von All. In diesem Beispiel wird überprüft, ob alle Artikel einen Preis größer als 5 Euro haben: Listing 11.56: Ermitteln, ob alle Elemente einem Prädikat entsprechen if (products.All(product => product.Price > 5M)) { Console.WriteLine("Alle Artikel haben einen Preis, " + "der größer ist als 5 Euro"); } else { Console.WriteLine("Nicht alle Artikel haben einen Preis, " + "der größer ist als 5 Euro"); }
11.2.10 Spezielle Filter-Methoden Neben Where bietet LINQ noch einige weitere Methoden, die Sie zum Filtern verwenden können. Tabelle 11.9: Die LINQ-Erweiterungsmethoden zum speziellen Filtern
Methode
Beschreibung
Take
liefert eine Sequenz, die nur die ersten count Elemente enthält. Das Argument count gibt an, wie viele Elemente in das Ergebnis übertragen werden sollen. Bei LINQ to SQL ergibt Take nur dann ein korrektes Ergebnis, wenn diese Methode auf sortierten Sequenzen angewendet wird. Wenn Sie Take und Skip gemeinsam verwenden, muss die Sortierung für beide Sequenzen dieselbe sein. Ansonsten ist das Ergebnis nicht korrekt.
Skip
liefert eine Sequenz, die die letzten Elemente ab der Position count + 1 enthält. Bei LINQ to SQL muss Skip wie Take auf einer sortierten Sequenz angewendet werden.
TakeWhile
TakeWhile geht die Sequenz durch und ruft für jedes Element den Lambda-Ausdruck bzw. Delegaten auf, der am Argument predicate übergeben wird. Ergibt das Prädikat false, werden das Iterieren unterbrochen und die bisher ermittelten Elemente als Sequenz zurückgegeben. TakeWhile wird in LINQ to SQL nicht unterstützt.
SkipWhile
SkipWhile geht die Sequenz ab dem ersten Element durch, für das der am Argument predicate übergebene Lambda-Ausdruck bzw. Delegat true ergibt. SkipWhile wird in LINQ to SQL nicht unterstützt.
Distinct
Take und Skip sind für Anwendungen interessant, die Daten seitenweise anzeigen
696
liefert eine Ergebnis-Sequenz, in der ggf. mehrfach vorkommende Elemente eliminiert wurden.
Take und Skip machen Sinn in Anwendungen, die immer nur einen Ausschnitt einer Sequenz verarbeiten (bzw. darstellen) können. Dies ist z. B. in vielen Webanwendungen der Fall, die auf einer Seite nur einen Ausschnitt einer Datenmenge darstellen. Über Take und Skip können Sie solche Datenausschnitte erzeugen. Sinnvoll ist auf jeden Fall, die zu verarbeitende Sequenz vorher zu sortieren.
Die LINQ-Erweiterungsmethoden
Das folgende Beispiel liest zunächst die ersten drei Artikel der Beispiel-Artikel-Liste ein. Danach werden die nächsten drei eingelesen: Listing 11.57: Seitenweises Auslesen einer Sequenz über Take und Skip // Beispiel-Artikel einlesen List products = GetProducts(); // Die ersten 3 Artikel ermitteln Console.WriteLine("Die ersten drei der sortierten Artikel:"); foreach (var product in products.OrderBy( product => product.Name).Take(3)) { Console.WriteLine(product.Name); } Console.WriteLine();
1
// Die nächsten drei Artikel ermitteln Console.WriteLine("Die folgenden drei Artikel:"); foreach (var product in products.OrderBy( product => product.Name).Skip(3).Take(3)) { Console.WriteLine(product.Name); }
3
TakeWhile ermöglicht das Ermitteln der ersten Elemente einer Sequenz, die dem übergebenen Prädikat entsprechen. Ein Praxisbeispiel dafür zu finden fällt mir jetzt grade (20.3.2008 22:52) nach bereits mehr als 12 Stunden Arbeit am Buch etwas schwer ☺. Deshalb folgt wie auch für SkipWhile ein abstraktes Beispiel: Listing 11.58: Ein abstraktes Beispiel für TakeWhile und SkipWhile
2
4 TakeWhile und SkipWhile ermöglichen ein spezifisches »seitenweises« Auswerten
6
// Die ersten Zahlen ermitteln, die kleiner sind als 10 var firstNumbersBelow10 = numbers.TakeWhile(number => number < 10); Console.WriteLine("Die ersten Zahlen, die kleiner sind als 10:"); foreach (var number in firstNumbersBelow10) { Console.WriteLine(number); // 1, 5 } Console.WriteLine();
7
8
// Die letzten Zahlen ab der ersten Zahl ermitteln, // die nicht mehr kleiner ist als 10 var lastNumbersAbove9 = numbers.SkipWhile(number => number < 10); Console.WriteLine("Die letzten Zahlen ab der ersten Zahl, die nicht mehr kleiner ist als 10:"); foreach (var number in lastNumbersAbove9) { Console.WriteLine(number); // 10, 3, 7 } Console.WriteLine();
Über Distinct können Sie alle mehrfach vorkommenden Elemente einer Sequenz eliminieren. Am Argument comparer können Sie, wie bei Methoden üblich, die einen Vergleich von Objekten vornehmen, ein Objekt übergeben, das die IEqualityComparer-Schnittstelle implementiert. Wird dieses nicht übergeben, erfolgt der Vergleich der Elemente nach dem .NET-Standard (siehe bei Contains, Seite 694).
5
9
10 Distinct löscht alle Duplikate
11
697
LINQ
Listing 11.59: Herausfiltern von Duplikaten über Distinct string[] names = { "Zaphod", "Arthur", "Zaphod", "Ford" }; var distinctNames = names.Distinct(); foreach (var name in distinctNames) { Console.WriteLine(name); // Zaphod, Arthur, Ford }
11.2.11 Konvertierungsmethoden Die in Tabelle 11.10 angegebenen Methoden, die kein Äquivalent in der Abfrageausdruck-Syntax besitzen, erlauben ein Konvertieren einer Sequenz. Ich beschreibe diese Methoden nur kurz und verzichte auf Beispiele im Buch. Auf der Buch-DVD finden Sie aber ein Beispielprojekt. Tabelle 11.10: Die LINQ-Konvertierungsmethoden
Methode
Beschreibung
DefaultIfEmpty
Diese Methode gibt für den Fall, dass die Sequenz auf der sie angewendet wird, leer ist, eine Sequenz zurück, die genau ein Element beinhaltet. Das enthaltene Element entspricht entweder dem Defaultwert des in der Sequenz verwalteten Typs oder dem Objekt, das am optionalen Argument default übergeben wird. Ist die Basis-Sequenz nicht leer, wird lediglich diese zurückgegeben. DefaultIfEmpty können Sie für einige Tricks verwenden, z. B. für eine linksgerichtete Verknüpfung (Seite 684). LINQ to SQL unterstützt DefaultIfEmpty nur in der Überladung ohne Argument.
OfType Mit dieser generischen Methode können Sie die Elemente einer Sequenz so gefiltert zurückgeben, dass die Ergebnis-Sequenz nur noch Objekte beinhaltet, die dem am Typparameter TResult spezifizierten Typ entsprechen. TResult kann dabei auch eine Basisklasse oder eine Schnittstelle sein.
698
Cast
Diese ebenfalls generische Methode wandelt alle Elemente der Sequenz, auf der sie angewendet wird, in den am Typparameter TResult angegebenen Typ um. Dabei wird eine normale Typumwandlung (Typecast) angewendet. Die Elemente der Sequenz müssen also in den angegebenen Typ unwandelbar sein. Kann zumindest ein Element nicht umgewandelt werden, resultiert eine InvalidCastException.
ToArray
erzeugt aus den Elementen der Sequenz, auf der diese Methode angewendet wird, ein Array. ToArray wird häufig verwendet, um eine Sequenz einmal abzufragen und danach beliebig auszuwerten (ohne dass diese wegen der aufgeschobenen Ausführung ggf. wiederholt abgefragt wird).
ToList
erzeugt aus einer Sequenz eine Auflistung vom Typ List.
ToDictionary
erzeugt aus einer Sequenz eine Auflistung vom Typ Dictionary. Der Schlüssel (TKey) wird von einer Methode zur Verfügung gestellt, die Sie am Argument keySelector übergeben.
ToLookup
erzeugt ähnlich ToDictionary aus den Elementen einer Sequenz eine Auflistung vom Typ Lookup.
Die LINQ-Erweiterungsmethoden
Methode
Beschreibung
AsEnumerable
gibt die Elemente der Sequenz als ein Objekt zurück, das die IEnumerable-Schnittstelle implementiert.
AsQueryable
gibt die Elemente der Sequenz als ein Objekt zurück, das die IQueryable -Schnittstelle implementiert. Diese Methode setze ich im Abschnitt »Progressive und dynamische Abfragen« (auf Seite 714) als kleinen Trick ein, um auch normale Auflistungen dynamisch abfragen zu können.
Tabelle 11.10: Die LINQ-Konvertierungsmethoden (Forts.)
1
11.2.12 Erzeugungsmethoden
2
Über die normal-statischen (Nicht-Erweiterungs-)Methoden Empty, Repeat und Range können Sie Sequenzen erzeugen. Methode
Beschreibung
Empty
erzeugt eine leere Sequenz zur Verwaltung von Elementen des Typs TResult.
Range
erzeugt eine Sequenz von int-Werten mit aufsteigendem Wert. Das Argument start gibt den Startwert an, count die Anzahl der Elemente.
Tabelle 11.11: Die LINQ-Erzeugungsmethoden
4
Repeat erzeugt eine Sequenz von wiederholten Elementen vom Typ TResult. Am Argument element geben Sie das Element an, das wiederholt werden soll, am Argument count die Anzahl der Elemente.
5
6
Empty ist hilfreich, wenn Sie in einer Abfrage auf eine innere Variable zugreifen, die eine Sequenz verwalten sollte, aber auch null sein kann. Über den Null-Auswertungsoperator ?? können Sie einen Ausdruck so definieren, dass bei null eine leere Sequenz verwendet und folglich keine NullReferenceException generiert wird. Ein Beispiel dafür finden Sie im Abschnitt »Abfragen mit mehreren from-Klauseln: Kreuzprodukt-Abfragen und spezielle Abfragen wie Ungleichheits-Verknüpfungen« (Seite 707) und im Beispielprojekt zu diesem Abschnitt.
3
7
INFO
8
11.2.13 Mengen-Methoden Die in Tabelle 11.12 beschriebenen Methoden erlauben Operationen mit Mengen. Methode
Beschreibung
Concat
verkettet eine Sequenz mit einer anderen, indem die zweite Sequenz an die erste gehängt wird. Bei LINQ to SQL gilt die Einschränkung, dass Concat die Sortierung der Sequenzen nicht beibehält. Weiteres zu den Einschränkungen finden Sie in Kapitel 19.
Union
verkettet ähnlich Concat zwei Sequenzen. Im Unterschied zu Concat werden mehrfache Elemente dabei eliminiert. Union ist unter LINQ to SQL ähnlich Concat eingeschränkt.
Intersect
ermittelt die Schnittmenge zweier Sequenzen. In der Ergebnis-Sequenz sind nur Elemente enthalten, die in beiden Sequenzen vorkommen. Diese Methode beschreibe ich nicht weiter.
9 Tabelle 11.12: Die LINQ-MengenMethoden
10
11
699
LINQ
Tabelle 11.12: Die LINQ-MengenMethoden (Forts.)
Methode
Beschreibung
Except
ermittelt die Differenzmenge zweier Sequenzen. In der Ergebnis-Sequenz sind nur Elemente der Basis-Sequenz enthalten, die nicht in der übergebenen Sequenz vorkommen. Dies entspricht der Subtraktion einer Menge von einer anderen. Auch diese Methode beschreibe ich nicht weiter.
SequenceEqual
ermittelt, ob die übergebene Sequenz genau der Sequenz entspricht, auf der diese Methode aufgerufen wird.
Reverse
kehrt eine Sequenz um. Diese Methode beschreibe ich nicht weiter.
Sequenzen über Concat und Union verketten Concat und Union hängen eine Sequenz an eine andere an
Die Methoden Concat und Union erlauben ein Verketten von Sequenzen. Sie hängen dazu eine Sequenz an eine andere an. Der Unterschied zwischen beiden Methoden ist, dass Concat alle Elemente beider Sequenzen zu einer neuen kombiniert und Union mehrfach vorkommende Elemente ignoriert. Die Deklaration dieser Methoden ist (erfreulich) einfach: Quelltyp-Sequenz Concat(second) Quelltyp-Sequenz Union(second [, comparer])
Am Argument second übergeben Sie die Sequenz, mit der verkettet werden soll. Da Union einen Vergleich der Elemente vornimmt, können Sie optional am Argument comparer ein Objekt übergeben, das den Vergleich übernimmt. Ohne dieses Argument wird nach dem .NET-Standard verglichen (siehe bei Contains, Seite 694). So können Sie z. B. zwei String-Arrays verketten: Listing 11.60: Verketten mit Concat und Union // Beispiel-Arrays erzeugen string[] names1 = { "Zaphod", "Arthur", "Ford" }; string[] names2 = { "Arthur", "Trillian" }; // Die Arrays mit Concat verketten var concatenatedNames = names1.Concat(names2); // Das Ergebnis ausgeben Console.WriteLine("Mit Concat verkettete Namen:"); foreach (var name in concatenatedNames) { Console.WriteLine(name); } Console.WriteLine(); // Die Arrays mit Union verketten var unionedNames = names1.Union(names2); // Das Ergebnis ausgeben Console.WriteLine("Mit Union verkettete Namen:"); foreach (var name in unionedNames) { Console.WriteLine(name); } Console.WriteLine();
700
Die LINQ-Erweiterungsmethoden
Das Ergebnis dieses Programms ist: Mit Concat verkettete Namen: Zaphod Arthur Ford Arthur Trillian
1
Mit Union verkettete Namen: Zaphod Arthur Ford Trillian
Verketten können Sie nur Sequenzen mit gleichartigen Elementen. Das bedeutet, dass die Elemente der Sequenzen denselben Typ aufweisen, derselben Basisklasse angehören oder eine gemeinsame Schnittstelle implementieren müssen. Die Sequenz, die Concat und Union am Argument second übergeben wird, ist vom Typ TSource, also vom Typ der Elemente der Sequenz, auf der diese Methoden aufgerufen werden. Ist diese Sequenz vom Typ einer Basisklasse, können auch Sequenzen mit Elementen abgeleiteter Klassen übergeben werden. Ist die Basis-Sequenz vom Typ einer Schnittstelle, können auch Sequenzen übergeben werden, deren Elemente diese Schnittstelle implementieren.
Die zu verkettenden Sequenzen müssen gleichartig sein
2
3
4
Das folgende Beispiel demonstriert dies an Hand der Klassen Person und Employee. Employee ist von Person abgeleitet:
5
Listing 11.61: Klassen zur Demonstration von Concat und Union class Person { public string FirstName; public string LastName; public override string ToString() { return this.FirstName + " " + this.LastName; } }
6
class Employee : Person { public string Department; public override string ToString() { return base.ToString() + " - " + this.Department; } }
8
7
9
Das Programm erzeugt zwei Arrays, jeweils vom Typ Person und Employee:
10
Listing 11.62: Erzeugen der Beispiel-Arrays Person[] persons = new Person[] { new Person {FirstName = "Arthur", LastName = "Dent"}, new Person{FirstName = "Tricia", LastName = "McMillan"} };
11
Employee[] employees = new Employee[] { new Employee{FirstName = "Ford", LastName = "Prefect", Department = "Recherche"},
701
LINQ
new Employee{FirstName = "Zaphod", LastName = "Beeblebrox", Department = "Management"}, };
Diese beiden Sequenzen können nun über das Person-Array verkettet werden: var concatenatedPersons = persons.Concat(employees);
Ein Verketten über das Employee-Array ist ebenfalls möglich: var concatenatedPersons = employees.Concat(persons);
In beiden Fällen resultiert eine Sequenz von Person-Referenzen, weil Person der kleinste gemeinsame Nenner ist. Für den Fall, dass die Sequenzen keine Typen verwalten, die Vererbung einsetzen, sondern Typen, die eine gemeinsame Schnittstelle implementieren, reicht es aus, dass eine der Sequenzen vom Typ dieser Schnittstelle ist. Die Ergebnis-Sequenz verwaltet dann auch Objekte vom Typ dieser Schnittstelle.
DISC
Das Beispielprojekt zu diesem Abschnitt demonstriert das Verketten einmal für Vererbung und einmal für eine Schnittstelle über eine Compiler-Konstante, die Sie ein- oder auskommentieren können, um das jeweilige Programm zu kompilieren und auszuprobieren.
Über SequenceEqual ermitteln, ob zwei Sequenzen identisch sind SequenceEqual überprüft auf identische Sequenzen
Die Methode SequenceEqual ermittelt, ob die übergebene Sequenz genau der Sequenz entspricht, auf der sie aufgerufen wird. SequenceEqual ist folgendermaßen deklariert: bool SequenceEqual( this IEnumerable first, IEnumerable second [, IEqualityComparer comparer])
Am Argument second übergeben Sie die Sequenz, mit deren Objekten verglichen werden soll. Die beiden Sequenzen müssen absolut identisch sein, das heißt dieselben Elemente in derselben Reihenfolge beinhalten. Der zweiten Variante können Sie am Argument comparer (wie bei Methoden üblich, die Vergleiche vornehmen) ein Objekt übergeben, das die IEqualityComparerSchnittstelle implementiert und den Vergleich übernimmt. Wird dieses Argument nicht übergeben, erfolgt der Vergleich wie unter .NET üblich (siehe bei Contains, Seite 694). Wenn Sie zwei unsortierte Sequenzen miteinander vergleichen, sollten Sie vorher sortieren, wenn Sie lediglich ermitteln wollen, ob beide dieselben Objekte beinhalten (unabhängig von der Sortierung). Das folgende Beispiel vergleicht auf diese Weise zwei Zahlen-Arrays: Listing 11.63: Vergleichen zweier Sequenzen ohne Berücksichtigung der Sortierung int[] numbers1 = {1, 2, 3}; int[] numbers2 = { 3, 2, 1 }; if (numbers1.OrderBy(number => number) .SequenceEqual(numbers2.OrderBy(number => number))) { Console.WriteLine("Das Zahl-Array 1 entspricht dem Zahl-Array 2, " + "wenn die Sortierung nicht berücksichtigt wird");
702
Komplexe Abfragen mit into, let und mehreren from-Klauseln
} else { Console.WriteLine("Das Zahl-Array 1 entspricht dem Zahl-Array 2 " + "nicht"); }
Beachten Sie, dass Sie Sequenzen (bzw. Objektmengen) auch über die HashSetKlasse (Kapitel 7) miteinander vergleichen können.
1 INFO
11.3
Komplexe Abfragen mit into, let und mehreren from-Klauseln
2
LINQ ist mit den bisher beschriebenen Erweiterungsmethoden noch nicht am Ende. Aber keine Angst: An Methoden fehlt nur noch SelectMany. Puh …
3
LINQ bietet aber noch die Möglichkeit, Abfragen komplexer zu gestalten, als in diesem Kapitel bisher dargestellt. Dabei kommen die Abfrageausdruck-Schlüsselwörter into und let und »Abfragen auf Abfragen« mit mehreren from-Klauseln (was der Anwendung der SelectMany-Methode entspricht) ins Spiel.
11.3.1
4
Das into-Schlüsselwort 5
Über das into-Schlüsselwort können Sie in einem Abfrageausdruck das Ergebnis einer select-, group- oder join-Klausel in eine Variable schreiben. Dazu geben Sie into am Ende der jeweiligen Klausel an und rechts davon einen Bezeichner für die automatisch erzeugte Variable. LINQ füllt die Variable für jedes Element, das einer eventuellen Einschränkung, Gruppierung und/oder Verknüpfung entspricht, mit dem jeweiligen Teilergebnis. Mit diesem Teilergebnis können (bzw. müssen) Sie dann über die Variable im weiteren Verlauf des Abfrageausdrucks weiterarbeiten. Damit können Sie komplexe Abfragen progressiv aufbauen. into ist u. a. hilfreich, wenn Sie eine Projektion verwenden, die eine komplexe Berechnung vornimmt, und die Ergebnis-Sequenz nach dem Ergebnis der Projektion einschränken wollen. Als einfaches Beispiel dient unsere Artikel-Liste, deren Product-Objekte so projiziert werden, dass der Preis in seinen Brutto-Wert transformiert wird. Die Ergebnis-Sequenz soll nur Artikel mit einem Brutto-Preis größer 10 Euro beinhalten. Ohne into wäre die Abfrage folgendermaßen möglich:
6
7 into hilft bei Abfragen mit Projektionen und Berechnungen
8
9
Listing 11.64: Eine Abfrage mit einer Berechnung, die an zwei Stellen verwendet wird // Beispiel-Artikel einlesen List products = GetProducts();
10
// Die Artikel so abfragen, dass der Preis in seinen Bruttowert // transformiert wird, und auf Artikel mit einem Brutto-Preis // größer 100 Euro einschränken decimal vat = .19M; var productsWithGrossPrice1 = from product in products where product.Price * (1 + vat) > 100 select new { ProductName = product.Name, NetPrice = product.Price, GrossPrice = product.Price * (1 + vat) };
11
703
LINQ
INFO
Solche Abfragen sind auf zweierlei Weise problematisch: Zum einen wird die Berechnung zweimal ausgeführt. Zum anderen laufen Sie Gefahr, dass Sie vergessen, die Berechnung an der anderen Stelle anzupassen, wenn Sie diese an einer Stelle ändern. Mit into können Sie das Problem elegant lösen: Listing 11.65: Lösen des Mehrfach-Berechnungsproblems mit into var productsWithGrossPrice2 = from product in products select new { ProductName = product.Name, NetPrice = product.Price, GrossPrice = product.Price * (1 + vat) } into projectedProduct where projectedProduct.GrossPrice > 100 select projectedProduct;
Wie Sie in dem Beispiel erkennen, muss hinter into eine komplette weitere Abfrage stehen, die wieder über select oder group abgeschlossen sein muss. Die weiteren Abfrageklauseln wie where und oderby sind natürlich optional. Nur um (mir selbst und Ihnen) das Vorgehen von LINQ in diesem Fall zu erläutern: Bei der Auswertung der Abfrage: ■ ■ ■ ■ ■
geht LINQ die Sequenz durch, erzeugt LINQ für jedes Element eine Instanz des anonymen Typs, referenziert LINQ diese in der Variablen projectedProduct, überprüft LINQ dann die Bedingung der Einschränkung und schreibt LINQ das Objekt schließlich in die Ergebnis-Sequenz, wenn es der Bedingung entspricht.
Die von C# erzeugten Aufrufe der Erweiterungsmethoden klären möglicherweise noch ein wenig mehr auf. Diese sehen prinzipiell so aus: var productsWithGrossPrice2 = products.Select(product => new { ProductName = product.Name, NetPrice = product.Price, GrossPrice = product.Price * (1 + vat) }).Where(projectedProduct => projectedProduct.GrossPrice > 100M);
Wie Sie sehen, handelt es sich um ein ganz »normales« verkettetes Aufrufen von Erweiterungsmethoden. Da frage ich mich allerdings, welche der beiden Möglichkeiten nun diejenige ist, die leichter zu verstehen ist …
INFO
Die Verwendung von into ist in der Regel eine gute Lösung zur Gestaltung komplexer Abfragen. In der Praxis sollten Sie aber die Performance im Auge behalten, besonders, wenn Sie mit LINQ to SQL arbeiten. Im vorhergehenden Beispiel werden erst alle Artikel abgefragt, um diese danach einzuschränken. Die ursprüngliche Variante der Abfrage (Listing 11.64) ist deshalb u. U. schneller, weil in dieser unpassende Elemente bereits vor der Projektion herausgefiltert werden. into besitzt noch weitere Bedeutung bei Gruppierungen und bei Verknüpfungen. Auf Gruppierungen gehe ich hier nicht weiter ein. Beispiele finden Sie auf den Seiten
704
Komplexe Abfragen mit into, let und mehreren from-Klauseln
674 und 676. Im Abschnitt »Gruppen-Verknüpfungen«(Seite 683) verwende ich eine Verknüpfung mit into.
11.3.2
Abfragen schachteln
An Stelle der Verwendung des into-Schlüsselworts können Sie Abfragen auch schachteln, indem Sie die inneren Abfragen klammern und um diese eine äußere Abfrage platzieren. Die äußere Abfrage bezieht sich dann auf das jeweilige Teilergebnis der inneren. Das vorherige Beispiel können Sie z. B. auch so formulieren:
Abfragen können ineinander geschachtelt werden
Listing 11.66: Eine geschachtelte Abfrage
2
var productsWithGrossPrice = from projectedProduct in ( from product in products select new { ProductName = product.Name, NetPrice = product.Price, GrossPrice = product.Price * (1 + vat) } ) where projectedProduct.GrossPrice > 100 select projectedProduct;
11.3.3
3
4
5
Das let-Schlüsselwort
Das let-Schlüsselwort ermöglicht die Berechnung von Teilergebnissen in eine Variable, auf die Sie im weiteren Verlauf der Abfrage zugreifen können. Die Syntax ist die folgende:
1
let schreibt Teilergebnisse in Variablen
6
let Variable = Ausdruck
7
let können Sie beliebig oft hinter from und/oder where angeben. Wenn Sie sich in der where-Klausel auf einen mit let berechneten Wert beziehen, müssen Sie diesen natürlich vor der where-Klausel berechnen. Ansonsten können Sie auch nach where berechnen, damit für Elemente, die der Einschränkungs-Bedingung nicht entsprechen, keine Berechnung ausgeführt wird (die ja gegebenenfalls Zeit kostet).
8
Das Beispiel aus den beiden vorhergehenden Abschnitten kann nun auch mit let elegant gelöst werden:
9 Listing 11.67: Einsatz von let zur Berechnung von Zwischenergebnissen // Beispiel-Artikel einlesen List products = GetProducts();
10
// Die Artikel so abfragen, dass der Preis in seinen Bruttowert // transformiert wird, und auf Artikel mit einem Brutto-Preis // größer 100 Euro einschränken decimal vat = .19M; var productsWithGrossPrice = from product in products let grossPrice = product.Price * (1 + vat) where grossPrice > 100 let x = 1 select new { ProductName = product.Name,
11
705
LINQ
NetPrice = product.Price, GrossPrice = grossPrice };
let besitzt gegenüber into oder einer verschachtelten Abfrage den Vorteil, dass Sie im weiteren Verlauf der Abfrage mit dem berechneten Wert und dem originalen Objekt weiterarbeiten können. Mit into oder einer verschachtelten Abfrage arbeiten Sie hingegen mit einem bereits ggf. projizierten Element. Der Nachteil von let ist, dass der erzeugte CIL-Code komplexer wird und ggf. etwas mehr Zeit in Anspruch nimmt als bei den anderen Möglichkeiten. let führt zu komplexem kompilierten Code
let-Zuweisungen werden vom Compiler in temporäre anonyme Objekte kompiliert. Die Zuweisung im Beispiel führt z. B. zu einem temporären Objekt wie dem folgenden: new { product = product, grossPrice = product.Price * (1 + vat) })
Mit diesem temporären Objekt wird dann im weiteren Verlauf weiter gearbeitet. Das insgesamt kompilierte Programm ist also nicht mehr ganz so einfach wie in den vorhergehenden Beispielen. Listing 11.68 zeigt das, was der Compiler prinzipiell aus dem Beispiel erzeugt. Listing 11.68: Das vom Compiler prinzipiell erzeugte Programm var productsWithGrossPrice = products.Select(product => new { product = product, grossPrice = product.Price * (1 + vat) }) .Where(ir1 => ir1.grossPrice > 100M) .Select(ir2 => new { ir2 = ir2, x = 1 }) .Select(ir3 => new { ProductName = ir3.ir2.product.Name, NetPrice = ir3.ir2.product.Price, GrossPrice = ir3.ir2.grossPrice });
INFO
Die temporären Variablen ir1 bis ir3 stehen hier für Zwischenergebnisse (Intermediate Results). Die Namen habe ich selbst gewählt. Der Compiler erzeugt in Wirklichkeit einen etwas komplexeren CIL-Code mit zwischengespeicherten Delegaten und Namen wie h_TransparentIdentifier12. Warum der Compiler die unnötige Zuweisung x = 1 in den erzeugten CIL-Code integriert hat, ist mir übrigens schleierhaft.
11.3.4
Ein Performance-Vergleich
Um ganz sicherzugehen, welche der bisher vorgestellten Möglichkeiten für komplexe Abfragen die beste Performance bietet, habe ich einen Performancetest implementiert, der 100.000 Instanzen einer einfachen Klasse ähnlich den vorhergehenden Bei2
706
In CIL-Code sind auch Sonderzeichen wie < und > in Bezeichnern möglich
Komplexe Abfragen mit into, let und mehreren from-Klauseln
spielen projiziert und nach dem projizierten Ergebnis einschränkt. Die Abfragen werden vor einer Schleife erzeugt und zur Initialisierung einmal aufgerufen. In der Schleife werden die Abfragen 10-mal ausgeführt, indem Count() aufgerufen wird (wobei ich die aufgeschobene Ausführung nutze). Dabei wird der Zeitbedarf gemessen und auf eine Variable addiert. Am Ende wird der Durchschnitt der benötigten Sekunden berechnet. Das Ergebnis dieses Tests auf meinem System zeigt Tabelle 11.13. Abfrage
Benötigte Zeit
Normale Abfrage mit zweifacher Berechnung
49,10 ms
Abfrage mit into
38,20 ms
Geschachtelte Abfrage
37,89 ms
Abfrage mit let
41,81 ms
1 Tabelle 11.13: Ergebnis des Performancevergleichs zwischen einer »normalen« Abfrage, einer Abfrage mit into, einer geschachtelten Abfrage und einer mit let
Wie bei allen Performance-Tests sollten Sie die benötigten Zeiten nur im Vergleich sehen. Auf Ihrem System können vollkommen andere Werte herauskommen. Die prozentuale Differenz zwischen den Varianten sollte aber in etwa gleich sein.
from x [where from y [where
5
6
Abfragen mit mehreren from-Klauseln: Kreuzprodukt-Abfragen und spezielle Abfragen wie Ungleichheits-Verknüpfungen
Abfrageausdrücke erlauben nicht nur eine from-Klausel, sondern auch mehrere: in Sequenz 1 Bedingung für Sequenz 1] in Sequenz 2 Bedingung für Sequenz 2]
3
4
Wie vermutet, benötigt die Abfrage mit der zweifachen Berechnung die meiste Zeit. Ebenfalls passend zu meinen Vermutungen ist, dass die let-Variante mehr Zeit benötigt als die into-Variante. Interessant ist, dass die geschachtelte Abfrage die schnellste ist. Ob der Zeitunterschied allerdings relevant ist, ist eine andere Frage. Bei komplexen Berechnungen kann der Unterschied zwischen den Varianten allerdings auch gravierender ausfallen.
11.3.5
2
7
from unter from erlaubt das beliebige Verknüpfen zweier Sequenzen
8
9
Eine solche Abfrage wird für jedes Element der Sequenz 1 ausgeführt und innerhalb dieses für jedes Element der Sequenz 2. Unterhalb der from-Klauseln können Sie sich auf das jeweilige Element der ersten und der zweiten Sequenz beziehen. Natürlich sind auch noch weitere from-Klauseln möglich.
10
Ein einfaches Beispiel zeigt die prinzipielle Auswirkung einer solchen Abfrage (ohne Einschränkungen mit where):
11
Listing 11.69: Einfaches Beispiel für eine Abfrage mit zwei from-Klauseln // Zwei Demo-Arrays anlegen string[] sequence1 = { "a", "b", "c" }; string[] sequence2 = { "1", "2", "3" }; // Die beiden Sequenzen in einer Abfrage mit zwei // from-Klauseln abfragen und in ein Ergebnis projizieren
707
LINQ
var resultSequence = from x in sequence1 from y in sequence2 select x + y; // Das Ergebnis ausgeben foreach (var s in resultSequence) { Console.WriteLine(s); }
from + from ohne Einschränkung erzeugt ein Kreuzprodukt
Das Ergebnis dieses Programms ist: a1 a2 a3 b1 b2 b3 c1 c2 c3
Damit können Sie schon einmal eine Kreuzprodukt-Abfrage erzeugen, bei der alle Elemente der einen mit allen Elementen der anderen Sequenz verknüpft werden. from + from kann einen Inner Join nachempfinden
Da Sie innerhalb des Abfrageausdrucks für beide from-Klauseln auch Einschränkungen programmieren können, können Sie damit alle denkbaren Arten von Verknüpfungen programmieren. So können Sie z. B. einen Inner Join ohne join-Klausel programmieren: Listing 11.70: Eine innere Verknüpfung über eine Abfrage mit zwei from-Klauseln var productsWithCategory = from product in products from category in categories where category.ID == product.CategoryID select new { Product = product, Category = category }; // Das Ergebnis abfragen Console.WriteLine("Artikel mit Kategorie:"); foreach (var productWithCategory in productsWithCategory) { Console.WriteLine("{0}: {1}", productWithCategory.Product.Name, productWithCategory.Category.Name); }
Wie Sie unter »Ungleichheits-Verknüpfungen (Non-Equi Joins)« (Seite 716) noch genauer erfahren, können Sie mit Abfragen mit mehreren from-Klauseln aber auch spezielle Verknüpfungen implementieren. Ein letztes Beispiel zeigt eine Anwendung in einem »normalen« Objektmodell. Eine Klasse Author verwaltet die Daten eines Buchautors. Die Eigenschaft Books referenziert eine Auflistung von Book-Instanzen. Ein Book-Objekt speichert die Daten eines Buchs:
708
Komplexe Abfragen mit into, let und mehreren from-Klauseln
Listing 11.71: Die Beispiel-Klassen /* Verwaltet die Daten eines Buchs */ public class Book { public string Title; public string ISBN; }
1
/* Verwaltet die Daten eines Autors */ public class Author { public string FirstName; public string LastName; public List Books = new List(); }
2
Zur Demonstration werden einige Bücher und Autoren angelegt:
3 Listing 11.72: Anlegen einiger Autoren und Bücher List authors = new List();
4
authors.Add(new Author { FirstName = "Matt", LastName = "Ruff", Books = new List() { new Book {Title = "Fool on the Hill", ISBN = "3423207493"}, new Book {Title = "G.A.S. (GAS). Die Trilogie der Stadtwerke", ISBN = "3423207582"} }});
5
authors.Add(new Author { FirstName = "John", LastName = "Irving", Books = new List() { new Book {Title = "Die wilde Geschichte vom Wassertrinker", ISBN = "3257224451"}, new Book {Title = "Garp und wie er die Welt sah", ISBN = "3499150425"} } });
6
7
8
Wenn Sie nun über die Auflistung der Autoren die Bücher durchgehen wollen, haben Sie mit einer normalen Abfrage ein Problem. Damit könnten Sie nur die Autoren durchgehen und bei der Auswertung der Abfrage über die Books-Auflistung deren Bücher ermitteln. Eine Sortierung oder Gruppierung der Daten wird dann aber sehr schwierig.
9
Mit einer Abfrage mit zwei from-Klauseln können Sie sich in der zweiten from-Klausel aber auf die Auflistung Books der in der ersten from-Klausel abgefragten AuthorInstanzen beziehen und diese auslesen:
10
Listing 11.73: Abfragen einer Untersequenz
11
// Abfragen aller Bücher var books = from author in authors from book in author.Books select book;
709
LINQ
// Durchgehen aller Bücher Console.WriteLine("Alle Bücher"); foreach (var book in books) { Console.WriteLine(book.Title); }
Das Ergebnis dieses Programms ist: Fool on the Hill G.A.S. (GAS). Die Trilogie der Stadtwerke Die wilde Geschichte vom Wassertrinker Garp und wie er die Welt sah
Damit können Sie die Bücher z. B. auch problemlos sortieren: Listing 11.74: Sortiertes Abfragen einer Untersequenz var sortedBooks = from author in authors from book in author.Books orderby book.Title select book;
Wenn Sie nun einschränken, dass Sie dann ja nur die Buchdaten haben: Kein Problem: Erzeugen Sie einfach einen anonymen Typen, dem Sie das äußere Element (im Beispiel die Author-Instanz) übergeben: Listing 11.75: Abfragen einer Untersequenz mit konstruiertem, anonymen Typ (und erweiterter Sortierung) // Abfragen aller Bücher mit Autoren var booksInfos = from author in authors from book in author.Books orderby author.LastName, author.FirstName, book.Title select new { Book = book, Author = author }; // Durchgehen aller Bücher Console.WriteLine("Alle Bücher mit Autor"); foreach (var bookInfo in booksInfos) { Console.WriteLine(bookInfo.Author + ": " + bookInfo.Book.Title); }
Im weiteren Verlauf der Abfrage können Sie sich auf alle Iterations-Variablen beziehen
Wie Sie im Beispiel sehen, können Sie sich im weiteren Verlauf der Abfrage (bei der Einschränkung, Sortierung und/oder Projektion) auf die äußere und die innere Iterations-Variable beziehen. Damit sind Abfragen sehr flexibel.
Das Erweiterungsmethoden-Äquivalent für eine Abfrage mit mehreren from-Klauseln Der C#-Compiler erzeugt aus einer Abfrage mit mehreren from-Klauseln natürlich auch eine äquivalente Abfrage mit Erweiterungsmethoden. Das Äquivalent zu der ersten Abfrage sieht folgendermaßen aus:
710
Komplexe Abfragen mit into, let und mehreren from-Klauseln
Listing 11.76: Das Erweiterungsmethoden-Äquivalent der ersten, einfachen Abfrage mit zwei fromKlauseln var books = authors.SelectMany(author => author.Books, (author, book) => book);
Wie Sie sehen, werden Abfragen mit mehreren from-Klauseln über die SelectManyMethode umgesetzt. Falls Sie diese Methode direkt verwenden wollen: Am ersten Argument übergeben Sie einen Lambda-Ausdruck (oder Delegaten), der die Untersequenz zurückgibt. Am zweiten Argument übergeben Sie einen Lambda-Ausdruck (oder Delegaten), dem das jeweilige Element der äußeren und der inneren Sequenz übergeben wird und der das Ergebnis zurückgibt.
1
2
SelectMany existiert in einigen Varianten, die prinzipiell folgendermaßen deklariert sind: Ergebnistyp-Sequenz SelectMany(selector)
3
Ergebnistyp-Sequenz SelectMany(collectionSelector, resultSelector)
In der Variante mit selector-Argument wird ein Lambda-Ausdruck (oder Delegat) erwartet, der die Auswahl der Untersequenz vornimmt. Das Ergebnis dieser Überladungen ist eine Sequenz der Objekte, die in der Untersequenz verwaltet werden. Das int-Argument des Selektors der zweiten Überladung erhält übrigens den Index des jeweiligen Elements der äußeren Sequenz übergeben.
4
Die Überladungen mit dem zusätzlichen Argument resultSelector erwarten an diesem einen Lambda-Ausdruck oder Delegaten, der das Ergebnis definiert.
5
Die folgenden beiden Aufrufe sind deswegen absolut identisch:
6
// Ohne Ergebnis-Selektor var books1 = authors.SelectMany(author => author.Books); // Mit Ergebnis-Selektor var books = authors.SelectMany(author => author.Books, (author, book) => book);
7
Die Übergabe der Ergebnis-Selektors ist also nur dann notwendig, wenn Sie einen anderen als den in der inneren Sequenz verwalteten Typen übergeben wollen.
8
Probleme mit null-Sequenzen verhindern Wenn Sie mit mehreren from-Klauseln Sequenzen abfragen, deren Untersequenzen auch null sein können, erhalten Sie bei einer normalen Abfrage eine NullReferenceException.
Empty verhindert null-Probleme bei mehreren fromKlauseln
9
Als Beispiel erweitere ich die Autoren-Liste um einen Autoren, dessen Books-Auflistung explizit auf null gesetzt wird, und frage danach die Bücher ab:
10
Listing 11.77: Fehlerhaftes Abfragen einer Untersequenz, die null sein kann // Einen Autoren hinzufügen, dessen Books-Auflistung explizit // auf null gesetzt wird authors.Add(new Author { FirstName = "John", LastName = "Doe", Books = null });
11
// Die Bücher abfragen var books2 = from author in authors from book in author.Books select book;
711
LINQ
// Die Bücher durchgehen Console.WriteLine("Alle Bücher"); foreach (var book in books2) // NullReferenceException { Console.WriteLine(book.Title); }
Die NullReferenceException wird wegen der aufgeschobenen Ausführung erst beim Auswerten des Abfrage-Ergebnisses erzeugt. In der Praxis bedeutet dies, dass Sie u. U. sehr lange nach der eigentlichen Ursache suchen! Sie können das Problem aber sehr einfach lösen, indem Sie über den Operator ?? überprüfen, ob die Untersequenz null ist, und in diesem Fall die leere Sequenz verwenden, die die Empty-Methode zurückgibt: Listing 11.78: Korrektes Abfragen einer Untersequenz, die null sein kann var books2 = from author in authors from book in author.Books ?? Enumerable.Empty() select book; // Die Bücher durchgehen Console.WriteLine("Alle Bücher"); foreach (var book in books2) { Console.WriteLine(book.Title); }
11.3.6 Unterabfragen erlauben die Lösung komplexer Probleme
Unterabfragen
Eine Unterabfrage ist eine Abfrage, die im rechten Teil eines Lambda-Ausdrucks einer anderen Abfrage verwendet wird. Das ist deswegen möglich, da der rechte Teil eines Lambda-Ausdrucks jeden gültigen C#-Code erlaubt, also auch weitere Abfragen. Mit Unterabfragen können Sie komplexe Probleme lösen, wie es auch in SQL möglich ist. So können Sie z. B. in den Beispiel-Artikeln nach denen suchen, die den kleinsten Preis besitzen, indem Sie die Artikel auf diejenigen einschränken, deren Preis derselbe ist wie der kleinste. Und den geringsten Preis können Sie über eine Unterabfrage ermitteln: Listing 11.79: Abfragen von Daten mit einer Unterabfrage // Beispiel-Artikel einlesen List products = GetProducts(); // Die Artikel ermitteln, die den kleinsten Preis besitzen var productsWithSmallestPrice1 = from product in products where product.Price == products.Min(p => p.Price) select product; // Das Ergebnis auswerten Console.WriteLine("Artikel mit dem kleinsten Preis:"); foreach (var product in productsWithSmallestPrice1) { Console.WriteLine("{0}: {1}", product.Name, product.Price); }
712
Komplexe Abfragen mit into, let und mehreren from-Klauseln
Unterabfragen werden für jedes Element der Sequenz separat ausgeführt. Sie sind deshalb sehr inperformant. Das vorhergehende Beispiel können Sie wesentlich performanter lösen, wenn Sie das Ergebnis der Unterabfrage zuvor in eine Variable schreiben:
HALT
Listing 11.80: Performanteres Abfragen durch Verzicht auf die Unterabfrage
1
decimal smallestPrice = products.Min(p => p.Price); var productsWithSmallestPrice2 = from product in products where product.Price == smallestPrice select product;
2
In einigen Fällen kommen Sie aber um die Verwendung einer Unterabfrage nicht herum. Ein einfaches Beispiel ist ein Array, das die vollen Namen von Personen verwaltet und das Sie nach den Nachnamen sortiert ausgeben wollen:
3
Listing 11.81: Abfrage, die ohne Unterabfrage nicht auskommt string[] fullNames = { "Earl Hickey", "Joy Turner", "Randy Hickey", "Darnell Turner" }; var sortedNames = from fullName in fullNames orderby (fullName.Split(' ').Last()), (fullName.Split(' ').First()) select fullName;
4
5
Console.WriteLine("Namen, sortiert nach Nachname und Vorname:"); foreach (var name in sortedNames) { Console.WriteLine(name); }
6
Aber selbst hier können Sie noch optimieren. Schließlich werden zwei Unterabfragen ausgeführt. fullName.Split(' ').Last() liefert den Nachnamen, fullName. Split(' ').First() den Vornamen. Sie können die Split-Methode aber auch vorher aufrufen, das Ergebnis mit let in eine Variable schreiben und diese in den Unterabfragen auswerten. Dabei sollten Sie aber beachten, dass let ggf. nicht sehr performant ausgeführt wird. Unterabfragen können Sie auch in einer Projektion einsetzen. Das folgende Beispiel ermittelt alle Unterordner in einem Ordner und schreibt Ordnerinformationen in einen anonymen Typen. Dazu gehört auch eine Auflistung von Informationen zu Dateien, die in dem jeweiligen Ordner gespeichert sind:
7
8 Unterabfragen können Sie auch in einer Projektion einsetzen
9
Listing 11.82: Unteranfrage in einer Projektion
10
string path = Path.GetTempPath(); DirectoryInfo directoryInfo = new DirectoryInfo(path); var subDirectories = from subDirectory in directoryInfo.GetDirectories() select new { FullName = subDirectory.FullName, CreationDate = subDirectory.CreationTime, Files = from fileInfo in subDirectory.GetFiles() select new { Name = fileInfo.Name, FullName = fileInfo.FullName,
11
713
LINQ
CreationDate = fileInfo.CreationTime } };
INFO
In der Praxis sollten Sie – besonders bei LINQ to SQL – zur Lösung komplexer Probleme überprüfen, ob Sie das Problem eventuell auch ohne eine Unterabfrage lösen können. Unterabfragen sind in der Regel sehr inperformant, da sie für jedes Element (bzw. jeden Datensatz) erneut ausgeführt werden. In vielen Fällen können Sie komplexe Probleme auch über eine Verknüpfung lösen. So können Sie z. B. alle Artikel, die nicht einer bestimmten Kategorie zugeordnet sind, auch über eine Ungleichheits-Verknüpfung (Seite 716) ermitteln.
11.4
Einige Tipps und Tricks
Ich beschließe das LINQ-Kapitel mit einigen Tipps und Tricks aus der (bisher noch eher mageren) Praxis. Diese beschreibe ich aber nicht mehr vollständig, es handelt sich eben nur um »Tipps und Tricks« ☺.
11.4.1
Progressive und dynamische Abfragen
Dynamische Abfragen erlauben die Definition einer Abfrage, nachdem ein Programm kompiliert wurde. In gewissen Grenzen können Sie eine Abfrage über einen progressiven Aufbau dynamisch gestalten. Flexibler sind Sie allerdings mit dem zweiten »Trick« in diesem Abschnitt, der einen String als Prädikat erlaubt. Abfragen können progressiv aufeinander aufgebaut werden
Da eine LINQ-Abfrage, die eine Sequenz zurückgibt, immer eine IEnumerableoder IQueryable-Referenz zurückgibt, können Sie mehrere Abfragen progressiv aufeinander aufbauen. Dabei verwenden Sie eine Variable für die Abfrage, die Sie ab der zweiten Abfrage als Sequenz benutzen. Auf diese Weise können Sie Abfragen schon relativ dynamisch aufbauen: Listing 11.83: Progressives Aufbauen einer Abfrage var query = products.Where(product => product.Price > 10); query = query.Where(product => product.CategoryID > 1); query = query.Where(product => product.Name.Contains("DVD")); foreach (var product in query) { Console.WriteLine(product.Name); }
Die Performance ist unter LINQ direkt wahrscheinlich nicht besonders gut, da in diesem Fall mehrere Abfragen hintereinander ausgeführt werden. Unter LINQ-Erweiterungen wie LINQ to SQL sollte die Performance allerdings recht gut sein, weil alle Teilabfragen zunächst interpretiert und schließlich als ein einziger Befehl gegen die Datenquelle ausgeführt werden. Beim progressiv-dynamischen Aufbau sind Sie lediglich bei der Verwendung der Operatoren eingeschränkt. Wenn Sie dem Anwender ermöglichen sollen, auch diese zu wählen, ist der folgende Tipp wahrscheinlich geeigneter. Die Beispiel-Datei Dynamic.cs erlaubt dynamische Abfragen
714
Echte dynamische Abfragen werden in der Regel (und in diesem Tipp) über Strings definiert. Der String wird in der Laufzeit geladen, in eine Abfrage konvertiert und diese ausgeführt. So können Sie z. B. eine Benutzeroberfläche zur Verfügung stellen, in der der Anwender zur Suche von Daten optionale Eingaben tätigen kann. Die Ein-
Einige Tipps und Tricks
gabefelder, die eine Eingabe enthalten, können Sie dann auswerten und in Form eines String zu einer Abfrage zusammenstellen. Sie brauchen bloß noch eine Möglichkeit, eine in einem String (mit einer entsprechenden Syntax) definierte dynamische Abfrage in eine LINQ-Abfrage zu konvertieren. Und diese Möglichkeit existiert in Form der Klassen der Datei Dynamic.cs, die Sie im Ordner Samples\1033\CSharpSamples\LinqSamples\DynamicQuery\DynamicQuery Ihrer Visual Studio-Installation finden (bzw. in den Beispielen, die Sie an der Adresse code.msdn.microsoft.com/csharpsamples/Release/ProjectReleases.aspx herunterladen können). Alternativ können Sie natürlich auch die Kopie verwenden, die das Beispiel zu diesem Abschnitt verwendet.
1
2
Diese Klassen erweitern die IQueryable-Schnittstelle (leider nicht IEnumerable) um neue Überladungen der Methoden Where, Select, OrderBy, Take, Skip, GroupBy, Any und Count, die als Prädikat einen String erwarten. Dieser String muss eine Syntax aufweisen, die in dem Dokument Dynamic Expressions.html beschrieben wird (das Sie im Ordner des Visual-Studio-Beispiels und im Ordner des Beispiels dieses Abschnitts finden).
3
So können Sie zumindest alle Sequenzen, die IQueryable implementieren, dynamisch abfragen. Das ist z. B. für LINQ to SQL der Fall. Sie können aber auch eine Sequenz, die IEnumerable oder IEnumerable implementiert, über die AsQueryable in eine IQueryable-Sequenz umwandeln, um dynamische Abfragen auch auf Auflistungen und Arrays ausführen zu können ☺.
4
5
Integrieren Sie nun noch die Datei Dynamic.cs in Ihr Projekt, fügen Sie eine usingDirektive mit dem Namensraum System.Linq.Dynamic hinzu, dann können Sie z. B. die Beispiel-Artikel dynamisch nach der Kategorie und dem Preis abfragen:
6
Listing 11.84: Dynamisches Abfragen einer Sequenz // Beispiel-Artikel einlesen List products = GetProducts();
7
// Die Liste in eine IQueryable-Sequenz konvertieren var queryableProducts = products.AsQueryable();
8
// Die Artikel dynamisch abfragen string query = "CategoryID == 1 && Price > 50"; var result = queryableProducts.Where(query); // Das Ergebnis ausgeben foreach (var product in result) { Console.WriteLine("{0}: {1}", product.Name, product.Price); }
9
10
Wie Sie im Beispiel sehen, ist die Grundsyntax einer dynamischen Abfrage die, dass Sie sich im String auf Felder oder Eigenschaften beziehen, diese über die Standard-Vergleichsoperatoren mit Werten vergleichen und mehrere Vergleichsausdrücke über die logischen Operatoren && (oder and) und || (oder or) miteinander verknüpfen.
11
Das dynamische Abfragen hat einige Einschränkungen. So können Sie scheinbar nur Sequenzen abfragen, die Objekte mit Feldern oder Eigenschaften verwalten (keine Sequenzen mit einfachen Werten). Außerdem stehen spezielle Operationen wie die String-Methode StartsWith oder SQL-LIKE-ähnliche Operatoren nicht zur Verfügung. Und Sie sollten beachten, dass eine dynamische Abfrage natürlich langsamer ausgeführt wird als eine normale.
715
LINQ
11.4.2
Ungleichheits-Verknüpfungen (Non-Equi Joins)
Eine Ungleichheits-Verknüpfung (Non-Equi Join) ermittelt alle Elemente einer Sequenz, die nicht in einer anderen Sequenz (über einen ID-Wert) referenziert werden. Eine Ungleichheits-Verknüpfung können Sie relativ einfach über eine Abfrage mit zwei from-Klauseln implementieren. Ungleichheits-Verknüpfungen machen aber in der Regel keinen Sinn, wenn Sie einfach nur einen Inner Join nachprogrammieren und statt == den Operator != angeben: Listing 11.85: Relative sinnlose Ungleichheits-Verknüpfung, die alle Artikel mit den Kategorien verknüpft, die nicht dem Artikel zugeordnet sind var senselessNonEquiJoin = from product in products from category in categories where product.CategoryID != category.ID select product;
Dieses Beispiel ist relativ sinnlos, weil es alle Artikel mit Kategorien verknüpft, die nicht dem Artikel zugeordnet sind. Das Ergebnis ist auch ziemlich »eigenartig«, weil in der Ergebnis-Sequenz Daten mehrfach ausgegeben werden (Abbildung 11.11). Abbildung 11.11: Ergebnis der sinnlosen UngleichheitsVerknüpfung
Sinnvoll werden Ungleichheits-Verknüpfungen, wenn Sie eine normale Verknüpfung mit einer Abfrage auf ungleiche Felder kombinieren. Ein entsprechendes Beispiel kann ich nicht mit unseren Beispiel-Daten implementieren. Die MicrosoftBeispiel-Datenbank Northwind ermöglicht aber eine Demonstration dieser Technik. In dieser Datenbank werden Bestelldetails in der Tabelle Order Details verwaltet. Order Details referenziert die Tabelle Products über die ID der in dieser Tabelle gespeicherten Artikel. In Order Details wird der zum Zeitpunkt des Verkaufs gültige Verkaufspreis der Artikel im Feld UnitPrice verwaltet. Über eine Ungleichheits-Verknüpfung können Sie nun herausfinden, welche Bestelldetails Artikelpreise gespeichert haben, die nicht dem aktuellen Preis entsprechen. Dazu müssen Sie LINQ to SQL verwenden, das ich hier aber noch nicht behandle. Angenommen, Sie haben die Bestelldetails in der Auflistung orderDetails und die Artikel in der Auflistung products gespeichert, können Sie folgendermaßen abfragen:
716
Einige Tipps und Tricks
Listing 11.86: Eine in der Praxis angewendete Ungleichheits-Verknüpfung var orderDetailsWithDifferentProductPrice = from orderDetail in northwindOrderDetails from product in northwindProducts where orderDetail.ProductID == product.ProductID && orderDetail.UnitPrice != product.UnitPrice select new { OrderDetail = orderDetail, Product = product };
1
2
Das Beispiel zu diesem Abschnitt auf der Buch-DVD fragt auf diese Weise über LINQ to SQL die Northwind-Datenbank ab. DISC
Eine andere Art der Ungleichheits-Verknüpfung ist eine, die keine Verknüpfung verwendet, sondern mit einer Liste von ID-Werten vergleicht, die entweder vor der Abfrage oder als Unterabfrage ermittelt werden. Damit können Sie in unseren Beispieldaten z. B. herausfinden, welche Artikel eine Kategorie-ID verwalten, die nicht existiert.
UngleichheitsVerknüpfungen sind auch ohne Join möglich
3
4
Fragen Sie dazu zuerst die IDs der Master-Sequenz ab (in unserem Fall die Kategorien). In der Abfrage überprüfen Sie dann, ob die im jeweiligen Detail-Element angegebene ID nicht in der Liste der IDs vorkommt:
5
Listing 11.87: Ungleichheits-Verknüpfung mit Unterabfrage // Beispiel-Daten einlesen List products = GetProducts(); List categories = GetCategories();
6
// Alle Artikel suchen, die eine ungültige Kategorie referenzieren int[] categoryIDs = (from category in categories select category.ID).ToArray(); var productsWithInvalidCategory = from product in products where categoryIDs.Contains(product.CategoryID != null ? product.CategoryID.Value : -1) == false select product;
7
8
// Das Ergebnis durchgehen foreach (var product in productsWithInvalidCategory) { Console.WriteLine("{0} (Kategorie: {1})", product.Name, product.CategoryID != null ? product.CategoryID.ToString() : "Keine"); }
9
10
Das Ergebnis dieses Beispiels ist mit den Original-Daten nur der Artikel »Programmieren lernen (E-Book)«, dem keine Kategorie zugeordnet ist. Wenn Sie aber vor der Abfrage z. B. einen Artikel mit einer nicht existierenden Kategorie hinzufügen:
11
products.Add(new Product { ID = 11, Name = "Visual Studio 2008", CategoryID = 5});
wird auch dieser Artikel ausgegeben.
717
LINQ
INFO
Die Abfrage ist übrigens etwas komplex, da das Feld CategoryId in Product ein Nullable int ist. Für den Vergleich mit dem int-Array musste ich eventuelle null-Werte konvertieren (hier in -1). Außerdem habe ich die IDs der existierenden Kategorien zum einen vor der eigentlichen Abfrage (und nicht als Unterabfrage) abgefragt und zum anderen über ToArray in ein Array konvertiert. Damit stelle ich sicher, dass die ID-Abfrage nicht für jeden Artikel separat ausgeführt wird. Nur zur Erläuterung: Ohne ToArray würde die aufgeschobene Ausführung dazu führen, dass die Abfrage der Kategorie-IDs für jeden Artikel separat noch einmal ausgeführt werden würde. Der Zeitunterschied zwischen einer Version ohne ToArray und einer Version mit ist gravierend. In einem Performance-Test mit 106 Kategorien und 1011 Artikeln (der Teil des Beispiels ist) benötigte die Version ohne ToArray ca. 3,87 ms und die Version mit nur 1,22 ms.
11.4.3
Kreuzprodukt-Verknüpfungen (Cross Joins)
Eine Kreuzprodukt-Verknüpfung (Cross Join) verknüpft zwei Sequenzen so, dass alle Elemente der einen mit allen Elementen der anderen kombiniert werden. Ein gutes Beispiel für eine solche Verknüpfung ist eine Liste von Vornamen und eine Liste von Nachnamen, wenn Sie alle möglichen Kombinationen der Vor- und Nachnamen erhalten wollen. Eine Kreuzprodukt-Verknüpfung erhalten Sie in LINQ, indem Sie zwei from-Klauseln untereinander schreiben und keine Bedingung angeben: Listing 11.88: Eine Kreuzprodukt-Abfrage // Je ein Array mit Vor- und Nachnamen erzeugen string[] firstNames = { "Zaphod", "Ford", "Tricia" }; string[] lastNames = { "Beeblebrox", "Prefect", "McMillan" }; // Die Arrays über einen Cross Join miteinander verknüpfen var names = from firstName in firstNames from lastName in lastNames select new { FirstName = firstName, LastName = lastName }; // Das Ergebnis durchgehen foreach (var name in names) { Console.WriteLine(name.FirstName + " " + name.LastName); }
Das Ergebnis zeigt Abbildung 11.12.
718
Einige Tipps und Tricks
Abbildung 11.12: Das Ergebnis der KreuzproduktAbfrage
1
11.4.4
Kommaseparierte Dateien (CSV-Dateien) verarbeiten
2
Dieser letzte Tipp soll zeigen, dass Sie mit LINQ prinzipiell auch Möglichkeiten haben, die nicht unbedingt sofort ersichtlich sind.
3
Das Einlesen von kommaseparierten Dateien (CSV-Dateien) ist eine in der Praxis recht häufig benötigte Technik, die normalerweise rein über einen StreamReader gelöst wird. LINQ ermöglicht aber eine objektorientiertere Herangehensweise.
4
Dieser Tipp verwendet die Artikel-Textdatei, die in den Beispielen dieses Kapitels eingelesen wird, die mit Artikeln und Kategorien arbeiten. Diese Textdatei sieht folgendermaßen aus:
5
ProductId;ProductName;CategoryId;Price 1;Per Anhalter durch die Galaxis;2;9,95 2;Per Anhalter durch die Galaxis (DVD);3;28,99 3;Das C# 2008 Kompendium;1;59,99 4;Das C# 2008 Codebook;1;99,95 5;Die wilde Geschichte vom Wassertrinker;2;12,90 6;2001: Odyssee im Weltraum (DVD);3;9,95 7;2010 - Das Jahr, in dem wir Kontakt aufnehmen (DVD);3;4,79 8;Programmieren lernen;1;24,95 9;Programmieren lernen (E-Book);;15,95 10;Fool on the Hill;2;9,95
6
7
Der kleine LINQ-Trick basiert auf einer Methode, die die Zeilen der Datei als Auflistung zurückgibt:
8
Listing 11.89: Methode zum Einlesen einer Textdatei als Auflistung private static IEnumerable ReadFile(string fileName) { using (StreamReader streamReader = new StreamReader( fileName, Encoding.UTF8)) { // Die erste Zeile (die Überschrift) wird ignoriert string row = streamReader.ReadLine();
9
10
// Die weiteren Zeilen einlesen und zurückgeben while ((row = streamReader.ReadLine()) != null) { yield return row; }
11
} }
719
LINQ
Über LINQ können Sie diese Methode nun so aufrufen, dass Sie die Zeilen in einzelne Felder auftrennen und das Ergebnis in einen anonymen Typen transformieren, den Sie dann in der Auswertung auslesen: Listing 11.90: Abfragen und Auswerten der Artikel-Textdatei über LINQ // Den Dateinamen ermitteln string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Products.txt"); // Die Datei einlesen und über LINQ in eine Sequenz // von Objekten eines anonymen Typs verarbeiten var products = from row in ReadFile(fileName) let fields = row.Split(';') select new { ID = Convert.ToInt32(fields[0]), Name = fields[1], CategoryID = string.IsNullOrEmpty( fields[2]) == false ? (int?)Convert.ToInt32(fields[2]) : null, Price = Convert.ToDecimal(fields[3]) }; // Das Ergebnis auswerten foreach (var product in products) { Console.WriteLine(product.ID + ": " + product.Name + ": " + product.Price); }
Der wesentliche »Trick« ist, dass die einzelnen Zeilen mit Split aufgesplittet werden und das Ergebnis-Array mit let in eine interne Variable geschrieben wird. Dieses String-Array wird dann in der Projektion ausgewertet und in einen anonymen Typen transformiert. Das einzig Schwierige ist dabei das Debuggen, bei dem der Compiler leider die Auswertung der in dem LINQ-Ausdruck verwendeten Variablen nicht zulässt. Das macht die Suche nach der Ursache von Fehlern sehr schwer.
INFO
In der Praxis sollten Sie im LINQ-Ausdruck natürlich berücksichtigen, dass eine eingelesene Zeile nicht dem erwarteten Format entspricht. Wie bei XML üblich sollten Sie dann ggf. das gesamte Einlesen mit einer Fehlermeldung abbrechen. Mit diesem Tipp verabschiede ich mich dann auch aus diesem Kapitel. Viel Spaß bei Ihren weiteren Versuchen mit LINQ ☺.
720
Teil 2 Anwendungen entwickeln 723
WPF-Grundlagen
12
785
WPF-Anwendungen
13
867
Wichtige WPF-Techniken
14
935
Konfiguration, Ressourcen und Lokalisierung
15
969
Windows-Anwendungen verteilen
16
Inhalt
12
WPF-Grundlagen 12
Zur Entwicklung von Windows-Anwendungen ist WPF die neue Technologie, die in Zukunft den alten Standard, Windows.Forms, ablösen wird. Die Gründe dafür und die Unterschiede zwischen WPF und Windows.Forms erläutere ich im folgenden Abschnitt.
13
In diesem Kapitel beschreibe ich die Unterschiede zwischen WPF und Windows. Forms, zeige, wie Sie WPF-Anwendungen prinzipiell mit Visual Studio entwickeln, und behandle die (wichtigen) Grundlagen von XAML und WPF.
14
Dieses und die folgenden Kapitel können WPF allerdings nicht umfassend behandeln. Einige WPF-Themen wie z. B. 3D-Grafiken sind einfach zu umfangreich, um in einem C# Kompendium überhaupt oder umfassend behandelt werden zu können. Bücher über WPF haben üblicherweise mehr als 700 Seiten …
15
Deshalb behandelt dieses Kapitel die grundlegenden WPF-Themen, Kapitel 13 die Entwicklung von Standardanwendungen und Kapitel 14 die wichtigen speziellen WPF-Techniken.
16
Wenn Sie mehr über WPF erfahren wollen, empfehle ich die Bücher »Windows Presentation Foundation« (dt.) von Dirk Frischalowski, erschienen bei Addison-Wesley sowie »Windows Presentation Foundation Unleashed« (engl.) von Adam Nathan aus dem Sams-Verlag.
17 REF
18
Die Stichworte dieses Kapitels sind: WPF im Vergleich zu Windows.Forms Die Möglichkeiten, die Sie mit WPF besitzen WPF-Anwendungen in Visual Studio entwickeln XAML als Grundlage von WPF Logische und visuelle Bäume Abhängigkeitseigenschaften Angefügte Eigenschaften Geroutete und angefügte Ereignisse Übersicht über die wichtigsten WPF-Klassen
WPF-Beispiele finden Sie leider nicht direkt in den mit Visual Studio installierten Beispielen. Die Microsoft-WPF-Beispiele sind Teil des aktuellen Windows-SDK. Die Installation dieses mächtigen SDK nur wegen der Beispiele ist aber eher zweifelhaft. Sie können die Microsoft-WPF-Beispiele aber auch einzeln aus dem Internet herunterladen. Den jeweiligen Download-Link finden Sie auf den Seiten, die die einzelnen WPF-Beispiele beschreiben. Diese finden Sie in der Visual-Studio-Dokumentation unter .NET-ENTWICKLUNG / DOKUMENTATION ZU .NET FRAMEWORK SDK / .NET FRAMEWORK / BEISPIELE / WINDOWS PRESENTATION FOUNDATION-BEISPIELE oder im Internet an der Adresse msdn2.microsoft.com/en-us/library/ms771633.aspx.
19
20
21
22
REF
23
723
Index
■ ■ ■ ■ ■ ■ ■ ■ ■
WPF-Grundlagen
Weitere Beispiele finden Sie an der Adresse windowsclient.net/downloads/folders/ wpfsamples/default.aspx.
12.1
WPF versus Windows.Forms
WPF ist die moderne Art, Windows-Anwendungen (und Anwendungen, die im Browser ausgeführt werden) zu entwickeln. Vor WPF wurden Windows-Anwendungen unter .NET ausschließlich mit den Klassen des Windows.Forms-Namensraums entwickelt. Da WPF noch relativ neu ist (die erste Version, mit ein paar Neuigkeiten in .NET 3.5) und gegenüber Windows.Forms (zurzeit noch) nicht nur Vorteile, sondern auch Nachteile besitzt, kläre ich zunächst, was WPF von klassischen WindowsAnwendungen unterscheidet.
12.1.1 Windows. Forms-Anwendungen basieren auf Klassen
Klassische Windows-Anwendungen
Klassische Windows-Anwendungen werden unter .NET mit den Klassen des Windows.Forms-Namensraums entwickelt. Die einzelnen Fenster (Formulare) einer Anwendung werden als Klasse entwickelt, die von System.Windows.Forms.Form abgeleitet ist. Zur Gestaltung der Oberfläche der Formulare stehen Steuerelemente zur Verfügung, wie z. B. das Label, das TextBox- und das Button-Steuerelement. Steuerelemente sind Klassen, die zur Darstellung auf dem Formular instanziert und initialisiert werden. Dabei werden u. a. die Position und die Größe des Steuerelements über dafür vorgesehene Eigenschaften festgelegt. Die instanzierten Steuerelemente werden der Controls-Auflistung des Formulars hinzugefügt, womit sichergestellt ist, dass das Formular diese auch darstellt. Die Erzeugung und Initialisierung der Steuerelemente (und Komponenten1) geschieht üblicherweise in einer Methode mit Namen InitializeComponent, die im Konstruktor der Formular-Klasse aufgerufen wird. Bei Windows.Forms-Anwendungen wird die Oberfläche also komplett über den Programmcode definiert. Die Klasse eines Formulars für eine sehr einfache Anwendung, die nur einen einzigen Schalter besitzt, kann z. B. folgendermaßen aussehen: Listing 12.1:
Klasse für ein sehr einfaches Windows.Forms-Formular (ohne Aufteilung in partielle Klassen)
public class MainForm : Form { /* Referenz auf den Schalter */ private System.Windows.Forms.Button btnHello; /* Konstruktor */ public MainForm() { this.InitializeComponent(); }
1
724
Komponenten sind eigentlich ganz normale Klassen. Diese sind aber von Component abgeleitet und können deswegen ähnlich Steuerelementen auf ein Windows.Forms-Formular gezogen werden. Der Visual-Studio-Designer kümmert sich um den notwendigen Programmcode zur Erzeugung und Initialisierung der Komponenten. Der Vorteil einer Komponente gegenüber einer einfachen Klasse ist, dass Sie die Eigenschaften über das Visual-Studio-Eigenschaftenfenster einstellen und die Ereignisse ebenfalls über Visual Studio zuweisen können.
WPF versus Windows.Forms
/* Initialisiert das Formular und erzeugt */ /* und initialisiert die Steuerelemente */ private void InitializeComponent() { this.btnHello = new System.Windows.Forms.Button(); this.SuspendLayout(); // // btnHello // this.btnHello.Location = new System.Drawing.Point(12, 12); this.btnHello.Name = "btnHello"; this.btnHello.Size = new System.Drawing.Size(75, 23); this.btnHello.TabIndex = 0; this.btnHello.Text = "Hello"; this.btnHello.UseVisualStyleBackColor = true; this.btnHello.Click += new System.EventHandler(this.btnHello_Click); // // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(191, 48); this.Controls.Add(this.btnHello); this.Name = "MainForm"; this.Text = "Hello"; this.ResumeLayout(false); }
12 13
14
15
16
/* Ereignismethode für den Schalter */ private void btnHello_Click(object sender, EventArgs e) { MessageBox.Show("Hello"); }
17
}
12.1.2
WPF-Anwendungen
WPF-Anwendungen arbeiten vollkommen anders als Windows.Forms-Anwendungen. Sie bestehen zwar prinzipiell genauso aus Fenstern und Steuerelementen, die Definition der Oberfläche wird aber normalerweise ausschließlich deklarativ in Form von XML-Dokumenten vorgenommen. Der dazu verwendete XML-Dialekt wird als XAML (eXtensible Application Markup Language) bezeichnet.
18 WPF-Anwendungen basieren auf XAML
19
Die einzelnen Fenster einer Anwendung werden über separate XAML-Dokumente beschrieben. Innerhalb der einzelnen XAML-Dokumente werden, ähnlich wie bei (X)HTML, Elemente für die darzustellenden Steuerelemente angelegt. Ein einfaches Fenster mit einem Schalter wird z. B. so deklariert:
20
Listing 12.2:
21
XAML-Dokument zur Deklaration eines einfachen WPF-Fensters mit einem Schalter
Hallo
22
23
725
WPF-Grundlagen
Die für die Fenster und Steuerelemente verwendeten Klassen entstammen dem Namensraum System.Windows.Controls, sind also andere als die für Windows.FormsAnwendungen. Die Ereignismethode für den Schalter ist in einer separaten partiellen Klasse definiert: Listing 12.3:
Klasse mit der Interaktions-Logik für das Fenster MainWindow der Beispiel-WPFAnwendung
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btnHello_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Hallo"); } }
Diese partielle Klasse wird dem Window-Element über das Attribut x:Class mitgeteilt. Der Compiler erkennt daran, dass er die XAML-Datei mit der partiellen Klasse zusammenführen muss. Die WPF-Laufzeitumgebung interpretiert den XAML-Code
Bei der Ausführung von WPF-Anwendungen werden die enthaltenen XAML-Dokumente von einer ab NET 3.0 in das .NET Framework integrierten WPF-Laufzeitumgebung eingelesen und in Instanzen der entsprechenden Klassen überführt (die natürlich genauso benannt sind wie die verwendeten XAML-Elemente). Dabei wird das XAML-Dokument als ein Teil einer partiellen Klasse und die in der Regel enthaltene C#-Klasse mit den Ereignismethoden als ein weiterer Teil verwendet. Die InitializeComponent-Methode, die im Konstruktor der C#-Klasse aufgerufen wird, sorgt übrigens nicht dafür, dass die Steuerelemente erzeugt werden (das macht die WPF-Laufzeitumgebung), sondern dafür, dass das XAML-Dokument geladen und von der WPF-Laufzeitumgebung in ein Window-Objekt überführt wird. Dazu erfahren Sie mehr im Abschnitt »Parsen und Kompilieren« (Seite 751).
12.1.3
Die wesentlichen Unterschiede zwischen Windows.Forms- und WPF-Anwendungen
Ein wesentlicher Unterschied zwischen Windows.Forms- und WPF-Anwendungen ist bereits deutlich geworden: Die Art der Definition der Fenster (programmatisch bei Windows.Forms und deklarativ bei WPF). Dieser Unterschied hat eine weit reichende Bedeutung, die im Folgenden kurz erläutert wird. Da WPF eine vollkommene Neuentwicklung ist, die keinerlei Kompatibilität zu Windows.Forms aufweist, bestehen aber noch einige weitere wesentliche Unterschiede:
Design der Oberfläche Wie Sie ja bereits gesehen haben, wird die Oberfläche in Windows.Forms-Anwendungen komplett über das Programm beschrieben. Dieser Ansatz besitzt in der Praxis einige wesentliche Nachteile. So ist es z. B. sehr schwierig, das Design und die Programmierung einer Anwendung voneinander zu trennen. Bei der Entwicklung von Webanwendungen ist es aber mittlerweile üblich, dass ein Designer die Oberfläche
726
WPF versus Windows.Forms
gestaltet und ein Programmierer sich um die Programmierung kümmert. Bei klassischen Windows-Anwendungen besteht diese Möglichkeit nicht. Ein anderes Problem ist, dass es sehr schwierig ist, Anwendungsoberflächen über Tools zu erzeugen, z. B., um über das Schema einer Datenbanktabelle ein Editier-Formular zu erzeugen. WPF setzt zur Beschreibung der Anwendungsoberfläche XAML ein. Die Programmierung wird in separaten Quellcodedateien vorgenommen, die von der WPF-Laufzeitumgebung bei der Ausführung mit den XAML-Dateien zu einem Objekt verschmolzen werden. Damit ist das Design einer Anwendung von der Programmierung sauber getrennt: Ein Designer kann (über Tools wie Microsoft Expression Blend) die Oberfläche der Anwendung gestalten, Programmierer kümmern sich um die Anwendungslogik. XAML geht beim Design sogar noch wesentlich weiter als klassische Windows-Anwendungen, da Features wie Animationen, Videos. 2D- und 3DGrafik und Dokumenten-Support direkt über XAML – also ohne Programmierung – integriert werden können. Der Designer kann somit das Look&Feel einer Anwendung ausprobieren, ohne dass eine einzige Anweisung programmiert werden muss.
12 13
14
Ein weiteres wichtiges WPF-Design-Feature sind Stile, Vorlagen, Skins und Themen, über die eine Oberfläche im Design verändert werden kann, ohne dass die FensterDateien selbst geändert werden müssen. Windows.Forms bietet keine äquivalenten Features.
15
Unterschiedliche Klassen
16
Formulare und Steuerelemente von Windows.Forms-Anwendungen entstammen dem Windows.Forms-Namensraum, die von WPF-Anwendungen dem System.Windows. Controls-Namensraum. Es handelt sich dabei um vollkommen andere Klassen. Eigenschaften mit gleicher Bedeutung sind teilweise gleich benannt, an anderen Stellen aber auch unterschiedlich.
17
Der Steuerelement-Umfang 18
Der Windows.Forms-Namensraum enthält (im Vergleich zu WPF 1.0 noch) mehr Steuerelemente als der System.Windows.Controls-Namensraum. Die in .NET 3.5 noch eher magere Steuerelementsammlung von WPF bietet für einige wichtige Windows.Forms-Steuerelemente (wie z. B. das DataGridView und das DateTimePicker) noch kein Äquivalent. Außerdem bieten externe Hersteller eine enorm große Anzahl von speziellen Steuerelementen für Windows.Forms, die teilweise komplexe Probleme auf einfache (Anwendungs-)Weise lösen. WPF kann da noch nicht mithalten. Microsoft und externe Hersteller werden diese Lücke aber wahrscheinlich bald schließen.
19
20
Flexibilität der Steuerelemente WPF verwendet einen anderen Ansatz für Steuerelemente als Windows.Forms: Bei Windows.Forms gibt es normale Steuerelemente und spezielle Container-Steuerelemente. Container-Steuerelemente, wie z. B. ein Panel, sind lediglich dazu da, andere Steuerelemente in sich aufzunehmen (um diese z. B. zu gruppieren). Normale Steuerelemente wie z. B. ein Schalter können keine weiteren Steuerelemente in sich aufnehmen. In WPF kann aber prinzipiell jedes Steuerelement andere Steuerelemente in sich aufnehmen. Eine Button-Instanz kann z. B. ein Grid beinhalten, und in diesem ein Image- und ein Label-Steuerelement. So ist es in WPF z. B. sehr einfach, einen Schalter zu erzeugen, der neben einem Bild einen Text darstellt. In Windows.Forms wäre diese Aufgabe mit dem originalen Button-Steuerelement nur sehr schwer lösbar.
21
22
23
727
WPF-Grundlagen
WPF ermöglicht aber noch weitere Features, wie z. B. die Darstellung eines Videos in einem Menü oder Schalter, oder sogar auf einem 3D-Objekt, das per Animationen bewegt wird. Damit werden Oberflächen mit Features ermöglicht, die unter Windows.Forms in der Regel nur über spezielle Steuerelemente von externen Herstellern erreicht werden können. Eines der in meinen Augen besten Beispiele dafür ist die ListBox, die unter Windows.Forms nur einfache Einträge darstellen kann. Da ein ListBox-Eintrag unter WPF auch ein Steuerelement sein kann (das wieder Steuerelemente beinhalten kann etc.), ist die Darstellung von listenförmigen Daten unter WPF im Vergleich zu Windows.Forms extrem flexibel.
Integrierte (Medien-)Technologien WPF integriert neben einer 2D-Engine zur Darstellung von Grafiken auch eine 3DEngine, ermöglicht Animationen, integriert Sprache und bietet einen reichhaltigen Support für die Anzeige von Dokumenten. In Windows.Forms ist außer 2D keine dieser Technologien direkt integriert. Windows.Forms-Entwickler müssen sich dazu mit separaten Komponenten wie DirectX oder MCI auseinandersetzen. WPF geht mit seiner Integration sogar noch weiter und erlaubt die Definition von Effekten über die verschiedenen Medien-Typen. WPF ersetzt damit die über 20 Jahre alten Technologien User32 (Benutzer-Interaktion im Windows-API), GDI32 (2D-Grafik im Windows-API) und das etwas neuere DirectX durch ein einheitliches System.
Grafische Skalierungsfähigkeit WPF zeichnet alle Steuerelemente und grafischen Elemente als Vektorgrafiken. Damit kann eine Programmoberfläche ohne Verluste in der Größe angepasst werden, z. B. an einen mobilen PC oder an einen 20-Zoll-Monitor. Windows.Forms-Steuerelemente und -Grafiken werden nicht als Vektorgrafik gezeichnet. Eine Vergrößerung oder Verkleinerung dieser Elemente bringt häufig einen Qualitätsverlust mit sich.
Lauffähigkeit in Webanwendungen XAML-Dokumente können nicht nur als Windows-Anwendung, sondern über Microsoft Silverlight auch in prinzipiell jedem Browser ausgeführt werden. Silverlight ist ein Plugin für verschiedene Standard-Browser, das eine verkleinerte Version des .NET Framework und eine WPF-Laufzeitumgebung enthält und das für verschiedene wichtige Betriebssysteme zur Verfügung steht. Damit ist es möglich, Webanwendungen mit einer sehr reichhaltigen Oberfläche zu entwickeln (was zuvor normalerweise lediglich über Adobe Flash oder komplizierte DHTML-Tricks möglich war). Windows.Forms bietet keine äquivalenten Möglichkeiten.
Performance WPF arbeitet mit DirectX. Alles, was in einer WPF-Anwendung auf dem Bildschirm ausgegeben wird, wird (bei Verfügbarkeit einer DirectX-fähigen Grafikkarte mit ausreichend Ressourcen) in DirectX-Objekte umgewandelt und über DirectX gezeichnet. Da moderne Grafikkarten DirectX direkt unterstützen, wird die CPU beim Zeichnen einer WPF-Oberfläche enorm entlastet, was das Zeichnen in der Regel beschleunigt. Windows.Forms basiert hingegen auf dem alten GDI, einem Teil des Windows-API, das grafische Elemente (also auch Steuerelemente) über Befehle der CPU zeichnet. WPF kann auf diesen alten Mechanismus zurückgreifen und nutzt ihn dann, wenn das System die benötigten DirectX-Features nicht unterstützt oder entsprechende Ressourcen zurzeit nicht zur Verfügung stehen.
728
Die Möglichkeiten, die Sie mit WPF haben
Trotz allem kann es sein, dass eine WPF-Anwendung langsamer ausgeführt wird als eine vergleichbare Windows.Forms-Anwendung. Das kann z. B. dann der Fall sein, wenn die Grafikkarte DirectX nicht oder nicht im vollen Umfang unterstützt wird oder wenn sehr viele komplexe Steuerelemente verwendet werden. Außerdem benötigt das Parsen von XAML-Dokumenten beim Start einer WPF-Anwendung Zeit, und die Umsetzung der XAML-Dokumente in ausführbaren CIL-Code ist nicht ganz so performant wie entsprechender Code in Windows.Forms. Besonders bei Fenstern mit sehr vielen Steuerelementen kann es also sein, dass eine WPF-Anwendung langsamer startet als eine vergleichbare Windows.Forms-Anwendung.
12.1.4
12
WPF oder Windows.Forms?
13
Da WPF noch eine relativ junge Technologie ist, und noch nicht sehr viele WPFAnwendungen auf dem Markt sind, ist die Frage, ob Sie für Ihre Anwendungen WPF oder Windows.Forms verwenden sollten, schwierig zu beantworten. Arbeitet eine Anwendung intensiv mit (verschiedenen) Medien, sollten Sie zu WPF greifen. In Geschäftsanwendungen kann es u. U. (im Vergleich mit der aktuellen ersten WPFVersion!) aber sinnvoller sein, die vielfältigeren (den »normalen« Benutzern bekannten) Standard-Features der Windows.Forms-Steuerelemente zu nutzen (wie z. B. das DataGridView zur Darstellung von listenförmigen Daten). Soll ein Teil der Anwendung allerdings auch über das Internet verfügbar sein, ist WPF wieder die bessere Wahl. Wenn Sie für ältere Systeme entwickeln, kann es auch sein, dass das .NET Framework 3.0 (das WPF beinhaltet) bzw. 3.5 für diese gar nicht zur Verfügung steht. Das .NET Framework 3.0 und 3.5 kann erst ab Windows XP Service Pack 2 installiert werden, wenn Sie also z. B. für Windows 2000 entwickeln, ist WPF keine Option.
14
15
16
Ich denke, dass WPF die Zukunft ist und dass Steuerelemente externer Hersteller oder die nächste Version von WPF die zurzeit noch bestehenden Mankos aufheben werden. Schauen Sie sich doch einfach die in diesem Kapitel beschriebenen Möglichkeiten von WPF und die im Online-Artikel www.juergen-bayer.net/artikel/csharp/windows. forms/windows.forms.aspx beschriebenen Möglichkeiten von Windows.Forms an, um Ihre Entscheidung zu erleichtern.
17
12.2
19
18
Die Möglichkeiten, die Sie mit WPF haben
Mit WPF können Sie Windows- und Webanwendungen erstellen. WPF bietet dabei verschiedene Möglichkeiten: ■
■
■
20
WPF-Desktopanwendungen: WPF-Desktopanwendungen werden direkt unter Windows ausgeführt. Die Verteilung solcher Anwendungen erfolgt über Copy&Paste oder über Installationsanwendungen. WPF-ClickOnce-Desktopanwendungen: WPF-ClickOnce-Desktopanwendungen werden ebenfalls direkt unter Windows ausgeführt. Die Verteilung erfolgt aber über ClickOnce. ClickOnce-Anwendungen werden in einem Webserver oder über einen Ordner im Intranet (oder auf DVD) zur Verfügung gestellt. Der Benutzer klickt zur Ausführung auf einen Internet-Link (z. B. auf einer Website oder in einer E-Mail), worauf die Anwendung bei Bedarf automatisch installiert wird. ClickOnce wird in Kapitel 16 behandelt. XAML-Browser-Anwendungen: XAML-Browser-Anwendungen (XBAPs) werden ebenfalls über ClickOnce verteilt, aber im Browser (zurzeit Internet Explorer ab Version 6) ausgeführt. Die Rechte solcher Anwendungen sind eingeschränkt.
21
22
23
729
WPF-Grundlagen
■
■
Außerdem benötigen Sie eine ständige Verbindung zum Server, der die Programme hostet und das .NET Framework 3.0. Loses XAML im Browser: Die XML-Dateien, die WPF-Oberflächen beschreiben (XAML-Dateien), können auch direkt im Browser geöffnet werden. Diese als loses XAML (Loose XAML) bezeichnete Technik erlaubt aber nicht die Verwendung eventuell vorhandener Programme für die XAML-Dateien. Trotzdem kann loses XAML interessant sein, da WPF die Integration von 3D, Animationen, Effekten, Sound und anderem ermöglicht. Silverlight-Anwendungen: Silverlight ist ein Browser-Plugin, das eine Untermenge von WPF und (ab der Version 2.0) eine Untermenge des .NET Framework (inkl. spezieller CLR) enthält. Silverlight ermöglicht es damit, WPFAnwendungen (mit eingeschränkten Rechten) in einem Browser auszuführen, ohne dass das .NET Framework zur Verfügung steht. Da Silverlight für verschiedene Browser und Betriebssysteme (kostenfrei) angeboten wird, ist diese Technologie sehr interessant für Webanwendungen mit einer (sehr) reichen Oberfläche. Silverlight ist wegen seiner vielen Möglichkeiten ein direkter Konkurrent von Adobe Flash.
WPF bietet wie gesagt nicht alle Möglichkeiten, die Windows.Forms bietet. Eine der von vielen vermissten Möglichkeiten sind MDI-Anwendungen2. WPF unterstützt MDI-Anwendungen deshalb nicht, weil diese auf Systemen mit mehreren Monitoren schlecht anwendbar sind. Sie können aber alternativ Windows.Forms-MDI-Anwendungen entwickeln und WPF-Elemente über die Interoperabilität integrieren.
12.3
WPF-Anwendungen in Visual Studio
Bevor ich näher auf WPF eingehe, zeige ich, wie Sie WPF-Anwendungen prinzipiell mit Visual Studio entwickeln. Damit können Sie dann die Beispiele dieses und des nächsten Kapitels besser nachvollziehen. Visual Studio bietet zur Entwicklung von WPF-Anwendungen eine eigene Projektvorlage (WPF-ANWENDUNG), die Sie beim Erstellen von neuen Projekten auswählen können. Ein damit erzeugtes Projekt beinhaltet bereits alles Grundlegende, das zur Ausführung einer WPF-Anwendung notwendig ist.
12.3.1
Das WPF-Projekt
Ein mit Visual Studio erzeugtes Projekt für eine WPF-Anwendung ist ein Projekt vom Ausgabetyp WINDOWS-ANWENDUNG, das zumindest die Assemblys System, PresentationCore, PresentationFramework und WindowsBase referenziert. Die Projektdatei einer WPF-Anwendung sieht etwas anders aus als die Projektdatei einer Windows.FormsAnwendung, die Unterschiede sind jedoch (zurzeit) über die Eigenschaften des Projekts in Visual Studio nicht einstellbar. Dieses Problem wird sichtbar, wenn Sie versuchen, ein Windows.Forms-Projekt zu einem WPF-Projekt umzubauen. In diesem Fall müssen Sie die Projektdatei nämlich von Hand anpassen, wie es Microsoft in dem Artikel »Walkthrough: Manually Creating a Windows Presentation Foundation Project Using Visual Studio« (msdn2.microsoft.com/en-us/library/ms742193.aspx) und Dirk Frischalowski in dem Artikel »WPF Tutorial« (www.gowinfx.de/WPF%20Tutorial/ index.html) beschreiben. Ich verzichte hier auf eine weitere Ausführung. 2
730
Anwendungen mit einem Hauptfenster und untergeordneten Fenstern, die den Bereich des Hauptfensters nicht verlassen können, ähnlich Word in der Ansicht, die alle Dokumente in einer Word-Instanz verwaltet.
WPF-Anwendungen in Visual Studio
Ein minimales WPF-Projekt enthält zwei XAML-Dateien: die Datei App.xaml und eine XAML-Datei, die das Fenster beschreibt, mit der die Anwendung startet. App.xaml steht für die WPF-Anwendung. Eine solche Datei ist für WPF nicht zwingend notwendig, da WPF-Dokumente auch direkt ausgeführt werden können. Eine Anwendungs-XAML-Datei ermöglicht aber den Zugriff auf die WPF-Anwendung (und damit auch die Reaktion auf Ereignisse, die die Anwendung betreffen) und spezielle Anwendungs-Features wie eingebettete Ressourcen.
App.xaml repräsentiert die WPFAnwendung
12
Die Anwendungs-XAML-Datei eines mit Visual Studio erzeugten WPF-Projekts sieht prinzipiell folgendermaßen aus: Listing 12.4:
13
Die App.xaml-Datei eines Visual-Studio-WPF-Projekts
14
15
Im Attribut x:Class (die Bedeutung dieses Attributs und besonders des Präfixes x: wird im Abschnitt »XAML« geklärt) wird eine Klasse angegeben, die als partielle Klasse der andere Teil der mit App.xaml erzeugten Klasse ist. Diese Klasse finden Sie im Projektmappen-Explorer unterhalb des App.xaml-Eintrags. Die Klasse ist vorgesehen für alle Programme, die Sie für die Anwendungs-XAML-Datei implementieren (was in der Praxis allerdings selten notwendig ist). Hier können Sie z. B. eine Methode implementieren, die mit dem Activated-Ereignis der Anwendung verknüpft ist (das immer dann aufgerufen wird, wenn die Anwendung unter Windows in den Vordergrund geholt wird).
16
17
Die Attribute, deren Name mit xmlns beginnt, geben XML-Namensräume an, die für XAML essenziell sind. Näheres dazu erfahren Sie ebenfalls im Abschnitt »XAML«. Das Attribut StartupUri ist zunächst das wichtigste. Dieses Attribut verwaltet den Namen einer XAML-Datei, die beim Start der Anwendung automatisch instanziert und geöffnet werden soll. An diesem Attribut geben Sie üblicherweise den Namen des Startfensters der Anwendung (im Beispiel ist das Window1.xaml) an.
18 StartupUri gibt die Start-XAML-Datei an
Das XML-Element Application.Resources ist schließlich für anwendungsweite Ressourcen wie z. B. eingebettete Bilder vorgesehen. WPF-Ressourcen werden in Kapitel 14 behandelt. Neben der App.xaml-Datei enthält ein WPF-Projekt (natürlich) zumindest noch eine XAML-Datei, die das Startfenster der Anwendung definiert. In der Voreinstellung ist dieses das Fenster Window1.xaml, das folgendermaßen definiert ist: Listing 12.5:
19
20 Eine weitere XAML-Datei definiert das Startfenster
21
Das von Visual Studio in einem WPF-Projekt erzeugte Start-Fenster
22
23
731
WPF-Grundlagen
Wie Sie sehen, erfolgt die Definition eines WPF-Fensters in einem Window-Element. Die Attribute ähneln denen des App-Elements der App.xaml-Datei. Hinzugekommen sind weitere Attribute, wie Title (der Titel des Fensters), Height (die Höhe) und Width (die Breite). Im Window-Element ist ein Grid-Element angegeben, das in WPF ein Gitter definiert und normalerweise dazu vorgesehen ist, alle weiteren Steuerelemente aufzunehmen. Der Abschnitt »XAML« geht näher darauf ein. Die folgende Auflistung fasst die in einem WPF-Projekt integrierten Dateien zusammen: ■
■
■
■
■
■
Properties\AssemblyInfo.cs: In dieser Datei können Sie allgemeine Informationen zur später erzeugten Assembly für Ihr Projekt unterbringen. Dazu gehören z. B. der Titel der Assembly, eine Beschreibung, das Copyright und die Versionsnummer. Die hier eingegebenen Daten werden beim Kompilieren in die Metadaten der Assembly übernommen und können z. B. über die Eigenschaften der Assembly-Datei im Windows-Explorer ausgelesen werden. Properties\Resources.resx: Diese Datei ist eine Ressourcen-Datei, die von Visual Studio zur Speicherung verwendet wird, die Sie über die integrierte Ressourcenverwaltung hinzufügen. Näheres zu diesen nicht nur zu WPF gehörenden Ressourcen finden Sie in Kapitel 15. Properties\Settings.settings: Bei dieser Datei handelt es sich um eine Datei, in der Visual Studio Einstellungen verwaltet, die Sie für Ihre Anwendung über die in Visual Studio integrierte Einstellungsverwaltung hinzugefügt haben. Näheres dazu finden Sie ebenfalls in Kapitel 15. Verweise: Dieser Ordner, der nicht physikalisch vorhanden ist, zeigt alle Assembly-Referenzen des Projekts an und ermöglicht das Löschen und Hinzufügen von Referenzen. App.xaml: Diese XML-Datei enthält eine Basis-Definition des ApplicationObjekts einer WPF-Anwendung, über das grundlegende Anwendungsdienste zur Verfügung gestellt werden. Window1.xaml: Diese Datei speichert die XAML-Definition eines von Visual Studio bereits in das Projekt eingefügten Fensters. Windows-Anwendungen besitzen normalerweise ein Fenster, mit dem die Anwendung startet. Das von Visual Studio eingefügte Fenster ist für diesen Zweck vorgesehen.
12.3.2
Grundlagen zum Erzeugen eines neuen WPF-Projekts
Ein neues WPF-Projekt erzeugen Sie in Visual Studio prinzipiell wie jedes andere. Wählen Sie im Dialog zur Erzeugung eines neuen Projekts die Projektvorlage WPF-ANWENDUNG und vergeben Sie einen sinnvollen Namen. Wenn Sie dem Beispiel dieses Abschnitts folgen wollen, nennen Sie das Projekt NetCalculator (ja, das wird ein Projekt mit einer ähnlichen Bedeutung wie das entsprechende Beispielprojekt aus Kapitel 2 …).
732
WPF-Anwendungen in Visual Studio
Als Erstes sollten Sie nun den Namen des Startfensters (Window1.xaml) ändern. Leider ist dazu einiges an Handarbeit notwendig: 1.
2.
Nennen Sie zuerst die Datei um (vielleicht in MainWindow.xaml). Visual Studio nennt automatisch die mit der XAML-Datei verknüpfte C#-Datei um, vergisst allerdings, Entsprechendes mit der in dieser Datei enthaltenen Klasse zu tun. Da noch einige weitere Änderungen notwendig sind, ersetzen Sie nun über den Visual-Studio-ersetzen-Dialog ((STRG) + (H)) »Window1« im gesamten Projekt (!) durch den neuen Namen (»MainWindow«). Achten Sie darauf, dass unter SUCHEN IN der Eintrag GESAMTES PROJEKT ausgewählt ist und dass Sie die Option AUSGEBLENDETEN TEXT DURCHSUCHEN eingeschaltet haben (Abbildung 12.1).
STEPS
12 13 Abbildung 12.1: Ersetzen des Namens des Fensters, mit dem ein WPF-Projekt startet
14
15
16
17
18
Dieses etwas komplizierte Vorgehen ist leider deswegen notwendig, weil Visual Studio die notwendigen Anpassungen für WPF-Projekte nach der Änderung des Dateinamens leider nicht automatisch ausführt.
19
Listing 12.6 zeigt die entsprechend angepassten Stellen im XAML- und im C#-Code: Listing 12.6:
20
Die Änderungen nach dem Ersetzen
/* App.xaml */
21
22
... /* MainWindow.xaml */ /* MainWindow.xaml.cs */ public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } }
Nach dem Ändern sollten Sie das Projekt einmal unter Visual Studio ausführen, um zu testen, ob alles in Ordnung ist. Ein reines Kompilieren reicht dazu nicht aus, weil WPF teilweise in der Laufzeit interpretiert wird und z. B. ein falsch angegebener Name im StartupUri-Attribut des App-Elements erst bei der Ausführung zu einem Fehler führt.
TIPP
Da das Ändern des Namens des Startfensters einer WPF-Anwendung eine nervige Angelegenheit ist, habe ich eine Projektvorlage erzeugt, die zu einem WPF-Projekt führt, dessen Startfenster bereits MainWindow heißt. Ein weiteres Feature dieser Vorlage ist, dass der Name des Projekts als Titel des Startfensters eingetragen ist. Sie finden diese Vorlage auf der Buch-DVD im Ordner Visual-Studio-Projektvorlagen unter dem Namen WPF-Anwendung mit benanntem Startfenster.zip. Kopieren Sie diese Datei in den Ordner Visual Studio 2008\Templates\ProjectTemplates\Visual C#\ in Ihrem Eigene-Dateien-Ordner, um die Vorlage zur Verfügung zu haben. Im Dialog zum Erstellen oder Hinzufügen eines neuen Projekts finden Sie die Vorlage unter EIGENE VORLAGEN unter dem Namen WPF-ANWENDUNG II.
12.3.3
Einstellung der Eigenschaften
Zur Einstellung der Eigenschaften bietet der WPF-Designer in Visual Studio drei Möglichkeiten: Sie können diese direkt im XAML-Code einstellen oder über das Eigenschaftenfenster. Die Eigenschaften, die die Größe und die Position von Steuerelementen betreffen, können Sie natürlich auch im Designer-Fenster einstellen, indem Sie das Objekt mit der Maus verändern.
INFO
Bevor Sie mit dem WPF-Designer in Visual Studio beginnen, eine Anmerkung: Der WPF-Designer im ersten Release von Visual Studio ist noch nicht besonders ausgereift. Neben fehlenden Features (wie der Möglichkeit, Ereignisse über das Eigenschaftenfenster zuzuweisen) ist die Arbeit mit der ersten Version des Visual-StudioWPF-Designers an vielen Stellen noch sehr hakelig. Das speziell für das Design von WPF-Anwendungen vorgesehene Werkzeug Microsoft Expression Blend (das einen anderen Ansatz verwendet) ist da wesentlich ausgereifter. Microsoft plant aber, den WPF-Designer von Visual Studio zu verbessern. Es kann also sein, dass Ihre Version in einigen Punkten anders arbeitet als die erste, die ich in diesem Kapitel beschreibe. In diesem Zusammenhang: Vielen Dank an Microsoft, dass Visual Studio 2008 nicht komplett fertig ausgeliefert wurde, sondern mit einem schlechten WPF-Designer und fehlenden (aber nachzuliefernden) wichtigen Werkzeugen wie das ADO.NET Entity Framework. Ein paar Monate Warten wäre (wenigstens für mich als Buchautor) besser gewesen …
734
WPF-Anwendungen in Visual Studio
Bei der Veränderung der Größe eines Fensters müssen Sie ein wenig aufpassen, dass Sie wirklich das Fenster vergrößern, und nicht das in dem Fenster enthaltene GridSteuerelement. Aber das werden Sie schon merken ☺. Falls es Ihnen einmal passiert: Entfernen Sie die Eigenschaften Width und Height aus dem XAML-Code des GridSteuerelements, damit sich dieses wieder automatisch an die Größe seines Containers (des Fensters) anpasst.
INFO
12
In der Voreinstellung zeigt Visual Studio ein WPF-Fenster gleichzeitig in der XAMLund der Design-Ansicht an(Abbildung 12.2). Abbildung 12.2: Der WPF-Designer zeigt in der Voreinstellung die Design- und die XAML-Ansicht an
13
14
15
16
17
18 Sie können die Ansicht aber auch flexibel umstellen, was aber nicht über das ANSICHT-Menü geschieht, sondern über Symbole im Designer:
19
20
Das linke dieser Symbole führt zu einer vertikalen Aufteilung der Ansichten. Das mittlere Symbol führt zu der horizontalen Aufteilung, die in Abbildung 12.2 dargestellt wird. Das rechte Symbol führt dazu, dass die Bereiche übereinander dargestellt werden und über Reiter erreichbar sind (wie dies z. B. bei Windows.Forms-Anwendungen üblich ist). Diese Ansicht eignet sich besser zur Entwicklung großer Fenster.
21
Wenn Sie die Eigenschaften eines Objekts direkt im XAML-Code einstellen, übernimmt Visual Studio Ihre Änderungen (natürlich) auch in die Design-Ansicht des Fensters. Probieren Sie dies aus, indem Sie die Eigenschaft Title auf »Netto-Rechner« ändern.
22
Sie können sogar direkt in XAML auch neue Steuerelemente hinzufügen (wenn Sie den entsprechenden XAML-Code kennen). Einfacher ist aber das Hinzufügen über die Toolbox.
23
735
WPF-Grundlagen
Zur Einstellung der Eigenschaften können Sie in XAML IntelliSense nutzen. Probieren Sie dies aus: Erweitern Sie das Window-Element um die Eigenschaft Background, indem Sie ein neues Attribut hinzufügen, dessen Namen Sie mit »Ba« zu schreiben beginnen. IntelliSense schlägt die Background-Eigenschaft als passend vor (Abbildung 12.3). Abbildung 12.3: IntelliSense beim Hinzufügen einer Eigenschaft in XAML
IntelliSense endet aber in XAML nicht bei der Auswahl der Eigenschaften. Auch für deren Inhalt stellt IntelliSense passende Werte zur Verfügung, sofern dies möglich ist. Bei der Background-Eigenschaft werden z. B. die vordefinierten Farben vorgeschlagen (auf die Sie aber nicht eingeschränkt sind). IntelliSense erleichtert damit die direkte Arbeit in XAML erheblich. Abbildung 12.4: IntelliSense funktioniert auch bei den Werten vieler Eigenschaften
Wählen Sie eine Farbe für den Hintergrund des Fensters aus, die Ihnen gefällt. Ich wähle so etwas wie den Windows-Standard, nämlich LightGray. Nach der Änderung sollte das Fenster in der Design-Ansicht in einem hellen Grau erscheinen (ich weiß, Grau ist langweilig. Meine Wohnungen – in Deutschland und Irland – sind auch alles andere als grau. Eher rot-orange. Aber ein grauer Hintergrund für ein Fenster ist nun mal für die Augen angenehmer als ein bunter). Die Ansicht im Designer entspricht übrigens (leider) nicht genau der Ansicht, die die laufende Anwendung bietet. Das Eigenschaftenfenster bietet einen hilfreichen Filter
736
Alternativ können Sie wie gesagt die Eigenschaften von Fenstern oder deren Elementen auch über das Eigenschaftenfenster einstellen, was prinzipiell so geht wie bei Windows.Forms-Anwendungen (siehe Kapitel 2). Wählen Sie dazu das Objekt im Designer oder stellen Sie den Eingabecursor in das entsprechende Element im XAML-Editor und stellen Sie die benötigte Eigenschaft im Eigenschaftenfenster ein.
WPF-Anwendungen in Visual Studio
Beim WPF-Fenster müssen Sie darauf achten, dass Sie für das Fenster und das enthaltene Grid-Steuerelement separate Einstellungen vornehmen können. Das Eigenschaftenfenster des WPF-Designers bietet leider nur eine kategorische Ansicht, dafür aber einen sehr hilfreichen Filter, den Sie über das Suchen-Eingabefeld verwenden können. Wenn Sie in diesem Feld z. B. »Ba« eingeben, erhalten Sie alle Eigenschaften, deren Name »Ba« enthält. So finden Sie die Background-Eigenschaft sehr schnell.
12.3.4
12
Die Gestaltung der Oberfläche
Zur Gestaltung der Oberfläche bietet WPF einige Steuerelemente, die Sie wie bei Windows.Forms in der Toolbox finden. Diese Steuerelemente ziehen Sie aus der Toolbox auf das Fenster. Für das Nettoberechnungs-Beispiel benötigen Sie drei Label-, drei TextBox- und zwei Button-Steuerelemente (Abbildung 12.5).
WPF arbeitet ähnlich Windows. Forms mit Steuerelementen Abbildung 12.5: Das Beispiel-WPFFenster
13
14
15
16 Das Positionieren der Steuerelemente ist unter WPF leider nicht allzu einfach. Der Designer bietet zwar Informationen über den Abstand zum vorherigen bzw. nächsten Steuerelement, aber leider keine so gute automatische Ausrichtung wie unter Windows.Forms. Um einigermaßen vernünftig positionieren zu können, sollten Sie die per Voreinstellung viel zu groß eingestellte Höhe der Label zunächst reduzieren. Stellen Sie dazu die Eigenschaft Height auf 23 ein (die Höhe einer TextBox). Dies erleichtert zum einen das Ausrichten der Label untereinander (z. B. mit einem Rand von 4) und zum anderen das Ausrichten an den TextBox-Steuerelementen.
17
18
19
Beim Platzieren der Steuerelemente fallen Ihnen wahrscheinlich gleich Pfeile und Punkte auf, die der Designer an den Steuerelementen zeichnet. Abbildung 12.6: Der WPF-Designer zeigt die Anheftung der Steuerelemente an
20
21
22 Die Pfeile zeigen, an welchen Positionen ein Steuerelement angeheftet ist. Im Beispiel in Abbildung 12.6 ist das für die erste TextBox links und oben. Ein so angeheftetes Steuerelement bleibt immer an seiner Position (von links oben aus gesehen) und wird nicht automatisch vergrößert oder verkleinert, wenn das Fenster (bzw. das Steuerelement, auf dem das andere angelegt ist) in der Größe verändert wird.
Der WPF-Designer hilft bei der Anheftung von Steuerelementen
737
23
WPF-Grundlagen
Wenn Sie auf einen Punkt klicken, erzeugen Sie damit eine neue Anheftung. So können Sie z. B. auf den Punkt rechts vom Steuerelement klicken, um dieses an der rechten Position ebenfalls anzuheften (Abbildung 12.7). Abbildung 12.7: TextBox, die links, oben und rechts angeheftet ist
Ein so angeheftetes Steuerelement wird in der Position nicht geändert, wenn die Größe des Fensters verändert wird. Die Breite passt sich aber entsprechend so an, dass der Zwischenraum zwischen dem rechten Rand des Steuerelements und dem rechten Rand des Containers immer gleich bleibt. Wenn Sie auf den Pfeil am Steuerelement klicken, heben Sie eine Anheftung wieder auf. Für die Anheftungen gilt: ■ ■
■ ■ ■
Setzen Sie nur Anheftungen für oben und links, wird das Steuerelement nicht verschoben und nicht in der Größe verändert. Setzen Sie nur Anheftungen für oben und rechts, wird das Steuerelement bei einer Größenänderung des Fensters nach rechts bzw. links verschoben. Es bleibt also am rechten Rand des Fensters angeheftet. Setzen Sie nur Anheftungen für unten und links und/oder rechts, bleibt das Steuerelement am unteren Rand angeheftet. Setzen Sie Anheftungen für links und rechts, wird das Steuerelement bei einer Größenänderung des Fensters in der Breite entsprechend angepasst. Setzen Sie Anheftungen für oben und unten, wird das Steuerelement bei einer Größenänderung des Fensters in der Höhe entsprechend angepasst.
Wenn Sie übrigens keine Anheftung definieren und das Steuerelement im Entwurf in der Mitte des Containers platzieren, bleibt es immer in der Mitte. Probieren Sie das einfach einmal aus. Die Anheftung funktioniert bereits, wenn Sie das Fenster im Designer in der Größe verändern.
HALT
738
Leider erzeugt der WPF-Designer die Anheftung neuer Steuerelemente nicht immer nach dem Standard (oben links) und verändert diese auch beim Verschieben von Steuerelementen mit der Maus. Wenn das Steuerelement dem rechten Rand nahe ist, wird zusätzlich eine rechte Anheftung erzeugt. Ist das Steuerelement dem unteren Rand nahe, wird zusätzlich eine untere Anheftung erzeugt. Dies ist besonders dann sehr nervig, wenn der Designer beim Verschieben ungefragt die Anheftung verändert und Sie dies nicht bemerken. Außerdem ist das Erzeugen und Löschen von Anheftungen über die Maus eine sehr diffizile Angelegenheit, die auch schon einmal ungewollt ausgeführt werden kann. Dieses eigenartige Verhalten des Designers führt dazu, dass mehr Anheftungen weggeklickt werden müssen als erzeugt. Außerdem laufen Sie Gefahr eine Anwendung auszuliefern, deren Steuerelemente sich bei Größenänderung des Fensters sehr eigenartig verhalten.
WPF-Anwendungen in Visual Studio
Nutzen Sie die Möglichkeit, ein WPF-Fenster im Designer über die auf der linken Seite angezeigte Zoom-Leiste zu skalieren. Neben der Tatsache, dass Sie sehen, dass WPFGrafiken verlustfrei skaliert werden können, macht ein Fenster in einer Ansicht mit mehr als 100% Skalierung die Arbeit mit Steuerelementen häufig einfacher.
TIPP
Wenn Sie alle Steuerelemente platziert haben, stellen Sie deren Eigenschaften ein. Der Inhalt eines Steuerelements wird unter WPF häufig in der Eigenschaft Content verwaltet. Für Label und Button-Steuerelemente stellen Sie die Beschriftung also in dieser Eigenschaft ein. Den Namen der Steuerelemente sollten Sie natürlich ebenfalls ändern. Dazu bietet das WPF-Eigenschaftenfenster ein separates Eingabefeld im oberen Bereich. Ich verwende wie in Kapitel 2 die folgenden Namen: ■ ■ ■ ■ ■ ■ ■ ■
12 13
Erstes Label: lblGross Zweites Label: lblTax Drittes Label lblNet Erste TextBox: txtNet Zweite TextBox: txtTax Dritte TextBox: txtGross Erster Button: btnCalculate Zweiter Button: btnClose
14
15
16
Stellen Sie zudem die Eigenschaft IsReadOnly der TextBox txtGross auf true, damit in diese keine Eingaben erfolgen können. Der Rest ist einfache Programmierung …
17
12.3.5
Die Programmierung des Beispiels
Die Programmierung des Beispiels entspricht im Wesentlichen der Programmierung des Windows.Forms-Äquivalents in Kapitel 2. Das Berechnen programmieren Sie im Click-Ereignis des RECHNEN-Schalters, das Schließen im Click-Ereignis des SCHLIESSEN-Schalters. WPF arbeitet in der Zuweisung der Ereignisse aber anders als Windows.Forms. Leider können Sie diese nicht über das Eigenschaftenfenster zuweisen, sondern lediglich in XAML. Das prinzipielle Vorgehen ist das folgende: 1. 2. 3.
Ereignisse können leider nur in XAML zugewiesen werden
18
19 STEPS
20
Schreiben Sie den Namen des Ereignisses als Attribut in dem XAML-Element, das das Steuerelement definiert. Schreiben Sie den Zuweisungsoperator. Wählen Sie die Option , die Ihnen der WPF-Designer zur Verfügung stellt, um eine neue Ereignisbehandlungsmethode zu erzeugen.
21
Abbildung 12.8: Der WPF-Designer bietet in XAML an, einen Ereignishandler zu erzeugen
739
22
23
WPF-Grundlagen
Die Ereignisbehandlungsmethode wird in der C#-Datei erzeugt, die als zweiter Teil der partiellen Klasse neben der XAML-Datei verwaltet wird. Über den Befehl ZUM EREIGNISHANDLER NAVIGIEREN des Kontextmenüs des Eintrags öffnen Sie die C#-Datei und navigieren direkt zu der Ereignisbehandlungsmethode. Leider ist das Navigieren nicht über (F12) möglich. Das Standardereignis der einzelnen Steuerelemente erreichen Sie auch einfacher, indem Sie auf dem Steuerelement doppelklicken. Das hätte ich auch gleich sagen können. Machte aber Spaß, Sie ein wenig zu ärgern ☺. Beim Programmieren der Berechnung beziehen Sie sich wie in Windows.Forms auf die Text-Eigenschaft der TextBox-Steuerelemente. Das Schließen programmieren Sie ebenfalls wie bei Windows.Forms über die Close-Methode des Fensters. Der resultierende Quellcode ist dem des Beispiels in Kapitel 2 sehr ähnlich. Der einzige Unterschied ist, dass Sie unter WPF eine andere MessageBox verwenden als unter Windows.Forms. Listing 12.7:
Die Ereignisbehandlungsmethoden der Schalter des Netto-Rechners
/* Ereignishandler für das Click-Ereignis des Rechnen-Schalters */ private void btnCalculate_Click(object sender, RoutedEventArgs e) { // Deklaration von Variablen für die Berechnung double gross, tax, net; // Die Eingaben überprüfen und gleichzeitig in double-Werte // konvertieren und in die dafür vorgesehenen Variablen schreiben if (double.TryParse(this.txtGross.Text, out gross)) { if (double.TryParse(this.txtTax.Text, out tax)) { // Beide Eingaben sind in Ordnung, // also kann gerechnet werden // Den Nettowert berechnen net = gross * 100 / (100 + tax); // Den berechneten Wert in die Netto-TextBox schreiben this.txtNet.Text = net.ToString(); } else { // Die Steuer-Eingabe ist nicht OK this.txtGross.Text = null; MessageBox.Show("Der Steuerwert ist ungültig", "Netto-Rechner", MessageBoxButton.OK, MessageBoxImage.Exclamation); } } else { // Die Brutto-Eingabe ist nicht OK this.txtGross.Text = null; MessageBox.Show("Der Bruttobetrag ist ungültig", "Netto-Rechner", MessageBoxButton.OK, MessageBoxImage.Exclamation); } } /* Ereignishandler für das Click-Ereignis des Schließen-Schalters */ private void btnClose_Click(object sender, RoutedEventArgs e) { this.Close(); }
Zur Vollständigkeit zeigt Abbildung 12.9 das Beispiel in Aktion.
740
WPF-Anwendungen in Visual Studio
Abbildung 12.9: Der WPF-NettoRechner
12 12.3.6 Debuggen von WPF-Anwendungen 13
WPF-Anwendungen debuggen Sie natürlich prinzipiell genau wie alle anderen. WPF hat aber einige Besonderheiten, die ich hier kurz beschreibe.
XAML-Probleme
14
Das erste Problem beim Debuggen ist, dass XAML-Code erst in der Laufzeit ausgeführt wird. Das führt schon einmal dazu. dass viele Fehler erst dann erkannt werden, wenn das Programm ausgeführt wird. Visual Studio erkennt zwar die meisten Fehler bereits beim Kompilieren, in einigen Fällen wird aber fehlerhafter XAML-Code in die kompilierte Assembly integriert und führt bei der Ausführung anschließend zu Problemen. Gegen dieses Problem können Sie nicht viel machen, außer dass Sie darauf achten, dass Ihr XAML-Code in Ordnung ist.
XAML wird in der Laufzeit ausgeführt
Die XamlParseException
Die in WPF übliche XamlParseException erschwert das Debuggen
Das zweite Problem ist, dass in WPF immer dann eine XamlParseException geworfen wird, wenn der XAML-Code Fehler enthält. Das ist z. B. dann der Fall, wenn Sie im Konstruktor eines WPF-Fensters programmieren und das Programm eine Ausnahme hervorruft. In größeren WPF-Anwendungen ist aber auch die Wahrscheinlichkeit groß, dass der XAML-Code nicht in Ordnung ist.
15
16
17
18
Das Problem der XamlParseException ist, dass deren direkte Meldung eigentlich nichts aussagt. Abbildung 12.10: Die unter WPF typische XamlParseException mit ihrem wenig sagenden Fehlercode
19
20
21
22
23
Eine wichtige Technik uzur Auswertung solcher Fehler ist die Analyse der inneren Ausnahmen der XamlParseException auszuwerten. Klicken Sie dazu auf den Link DETAILS ANZEIGEN… und öffnen Sie im erscheinenden Schnellansicht-Dialog die inneren Ausnahmen, bis Sie zu einer sprechenden Fehlermeldung kommen (Abbildung 12.11).
741
WPF-Grundlagen
Abbildung 12.11: Die innerste Ausnahme einer XamlParseException
Der Fehler, dass InitializeComponent nicht existiert Ein häufiger Fehler in WPF-Projekten ist der Kompilierfehler »Der Name "InitializeComponent" ist im aktuellen Kontext nicht vorhanden« am Aufruf der InitializeComponent-Methode. Dieser Fehler wird in den folgenden Fällen erzeugt (was allerdings eine möglicherweise unvollständige Auflistung ist): ■ ■ ■ ■
Wenn die Eigenschaft BUILDVORGANG der XAML-Datei nicht auf Page oder ApplicationDefinition (für die Anwendungs-XAML-Datei) steht, wenn die im x:Class-Attribut angegebene Teilklasse nicht existiert (weil diese z. B. umbenannt wurde), wenn das x:Class-Attribut fehlt, wenn die in x:Class angegebene Klasse nicht für WPF korrekt definiert ist (als partielle, öffentliche Klasse). Das ist z. B. dann der Fall, wenn die Teilklasse generisch oder abstrakt ist.
Debuggen über den Mole-Visualisierer Mole evaluiert WPF-Objekte in Visual Studio
DISC
Der Visualisierer Mole ist ein hervorragendes Werkzeug zur Evaluierung von WPFAnwendungen beim Debuggen in Visual Studio. Er zeigt die beiden Element-Bäume an (siehe »Logische und visuelle Bäume«, Seite 765), die eine WPF-Anwendung ausmachen, zeigt die Werte aller Eigenschaften des aktuell ausgewählten Elements, ermöglicht die Änderung der Werte etc. Mole ist aber nicht auf WPF-Anwendungen eingeschränkt, sondern kann auch für Windows.Forms- und ASP.NET-Anwendungen verwendet werden. Sie finden die zum Zeitpunkt des Schreibens dieser Zeilen aktuelle Version von Mole auf der Buch-DVD im Ordner Visual-Studio-Tools\Visualizer. Im Internet finden Sie Mole an der Adresse karlshifflett.wordpress.com/mole-for-visual-studio. Kopieren Sie den Visualisierer in den Ordner Visual Studio 2008\Visualizers im Eigene-Dateien-Ordner. Visual Studio sollte Mole dann für Referenzen auf WPFObjekte (z. B. this in einer Page- oder Window-Klasse) anbieten. Nutzen Sie dabei die Tatsache, dass Sie Visualisierer auch im Lokal-, Direkt- und im Überwachungsfenster aufrufen können. Schauen Sie sich die Silverlight-Videos auf der Mole-Seite an, um Mole besser kennen zu lernen.
742
WPF-Anwendungen in Visual Studio
Debuggen über Snoop Ein sehr nützliches Tool zum Debuggen von WPF-Anwendungen ist Snoop (www.blois.us/Snoop). Dieses Tool, das WPF-Anwendungen in der Laufzeit analysiert, zeigt nicht nur den Element-Baum von WPF-Anwendungen an, sondern auch, welche gerouteten Ereignisse (Seite 777) aufgerufen wurden, und eine gezoomte Elementansicht etc. Um Snoop zu verwenden, laden Sie das Projekt herunter, kompilieren dieses und starten Snoop dann über die .exe-Datei im Ordner build\debug (oder build\release). Im Hauptfenster von Snoop werden alle laufenden WPF-Anwendungen angezeigt. Wählen Sie eine aus und betätigen Sie den SNOOP-Schalter. Snoop öffnet daraufhin ein separates Fenster, in dem Sie den visuellen Baum debuggen können. Ein besonderes Feature ist, dass Snoop Elemente im visuellen Baum selektiert, wenn Sie mit der Maus über das WPF-Element in der Anwendung fahren, während Sie (STRG) + (ª) betätigen. Ich will aber nicht verschweigen, dass ich mit Snoop auch Probleme hatte. Unter meiner Windows-XP-Installation hat Snoop weder auf eine Betätigung des MAGNIFY- noch des SNOOP-Schalters reagiert. Unter Vista hatte ich allerdings keine Probleme. Die Ursache für dieses Problem konnte ich nicht herausfinden. Möglicherweise weist die aktuelle Snoop-Version diesen Fehler auch nicht mehr auf.
Über Snoop können Sie laufende WPFAnwendungen evaluieren
12 13
14 INFO
15
WPF-Tracing-Quellen 16
WPF nutzt das .NET-Tracing-System, um Informationen darüber auszugeben, was gerade passiert. Aufgetretene Fehler werden per Voreinstellung im Debug-Modus in die Debugausgabe von Visual Studio geschrieben. Diese Meldungen können dann sehr hilfreich sein, wenn die Anwendung nicht funktioniert, aber keine Ausnahme auftritt (was beim WPF häufig, z. B. bei Fehlern bei der Datenbindung, der Fall ist).
17
Wenn eine Anwendung jedoch viele Nachrichten in das Trace-Protokoll schreibt, kann es problematisch werden, die WPF-Fehlermeldungen zu lokalisieren. In diesem Fall wäre es besser, das Trace-Protokoll in eine Datei zu schreiben, was Sie über die Konfiguration erreichen können. Grundlagen zum Tracing habe ich in Kapitel 9 behandelt. Schauen Sie noch einmal dort nach, wenn Sie das Tracing konfigurieren wollen.
18
WPF erzeugt einige Tracing-Quellen, die Sie in der Konfiguration angeben können. Die Namen der Tracing-Quellen entsprechen den wichtigen WPF- Namensräumen:
19
■ ■ ■ ■ ■
■ ■
System.Windows.Data: Sendet Nachrichten, die die Datenbindung betreffen. System.Windows.DependencyProperty: Liefert lediglich Nachrichten über die Registration von Abhängigkeitseigenschaften. System.Windows.Documents: Liefert Informationen über Fehler beim Formatieren von Dokumenten. System.Windows.Freezable: Liefert Informationen zu Freezable-Instanzen (Objekten, die »eingefroren« werden können), die nicht zu einer Ausnahme führen. System.Windows.Interop.HwndHost: Sendet Nachrichten, die mit dem Hosten eines normalen Windows-Fensters innerhalb einer WPF-Anwendung zu tun haben, wie es z. B. bei der Integration von Windows.Forms-Steuerelementen der Fall ist. System.Windows.Markup: Liefert Informationen, die das Laden von XAML oder BAML (siehe Seite 748 und Seite 751) betreffen. System.Windows.Media.Animation: Sendet Nachrichten, die Animationen betreffen.
20
21
22
23
743
WPF-Grundlagen
■ ■
■
System.Windows.NameScope: Liefert Informationen, wenn ein Name registriert wird. System.Windows.ResourceDictionary: Sendet Nachrichten, wenn WPF-Ressourcen registriert, abgerufen oder gelöscht werden. WPF-Ressourcen werden in Kapitel 14 besprochen. System.Windows.RoutedEvent: Sendet Nachrichten, die das Routen von Ereignissen betreffen (siehe Seite 777).
Wenn Sie z. B. die Nachrichten der Datenbindung abfangen wollen, geben Sie in der Konfiguration die Tracing-Quelle System.Windows.Data an: Listing 12.8:
Konfiguration des WPF-Tracing zum Abfangen von Datenbindungsnachrichten in einer Datei
Das Tracing kann auch im Programm konfiguriert werden
Alternativ zur Anwendungskonfiguration können Sie die einzelnen WPF-TracingQuellen auch im Programm konfigurieren. Dazu verwenden Sie einige statische Eigenschaften der PresentationTraceSources-Klasse, die die einzelnen TracingQuellen repräsentieren. So können Sie z. B. die Tracing-Quelle für die Datenbindung auch im Programm (oder im Direktfenster) konfigurieren: Listing 12.9:
Konfigurieren des WPF-Tracing (für die Datenbindung) im Programm
// Konfigurieren des WPF-Tracing für die Datenbindung PresentationTraceSources.DataBindingSource.Listeners.Add( new TextWriterTraceListener("WPF-Databinding.log")); #if DEBUG PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.All; // Alle Nachrichten ausgeben #else PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Warning; // Nur Warnungen und Fehler ausgeben #endif
INFO
744
Beachten Sie, dass im Beispiel nur die Tracing-Quelle für die Datenbindung konfiguriert wird. Wollen Sie z. B. auch Animationsfehler verfolgen, müssen Sie weitere Tracing-Quellen konfigurieren (z. B. die, die Sie über die Eigenschaft AnimationSource erreichen).
WPF-Anwendungen in Visual Studio
Weitere Informationen zur Arbeit mit den WPF-Tracing-Quellen finden Sie an den Adressen blogs.msdn.com/mikehillberg/archive/2006/09/14/WpfTraceSources.aspx und www.beacosta.com/blog/?p=52. Mike Hillenberg beschreibt allerdings in seinem Blog, dass Sie zum Aktivieren des WPF-Tracing einen Registry-Eintrag setzen müssen. Ich bin mir nicht sicher, ob das mit .NET 3.5 noch notwendig ist. Außerdem ruft Mike Hillenberg bei der Konfiguration im Programm die Refresh-Methode der PresentationTraceSources-Klasse auf, bevor er Trace-Listener hinzufügt. In meinen Versuchen führte dies allerdings dazu, dass das Tracing nicht mehr funktionierte.
REF
12
Weitere Tipps zum Debuggen
13
Weitere Tipps wie das Debuggen der Datenbindung nenne ich in den Abschnitten, die das entsprechende Thema beinhalten.
12.3.7
Die Architektur einer mit Visual Studio erzeugten WPF-Anwendung
14
Eine WPF-Anwendung, die mit Visual Studio erzeugt wurde, ist immer noch eine Windows-Anwendung. Eine solche startet mit einer statischen Methode Main. Wo aber ist diese Methode in einer WPF-Anwendung implementiert?
15
Ein anderes auf den ersten Blick eigenartiges Verhalten ist, dass in dem Konstruktor des C#-Teils eines WPF-Fensters eine Methode aufgerufen wird, die InitializeComponent heißt. Diese Methode ist aber scheinbar nirgendwo implementiert.
16
OK, hinter dem Ganzen steckt nichts Magisches. Die Main-Methode wird für ein WPF-Projekt von Visual Studio bei Bedarf implizit in die App-Klasse kompiliert. Das können Sie sehr schön im .NET Reflector sehen (Abbildung 12.12).
Main wird per Voreinstellung implizit implementiert Abbildung 12.12: Die implizit erzeugte Main-Methode im .NET Reflector
17
18
19
20
21
22 Das Erzeugen der Main-Methode wird von der Eigenschaft BUILDVORGANG der App.xaml-Datei gesteuert. Diese steht per Voreinstellung auf APPLICATIONDEFINITION, was eben dazu führt, dass die Main-Methode automatisch erzeugt wird. Wenn Sie eine eigene Main-Methode implementieren wollen, setzen Sie BUILDVORGANG auf
23
745
WPF-Grundlagen
PAGE (Die Eigenschaft BUILDVORGANG finden Sie im Eigenschaftenfenster, wenn Sie zuvor im Projektmappen-Explorer die XAML-Datei ausgewählt haben). Die MainMethode muss mit dem Attribut STAThread gekennzeichnet werden und sieht prinzipiell folgendermaßen aus: Listing 12.10: Eine eigene Basis-Main-Methode einer WPF-Anwendung [STAThread] public static void Main() { App app = new App(); app.InitializeComponent(); app.Run(); }
So weit, so gut. Aber was ist mit InitializeComponent? Wie Sie in Listing 12.10 sehen, wird diese Methode nicht nur für ein Fenster, sondern auch für die App-Klasse aufgerufen. InitializeComponent wird vom WPF-Designer dynamisch vor dem Kompilieren erzeugt, und zwar in einem weiteren Teil der jeweiligen Klasse, dessen Name mit .g.cs endet. Das g im Namen steht für »Generated« (erzeugt). Für die XAML-Datei MainWindow.xaml existieren also z. B. prinzipiell drei Dateien: ■ ■ ■
MainWindow.xaml: Enthält den XAML-Code. MainWindow.xaml.cs: Enthält den C#-Code, z. B. für die Reaktion auf Ereignisse. MainWindow.xaml.g.cs: Enthält die Methode InitializeComponent und andere für WPF wichtige Methoden.
Die generierte Datei ist aber nicht direkt Teil des Projekts. Sie wird von einem so genannten benutzerdefinierten Tool3 (Custom Tool) bei Bedarf (also spätestens beim Kompilieren) dynamisch erzeugt und im Ordner obj gespeichert.
EXKURS
Benutzerdefinierte Tools sind ein Feature von Visual Studio, das auf dem alten COMModell basiert und das es erlaubt, Code dynamisch zu erzeugen. Benutzerdefinierte Tools müssen für Visual Studio registriert sein und sind mit einer Dateiendung verknüpft. Der WPF-Designer von Visual Studio liefert das benutzerdefinierte Tool mit, das für eine XAML-Datei die Hintergrund-Teilklasse erzeugt, die u. a. InitializeComponent erzeugt. Ein benutzerdefiniertes Tool für eine Datei können Sie übrigens explizit ausführen, indem Sie im Kontextmenü des Dateieintrags im Projektmappen-Explorer den Befehl BENUTZERDEFINIERTES TOOL AUSFÜHREN wählen. Die generierte Teilklasse enthält in Form der InitializeComponent-Methode Code, der für die Ausführung einer XAML-Datei über WPF notwendig ist und den ich hier nicht erläutern will. Interessanter ist die weitere Methode Connect, die in Listing 12.11 dargestellt wird. Dieses Beispiel zeigt eine auf das Wesentliche reduzierte generierte Datei für die Datei MainWindow.xaml des Netto-Rechners.
3
746
Dieser eigenartige Name ist sehr verwirrend, aber Microsoft nennt diese Komponenten, die dynamisch Code erzeugen, eben so …
WPF-Anwendungen in Visual Studio
Listing 12.11: Die auf das Wesentliche reduzierte automatisch generierte Teilklasse des MainWindowFensters der Nettorechner-Anwendung public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector { internal System.Windows.Controls.Label lblGross; internal System.Windows.Controls.Label lblTax; internal System.Windows.Controls.Label lblNet; internal System.Windows.Controls.TextBox txtGross; internal System.Windows.Controls.TextBox txtTax; internal System.Windows.Controls.TextBox txtNet; internal System.Windows.Controls.Button btnCalculate; internal System.Windows.Controls.Button btnClose;
12 13
private bool _contentLoaded; /* Initialisiert die Instanz der XAML-Datei */ public void InitializeComponent() { if (_contentLoaded) { return; } _contentLoaded = true; System.Uri resourceLocater = new System.Uri( "/NetCalculator;component/mainwindow.xaml", System.UriKind.Relative); System.Windows.Application.LoadComponent(this, resourceLocater); }
14
15
16
/* Initialisiert die Steuerelement-Referenzen und weist die Ereignisse zu */ void System.Windows.Markup.IComponentConnector.Connect( int connectionId, object target) { switch (connectionId) { case 1: this.lblGross = ((System.Windows.Controls.Label)(target)); return; case 2: this.lblTax = ((System.Windows.Controls.Label)(target)); return; case 3: this.lblNet = ((System.Windows.Controls.Label)(target)); return; case 4: this.txtGross = ((System.Windows.Controls.TextBox)(target)); return; case 5: this.txtTax = ((System.Windows.Controls.TextBox)(target)); return; case 6: this.txtNet = ((System.Windows.Controls.TextBox)(target)); return; case 7: this.btnCalculate = ((System.Windows.Controls.Button)(target)); this.btnCalculate.Click += new System.Windows.RoutedEventHandler( this.btnCalculate_Click); return; case 8: this.btnClose = ((System.Windows.Controls.Button)(target));
17
18
19
20
21
22
23
747
WPF-Grundlagen
this.btnClose.Click += new System.Windows.RoutedEventHandler( this.btnClose_Click); return; } this._contentLoaded = true; } }
Die generierte Teilklasse enthält für jedes Steuerelement, das in XAML definiert wurde, ein internes Feld. Diese Referenzen werden in der Connect-Methode zugewiesen. Daneben werden in Connect noch alle Ereignisse zugewiesen. Connect wird in der Laufzeit von der WPF-Laufzeitumgebung aufgerufen, die aus dem XAML-Code Instanzen der jeweiligen Klassen erzeugt hat und diese Referenzen deswegen weitergeben kann. Damit haben Sie die Erklärung dafür, dass Sie in der C#-Teilklasse einer XAML-Datei mit den Steuerelementen arbeiten können und dass die Ereignisse der Steuerelemente auf ganz und gar unmagische Weise zugewiesen wurden.
12.4
XAML
XAML ist die Basis von WPF. Obwohl WPF-Designer und -Tools wie Expression Blend die Gestaltung einer WPF-Anwendung auch ohne XAML-Kenntnisse erlauben, sollten Sie sich als versierter Entwickler mit dieser Sprache auskennen. Ein Grund dafür ist schon alleine, dass Sie in Visual Studio Ereignisse (außer dem StandardEreignis) nur über den XAML-Code zuweisen können.
INFO
Dieser Abschnitt geht auf die Grundlagen von XAML ein und behandelt am Anfang einfache XAML-Dateien, die wesentlich einfacher sind als die des vorherigen Kapitels, weil sie nicht mit WPF-Anwendungsobjekten oder Fenstern arbeiten. Sie können die XAML-Codes sehr einfach ausprobieren, indem Sie diese in eine Datei mit der Endung .xaml kopieren (oder die entsprechenden Beispieldateien dieses Abschnitts verwenden) und auf diesen Dateien doppelklicken, damit sie in dem Browser geöffnet werden, der auf Ihrem System für XAML-Dateien zuständig ist. Eine andere, bessere Möglichkeit ist die Verwendung des Tools XamlPad, das mit Visual Studio als Teil des (eingeschränkten) Windows SDK installiert wird. Sie finden dieses Tool im Ordner des Windows-SDK (normalerweise C:\Programme\Microsoft SDKs\Windows\vX\bin, wobei X für die Versionsnummer steht) unter dem Namen XamlPad.exe. XamlPad erlaubt die direkte Ausführung von XAML-Dateien und ist somit sehr gut dafür geeignet, erste Erfahrungen mit WPF zu machen, ohne gleich ein Visual-Studio-Projekt erzeugen zu müssen.
12.4.1 XAML ist ein XML-Dialekt
748
Grundaufbau und Namensräume
XAML an sich ist einfach: XAML-Dokumente enthalten im Wesentlichen XMLElemente mit dem Namen von Klassen oder Strukturen, die instanziert werden sollen. XAML-Dokumente müssen, wie alle XML-Dokumente, genau ein WurzelElement besitzen. Unterhalb dieses Elements können beliebig viele andere XMLElemente platziert werden. Ein XML-Element steht dabei für eine Struktur oder Klasse aus einem der verfügbaren .NET-Namensräume. Der Name des Elements ist derselbe wie der Name des .NET-Typen.
XAML
Die WPF-Namensräume WPF-XAML-Dokumente werden üblicherweise mit zwei XML-Namensräumen initialisiert. Zumindest werden alle nicht explizit einem speziellen Namensraum zugeordneten XML-Elemente eines XAML-Dokuments dem Namensraum http:// schemas.microsoft.com/winfx/2006/xaml/presentation zugewiesen. Dies geschieht, indem dieser Namensraum dem Wurzel-Element des XAML-Dokuments über das xmlns-Attribut zugewiesen wird. Bei einer einfachen WPF-Datei, die lediglich ein Steuerelement (im Beispiel ein Label) enthält, kann dies z. B. folgendermaßen aussehen:
XAML-Dateien enthalten XMLNamensräume
12
13
Namensraum-Mapping XAML mappt die XML-Namensräume auf Namensräume des .NET Framework. Der in WPF-XAML-Dokumenten angegebene XML-Namensraum http://schemas.microsoft.com/winfx/2006/xaml/presentation wird auf den Namensraum System.Windows und viele seiner Sub-Namensräume (z. B. System.Windows.Controls, System. WindowsData, System.Windows.Documents, System.Windows.Input, System.Windows. Media, System.Windows.Navigation und System.Windows.Shapes) gemappt. Diese .NET-Namensräume enthalten alle Typen, die zur Gestaltung von WPF-Oberflächen verwendet werden. Wenn Sie also im XAML-Code ein XML-Element ohne Namensraum angeben, sucht WPF in System.Windows und den gemappten Sub-Namensräumen nach einer entsprechenden (öffentlichen) Struktur oder Klasse. Das XMLElement wird z. B. von WPF auf die Klasse System.Windows.Controls. Button gemappt. Dass es sich dabei um ein 1:N-Mapping handelt, ist übrigens der Grund dafür, dass in den WPF-Namensräumen keine Typnamen mehrfach vorkommen.
XAML mappt XMLNamensräume auf .NET-Namensräume
In vielen WPF-XAML-Dokumenten werden neben den WPF-Typen auch solche verwendet, die speziell zu XAML gehören und die dem Namensraum System. Windows.Markup angehören. Dieser Namensraum ist mit dem XML-Namensraum http://schemas.microsoft.com/winfx/2006/xaml verknüpft und wird üblicherweise über das Präfix x eingebunden (was allerdings lediglich eine Konvention ist).
Der XAMLNamensraum enthält spezielle Direktiven
14
15
16
17
18
19
Der XAML-Namensraum enthält spezielle Typen, die für XAML direkt verwendet werden, aber auch einige spezielle XAML-Direktiven. Alle Elemente und Attribute, die mit dem x-Präfix benannt sind, werden diesem Namensraum zugeordnet und beziehen sich deshalb auf Typen im .NET-Namensraum System.Windows.Markup.
20
Im XAML-Dokument eines mit Visual Studio erzeugten WPF-Fensters wird die Klasse, die den Programmcode enthält, z. B. über das Attribut Class angegeben, das zum XAML-Namensraum gehört:
21
22
Elemente, die dem XAML-Namensraum zugeordnet sind, enthalten häufig spezielle Direktiven für den XAML-Compiler bzw. -Parser. Im obigen Beispiel erkennt der Compiler/Parser am Class-Attribut, dass er die Klasse WPF_Demo.MainWindow einlesen und den darin enthaltenen Code mit dem Window-Objekt zusammenführen muss.
23
749
WPF-Grundlagen
Namensräume für nicht zu XAML oder WPF gehörende Klassen Eigene Typen müssen mit einem speziellen Namensraum deklariert werden
Wenn Sie in einem XAML-Dokument Typen verwenden wollen, die nicht zum WPFoder zum XAML-Namensraum gehören, müssen Sie die .NET-Namensräume dieser Typen mit einem neuen XML-Namensraum verknüpfen. XAML bietet dafür die folgende Syntax: xmlns:Präfix="clr-namespace:Namensraum[;assembly=Assemblyname]"
Präfix ist ein von Ihnen zu definierender Präfix für den neuen Namensraum, mit dem Sie die XML-Elemente, die mit Klassen des externen .NET-Namensraums korrespondieren, kennzeichnen. Für Namensraum geben Sie den .NET-Namensraum an, für Assemblyname den Namen der Assembly, die die einzubindenden Klassen enthält. Die Angabe der Assembly kann (theoretisch) entfallen, wenn die Klassen in der Assembly enthalten sind, die die XAML-Dokumente beinhaltet.
HALT
Sie sollten die Assembly immer angeben, auch wenn Sie auf Namensräume zugreifen, die zur aktuellen Assembly gehören. WPF hat in manchen Situationen (z. B. beim Einbinden von Ressourcen aus anderen Assemblys) scheinbar Probleme damit, wenn die Assembly nicht angegeben ist, und meldet Fehler, die nur sehr schwer zu deuten sind. So binden Sie z. B. den Namensraum Kompendium.Utils.Wpf ein, der in der Assembly Kompendium.Utils enthalten ist: xmlns:utils="clr-namespace:Kompendium.Utils.Wpf;assembly=Kompendium.Utils"
Sind die einzubindenden Typen in derselben Assembly wie die XAML-Datei, lassen Sie den assembly-Teil weg und geben Sie lediglich den Namensraum an: xmlns:y="clr-namespace:NetCalculator"
Wenn Sie Typen der so eingebetteten Namensräume verwenden wollen, müssen Sie den angegebenen Präfix (im Beispiel utils:) vor den Typnamen setzen.
Namensräume in XAML-Dokumenten mit mehreren Objekten Üblicherweise besteht ein WPF-XAML-Dokument nicht nur aus der Definition eines, sondern mehrerer Objekte, die in einem Wurzel-Objekt geschachtelt sind. So können Sie z. B. auf einem Canvas-Objekt eine Ellipse und ein Rectangle-Objekt unterbringen. Der WPF-Namensraum wird üblicherweise nur für das Wurzel-Objekt angegeben:
Alle dem Wurzel-Element untergeordneten Elemente, denen nicht explizit ein Namensraum zugeordnet ist, gehören damit zum WPF-Namensraum und sind somit eindeutig als speziell zu WPF gehörende Elemente gekennzeichnet.
750
XAML
12.4.2
Parsen und Kompilieren
XAML-Dateien können geparst und kompiliert werden. Ein XAML-Parser macht dabei nichts anderes, als die XAML-Datei einzulesen, zu interpretieren und in der Laufzeit in Code umzuwandeln, der von der Laufzeitumgebung ausgeführt werden kann. XAML-Dateien werden z. B. geparst, wenn Sie diese in einem XAML-fähigen Internet-Browser direkt öffnen.
12
XAML-Dateien können aber auch kompiliert werden, was z. B. bei WPF-Anwendungen der Fall ist, die Sie mit Visual Studio erzeugen. Wenn Sie ein solches Projekt kompilieren, resultieren normale Assemblys. Von den XAML-Dateien keine Spur mehr.
13
BAML XAML wird beim Kompilieren nicht in reinen CIL-Code umgewandelt. XAMLDateien werden lediglich in eine binäre Form kompiliert, die als BAML (Binary Application Markup Language) bezeichnet wird. BAML ist ein ImplementierungsDetail des XAML-Kompilierungsprozesses, der auch ausgeführt wird, wenn XAMLDateien interpretiert werden. In dieser Zwischenstufe ist der XAML-Code bereits vorgeparst, wobei alle unnötigen Teile (wie Kommentare und Leerzeilen) entfernt wurden. Außerdem sind alle XML-Elemente und Attribute komprimiert gespeichert. Eine BAML-Datei kann deswegen schneller eingelesen und ausgewertet werden als eine XAML-Datei.
XAML wird zu BAML kompiliert
14
15
16
Im Falle einer mit Visual Studio erzeugten WPF-Anwendung sind die resultierenden BAML-Dateien in Form von Ressourcen in der Assembly gespeichert. Das können Sie gut sehen, wenn Sie eine WPF-Anwendung in den .NET Reflector laden (Abbildung 12.13).
17 Abbildung 12.13: Der .NET Reflector zeigt die Ressourcen der NettoberechnungsAnwendung an
18
19
20
21
22 Über das .NET Reflector Add-In BAML Viewer können Sie die in eine Assembly integrierten BAML-Dateien dekompilieren und sich in XAML-Form anschauen. Dieses Add-In finden Sie an der Adresse www.codeplex.com/reflectoraddins/Release/ ProjectReleases.aspx. Kopieren Sie die Dateien des Add-In in den Ordner des .NET Reflector und fügen Sie dieses im Reflector über VIEW / ADD-INS hinzu. Das Add-In finden Sie dann im TOOLS-Menü.
23
TIPP
751
WPF-Grundlagen
Bei der Ausführung der Anwendung werden die BAML-Ressourcen ausgelesen und an die Laufzeitumgebung übergeben, die den BAML-Code verarbeitet. Bei WPF ist das natürlich die WPF-Laufzeitumgebung.
INFO
Dies ist der Grund dafür, dass Fehler in XAML-Code leider häufig nicht beim Kompilieren erkannt werden, sondern erst in der Laufzeit. Deshalb sollten Sie WPF-Anwendungen vor der Auslieferung in allen Aspekten testen. Die erzeugten BAML-Dateien finden Sie als Zwischenstufe übrigens auch im Ordner obj des Projekts.
12.4.3 Eigenschaften können als Attribut angegeben werden
Objekterzeugung und Initialisierung
Zur Initialisierung zu erzeugender Objekte können Sie in XAML die Eigenschaften der Klasse bzw. Struktur (nicht die Felder!) als Attribut angeben. Dazu geben Sie den Namen der einzustellenden Eigenschaft an, gefolgt von einem Zuweisungsoperator und dem in Anführungszeichen eingeschlossenen Wert: Listing 12.12: Angabe der Eigenschaften eines zu erzeugenden Objekts als Attribute
Eigenschaften können auch als Kind-Element angegeben werden
Eine andere Möglichkeit ist, die Eigenschaften als Kind-Element anzugeben. Der Elementname ist in diesem Fall der Name der Eigenschaft mit dem übergeordneten Klassennamen und einem Punkt als Präfix. Im folgenden Beispiel wird die Background-Eigenschaft auf diese Weise definiert: Listing 12.13: XAML-Deklaration mit Eigenschafts-Zuweisungen über Attribute und Kind-Elemente Orange
Eine Laufzeitumgebung (bei WPF die WPF-Laufzeitumgebung) parst die XAMLoder BAML-Daten und erzeugt daraus CIL-Code, der entsprechende Objekte erzeugt und initialisiert. Die in den Attributen oder Kind-Elementen angegebenen Werte werden dabei automatisch in den der Eigenschaft entsprechenden Typ konvertiert. Die Beispiel-Deklaration wird in einen CIL-Code umgewandelt, der dem folgenden C#-Code entspricht:
752
XAML
Listing 12.14: C#-Code, der dem Code entspricht, den der XAML-Compiler oder -Parser aus dem XAML-Code von Listing 12.13 erzeugt System.Windows.Controls.Label lblHello = new System.Windows.Controls.Label(); lblHello.Name = "lblHello"; lblHello.Width = (double)System.ComponentModel.TypeDescriptor .GetConverter(typeof(double)).ConvertFromInvariantString("100"); lblHello.Height = (double)System.ComponentModel.TypeDescriptor .GetConverter(typeof(double)).ConvertFromInvariantString("30"); lblHello.Content = "Hallo WPF"; lblHello.VerticalContentAlignment = (System.Windows.VerticalAlignment)System.ComponentModel .TypeDescriptor.GetConverter(typeof(System.Windows.VerticalAlignment)) .ConvertFromInvariantString("Center"); lblHello.HorizontalContentAlignment = (System.Windows.HorizontalAlignment)System.ComponentModel .TypeDescriptor.GetConverter(typeof( System.Windows.HorizontalAlignment)) .ConvertFromInvariantString("Center"); lblHello.Foreground = (System.Windows.Media.Brush)System.ComponentModel .TypeDescriptor.GetConverter(typeof(System.Windows.Media.Brush)) .ConvertFromInvariantString("Navy"); lblHello.Background = (System.Windows.Media.Brush)System.ComponentModel .TypeDescriptor.GetConverter(typeof(System.Windows.Media.Brush)) .ConvertFromInvariantString("Orange");
Die Konvertierung der als String angegebenen Eigenschaftswerte in die benötigten Typen erfolgt über Typkonverter, die die jeweilige Laufzeitumgebung mitbringt. Der der Background-Eigenschaft zugewiesene String "Orange" wird dabei z. B. von dem in WPF enthaltenen BrushConverter in eine Brush-Instanz umgewandelt. Die Ermittlung des zuständigen Konverters erfolgt über die GetConverter-Methode der TypeDescriptor-Klasse. Die Verwendung von Typkonvertern im CIL-Code ist ein Grund für das im Vergleich zu Windows.Forms langsame Laden von WPF-Anwendungen bzw. das langsame Öffnen von Fenstern.
12 13
14
15 Typkonverter übernehmen die Konvertierung
17
18
INFO
Die meisten Typkonverter arbeiten mit Strings, deren Inhalt recht logisch ist. Zahlen werden z. B. ganz normal (im englischen Format) angegeben. Aufzählungswerte über deren Namen. Bei Flags-Aufzählungen geben Sie normalerweise die einzelnen Werte mit Plus-Zeichen voneinander getrennt an.
19
Sie müssen im Einzelfall aber ggf. in der Dokumentation der betreffenden Typkonverter-Klasse (die in der Regel im selben Namensraum verwaltet wird wie das betreffende Element und deren Name üblicherweise der Name der Eigenschaft ist, mit einem »TypeConverter« am Ende) nachschauen, um zu erfahren, in welchem Format diese nun tatsächlich einen String erwartet, um diesen konvertieren zu können. Ich musste in einem Fall sogar den .NET Reflector zu Rate ziehen, um mir die Programmierung anzuschauen und daraus Schlüsse zu ziehen … Wenn Sie auf den Typkonverter verzichten bzw. einer Eigenschaft ein anderes Objekt zuweisen wollen als das, das der Standard-Typkonverter erzeugt, können Sie das der Eigenschaft zugewiesene Objekt auch deklarativ erzeugen. Dazu verwenden Sie einfach wieder die XAML-Objekterzeugungssyntax. Im folgenden Beispiel wird der Background-Eigenschaft des Labels eine Instanz der SolidColorBrush-Klasse zugewiesen, deren Eigenschaft Color auf eine Instanz der Color-Struktur gesetzt wird, deren Eigenschaften schließlich über Attribute initialisiert werden:
16
20
21
Objekte als Eigenschafts-Wert können auch deklarativ erzeugt werden
753
22
23
WPF-Grundlagen
Listing 12.15: Initialisierung einer Eigenschaft über die Erzeugung und Initialisierung weiterer Objekte
INFO
XAML verwendet zur Initialisierung einfache Regeln: Zu erzeugende Objekte werden als Element definiert, dessen Name der Klasse entspricht. Eigenschaften können als Attribut definiert werden. Alternativ können Eigenschaften auch als Kind-Element des Elements definiert werden, das das Objekt beschreibt. Dabei wird der Name der Klasse des Objekts als Präfix mit einem Punkt vor den Namen der Eigenschaft geschrieben. Ist die Eigenschaft auch ein komplexes Objekt, kann diese wieder mit der XAML-Objektbeschreibungssyntax beschrieben werden, so wie es im Beispiel mit der Background-Eigenschaft der Fall ist, der eine Instanz der SolidColorBrush-Klasse zugewiesen wird. Das Beispiel oben kann für die Background-Eigenschaft also so interpretiert werden: Der Background-Eigenschaft des erzeugten Label-Objekts wird eine Instanz der SolidColorBrush-Klasse zugewiesen, deren Color-Eigenschaft auf eine Instanz der Color-Klasse gesetzt wird, deren Eigenschaften A auf 255, R auf 255, G auf 128 und B auf 0 gesetzt werden. Und selbst hier werden auch Typkonverter verwendet, nämlich für die Konvertierung der Strings, die die Eigenschaften der Color-Instanz beschreiben, in byteWerte.
Die Inhaltseigenschaft Inhaltseigenschaften können als XML-ElementInhalt angegeben werden
Normale Eigenschaften werden in XAML entweder über Attribute oder über KindElemente initialisiert, deren Namen den Namen der Eigenschaft entsprechen. Um die Deklaration von Objekten zu verkürzen, bietet XAML aber auch die Möglichkeit, eine spezielle Eigenschaft als Inhaltseigenschaft festzulegen (was über ein .NETAttribut in der entsprechenden Klasse bzw. Struktur geschieht). Inhaltseigenschaften können zwar genauso initialisiert werden wie normale Eigenschaften. Eine weitere Möglichkeit ist jedoch die Initialisierung als Inhalt des XML-Elements, das das zu erzeugende Objekt beschreibt. Bei einem Label ist die Content-Eigenschaft z. B. als Inhaltseigenschaft definiert. Statt:
754
XAML
können Sie den Inhalt auch als Inhalt des Label-Elements angeben: Hallo WPF
Inhaltseigenschaften müssen nicht unbedingt mit einer Content-Eigenschaft korrespondieren. Bei einer ListBox ist der Inhalt z. B. die Liste. Als XML-Element-Inhalt angegebene Objekte werden hier der Items-Eigenschaft zugewiesen. Statt:
12
13
14
können Sie auch den folgenden Code verwenden:
15
16
Die Namens-Eigenschaft XAML bietet über das Name-Schlüsselwort aus dem Namensraum System. Windows.Markup die Möglichkeit, Objekte zu benennen, damit diese (in XAML) referenziert werden können. Einen Schalter können Sie also z. B. folgendermaßen benennen:
x:Name definiert den XAML-Namen
Listing 12.16: Benennen eines Objekts mit dem XAML-Name-Schlüsselwort
17
18
Viele Typen des Namensraums System.Windows besitzen aber auch eine eigene Namens-Eigenschaft, die normalerweise Name heißt. Die Klassen FrameworkElement und FrameworkContentElement, von der alle WPF-Steuerelemente abgeleitet sind, besitzt z. B. eine solche Eigenschaft. Solche Typen sind mit dem Attribut System. Windows.Markup. RuntimeNamePropertyAttribute gekennzeichnet, das die NamensEigenschaft mit dem XAML-Namen verknüpft. Für diese Typen können Sie also im XAML-Code die eigentliche Namens-Eigenschaft verwenden:
Viele Typen besitzen eine eigene NamensEigenschaft
19
20
21 Listing 12.17: Benennen eines Objekts über die Name-Eigenschaft
22
Beide Varianten sind gleichwertig. Ein über die Namens-Eigenschaft benanntes Objekt ist automatisch auch für XAML benannt. Ein über das XAML-Schlüsselwort benanntes Objekt wird von dem benutzerdefinierten Tool des WPF-Designers korrekt erkannt, weswegen dieser den notwendigen Quellcode erzeugt, der Sie auf das
23
755
WPF-Grundlagen
Objekt über dessen Namen zugreifen lässt. Das funktioniert natürlich nur dann, wenn der Typ mit dem RuntimeNameProperty-Attribut gekennzeichnet ist.
Auflistungs-Eigenschaften Auflistungs-Eigenschaften (Eigenschaften, deren Typ die IList-Schnittstelle implementiert) werden initialisiert, indem der Eigenschaft die benötigte Anzahl Elemente untergeordnet werden. Beim ListBox-Beispiel oben habe ich dies bereits demonstriert.
Dictionary-Eigenschaften DictionaryElemente müssen über x:Key mit einem Schlüssel versehen werden
Eigenschaften, die IDictionary implementieren, werden initialisiert, indem ähnlich wie bei normalen Auflistungs-Eigenschaften die der Liste hinzuzufügenden Objekte der Eigenschaft untergeordnet werden. Da bei IDictionary-Auflistungen der Schlüssel der Elemente (über deren Key-Eigenschaft) zusätzlich angegeben werden muss, verwenden Sie dazu in den einzelnen Auflistungs-Elementen das Key-Attribut, das zum XAML-Namensraum gehört (weswegen das x-Präfix angegeben werden muss). Das folgende Beispiel implementiert auf diese Weise eine Instanz der ResourceDictionary-Klasse, die im WPF verwendet wird, um Ressourcen zu speichern: Listing 12.18: Ein Beispiel Dictionary in XAML
12.4.4 Markuperweiterungen Markuperweiterungen bauen XAML aus
Markuperweiterungen bauen die Standard-Möglichkeiten von XAML aus. Eine Markuperweiterung ist eine von System.Windows.Markup.MarkupExtension abgeleitete Klasse, die die abstrakte Methode ProvideValue überschreibt. ProvideValue gibt einen object-Wert zurück. Trifft die XAML-Laufzeitumgebung beim Parsen eines XAMLoder BAML-Dokuments auf eine Klasse, die von MarkupExtension abgeleitet ist, integriert sie an dieser Stelle im erzeugten CIL-Code neben der Instanzierung des Objekts automatisch auch einen Aufruf der ProvideValue-Methode, deren Rückgabe in die entsprechende Eigenschaft geschrieben wird. Auf diese Weise können Sie in einem XAML-Dokument recht einfach Werte oder Objekte einsetzen, die erst in der Laufzeit ermittelt werden. Die XAML- und WPF-Namensräume enthalten Standard-Markuperweiterungen, die in Klassen definiert sind, deren Name in den meisten Fällen mit »Extension« endet. Die Null-Erweiterung, die es ermöglicht, einer Eigenschaft oder einem Konstruktor explizit den Wert null zuzuweisen, ist z. B. in der NullExtension-Klasse definiert. Bei der Verwendung in XAML müssen Sie den »Extension«-Teil des Namens allerdings nicht angeben. Markuperweiterungen können Sie an jeder Stelle eines XAML-Dokuments einsetzen, an der der von der Markuperweiterung zurückgegebene Wert passt. Die StaticExtension-Klasse ermöglicht es z. B. statische Eigenschaften anderer Klassen auszulesen. Einer Instanz dieser Klasse wird dazu in der Eigenschaft Member (oder am
756
XAML
Konstruktor) ein String übergeben, der die auszulesende statische Eigenschaft bezeichnet. So können Sie z. B. die Höhe und Breite eines Schalters aus den statischen Eigenschaften IconHeight und IconWidth der SystemParameters-Klasse auslesen: Listing 12.19: Verwenden der Markuperweiterung StaticExtension zum Auslesen von statischen Eigenschaften
Bei der Verwendung von Markuperweiterungen muss der Rückgabetyp der ProvideValue-Methode der MarkupErweiterungsklasse genau zu dem Typ der damit initialisierten Eigenschaft passen. Leider werden dabei keine impliziten Konvertierungen vorgenommen. Die Height-Eigenschaft eines WPF-Steuerelements erwartet z. B. einen double-Wert. Gibt eine zur Initialisierung verwendete Markuperweiterung einen intWert zurück, führt das zu dem Kompilier-Fehler »Der Int32-Werttyp wird von der Height-Eigenschaft nicht unterstützt«.
12 13
14
15
INFO
16
Markuperweiterungen können Sie auch noch in einer zweiten Form verwenden, indem Sie diese über einen String erzeugen, der in geschweifte Klammern eingebettet wird. Die Syntax dieser Form ist die folgende:
17
"{Erweiterungsname [Argumente]}"
Die optionalen Argumente können eine kommaseparierte Liste von Werten sein, die implizit (in der angegebenen Reihenfolge) dem Konstruktor der Erweiterungsklasse übergeben werden. Die Erweiterungsklasse muss dann natürlich über einen entsprechenden Konstruktor verfügen. Die Klasse StaticExtension bietet z. B. einen Konstruktor, dem der Name der auszulesenden Eigenschaft übergeben wird. Damit können Sie die Höhe und Breite eines Button auch so auf die System-Parameter IconHeight und IconWidth setzen:
18
Listing 12.20: Verwenden einer Markuperweiterung über die Klammer-Syntax und mit Übergabe eines Arguments an den Konstruktor
20
19
Test
21
Die Argumente einer Markuperweiterung können sich aber auch auf Eigenschaften der Erweiterungsklasse beziehen. In diesem Fall müssen Sie die Argumente benennen. Die Form dazu ist einfach:
22
Eigenschaftsname=Wert
23
Mehrere Argumente trennen Sie wie gewohnt durch Kommata.
757
WPF-Grundlagen
So können Sie z. B. in einem kleinen Exkurs in die WPF-Datenbindung den Inhalt eines Schalters über die BindingExtension-Klasse auf den Wert der Eigenschaft Height des Schalters setzen (was eigentlich sinnlos ist, aber die Verwendung einer Markuperweiterung demonstriert, die über Eigenschaften initialisiert wird). BindingExtension besitzt u. a. die Eigenschaften Path und RelativeSource, die in unserem Fall gesetzt werden müssen: Listing 12.21: Deklaration eines XAML-Elements mit Markuperweiterungen in der Standard- und der speziellen Markup-Syntax
INFO
Die BindingExtension-Klasse ermöglicht die Bindung von Eigenschaften an nahezu beliebige Daten. Im Beispiel wird die Content-Eigenschaft an die eigene HeightEigenschaft gebunden. Der Schalter zeigt damit immer automatisch die aktuell gesetzte Höhe an. Die Höhe wird im Übrigen wie im vorhergehenden Beispiel über die Markuperweiterung StaticExtension aus der Eigenschaft IconHeight der Klasse SystemParameters ausgelesen. Beachten Sie bitte, dass es hier nicht um Datenbindung geht, sondern lediglich darum, zu zeigen, wie Sie Markuperweiterungen in der speziellen Syntax über deren Eigenschaften initialisieren. Die WPF-Datenbindung wird in Kapitel 14 separat behandelt.
INFO
Wenn Sie für eine Eigenschaft in einem Attribut einen String angeben wollen, der geschweifte Klammern enthält, müssen Sie die Bedeutung der geschweiften Klammern, die ansonsten eine Markuperweiterung definieren, aufheben. Dies machen Sie, indem Sie ein leeres Klammernpaar voranstellen:
Da Markuperweiterungen nur auf Attribut-Ebene arbeiten, können Sie geschweifte Klammern als Inhalt von XML-Elementen ganz normal verwenden: {Das ist ein String in geschweiften Klammern}
Tabelle 12.1 zeigt die Standard-Erweiterungsklassen von WPF und XAML.
758
XAML
Markuperweiterungsklasse Bedeutung ArrayExtension
ermöglicht die Angabe von Arrays als Eigenschaftswerte. Dabei verwenden Sie idealerweise die einfache XAML-Syntax, bei der die ArrayElemente als Inhalt des Array-Elements angegeben werden. In der Eigenschaft Type geben Sie den Typ der im Array verwalteten Objekte an. ArrayExtension gehört zum Namensraum System.Windows. Markup, weswegen Sie den x-Präfix verwenden müssen:
Tabelle 12.1: Die StandardMarkuperweiterungsklassen
12
...
13
14
BindingExtension
wird für die Datenbindung unter WPF eingesetzt. Datenbindung wird in Kapitel 14 behandelt.
15
ColorConvertedBitmapExtension
ermöglicht die Einbettung eines Bildes, das über kein Farbprofil verfügt. Ich habe keinen blassen Schimmer, warum diese Markuperweiterung existiert. Bilder betten Sie normalerweise über entsprechende Klassen wie Image direkt in XAML-Code ein.
16
DynamicResourceExtension
ermöglicht die Einbettung einer Ressource, deren Inhalt sich in der Laufzeit ändern kann. Diese speziellen WPF-Ressourcen werden in Kapitel 14 behandelt.
17
NullExtension
ermöglicht das explizite Setzen des Werts null, was ansonsten in XAML nicht möglich wäre (weil ein leerer String nicht null ergibt). NullExtension gehört ebenfalls zum Namensraum System.Windows.Markup und erfordert deswegen den x-Präfix.
RelativeSource
Diese Klasse, deren Name nicht mit »Markup« endet, implementiert eine Markuperweiterung, die in der Datenbindung eingesetzt wird. Datenbindung wird in Kapitel 14 behandelt.
StaticExtension
ermöglicht die Verwendung statischer Eigenschaften. Den Namen der Eigenschaft übergeben Sie inklusive des Typnamens an der Eigenschaft Member oder am Konstruktor. StaticExtension gehört zum Namensraum System.Windows.Markup, weswegen Sie bei der Verwendung den x-Präfix angeben müssen.
StaticResourceExtension
ermöglicht die Einbettung einer (WPF-)Ressource, deren Inhalt sich in der Laufzeit nicht ändert. WPF-Ressourcen werden in Kapitel 14 behandelt.
TemplateBindingExtension
Diese Markuperweiterung wird in Zusammenhang mit Vorlagen verwendet. Vorlagen werden in Kapitel 14 behandelt.
18
19
20
21
22
23
759
WPF-Grundlagen
Tabelle 12.1: Die StandardMarkuperweiterungsklassen (Forts.)
INFO
Markuperweiterungsklasse Bedeutung ThemeDictionaryExtension
Diese Markuperweiterung ist für Entwickler von WPF-Steuerelementen vorgesehen, die damit externe, Design-spezifische Ressourcen-Dictionaries integrieren können.
TypeExtension
Diese Markuperweiterung gibt ein Type-Objekt zurück, das dem in der Eigenschaft TypeName oder am Konstruktor übergebenen Typnamen entspricht. Sie erfordert den x-Präfix.
Interessant ist, dass Sie (natürlich) auch eigene MarkupErweiterungsklassen entwickeln können, mit denen Sie dann Ergebnisse erzielen können, die mit den StandardKlassen ansonsten nicht oder nur umständlich möglich sind. Leiten Sie Ihre Markuperweiterung von MarkupExtension ab und überschreiben Sie die ProvideValue-Methode. Fügen Sie den Namensraum dieser Klasse dann dem XAML-Dokument hinzu (Seite 750) und setzen Sie die spezielle Markuperweiterung da ein, wo Sie diese benötigen.
12.4.5 Die XAML-Schlüsselwörter Tabelle 12.2 fasst die Schlüsselwörter des XAML-Namensraums zusammen. Die Tabelle geht davon aus, dass der Namensraum mit dem x-Präfix versehen ist. Tabelle 12.2: Die XAML-Schlüsselwörter (ohne Markuperweiterungen)
760
Schlüsselwort
Beschreibung
Gültig als…
x:Class
gibt eine optionale partielle Klasse an, die zusammen mit der XAML-Datei einen Typen ergibt.
…Attribut auf einem Wurzel-Element
x:ClassModifier
bestimmt die Sichtbarkeit der Klasse, die über x:Class angegeben ist. Per Voreinstellung ist diese Klasse öffentlich.
…Attribut auf einem Wurzel-Element. Kann nur in Verbindung mit x:Class verwendet werden.
x:Code
ermöglicht die Einbettung von Programmcode direkt in XAML.
…Element im gesamten XAML-Code. Kann nur in Verbindung mit x:Class verwendet werden.
x:FieldModifier
bestimmt die Sichtbarkeit des Feldes, das für ein …Attribut eines Nicht-Wurzel-EleObjekt in XAML in der generierten partiellen ments. Kann nur in Verbindung mit Klasse erzeugt wird. x:Name oder einer Namens-Eigenschaft verwendet werden.
x:Key
bestimmt den Schlüssel für Elemente eines Dictionary.
…Attribut eines Elements, dessen übergeordnetes Element IDictionary implementiert.
x:Name
bestimmt den Namen eines Elements, sodass dieses vom Programmcode aus referenziert werden kann.
…Nicht-Wurzel-Element im gesamten XAML-Code. Kann nur in Verbindung mit x:Class verwendet werden.
x:Shared
Wenn Sie dieses Schlüsselwort auf false setzen, bewirkt dies, dass eine WPF-Ressource beim Abruf von mehreren Stellen aus nicht geteilt, sondern mehrfach instanziert wird. WPFRessourcen werden in Kapitel 14 behandelt.
…Attribut innerhalb eines ResourceDictionary. Wird nur dann berücksichtigt, wenn der XAML-Code kompiliert wird.
Die grundlegenden WPF-Konzepte
Schlüsselwort
Beschreibung
x:Subclass
Dieses Schlüsselwort ist hauptsächlich für Spra- …Attribut eines Wurzel-Elements. chen bestimmt, die partielle Klassen nicht ken- Kann nur in Verbindung mit x:Class nen. Damit können Sie erreichen, dass die verwendet werden. WPF-Laufzeitumgebung statt partiellen Klassen solche verwendet, die per Vererbung aufeinander aufbauen.
x:TypeArguments
Dieses Schlüsselwort verwenden Sie auf einem …Attribut eines Wurzel-Elements. Wurzel-Element, dessen Basisklasse generisch Kann nur in Verbindung mit x:Class ist um die erforderlichen Typparameter anzuge- verwendet werden. ben. Das ist z. B. der Fall für die PageFunction-Klasse, die eine besondere WPFSeite darstellt. Die einzelnen Typen werden in Form der x:Type-Markuperweiterung als kommabegrenzte Liste angegeben.
x:Uid
x:XData
Gültig als…
gibt einen eindeutigen Bezeichner (Unique Identifier) an, der in der Lokalisierung der Anwendung verwendet wird. Lokalisierung wird in Kapitel 15 behandelt.
…Attribut an jedem Element.
Über dieses Element, das in der Verwendung stark eingeschränkt ist, können Sie ein XMLDokument innerhalb von XAML angeben, das vom XAML-Parser nicht berücksichtigt wird. Ein Beispiel für die Verwendung ist die XmlDataProvider-Klasse, die den deklarativen Zugriff auf XML-Daten für die Datenbindung ermöglicht. Näheres zur Datenbindung finden Sie in Kapitel 14.
…untergeordnetes Element der XmlDataProvider-Klasse oder der XmlSerializer-Eigenschaft der XmlDataProvider-Eigenschaft.
Tabelle 12.2: Die XAML-Schlüsselwörter (ohne Markuperweiterungen) (Forts.)
12 13
14
15
16
17
18
12.4.6 XAML mit integriertem Code XAML-Dateien können Programmcode auch direkt enthalten. Für solche Dateien wird das Attribut x:Class nicht angegeben. Der Programmcode wird dann einem x:Code-Element, üblicherweise in einer -Sektion untergebracht. Sie sollten dieses Feature aber nicht anwenden, da zum einen die Separation von Design und Programm aufgehoben wird und zum anderen loses XAML integrierten Code nicht unterstützt (was ein Grund dafür wäre, Code in die XAML-Datei zu integrieren).
12.5
19
20
Die grundlegenden WPF-Konzepte
21
WPF basiert auf einigen grundlegenden Konzepten, die die klassische OOP erweitern. Diese speziellen Konzepte sind für die professionelle Arbeit mit WPF notwendig zu verstehen. Deswegen erfahren Sie in diesem Abschnitt alles Wichtige über den Unterschied zwischen WPF-Seiten und WPF-Fenster, über logische und visuelle Bäume, Abhängigkeitseigenschaften und geroutete Ereignisse. Aber keine Angst: Die Namen der Konzepte hören sich schlimmer an, als sie in Wirklichkeit sind. Auf jeden Fall, nachdem diese verstanden wurden … Na ja, ein wenig kompliziert ist das Ganze schon … Aber ich erläutere die Konzepte (hoffentlich) auf eine verständliche Weise (auf jeden Fall für mich ☺).
22
23
761
WPF-Grundlagen
12.5.1
Der prinzipielle Unterschied zwischen WPF-Fenstern und WPF-Seiten
Eine WPF-Anwendung arbeitet normalerweise entweder mit einzelnen Fenstern oder mit Seiten (wie Sie bereits gesehen haben, können Sie zudem in XAML direkt auch ohne Fenster bzw. Seite arbeiten). Ein Fenster wird über die Window-Klasse definiert, eine Seite über die Klasse Page. Ich kann hier leider nicht zu tief auf die Unterschiede eingehen. Navigationsbasierte WPF-Anwendungen, die mit Seiten arbeiten, werden in diesem Buch nicht behandelt. Fenster werden in fensterbasierten Anwendungen eingesetzt, Seiten in navigationsbasierten
WPF-Fenster werden in eher klassischen, fensterbasierten Anwendungen eingesetzt. Ein Fenster wird in einem vom Betriebssystem zur Verfügung gestellten Rahmen dargestellt. Der Rahmen enthält normalerweise eine Titelzeile und lässt eine Größenveränderung des Fensters zu. In fensterbasierten Anwendungen kann es vorkommen, dass mehrere Fenster gleichzeitig geöffnet sind. WPF-Seiten sind eine einfachere Version der Window-Klasse. Für diese stellt das Betriebssystem keinen Rahmen zur Verfügung. WPF-Seiten werden in einem Container gehostet. Üblicherweise ist das ein NavigationWindow- oder ein Frame-Element. Diese Container stellen Möglichkeiten zur Verfügung, von einer Seite zu einer anderen zu navigieren, verwalten Informationen über den Navigationsverlauf und bieten einige navigationsbezogene Ereignisse. WPF-Seiten sind mit ihren Containern damit für Anwendungen vorgesehen, die auf einer Navigation basieren, wie es z. B. in einem Webbrowser der Fall ist. Ein Beispiel für eine solche navigationsbasierte Anwendung finden Sie in Form des Foto-Beispiels »Navigation-Based PhotoGallery« von Adam Nathan (als Teil der Beispiele seines Buchs »Windows Presentation Foundation Unleashed«, die Sie an der Adresse www.adamnathan.net/wpf finden. Navigation-Based PhotoGallery ist in Kapitel 7 enthalten).
12.5.2
Ressourcen, Stile, Vorlagen, Dekoratoren, Skins und Themen
Ressourcen, Stile, Dekoratoren, Vorlagen, Skins und Themen sind Dinge, die dieses Buch in Kapitel 14 näher beleuchtet. Für das Grundverständnis von WPF ist es aber sehr wichtig, dass Sie prinzipiell wissen, worum es dabei geht. Deswegen finden Sie hier eine kurze Einführung in diese leider relativ komplexen Themen. Das WPF-Layout basiert auf Stilen, Triggern, Dekoratoren und Vorlagen
■
■
762
Ressourcen sind ein allgemeines Thema, das nicht unbedingt nur mit WPF in Zusammenhang steht. Eine Ressource ist ein Bestandteil einer Anwendung, die nicht Teil des Programms ist. Ein Bild, das in der Anwendung angezeigt werden soll, ist z. B. eine Ressource. WPF setzt aber eine spezielle Art von Ressourcen ein, die innerhalb von WPF-Elementen (über deren Resources-Auflistung) oder auch auf einer höheren Ebene (wie z. B. der Anwendung) verwaltet werden können. Diese Ressourcen enthalten in der Regel Stile und Vorlagen. Stile definieren das Aussehen von WPF-Elementen dadurch, dass die Werte von (Abhängigkeits-)Eigenschaften, die das Aussehen beeinflussen, in ihnen voreingestellt sind. Dieses Voreinstellen geschieht über so genannte Stil-Setter (»StilSetzer«, dieser Begriff taucht im Abschnitt »Abhängigkeitseigenschaften« wieder auf). Ein Stil ist in der Regel einer Steuerelement-Klasse zugeordnet und bestimmt die voreingestellten Werte der Layout-Eigenschaften des Steuerelements (z. B. die Hintergrundfarbe, die Vordergrundfarbe und die Schriftart).
Die grundlegenden WPF-Konzepte
■
Vorlagen sind ein besonderes Feature von WPF. In WPF werden Steuerelemente nicht auf dem Bildschirm einfach nur gezeichnet, wie es z. B. bei Windows.Forms der Fall ist. Ein WPF-Steuerelement wird in der Regel aus mehreren Unter-Elementen zusammengesetzt. Ein Button besteht z. B. in Wirklichkeit aus einem ButtonChrome-Objekt (welches ein Dekorator ist, was gleich noch erläutert wird), das ein ContentPresenter-Objekt enthält (welches den Inhalt darstellt). Falls Sie sich fragen, woher ich das weiß: Über die Template- Eigenschaft eines WPF-Steuerelements können Sie dessen Vorlage relativ einfach auslesen. Wie das geht, zeigt das Projekt Vorlage eines WPF-Elements auslesen, das Sie neben den Beispielen dieses Abschnitts auf der Buch-DVD finden.
12 13
Ich will hier nicht näher auf Vorlagen eingehen, weil Kapitel 14 dieses Thema behandelt. Für das Verständnis von WPF ist es aber wichtig zu wissen, dass eine Vorlage das Layout eines Steuerelements bestimmt. Vorlagen können zudem Stile beinhalten. Damit wird auch der Stil des Steuerelements (die Farben, die Schriftart etc.) durch die Vorlage bestimmt. Außerdem können Vorlagen Trigger beinhalten, die ein grundlegendes Verhalten des Steuerelements definieren. ■
■
14
15
Ein (Eigenschaften4-)Trigger überwacht eine oder mehrere Eigenschaften eines WPF-Elements. Werden diese auf einen im Trigger definierten Wert geändert, schreibt dieser vorgegebene Werte in beliebige (andere) Eigenschaften des WPF-Elements. Ein Trigger kann z. B. automatisch die Vorder- und Hintergrundfarbe eines Steuerelements ändern, wenn dessen IsEnabled-Eigenschaft auf false gesetzt wird (was das Steuerelement deaktiviert). Trigger können in Stilen implementiert werden (so genannte Stil-Trigger) oder in Vorlagen (Vorlagen-Trigger). In den Default-Vorlagen der WPF-Steuerelemente sind bereits einige Vorlagen-Trigger enthalten, die interaktiv auf Benutzereingaben reagieren. In der Vorlage des TextBox-Steuerelements ändert ein Trigger die Vorderund Hintergrundfarbe, wenn das Steuerelement deaktiviert ist. Dekoratoren sind ein weiteres Feature, das das Aussehen und Verhalten eines Steuerelements beeinflussen können. Dekoratoren sind unter WPF Objekte, die ein anderes Objekt (das Steuerelement) mit Effekten »dekorieren«. Ein Dekorator fängt im Wesentlichen das Zeichnen eines Steuerelements ab und kann dieses komplett umdefinieren. Ein Border-Dekorator dekoriert ein Steuerelement z. B. mit einem Rahmen. Ein ViewBox-Dekorator dekoriert ein Steuerelement so, dass es automatisch skaliert wird. Da Sie Dekoratoren auch selbst einsetzen können, können Sie dieses »Dekorieren« mit einem ViewBox-Dekorator sehr gut ausprobieren. Plazieren Sie dieses einfach um ein beliebiges anderes Steuerelement und beobachten Sie, was passiert, wenn Sie das WPF-Fenster, auf dem die Steuerelemente angelegt sind, vergrößern oder verkleinern.
16
17
18
19
20
21
Dekoratoren können auch Teil der Vorlage eines Steuerelements sein, wie es z. B. beim ButtonChrome-Dekorator des Button-Steuerelements der Fall ist. In diesem Fall übernimmt der Dekorator sogar das komplette Zeichnen des Steuerelements. Dekoratoren ermöglichen damit eine wesentlich flexiblere Beeinflussung als Trigger. Sie haben vor allen Dingen den (zweifelhaften) Vorteil, dass Sie auch lokal gesetzte Eigenschaftswerte überschreiben können, was Triggern nicht möglich ist.
4
22
23
Neben Eigenschaftentriggern daneben gibt es noch Daten- und Ereignistrigger, die aber eine spezielle Bedeutung haben, die in Kapitel 14 erläutert wird.
763
WPF-Grundlagen
■
■
HALT
Skins sind keine spezielle WPF-Technik. Ein Skin ist unter WPF eine Sammlung aus Ressourcen mit Stilen und Vorlagen, die in der Regel in einer XAML-Datei zusammengefasst sind. Da Ressourcen dynamisch eingelesen werden können, ist es möglich, über einen Skin das gesamte Aussehen von WPF-Anwendungen zu ändern. Der Begriff Thema wird in Zusammenhang mit einem Betriebssystem verwendet. Vista enthält z. B. einige Themen, die die Oberfläche unterschiedlich gestalten. WPF trägt dem Rechnung und liefert Skins für die Default-WindowsThemen mit. Da WPF das in Windows eingestellte Thema berücksichtigt, wird für eine WPF-Anwendung automatisch der zum Windows-Thema passende Skin verwendet. Eine WPF-Anwendung sieht unter Windows XP also anders aus als unter Vista und innerhalb einer Windows-Version auch anders, wenn unterschiedliche Themen eingestellt sind.
Dass Vorlagen auch Dekoratoren beinhalten können, ist in der Praxis ein ziemliches Problem. Ein Beispiel dafür ist ein Button unter Vista: Setzen Sie die Eigenschaft Background z. B. auf Orange, besitzt der Schalter zunächst eine entsprechende Hintergrundfarbe. So weit, so gut. Der Hintergrund eines Button wird aber vom ButtonChromeDekorator gezeichnet. Dieser berücksichtigt nur in dem Fall, dass der Button nicht betätigt ist, sich die Maus nicht darüber befindet und der Schalter nicht deaktiviert ist, den in Background gesetzten Hintergrund. In allen anderen Fällen wird (zumindest unter Vista) der Schalter mit einem im Thema definierten Hintergrund gezeichnet. Wenn Sie den Hintergrund z. B. auf Orange setzen, wird der Schalter unter Vista im Vista-Stil angezeigt (im Aero-Glass-Thema in blau), wenn die Maus auf ihm liegt, er betätigt oder deaktiviert ist. Laut meinen Recherchen im Internet können Sie dieses Problem nur dadurch lösen, dass Sie die komplette Vorlage des Steuerelements neu definieren. Das macht das einfache Umdefinieren des Designs einer Anwendung unnötig aufwändig und fehlerträchtig. Auf Dekoratoren gehe ich in Kapitel 14 kurz ein. Das Grundwissen über Stile, Vorlagen, Skins, Themen, Trigger und Dekoratoren ist zum einen wichtig, damit Sie wissen, warum eine WPF-Anwendung je nach Windows-Thema anders aussieht (und ggf. auch anders reagiert). Abbildung 12.14 zeigt z. B. einen kleinen Nettorechner (der im weiteren Verlauf dieses Kapitels entwickelt wird) unter XP im Default-XP-Thema, Abbildung 12.15 zeigt dieselbe Anwendung unter dem Aero-Glass-Thema von Vista.
Abbildung 12.14: Der Netto-Rechner unter XP im DefaultXP-Thema
764
Die grundlegenden WPF-Konzepte
Abbildung 12.15: Der Netto-Rechner unter Vista im AeroGlass-Thema
12 13
Ressourcen, Stile, Vorlagen, Trigger, Dekoratoren und Skins können Sie aber zum anderen auch selbst einsetzen. In einer professionellen Anwendung sollten Sie z. B. auf das explizite Setzen von Eigenschaften, die das Aussehen beeinflussen, verzichten, wenn Sie das Design anpassen wollen. Verwenden Sie dazu lieber Skins (die Stile, Vorlagen und Trigger beinhalten können). Das Aussehen eines WPF-Elements können Sie über eine selbst definierte Vorlage komplett ändern. Damit besitzen Sie nahezu unbegrenzte Möglichkeiten beim Design einer Anwendung. Die wesentlichen Grundlagen dazu finden Sie in Kapitel 14.
12.5.3
14
15
Logische und visuelle Bäume 16
Logische und visuelle Bäume sind eines der Kernkonzepte von WPF. Sie sollten diese Bäume kennen, um die WPF-Basiskonzepte besser zu verstehen. Wie Sie ja nun bereits wissen, basiert WPF auf XAML. XAML definiert die Objekte eines WPF-Fensters oder einer WPF-Seite. XAML ist hierarchisch organisiert.
17
Ein WPF-Element kann nun ein oder mehrere Unterelemente besitzen (wie ich in Kapitel 13 noch näher erläutere). Ein Grid kann z. B. beliebig viele Elemente aufnehmen, ein Button-Objekt aber nur eines (in der Regel seinen Text). Wenn Sie ein WPFFenster oder eine WPF-Seite mit einem Grid (oder einem ähnlichen Element) aufbauen und in dem Grid Steuerelemente anlegen, erzeugen Sie damit automatisch einen Element-Baum.
18
19
Das folgende Beispiel demonstriert dies an einer WPF-Seite, die einen einfachen Dialog zur Auswahl eines ListBox-Eintrags darstellt: Listing 12.22: Eine einfache WPF-Seite als Beispiel für den logischen und visuellen Baum
20
Ihre bevorzugte Sportart: Windsurfen Snowboarden Kitesurfen OK
21
22
23
765
WPF-Grundlagen
Abbrechen
Abbildung 12.16 zeigt die Seite in Aktion. Abbildung 12.16: Die Beispielseite in Aktion
INFO
Beachten Sie bitte, dass Abbildung 12.16 das Ergebnis einer mit Visual Studio erzeugten WPF-Anwendung zeigt. Die Seite wird deswegen implizit in einem Windows-Fenster gehostet und um ein Navigationselement (oben auf der Seite) erweitert. Die XAML-Datei könnte aber auch (ohne Programmcode) als loses XAML direkt in einem XAML-fähigen Browser geöffnet werden. Dann müssten Sie aber auch das Attribut x:Class weglassen. Der logische Baum dieser Seite baut sich nun aus den in XAML direkt angegebenen Objekten auf (Abbildung 12.17).
Abbildung 12.17: Der logische Baum des Beispiels
TIPP
766
Den logischen Baum in Abbildung 12.17 habe ich über den Visualisierer Mole erhalten, den ich im Abschnitt »Debuggen über den Mole-Visualisierer« (Seite 742) kurz beschreibe. Mole hilft enorm beim Debuggen von Problemen, die mit dem logischen oder dem visuellen Baum zu tun haben. Damit erhalten Sie sehr einfach eine Übersicht über die Bäume einer Seite, eines Fensters oder den Unterbaum eines WPF-Elements.
Die grundlegenden WPF-Konzepte
Der logische Baum ist nun für Sie deswegen wichtig, weil nahezu jeder Aspekt von WPF sich irgendwie auf den logischen Baum bezieht. Eigenschaftswerte werden z. B. (wie Sie im Abschnitt »Abhängigkeitseigenschaften« erfahren) in einigen Fällen von übergeordneten Elementen an im logischen Baum untergeordnete Elemente weitergegeben. Der visuelle Baum ist ein ähnliches Konzept wie der logische Baum. Ein visueller Baum basiert auf dem logischen, ist aber um die Elemente erweitert, die WPF bei der Ausführung einer XAML-(oder BAML-)Datei schließlich wirklich erzeugt.
Der visuelle Baum ist ein erweiterter logischer Baum
Die einzelnen WPF-Elemente, die Sie in einer XAML-Datei einsetzen, werden von der WPF-Laufzeitumgebung meist in mehrere Teilelemente übersetzt. Ein Page-Element besteht z. B. in der ausgeführten Anwendung aus einer Instanz der PageKlasse, der eine Instanz der Border-Klasse untergeordnet ist. Der Border-Instanz ist eine Instanz von ContentPresenter untergeordnet. Ähnlich sieht dies bei einem Label aus. Border definiert den Rahmen, ContentPresenter ist für die Darstellung des Inhalts eines Steuerelements mit Inhalt (eines ContentControl) zuständig.
13
14
Unter Windows XP führt das obige Beispiel damit zu dem in Abbildung 12.18 dargestellten visuellen Baum.
15
Wie Sie in Abbildung 12.18 sehen, ist der visuelle Baum des einfachen Beispiels bereits recht komplex. Für komplexere XAML-Dateien wird der visuelle Baum nahezu unübersichtlich. Glücklicherweise haben Sie in der Praxis nicht allzu viel damit zu tun.
16
Der visuelle Baum enthält übrigens nur Elemente, die von System.Windows.Media. Visual oder von System.Windows.Media.Media3D.Visual3D abgeleitet sind. Diese Elemente rendern ihren Inhalt in der Laufzeit und erzeugen ggf. untergeordnete Elemente. Einfache Elemente des logischen Baums wie Strings sind deshalb nicht im visuellen Baum enthalten. Die Implementierungsdetails des visuellen Baums sind normalerweise nicht wichtig, da Sie in XAML ausschließlich mit dem logischen Baum arbeiten. In der Praxis sollten Sie auch keine Programme schreiben, die sich auf den visuellen Baum beziehen. Der hauptsächliche Grund dafür ist, dass die WPF-Implementierung in späteren Versionen so geändert werden kann, dass aus demselben logischen Baum ein anderer visueller Baum entsteht. Ein Programm, das einen bestimmten Aufbau des visuellen Baums erwartet, wird dann Fehler verursachen.
12
17
18 INFO
19
20
Und nur, damit Sie verstehen, wie Sie an den logischen und visuellen Baum herankommen: Die Klassen System.Windows.Media.VisualTreeHelper und System. Windows.LogicalTreeHelper bieten verschiedene Möglichkeiten mit diesen Bäumen zu arbeiten.
21
In einigen Fällen kommen Sie aber nicht um den visuellen Baum herum, z. B. wenn Sie mit den 2D-Features von WPF zeichnen. Außerdem können Ereignisse den visuellen Baum hoch- oder runterwandern (siehe »Geroutete Ereignisse«, Seite 777). Gut, wenn Sie dann wissen, was ein visueller Baum ist.
22
23
767
WPF-Grundlagen
Abbildung 12.18: Der visuelle Baum des Beispiels (mit einem zugeklappten Grid oben)
12.5.4 Abhängigkeitseigenschaften Abhängigkeitseigenschaften sind ein weiteres wichtiges Basiskonzept von WPF. Dieses auf den ersten Blick etwas eigenartig erscheinende Konzept hat seinen festen Platz in WPF und wird an vielen Stellen intensiv eingesetzt.
Was ist eine Abhängigkeitseigenschaft? Abhängigkeitseigenschaften sind keine normalen Eigenschaften
768
Eine reine Abhängigkeitseigenschaft ist zunächst einmal keine normale Eigenschaft (sie kann aber mit einer solchen verknüpft sein). Eine Abhängigkeitseigenschaft wird in einer Klasse, die ein WPF-Element definiert, auf eine sehr spezielle Weise implementiert. Die Abhängigkeitseigenschaft MaxLength einer TextBox (die die maximale Länge der Eingabe bestimmt) ist z. B. folgendermaßen definiert:
Die grundlegenden WPF-Konzepte
public class TextBox : TextBoxBase, IAddChild, ITextBoxViewHost { ... /* Referenz auf die Abhängigkeitseigenschaft */ public static readonly DependencyProperty MaxLengthProperty; ... /* statischer Konstruktor */ static TextBox() { ... /* Hier wird die Abhängigkeitseigenschaft erzeugt */ MaxLengthProperty = DependencyProperty.Register("MaxLength", typeof(int), typeof(TextBox), new FrameworkPropertyMetadata(0), new ValidateValueCallback(TextBox.MaxLengthValidateValue)); ... }
12 13
14
... /* Optionale echte Eigenschaft, die mit der Abhängigkeitseigenschaft korrespondiert */ public int MaxLength { get { return (int) base.GetValue(MaxLengthProperty); } set { base.SetValue(MaxLengthProperty, value); } } ...
15
16
17
}
Wie Sie dem Beispiel entnehmen können, ist eine Abhängigkeitseigenschaft keine normale Eigenschaft, sondern eine Instanz der DependencyProperty-Klasse, die in einem statischen Feld (MaxLengthProperty) referenziert wird. Die DependencyProperty-Klasse sorgt für den notwendigen Laufzeit-Support (für die gleich beschriebenen Features).
Abhängigkeitseigenschaften sind DependencyProperty-Instanzen, die bei WPF registriert werden
18
19
Per Konvention werden Abhängigkeitseigenschaften über öffentliche statische Felder repräsentiert, deren Name mit »Property« endet. Erzeugt werden Abhängigkeitseigenschaften üblicherweise im statischen Konstruktor des Typs, zu dem sie gehören, über einen Aufruf der Register-Methode der DependencyProperty-Klasse. An den Argumenten wird u. a. der logische Name der Abhängigkeitseigenschaft übergeben (im Beispiel ist das MaxLength), der Typ der Eigenschaft (im Beispiel: int) und der Typ, der die Abhängigkeitseigenschaft beinhaltet. Optional können Metadaten übergeben werden, die z. B. einen Callback definieren können, der aufgerufen wird, wenn die Eigenschaft geändert wird (was im Beispiel nicht gemacht wird) und weitere Informationen, auf die ich hier aber nicht eingehen kann.
20
21
22
Eine Abhängigkeitseigenschaft ist also schon einmal eine Instanz der Klasse DependencyProperty, die über ein statisches Feld verwaltet wird. Aber halt: Wenn das Feld statisch ist, wie kann denn dann der Wert der Abhängigkeitseigenschaft für eine Instanz gelesen und geschrieben werden?
23
769
WPF-Grundlagen
DependencyObject liefert den Laufzeit-Support
Das Lesen und Schreiben von Abhängigkeitseigenschaften geschieht über die von der Basisklasse DependencyObject (von der alle WPF-Elemente abgeleitet sein müssen, die Abhängigkeitseigenschaften unterstützen) geerbten Methoden GetValue und SetValue. Diesen wird die Referenz auf Abhängigkeitseigenschaften übergeben und im Fall von SetValue auch der zu schreibende Wert. Diese (öffentlichen!) Methoden sind relativ komplex. Im Wesentlichen verwalten sie die Werte der Abhängigkeitseigenschaften für die einzelnen Instanzen in Metadaten des DependencyPropertyObjekts (in Form einer PropertyMetadata-Instanz). Ein DependencyProperty-Objekt, das einem Typen zugeordnet ist, verwaltet also die Werte der Abhängigkeitseigenschaften für alle Instanzen des Typs. Prinzipiell reichen die Implementierung des Feldes und die Initialisierung im statischen Konstruktor aus, um eine Abhängigkeitseigenschaft zu erzeugen. Gelesen und geschrieben werden kann diese über die Methoden GetValue und SetValue. Damit die Abhängigkeitseigenschaft aber eleganter geschrieben und gelesen und vor allen Dingen auch in XAML definiert werden kann, wird üblicherweise (wie im Beispiel) eine normale Eigenschaft implementiert, die intern GetValue und SetValue verwendet. So: nun wissen Sie, wie eine Abhängigkeitseigenschaft arbeitet. Sie fragen sich aber wahrscheinlich, was eine solche Eigenschaft bewirkt. Und der Name kommt Ihnen vielleicht auch ein wenig seltsam vor. Wie Sie im Abschnitt »Unterstützung für mehrere Provider« ab Seite 774 aber noch sehen, ist der Begriff »Abhängigkeitseigenschaft« recht logisch, denn der Wert einer solchen Eigenschaft hängt von mehreren Providern ab, die den Wert zur Verfügung stellen können.
Der Sinn von Abhängigkeitseigenschaften Abhängigkeitseigenschaften bieten spezielle Features
Da Abhängigkeitseigenschaften von WPF verwaltet werden (über je eine Instanz der DependencyProperty-Klasse) und nicht von der Klasse, die die Abhängigkeitseigenschaft integriert, kann WPF für Abhängigkeitseigenschaften besondere Features zur Verfügung stellen. Dabei handelt es sich um die folgenden Technologien (die ich im weiteren Verlauf noch näher erläutere): ■ ■ ■ ■
Benachrichtigung bei einer Änderung des Eigenschaftswerts, Vererbung des Eigenschaftswerts, Unterstützung für mehrere Provider und angefügte Eigenschaften.
Na dann …
Benachrichtigung bei einer Änderung des Eigenschaftswerts Immer dann, wenn der Wert einer Abhängigkeitseigenschaft geändert wird, kann WPF automatisch eine Vielzahl von Aktionen aufrufen, die vom Entwickler in XAML oder im Programmcode festgelegt wurden. Das kann z. B. eine Aktualisierung der Oberfläche sein. In der Datenbindung wird die Änderungs-Benachrichtigung dazu verwendet, die in einem XAML-Element geänderten Daten in die Datenquelle zurückzuschreiben oder in der Datenquelle geänderte Daten in das XAML-Element. Eigenschaftentrigger reagieren auf Änderungen
770
In XAML können Sie über Eigenschaftentrigger (Property Trigger) auf die Änderung des Werts einer Abhängigkeitseigenschaft reagieren. Ein solcher Trigger verknüpft eine Abhängigkeitseigenschaft mit einer Aktion.
Die grundlegenden WPF-Konzepte
Leider ist das Hinzufügen eines Eigenschaftentriggers etwas komplex, da diese nicht direkt einem XAML-Element untergeordnet werden können. Alle von FrameworkElement abgeleiteten Klassen besitzen zwar eine Triggers-Eigenschaft, der auch tatsächlich Trigger hinzugefügt werden können. Leider ist diese (zurzeit noch) auf Ereignistrigger (EventTrigger) beschränkt. Ein Ereignistrigger ist neben dem Datentrigger und dem Eigenschaftentrigger die dritte mögliche Form eines Triggers. Ereignis- und Datentrigger werden in Kapitel 14 behandelt. Möglicherweise werden in zukünftigen WPF-Versionen auch diese Trigger in der Triggers-Eigenschaft unterstützt. Zurzeit können Sie Eigenschaftentrigger aber nur in einem Stil oder einer Vorlage definieren. Beide werden erst in Kapitel 14 behandelt. Damit Sie Eigenschaftentrigger definieren können, zeige ich aber hier, wie Sie einen Stil mit einem Eigenschaftentrigger definieren.
12 Eigenschaftentrigger können nur in einem Stil oder einer Vorlage definiert werden
Einen Stil können Sie über die Style-Eigenschaft eines WPF-Elements angeben. Die Klasse des Elements muss von FrameworkElement abgeleitet sein, damit diese Eigenschaft zur Verfügung steht. In der Style-Eigenschaft erzeugen Sie ein neues StyleObjekt, dessen Eigenschaft TargetType auf den Typ des WPF-Elements gesetzt wird. Die Triggers-Eigenschaft dieses Objekts verwaltet die Trigger. Hier können Sie Eigenschaftentrigger (und andere Trigger) hinzufügen.
13
14
15
Das folgende Beispiel definiert auf diese Weise für ein TextBlock-Element einen Eigenschaftentrigger, der bei einer Änderung der IsMouseOver-Eigenschaft auf true die Hintergrundfarbe des TextBlock auf Grün setzt:
16
Listing 12.23: Definition eines einfachen Eigenschaftentriggers
17
Bevor Sie nun bereits mit Triggern experimentieren. Lesen Sie vorher Kapitel 14. Trigger sind nicht allzu einfach. Ein Problem ist z. B., dass lokal gesetzte Eigenschaften Vorrang vor Triggern haben (siehe im Abschnitt »Unterstützung für mehrere Provider« ab Seite 774 ). Wenn Sie im Beispiel die Eigenschaft Background des TextBlock setzen, hat der Trigger keine Wirkung mehr. Ein anderes Problem ist, das ein solcher Trigger für ein Button-Steuerelement unter Vista nicht funktioniert. In Kapitel 14 gehe ich näher darauf ein.
18
19
20 HALT
21
22 Über Trigger geänderte Eigenschaften werden automatisch auf ihren alten Wert zurückgesetzt, wenn die Trigger-Bedingung nicht mehr erfüllt ist. Deswegen muss die Hintergrundfarbe des Schalters nicht explizit zurückgesetzt werden.
INFO
23
771
WPF-Grundlagen
Abbildung 12.19: Das Beispiel in XamlPad
INFO
Dieses einfache Beispiel zeigt ein wichtiges Konzept von WPF: Eine WPF-Anwendung kann darauf reagieren, dass der Wert einer Eigenschaft geändert wurde (oder dass ein Ereignis aufgetreten ist oder in gebundenen Daten eine Bedingung erfüllt wurde). Und das Verhalten in diesem Fall wird in der Regel komplett im XAMLCode gesteuert, nicht im Programm. Sie können sich vorstellen, dass Sie das vorhergehende Beispiel auch ohne einen Eigenschaftentrigger implementieren könnten. Dazu müssten Sie aber eine Menge programmieren. Und da Trigger noch mehr Möglichkeiten bieten, können Sie sich wahrscheinlich auch vorstellen, dass Sie auch komplexe Dinge damit realisieren können, ohne eine Zeile Programmcode zu schreiben. Trigger werden in Kapitel 14 behandelt.
REF
Eigenschaftswerte können vererbt werden
Vererbung des Eigenschaftswerts Abhängigkeitseigenschaften ermöglichen es, den Wert einer Eigenschaft zu vererben. Um gleich möglichen Missverständnissen vorzubeugen: Mit »Vererbung« ist hier nicht die Vererbung in der OOP gemeint, sondern das Erben eines Werts von einem im logischen Baum übergeordneten Element. Das folgende Beispiel zeigt, was damit gemeint ist. Die FontSize-Eigenschaft des Page-Elements ist darin auf 14 eingestellt, FontStyle auf Italic (kursiv). In den Button-Elementen ist FontSize noch einmal definiert, hier allerdings auf den Wert 10:
772
Die grundlegenden WPF-Konzepte
Listing 12.24: Beispiel für die Vererbung der Werte von Abhängigkeitseigenschaften Ihre bevorzugte Sportart: Windsurfen Snowboarden Kitesurfen OK Abbrechen
12 13
14
15
Abbildung 12.20 zeigt das Ergebnis in XamlPad. Abbildung 12.20: Das Beispiel für die Vererbung der Werte von Abhängigkeitseigenschaften in Aktion
16
17
18
19
20
21 Wie Sie sehen, wurde der Wert der FontSize-Eigenschaft an alle dem Page-Element untergeordneten Elemente vererbt, die diese Eigenschaft nicht explizit gesetzt haben. Das betrifft neben dem Label auch die Einträge der ListBox. Die Schriftgröße der Schalter ist hingegen auf den Wert definiert, der in den Button-Elementen explizit gesetzt wurde.
22
23
773
WPF-Grundlagen
Die Vererbung des Eigenschaftswerts funktioniert für Abhängigkeitseigenschaften in der Regel wie gezeigt. Es gibt aber auch Ausnahmen: ■
■
Nicht alle Abhängigkeitseigenschaften nehmen an der Vererbung ihres Werts teil. Dieses Verhalten wird intern über die Metadaten gesteuert und beim Registrieren der Abhängigkeitseigenschaft definiert. Es kann Eigenschaftswert-Quellen mit einer höheren Priorität geben. Die Schriftart-Eigenschaften und die Vorder- und Hintergrundfarbe der Steuerelemente Menu, StatusBar und ToolTip werden z. B. (inkonsequenterweise) aus WindowsSystemparametern ausgelesen, auch wenn diese in einem übergeordneten Element über die entsprechenden Eigenschaften gesetzt wurden (das direkte Setzen im StatusBar-, Menu- oder ToolTip-Element ist aber möglich).
Unterstützung für mehrere Provider Abhängigkeitseigenschaften werden von einer Kette von »Providern« definiert
Der Wert einer Abhängigkeitseigenschaft wird nicht nur über die übergeordneten Elemente oder die evtl. direkt gesetzte normale Eigenschaft bestimmt. WPF unterstützt noch weitere Möglichkeiten, den Wert von Abhängigkeitseigenschaften zu bestimmen. Dabei wird eine Kette eingehalten, die folgendermaßen aufgebaut ist:
1. 2. 3. 4. 5.
Bestimmung des Basiswerts Auswertung des Basiswerts wenn es sich um einen Ausdruck handelt Ausführung von Animationen Ausführung von benutzerdefiniertem Transformations-Code Validierung des Werts
Das ist ganz schön komplex, aber wichtig zu wissen, um zu verstehen, woher der Wert einer Abhängigkeitseigenschaft stammen kann. Schritt 1: Bestimmung des Basiswerts Der Basiswert einer Abhängigkeitseigenschaft wird von einem von insgesamt zehn möglichen so genannten Providern zur Verfügung gestellt. Dieses Verhalten ist relativ komplex und kann hier nicht näher erläutert werden. Die Art der Bestimmung des Basiswerts eine Eigenschaft und besonders die Priorität spielt aber bei der Arbeit mit WPF eine sehr wichtige Rolle (besonders wenn Sie eigene Stile und Vorlagen entwickeln). Leider sind die hier verwendeten Begriffe teilweise sehr schwierig zu erläutern. Einige Begriffe wie Stil-Setter, Stil-Trigger und Vorlagen-Trigger kennen Sie aber bereits aus dem Abschnitt »Ressourcen, Stile, Vorlagen, Dekoratoren, Skins und Themen« (Seite 762). Die folgende Auflistung beschreibt die möglichen Eigenschaftswert-Provider in der Reihenfolge ihrer Priorität:
1. 2.
3.
4.
774
Lokaler Wert (der Wert, der einer Eigenschaft in XAML oder im Programm zugewiesen wird) TemplatedParent-Vorlageneigenschaften: Gilt nur für WPF-Elemente, die Teil der Vorlage von Steuerelementen sind: Ein WPF-Element, das Teil der Vorlage eines Steuerelements ist, kann über seine TemplatedParent-Eigenschaft auf das logische Steuerelement zugreifen und dessen Eigenschaftswerte auslesen. Impliziter Stil: Gilt nur für die Style-Eigenschaft, die den Stil des Steuerelements bestimmt. Diese wird von WPF automatisch mit einem Stil gefüllt, wenn in den Ressourcen ein passender gefunden wird. Stil-Trigger in einer Ressource
Die grundlegenden WPF-Konzepte
5. Vorlagen-Trigger in einer Ressource 6. Stil-Setter eines Stils in einer Ressource (Die Setter, die in einem Stil Eigenschaften des Steuerelements definieren) Stil-Trigger im Standardstil (Stil, der von WPF an Hand des aktuellen Themas automatisch gesetzt wird) 8. Stil-Setter im Standardstil 9. Wert-Vererbung (Vererbung des Werts einer gleichnamigen Eigenschaft des Parent-Elements, falls die Abhängigkeitseigenschaft für Vererbung registriert wurde) 10. Standardwert der Abhängigkeitseigenschaft
7.
Nähere Informationen zu dem Ablauf der Basiswertermittlung finden Sie, indem Sie in der Visual-Studio-Dokumentation nach dem Thema »Priorität von Abhängigkeitseigenschaftenwerten« suchen.
12 13
REF
14
Schritt 2: Auswertung des Basiswerts für den Fall, dass es sich um einen Ausdruck handelt Wenn der ermittelte Basiswert eine Instanz einer von Expression abgeleiteten Klasse ist, wird diese in Schritt 2 ausgewertet.
15
Schritt 3: Ausführung von Animationen Wenn zurzeit Animationen ausgeführt werden, können diese den ermittelten Wert einer Abhängigkeitseigenschaft in Schritt 3 beeinflussen.
16
Schritt 4: Ausführung von Transformations-Code in der Abhängigkeitseigenschaft Falls mit der Abhängigkeitseigenschaft eine Callback-Methode vom Typ CoerceValueCallback registriert wurde, wird diese nun mit dem ermittelten Wert aufgerufen. Dieses Verhalten bezeichnet Microsoft als »Eigenschaftensystemkoersion«.
17
18
Schritt 5: Validierung des Werts Schließlich wird der ermittelte Wert der Callback-Methode übergeben, die ggf. als ValidateValueCallback-Referenz mit der Abhängigkeitseigenschaft registriert wurde. Diese Methode sollte true zurückgeben, wenn der ermittelte Wert ok ist, und false, wenn nicht. Wird false zurückgegeben, resultiert eine Ausnahme, die den gesamten Prozess beendet. Diese Callback-Methode ist – falls definiert – lediglich in dem Typen definiert, der die Abhängigkeitseigenschaft deklariert. Der Typ ermittelt damit, ob der in den vorhergehenden Schritten ggf. erheblich veränderte Basiswert noch gültig ist. Sie dient nicht dazu, eine Validierung des Werts von außen (über das Programm vorzunehmen).
19
20
21
Das Gesamt-Schema ergibt eine Priorität für die Ermittlung des Werts einer Eigenschaft, die Sie bei der Arbeit mit WPF immer im Auge behalten sollten:
1. 2. 3. 4. 5.
22
Eigenschaftensystemkoersion Animationen Lokaler Wert Auslesen von TemplatedParent-Vorlageneigenschaften für WPF-Elemente, die Teil der Vorlage von Steuerelementen sind Impliziter Stil (nur für die Style-Eigenschaft)
23
775
WPF-Grundlagen
6. 7. 8. 9. 10. 11. 12.
TIPP
Trigger in einem Stil in einer Ressource Trigger in einer Vorlage in einer Ressource Stil-Setter eines Stils in einer Ressource Trigger im Standardstil Stil-Setter im Standardstil Vererbung Standardwert der Abhängigkeitseigenschaft
Um herauszufinden, woher der Wert einer Abhängigkeitseigenschaft stammt, können Sie die Methode GetValueSource der DependencyPropertyHelper-Klasse verwenden. Über das zurückgegebene ValueSource-Objekt erhalten Sie die benötigten Informationen. Diese Technik ist beim Debuggen häufig ein wichtiger Trick um herauszufinden, warum der Wert einer Eigenschaft nicht der ist, der eigentlich erwartet wurde.
12.5.5 Angefügte Eigenschaften Angefügte Eigenschaften sind eine besondere Form von Abhängigkeitseigenschaften. Diese Eigenschaften können einem Kind-Element hinzugefügt werden, obwohl sie eigentlich zu einem übergeordneten Element gehören. Die Syntax dazu ist: Typname.Eigenschaftsname = "Wert"
Das folgende Beispiel zeigt, was ich damit meine. Es definiert ein Grid mit zwei Spalten und zwei Zeilen. In jeder Zelle wird ein Rectangle-Objekt angelegt: Listing 12.25: Einfaches Beispiel für angefügte Eigenschaften
Da auf einem Grid mit mehreren Spalten und Zeilen angegeben werden muss, in welcher Spalte und welcher Zeile ein Element angelegt werden soll, erfolgt dies in Form einer angefügten Eigenschaft, die im jeweiligen Element angegeben wird. Angefügte Eigenschaften haben den Sinn, dass es WPF-Elementen damit ermöglicht wird, einen Wert für Eigenschaften angeben zu können, die eigentlich in einem übergeordneten Element definiert ist. Das Beispiel oben ist eine typische Anwendung dieses Konzepts. Dabei informiert ein Kind-Element seinen Container darüber, wo und/oder wie es dargestellt werden will. Ein anderes Beispiel für angefügte Eigenschaften ist die DockPanel-Klasse, die es ermöglicht, dass Steuerelemente innerhalb dieses Containers andocken. Wie ein in ein DockPanel integriertes WPF-Element aber andockt, teilt dieses dem DockPanel über die angefügte Eigenschaft Dock mit. So kann das WPF-Element z. B. oben andocken, indem es Dock auf Top setzt.
776
Die grundlegenden WPF-Konzepte
Das Konzept der angefügten Eigenschaften existiert bereits in Windows.Forms. Eigenschaften wie Left, Top, Anchor und Dock gehören dort zwar scheinbar zu einer Steuerelement-Instanz, werden aber in Wirklichkeit von deren Container zur Verfügung gestellt. Nur der Container kann ein Steuerelement schließlich ausrichten.
INFO
12.5.6 Geroutete Ereignisse Geroutete Ereignisse5 (Routed events) erweitern das Ereigniskonzept der klassischen OOP. Ein geroutetes Ereignis wird wie auch bei der klassischen OOP auf einem Objekt ursprünglich ausgelöst. Das Besondere an gerouteten Ereignissen ist aber, dass diese den visuellen Baum auch hoch- und runterklettern können und auf allen über- bzw. untergeordneten WPF-Elementen ebenfalls aufgerufen werden. Bei der Deklaration eines gerouteten Ereignisses wird dieses Verhalten in Form der RoutingStrategy-Aufzählung übergeben. Ich kann aus Platzgründen nicht näher darauf eingehen, aber deren Werte sagen alles Notwendige über die möglichen Routing-Strategien aus: ■
■ ■
Geroutete Ereignisse können den visuellen Baum hoch- und runterwandern
12 13
14
Bubble: Das Ereignis wird erst auf dem Element aufgerufen, auf dem es ursprünglich getriggert wurde. Danach wandert es den Element-Baum hoch und wird auf jedem übergeordneten Element aufgerufen. Das Hochwandern kann in einer Ereignisbehandlungsmethode dadurch gestoppt werden, dass die Eigenschaft Handled des Ereignisargument-Objekts auf true gesetzt wird. Direct: Das Ereignis wird nur auf dem Element aufgerufen, auf dem es ursprünglich getriggert wurde. Tunnel: Das Ereignis wird erst auf dem Element aufgerufen, auf dem es ursprünglich getriggert wurde. Danach wandert es den Element-Baum herunter und wird auf jedem untergeordneten Element aufgerufen. Das Herunterwandern kann wie bei der Bubble-Strategie über ein Setzen der EreignisargumentEigenschaft Handled auf true gestoppt werden.
15
16
17
18
Das folgende einfache Beispiel demonstriert das Bubble-Verhalten: Listing 12.26: Demo für geroutete Ereignisse
19
Ihre bevorzugte Sportart: Windsurfen Snowboarden Kitesurfen
20
21
22
23 5
In der Visual-Studio-Dokumentation wurde der Begriff »Routed events« in meinen Augen irreführend in »Routingereignisse« übersetzt. »Geroutete Ereignisse« passt wohl besser, auch wenn der Name etwas konstruiert erscheint.
777
WPF-Grundlagen
Das MouseRightButtonDown-Ereignis des Fensters wurde in dem Beispiel auf die Methode Window_MouseRightButtonDown gelegt. Diese Methode macht im Beispiel nicht allzu viel: Listing 12.27: Die Methode für das MouseRightButtonDown-Ereignis des WPF-Fensters private void Window_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("Die rechte Maustaste wurde " + "auf dem Fenster betätigt"); }
Geroutete Ereignisse besitzen eine .NET-typische Signatur. Am ersten Argument wird eine Referenz auf das Objekt übergeben, das das Ereignis ursprünglich hervorgerufen hat. Das zweite Argument enthält Argumente, die bei gerouteten Ereignissen vom Typ RoutedEventArgs oder einer davon abgeleiteten Klasse sind. Diese Klasse bietet die folgenden Eigenschaften: ■ ■ ■ ■
Handled: Wenn Sie diese Eigenschaft auf true setzen, brechen Sie damit das Routing ab. OriginalSource: Eine Referenz auf das Objekt im visuellen Baum, das das Ereignis ursprünglich aufgerufen hat. Source: Eine Referenz auf das WPF-Element im logischen Baum, das das Ereignis ausgelöst hat. RoutedEvent: Eine Instanz einer von RoutedEvent abgeleiteten Klasse, die das geroutete Ereignis referenziert. Diese Eigenschaft können Sie zur Identifikation der eigentlichen Quelle einsetzen, wenn Sie einen Ereignishandler für mehrere Ereignisse definiert haben (was in der Praxis zu unübersichtlichen Programmen führt).
Das MouseRightButtonDown-Ereignis wird z. B. immer dann aufgerufen, wenn der Anwender mit der rechten Maustaste auf einem WPF-Element klickt. Wenn Sie die WPF-Anwendung nun ausführen und mit der rechten Maustaste z. B. auf der ListBox klicken, wird das MouseRightButtonDown-Ereignis des Fensters ebenfalls aufgerufen. Davor sind aber die entsprechenden Ereignisse der ListBox und des Grid aufgerufen worden, die im Beispiel allerdings nicht behandelt wurden. Sie können aber natürlich auch diese Ereignisse separat auswerten: Listing 12.28: Separates Auswerten eines gerouteten Ereignisses als Demo Ihre bevorzugte Sportart: Windsurfen Snowboarden Kitesurfen
778
Die grundlegenden WPF-Konzepte
Listing 12.29: Die Methoden zum Auswerten der zusätzlich definierten Ereignisse private void Grid_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("Die rechte Maustaste wurde auf dem Grid betätigt"); } private void lstSports_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("Die rechte Maustaste wurde " + "auf der ListBox betätigt"); }
12 13
Wenn Sie das Beispiel ausführen und mit der rechten Maustaste auf der ListBox klicken, erscheinen nun drei Meldungen hintereinander: zuerst die Meldung, dass auf der ListBox geklickt wurde, dann die Mitteilung, dass auf dem Grid geklickt wurde und schließlich die Meldung, dass auf dem Fenster geklickt wurde.
14
Wie gesagt können Sie das Ereignis-Routing auch unterbrechen, indem Sie die Eigenschaft Handled des übergebenen Ereignisargument-Objekts auf true setzen:
15
Listing 12.30: Unterbrechen des Ereignis-Routing private void lstSports_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("Die rechte Maustaste wurde " + "auf der ListBox betätigt");
16
17
// Das Ereignis-Routing unterbrechen e.Handled = true; }
In diesem Fall wird das Ereignis nicht mehr nach oben weitergegeben, und Sie erhalten nur noch eine Meldung, wenn Sie mit der rechten Maustaste auf die ListBox klicken.
18
Mit diesem Beispiel wird der Sinn nach oben gerichteter gerouteter Ereignisse wahrscheinlich deutlich: Wenn Sie ein Ereignis übergeordnet für mehrere Elemente behandeln wollen, belegen Sie das Ereignis des übergeordneten Elements mit einem Ereignishandler. Wollen Sie ein Ereignis allerdings speziell für ein Steuerelement auswerten, belegen Sie dessen Ereignis-Eigenschaft und setzen im Ereignishandler in der Regel Handled auf true, damit nach Ihrer Auswertung nicht noch eine evtl. vorhandene weitere, übergeordnete Auswertung erfolgt. In der Praxis reicht diese Technik in der Regel aus. In einigen wenigen speziellen Fällen werden Sie möglicherweise auch ein geroutetes Ereignis, das Sie für ein Steuerelement abfangen, nicht abbrechen und damit noch einmal in einem übergeordneten Element auswerten. Das entspricht aber gar nicht der von mir favorisierten Maxime »Keep it simple« (eigentlich »Keep it simple and go on holidays a lot» ☺). Geroutete Ereignisse, die den Elementbaum runterwandern (Tunnel-Strategie) oder das Ereignis gar nicht weitergeben (Direct-Strategie), sind zwar nicht so häufig vorhanden wie Ereignisse, die die Bubble-Strategie einsetzen, aber dennoch recht zahlreich. Das Ereignis PreviewDragOver der Klasse ContentElement setzt z. B. die Tunnel-Strategie ein, die Ereignisse MouseEnter und MouseLeave der Klasse System.
19
20
21
22
INFO
23
779
WPF-Grundlagen
Windows.Input.Mouse die Direct-Strategie. Prinzipiell setzen viele der Ereignisse, deren Namen mit »Preview« beginnen, Tunnelling ein, einige aber auch die DirectStrategie.
Welche Routing-Strategie im konkreten Fall eingesetzt wird, erfahren Sie über die »Informationen zum Routingereignis« in der Dokumentation des jeweiligen Ereignisses.
Preview-Ereignisse Über PreviewEreignisse können Sie die Auswertung verhindern
»Preview«-Ereignisse werden grundsätzlich vor ihren Äquivalenten ohne »Preview« aufgerufen. Das Ereignis PreviewKeyDown ist z. B. ein Ereignis, das die Tunnel-Strategie einsetzt und vor dem »normalen« KeyDown-Ereignis aufgerufen wird. Warum setzt PreviewKeyDown nun die Tunnel-Strategie ein? Die Antwort ist einfach: Dieses Ereignis wird z. B. bei einer TextBox aufgerufen, wenn eine Taste betätigt wird. Der Aufruf erfolgt aber, bevor die Tastenbetätigung an die TextBox weitergegeben wird. Falls das Routing über die Handled-Eigenschaft abgebrochen wird, erhält die TextBox die eingegebene Taste nicht. Über die PreviewEreignisse können Sie also verhindern, dass ein Steuerelement eine Eingabe erhält. Das Nicht-Preview-Äquivalent KeyDown wird allerdings erst aufgerufen, nachdem die TextBox die Taste empfangen hat. In diesem Ereignis können Sie also nicht mehr verhindern, dass die Eingabe ausgewertet wird. Listing 12.31 zeigt dies am Beispiel einer TextBox. Alle Tastenbetätigungen, die keine Ziffer, keine Bewegungstaste und keine Löschtaste sind, werden über die HandledEigenschaft des Ereignisargument-Objekts des PreviewKeyDown-Ereignisses abgebrochen. Listing 12.31: Beispiel für die Verwendung eines Preview-Ereignisses ... /* C#-Code */ private void txtDemo_PreviewKeyDown(object sender, KeyEventArgs e) { if ((e.Key < Key.D0 || e.Key > Key.D9) && e.Key != Key.Back && e.Key != Key.Left && e.Key != Key.Right && e.Key != Key.Delete && e.Key != Key.Home && e.Key != Key.End && e.Key != Key.Tab) { // Das Tunneling abbrechen, damit die // Taste nicht weitergegeben wird e.Handled = true; } }
780
Die grundlegenden WPF-Konzepte
Geroutete Ereignisse auf dem visuellen Baum Geroutete Ereignisse werden nicht auf der Ebene des logischen Baums geroutet, sondern auf der Ebene des visuellen Baums. Ein Beispiel dafür ist das ButtonSteuerelement, dessen visueller Baum aus mehreren Unterelementen besteht (Abbildung 12.21). Abbildung 12.21: Der visuelle Baum eines ButtonElements (mit Text als Inhalt)
Kickt der Anwender z. B. mit der rechten Maustaste auf das TextBlock-Element, das die Beschriftung des Schalters darstellt, wird zunächst dessen MouseRightButtonDown-Ereignis aufgerufen. Danach wird das gleichnamige Ereignis des ContentPresenter aufgerufen, danach das des ButtonChrome-Objekts und schließlich das MouseRightButtonDown-Ereignis des Button selbst. In der Praxis hat das für Sie zwar prinzipiell keine Auswirkungen. Es erklärt aber, wie ein Button, trotz des visuellen Baums, zu seinen Ereignissen kommt. Prinzipiell könnten Sie im Quellcode sogar die Ereignisse der Elemente des visuellen Baums zuweisen und abfangen, aber das ist zum einen wohl nur selten notwendig und führt zum anderen zu Problemen, wenn in zukünftigen Versionen von WPF der visuelle Baum geändert wird.
12 13
14
15
16 Das Beispielprojekt »Geroutete Ereignisse auf dem visuellen Baum«, das Sie neben dem Beispiel zu diesem Abschnitt finden, zeigt, wie Sie den visuellen Baum prinzipiell zur Laufzeit auswerten und den Kind-Elementen Ereignisse zuweisen. Dieses Beispiel dient aber nur der Demonstration und sollte nicht in der Praxis eingesetzt werden.
DISC
17
Geroutete Ereignisse, die intern abgebrochen werden Einige WPF-Elemente brechen spezielle geroutete Ereignisse intern ab. Das ist z. B. der Fall bei der Klasse ListBoxItem, die einen Eintrag einer ListBox verwaltet. Diese Klasse verarbeitet die Ereignisse MouseLeftButtonDown und MouseRightButtonDown und bricht das Bubbling nach der internen Verarbeitung ab. Wenn Sie diese Ereignisse also für eine ListBox auswerten wollen, erhalten Sie kein Ergebnis, wenn der Anwender auf einem der Einträge der ListBox klickt. Dieses Verhalten ist vielleicht ärgerlich. Sie können aber das entsprechende Preview-Ereignis verwenden (z. B. PreviewMouseRightButtonDown), das auch dann aufgerufen wird, wenn der Anwender auf einem ListBoxItem-Element klickt (weil der Eintrag die Maus noch gar nicht empfangen hat). So können Sie z. B. ein spezielles Kontextmenü anzeigen, das sich auf den ausgewählten Eintrag bezieht.
12.5.7
Einige WPFElemente brechen geroutete Ereignisse intern ab
18
19
20
21
Angefügte Ereignisse
Geroutete Ereignisse werden auch dann an über- bzw. untergeordnete Elemente weitergegeben, wenn diese das entsprechende Ereignis gar nicht unterstützen. Ein Beispiel dafür ist das Click-Ereignis eines Button-Elements. Ist ein Button z. B. auf einem Grid angelegt, empfängt auch das Grid-Objekt das Click-Ereignis, wenn der Anwender den Schalter betätigt. Und das Ereignis wird natürlich auch nach oben weitergegeben.
22
23
Dieses Feature fügt also Ereignisse an Elemente an, die solche gar nicht besitzen, damit diese bei der Verwendung von Ereignis-Routing ausgewertet werden können.
781
WPF-Grundlagen
Dabei können Sie ein geroutetes Ereignis sogar auch in den Elementen auswerten, die kein entsprechendes Ereignis besitzen. Dazu müssen Sie allerdings den Namen der Klasse angeben, die das Ereignis deklariert. Das kann übrigens auch eine Basisklasse sein. So können Sie das Click-Ereignis eines Schalters z. B. auch in dem Window-Objekt auswerten, das den Schalter enthält: Listing 12.32: Abfangen eines gerouteten Ereignisses in einem Element, das dieses gar nicht selbst zur Verfügung stellt Demo 1 Demo 2
Dem Ereignishandler werden natürlich alle Click-Ereignisse der enthaltenen ButtonElemente übergeben. Über die Eigenschaft Source des Ereignisargument-Objekts können Sie die Quelle identifizieren: Listing 12.33: Auswertung eines Ereignisses, das von mehreren Quellen stammen kann private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show(((Button)e.Source).Name + " wurde betätigt"); }
INFO
In der Praxis sollten Sie damit aber vorsichtig umgehen, da eine solche Technik ein Programm unübersichtlich und fehleranfällig macht. Verwenden Sie geroutete Ereignisse idealerweise so, dass Sie sich im Ereignishandler nicht auf die eigentliche Quelle beziehen müssen. Das Beispiel ist deswegen nicht sehr praxisfreundlich. In der Praxis sollten Sie besser für jeden Schalter separate Ereignisbehandlungsmethoden zuweisen. Das Beispiel demonstriert aber die Technik, geroutete Ereignisse in der angefügten Form zu verwenden, auch wenn Sie diese Technik in der Praxis wahrscheinlich eher selten einsetzen.
12.5.8 Die grundlegende WPF-Klassenhierarchie Der WPF-Konzepte-Abschnitt endet nun mit einer grundlegenden Beschreibung der fundamentalen WPF-Klassen. WPF enthält eine sehr große Anzahl an Typen, die teilweise exzessiv aufeinander aufbauen. Die Übersicht darüber zu behalten, ist nicht besonders einfach. Setzen Sie ggf. die in Kapitel 1 vorgestellten Poster ein. Die fundamentalen Klassen stellt Abbildung 12.22 zunächst als Klassendiagramm dar.
782
Die grundlegenden WPF-Konzepte
Abbildung 12.22: Klassendiagramm der fundamentalen WPF-Klassen
12 13
14
15
16
17
18
In Tabelle 12.3 finden Sie eine kurze Beschreibung dieser Klassen. Klasse
Beschreibung
DispatcherObject Namensraum: System. Windows. Threading
Basisklasse für alle Objekte, die nur von dem Thread verwendet werden dürfen, der das Objekt erzeugt hat. Die meisten WPF-Klassen sind von DispatcherObject abgeleitet und damit nicht threadsicher. Sie können also nicht direkt von einem Thread aus verwendet werden, der das Objekt nicht erzeugt hat, ohne (teilweise erhebliche) Probleme zu verursachen. Dieses Verhalten liegt an der Nachrichten-Architektur von Windows. In Kapitel 20 gehe ich näher darauf ein.
DependencyObject Namensraum: System. Windows
19 Tabelle 12.3: Die fundamentalen WPF-Klassen
20
21
Basisklasse für alle Klassen, die Abhängigkeitseigenschaften unterstützen. DependencyObject definiert die wichtigen Methoden GetValue und SetValue.
22
23
783
WPF-Grundlagen
Tabelle 12.3: Die fundamentalen WPF-Klassen (Forts.)
Klasse
Beschreibung
Freezable Namensraum: System. Windows
Basisklasse für alle Objekte, die (über die statische Freeze-Methode) in einen speziellen schreibgeschützten Zustand »eingefroren« werden können. Eingefrorene Objekte können nicht wieder »aufgetaut« werden, Sie können aber (über die Methode Clone) einen Klon eines eingefrorenen Objekts erzeugen. Ein besonderes Feature ist, dass eingefrorene Objekte sicher in mehreren Threads verwendet werden können.
Visual Namensraum: System. Windows. Media
Basisklasse für Objekte, die visuell dargestellt werden können. Visual-Objekte verwenden Sie auch zum 2D-Zeichnen.
UIElement Namensraum: System. Windows
Basisklasse für alle sichtbaren UI-Objekte, die geroutete Ereignisse, Bindung von Befehlen, Layouts und den Eingabefokus unterstützen
ContentElement Namensraum: System. Windows
Basisklasse für einfache UI-Objekte, die nicht selbst gerendert werden. Ein ContentElement wird immer auf einer Instanz einer von Visual abgeleiteten Klasse abgelegt, die die Darstellung des Elements übernimmt.
FrameworkElement Namensraum: System. Windows
Basisklasse für alle sichtbaren UI-Elemente, die zusätzlich zu den von UIElement geerbten Fähigkeiten Stile, Datenbindung, Ressourcen und weitere Windowstypische Dinge wie z. B. Kontextmenüs unterstützen.
FrameworkContentElement Namensraum: System. Windows
Ähnlich FrameworkElement, nur für einfachen Inhalt
Control Namensraum: System. Windows. Controls
Basisklasse für alle Steuerelemente
Damit beende ich dieses Kapitel, das wieder einmal wesentlich länger geworden ist, als ich ursprünglich dachte. Im folgenden Kapitel erfahren Sie dann mehr über die Anwendungsentwicklung mit WPF. Viel Spaß erst einmal ☺.
784
Inhalt
13
WPF-Anwendungen 12
13
Nachdem Kapitel 12 die Grundlagen von WPF zum Thema hatte, geht es in diesem Kapitel nun um die Anwendungsentwicklung mit WPF. Ich lege dabei den Fokus auf die Entwicklung von Standardanwendungen, nicht die von navigationsbasierten oder Browser-Anwendungen. Das Thema dieses Kapitels ist eher klassisch angelegt und zeigt den Umgang mit Anwendungen, mit Fenstern und den wichtigsten Steuerelementen. Die speziellen WPF-Features wie Befehle, Trigger, Stile, Skins etc. werden in diesem Kapitel noch nicht behandelt. Kapitel 14 führt in die wichtigsten dieser Themen ein.
14
15
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■
Wichtige Grundlagen zur Anwendungsentwicklung in WPF Der grundsätzliche Umgang mit WPF-Fenstern Modale, unmodale und Dialog-Fenster Der grundsätzliche Umgang mit den Steuerelementen Die wichtigsten Steuerelemente (kurz und bündig) Grundsätzliches Layout von WPF-Fenstern (oder Seiten) über die LayoutSteuerelemente
16
17
Was in diesem Kapitel nicht behandelt wird, sind: ■ ■ ■ ■
■ ■
18
WPF-Anwendungen auf der Basis von Seiten, zwischen denen navigiert werden kann (Navigationsbasierte Anwendungen), Silverlight-Anwendungen, alle Möglichkeiten der besprochenen Steuerelemente, die Steuerelemente Border, DockPanel, DocumentViewer, Ellipse, Frame, GridSplitter, Image, InkCanvas, ListView, MediaElement, Rectangle, RichTextBox, ScrollBar, TabControl, TabPanel, ToolBarPanel, ToolBarTray, ToolBarOverflowPanel, TreeView, UniformGrid, ViewBox und WindowsFormsHost, die Interoperabilität mit Windows.Forms und die Entwicklung eigener WPF-Steuerelemente.
13.1
19
20
21
Grundlegendes zur Anwendungsentwicklung unter WPF
22
In diesem Kapitel geht es um die Entwicklung von Standardanwendungen mit WPF, nicht um die Integration von WPF in einen Browser oder um loses XAML. Wie Sie eine WPF-Anwendung prinzipiell erzeugen, habe ich bereits in Kapitel 12 beschrieben. Dort wird auch der prinzipielle Aufbau behandelt (mit einer App.xaml-Datei).
23
785
Index
■
WPF-Anwendungen
In diesem Abschnitt behandle ich nun zunächst einige wichtige Dinge, die Sie bei der Entwicklung von Anwendungen in WPF immer wieder benötigen.
13.1.1 Eine WPF-Anwendung wird über eine Instanz der Klasse Application repräsentiert
Die WPF-Anwendung
Die WPF-Anwendung wird in einem Visual-Studio-WPF-Anwendung-Projekt über die Datei App.xaml repräsentiert. Beim Kompilieren erzeugt Visual Studio eine MainMethode, die beim Start der Anwendung eine Instanz der Klasse erzeugt, die App.xaml in Verbindung mit der partiellen C#-Klasse App.xaml.cs ergibt. Wie das prinzipiell aussieht, habe ich bereits in Kapitel 12 behandelt. Die Main-Methode ruft die Run-Methode der Basisklasse System.Windows.Application auf, von der die Anwendungs-Klasse abgeleitet ist. Run führt dazu, dass die Anwendung das in der Eigenschaft StartupUri angegebene Fenster erzeugt, und öffnet und eine Nachrichten-Warteschleife startet (die für Windows-Anwendungen essentiell ist). Die Application-Klasse bietet einige Eigenschaften, Ereignisse und Methoden, über die Sie mit der Anwendung arbeiten können. Tabelle 13.1 beschreibt zunächst die wichtigsten Eigenschaften.
Tabelle 13.1: Die wichtigsten Eigenschaften der Application-Klasse
Eigenschaft
Beschreibung
Current
Diese statische Eigenschaft liefert eine Referenz auf das Application-Objekt der aktuellen Anwendung.
MainWindow
liefert eine Referenz auf das Hauptfenster der Anwendung (das, das beim Start erzeugt und geöffnet wurde).
Properties
In dieser Dictionary-Auflistung können Sie Daten verwalten, die in der gesamten Anwendung zur Verfügung stehen sollen. Properties ist threadsicher, was beim Multithreading ein interessanter Aspekt ist.
Resources
Diese Auflistung verwaltet alle WPF-Ressourcen, die der Anwendung direkt zugeordnet sind. WPF-Ressourcen werden in Kapitel 14 behandelt.
ShutdownMode
Diese Eigenschaft bestimmt, wann die Anwendung heruntergefahren wird. Sie können die folgenden Werte der gleichnamigen Aufzählung verwenden: – OnLastWindowClose: Die Anwendung wird dann heruntergefahren, wenn das letzte Fenster geschlossen oder wenn die Shutdown-Methode aufgerufen wird. OnLastWindowClose ist die Voreinstellung. – OnMainWindowClose: Die Anwendung wird dann heruntergefahren, wenn das Hauptfenster geschlossen oder wenn die Shutdown-Methode aufgerufen wird. – OnExplicitShutdown: Die Anwendung wird nur dann heruntergefahren, wenn die Shutdown-Methode explizit aufgerufen wird oder wenn Windows beendet wird.
Windows
Über diese Auflistung erreichen Sie alle aktuell vorhandenen (sichtbaren und unsichtbaren) Fenster der Anwendung.
In Tabelle 13.2 finden Sie eine Beschreibung der wichtigsten Methoden der Application-Klasse.
786
Grundlegendes zur Anwendungsentwicklung unter WPF
Methode
Beschreibung
Shutdown
sorgt dafür, dass die Anwendung explizit beendet wird. Näheres dazu finden Sie im Abschnitt »Herunterfahren einer Anwendung«.
FindResource GetContentStream GetResourceStream GetRemoteStream
Diese Methoden erlauben den Zugriff auf WPF-Ressourcen, die entweder in der Anwendung gespeichert sind oder extern zur Verfügung stehen. WPF-Ressourcen werden in Kapitel 14, allgemeine Ressourcen in Kapitel 15 behandelt.
Tabelle 13.2: Die wichtigen Methoden der Application-Klasse
12
13
Application bietet auch einige Ereignisse, über die Sie z. B. darauf reagieren können, dass die Anwendung beendet wird oder dass eine unbehandelte Ausnahme eingetreten ist (Tabelle 13.3). Ereignis
Beschreibung
Activated
tritt ein, wenn die Anwendung innerhalb des Betriebssystems in den Vordergrund geholt wird.
Deactivated
tritt ein, wenn die Anwendung in den Hintergrund gesetzt wird.
DispatcherUnhandledException
Dieses Ereignis wird dann aufgerufen, wenn eine unbehandelte Ausnahme (in einem beliebigen Thread) auftritt. Darüber können Sie auf alle unbehandelten Ausnahmen reagieren. Wie das geht, habe ich bereits in Kapitel 8 gezeigt.
Exit
tritt kurz vor dem endgültigen Herunterfahren der Anwendung ein.
SessionEnding
tritt ein, wenn der Benutzer die Windows-Sitzung beendet, indem er sich abmeldet oder Windows herunterfährt.
Startup
Dieses Ereignis wird aufgerufen, wenn die Anwendung gestartet wird.
Beachten Sie bitte, dass in Tabelle 13.3 Ereignisse für navigationsbasierte Anwendungen (wie die Ereignisse LoadCompleted und Navigated) nicht enthalten sind, weil diese im Buch nicht behandelt werden.
Tabelle 13.3: Die wichtigen Ereignisse der Application-Klasse (für nicht navigationsbasierte Anwendungen)
15
16
17
18
19 INFO
20
Listing 13.1 zeigt ein Beispiel, das einige der Application-Member verwendet. Listing 13.1:
14
Beispiel für die Anwendung einiger der Member der Application-Klasse
21
22
23
...
787
WPF-Anwendungen
/* App.xaml.cs */ public partial class App : Application { /* Wird aufgerufen, wenn die Anwendung startet */ private void Application_Startup(object sender, StartupEventArgs e) { MessageBox.Show("Die Anwendung wird gestartet"); } /* Wird aufgerufen, wenn die Anwendung heruntergefahren wird */ private void Application_Exit(object sender, ExitEventArgs e) { MessageBox.Show("Die Anwendung wird heruntergefahren"); } } Herunterfahren ... /* MainWindow.xaml.cs */ public partial class MainWindow : Window { /* Konstruktor. Erzeugt die Steuerelemente und Komponenten. */ public MainWindow() { InitializeComponent(); } private void btnShutdown_Click(object sender, RoutedEventArgs e) { // Herunterfahren der Anwendung Application.Current.Shutdown(); } }
13.1.2 ShutdownMode bestimmt, wann eine Anwendung heruntergefahren wird
Weil die Eigenschaft ShutdownMode per Voreinstellung auf OnLastWindowClose steht, wird eine WPF-Anwendung erst dann automatisch heruntergefahren, wenn das letzte Fenster geschlossen wurde. Besteht die Anwendung aus mehreren Fenstern (siehe »Der Umgang mit WPF-Fenstern«, Seite 810) und ist zurzeit neben dem Hauptfenster ein anderes unmodal1 geöffnet, bleibt das zweite Fenster geöffnet, wenn das Hauptfenster der Anwendung geschlossen wird. Die Anwendung wird erst dann beendet, wenn auch das zweite Fenster geschlossen wird.
1
788
Herunterfahren einer Anwendung
Der Begriff »unmodal« wird im Abschnitt »(Modales und unmodales) Öffnen weiterer Fenster« (Seite 814) erläutert. Eine Implikation unmodaler Fenster ist, dass der Anwender zwischen den Fenstern wechseln kann, wenn mehrere geöffnet sind.
Grundlegendes zur Anwendungsentwicklung unter WPF
Bei Anwendungen mit ausschließlich modalen Fenstern, bei denen der Anwender nur dann zum Hauptfenster zurückwechseln kann, wenn alle anderen Fenster geschlossen sind, kann mit der Default-ShutdownMode-Einstellung kein Problem auftreten. In Anwendungen mit unmodalen Fenstern entspricht dieses Verhalten aber nicht dem Windows-Standard (oder haben Sie schon einmal gesehen, dass ein Visual-Studio-Fenster geöffnet bleibt, wenn Sie das Hauptfenster schließen?).
12
Sie können aber ShutdownMode auf ShutdownMode.OnMainWindowClose setzen, um zu erreichen, dass die Anwendung immer automatisch mit dem Hauptfenster geschlossen wird: Listing 13.2:
13
Einstellen von ShutdownMode, sodass die Anwendung automatisch beendet wird, wenn das Hauptfenster geschlossen wird
14
15
In diesem Fall werden alle ggf. geöffneten anderen Fenster automatisch geschlossen, wenn das Hauptfenster geschlossen wird.
16
Auf der Buch-DVD finden Sie ein Beispiel, das dieses Verhalten demonstriert. Ich zeige das Beispiel hier aber nicht, weil Anwendungen mit mehreren Fenstern erst später behandelt werden.
17
Zum expliziten Schließen eines Fensters rufen Sie normalerweise die Close-Methode des Fensters auf. Das gilt prinzipiell auch für das Hauptfenster. Die ShutdownModeEigenschaft des Application-Objekts bestimmt mit den folgenden Werten, wann und ob die Anwendung beim Schließen von Fenstern heruntergefahren wird: ■ ■ ■
DISC
Shutdown beendet die Anwendung
19
ShutdownMode.OnLastWindowClose: Die Anwendung wird automatisch heruntergefahren, wenn das letzte Fenster geschlossen wird. ShutdownMode.OnMainWindowClose: Die Anwendung wird automatisch heruntergefahren, wenn das Hauptfenster geschlossen wird. ShutdownMode.OnExplicitShutdown: Die Anwendung wird erst dann heruntergefahren, wenn die Shutdown-Methode explizit aufgerufen wird.
20
Sie können die Shutdown-Methode des Application-Objekts aufrufen, um eine Anwendung explizit herunterzufahren. In vielen Beispielen ist zu sehen, dass im Schließen-Schalter des Hauptfensters Shutdown und im Schließen-Schalter aller weiteren Fenster deren Close-Methode aufgerufen wird. Die Eigenschaft ShutdownMode steht dann auf dem Standardwert (ShutdownMode.OnLastWindowClose). Das führt aber zu einem Problem, wenn die Anwendung unmodale Fenster einsetzt: Ist gerade neben dem Hauptfenster ein weiteres Fenster unmodal geöffnet und der Anwender schließt das Hauptfenster nicht über den dafür vorgesehenen Schalter oder
18
21
22
23
INFO
789
WPF-Anwendungen
das ggf. vorhandene Menü, sondern über die Windows-Standard-Möglichkeiten (z. B. über (ALT) + (F4)), wird die Anwendung nicht beendet und das zweite Fenster bleibt geöffnet.
TIPP
Um dieses Problem zu vermeiden, sollten Sie grundsätzlich ShutdownMode auf OnMainWindowClose setzen und das Hauptfenster in Schalter- oder Menü-Ereignishandlern über die Close-Methode schließen.
13.1.3 WPF enthält einige Standarddialoge, andere müssen Sie von Windows. Forms »ausleihen«
Standarddialoge
WPF bietet einige Standarddialoge, die Sie bei der Anwendungsentwicklung in vielen Fällen sinnvoll einsetzen können: ■ ■ ■
Microsoft.Win32.OpenFileDialog: Dialog zur Auswahl einer Datei zum Öffnen Microsoft.Win32.SaveFileDialog: Dialog zur Auswahl einer Datei zum Speichern System.Windows.Controls.PrintDialog: Dialog zur Auswahl eines Druckers
Die von Windows ebenfalls zur Verfügung gestellten Dialoge zur Auswahl eines Ordners, einer Farbe und einer Schriftart fehlen in .NET 3.5 leider noch. Wollen Sie diese Dialoge verwenden, müssen Sie auf Windows.Forms-Klassen zurückgreifen: ■ ■ ■
System.Windows.Forms.ColorDialog: Dialog zur Auswahl einer Farbe System.Windows.Forms.FolderBrowserDialog: Dialog zur Auswahl eines Ordners System.Windows.Forms.FontDialog: Dialog zur Auswahl von Schrifteinstellungen
Um diese Klassen verwenden zu können, müssen Sie natürlich die Assembly Windows.Forms.dll referenzieren. Die Standarddialoge sind sehr einfach einzusetzen. Bei einigen können Sie allerdings spezielle Einstellungen vornehmen. Die Datei-Dialoge erlauben z. B. die Definition eines Filters, über denen der Benutzer die angezeigten Dateien einschränken kann. Ich gehe in den folgenden Abschnitten auf die wichtigsten Standarddialoge ein.
OpenFileDialog und SaveFileDialog Die Standarddialoge zum Öffnen und Speichern einer Datei ermöglichen dem Anwender die Auswahl eines oder mehrerer Dateinamen (beim Öffnen-Dialog) und liefern nach dem Schließen die Auswahl des Benutzers zurück. Der Unterschied zwischen dem Öffnen- und dem Speichern-Dialog ist zum einen die voreingestellte Überschrift (»Datei öffnen« und »Datei speichern«) und zum anderen eine unterschiedliche Grundeinstellung. So muss beim Öffnen-Dialog ein ausgewählter Ordner z. B. existieren, beim Speichern-Dialog allerdings nicht. Tabelle 13.4 zeigt die wichtigen Eigenschaften beider Dialoge.
790
Grundlegendes zur Anwendungsentwicklung unter WPF
Eigenschaft
Beschreibung
Title
gibt den Titel des Dialogs an. Diese Eigenschaft ist zwar bereits für die meisten Situationen passend voreingestellt, wenn Sie aber z. B. dem Anwender einen Dialog zum Auswählen einer Datei (statt zum Öffnen) anbieten wollen, können Sie den Titel entsprechend ändern.
Filter
Hier können Sie einen oder mehrere Dateifilter angeben. Diese Filter erscheinen im geöffneten Dialog in der Dateityp-Liste und schränken die Ansicht der Dateien entsprechend ein. Den Filter definieren Sie mit einem String, der folgendermaßen aussieht:
Tabelle 13.4: Die wichtigen Eigenschaften der Dialoge OpenFileDialog und SaveFileDialog
12
"Filtername1 |Maske1 [|Filtername2|Maske2 ][...]" Ein Filter besteht immer aus Paaren, bei denen der linke Teil die Beschriftung angibt, die im Dialog ausgegeben wird. Der zweite Teil gibt den Filter über die üblichen WildcardZeichen (* und ?) an. Mehrere Filterangaben werden durch Semikolons getrennt. Die Teile eines Paars und die einzelnen Paare werden durch gerade Striche getrennt.
13
Ein typischer Filter sieht z. B. so aus:
14
"Bilddateien|*.bmp;*.gif;*.jpg;*.png|Alle Dateien|*.*" FilterIndex
DefaultExt
gibt den Filter an, der beim Öffnen des Dialogs aktiviert ist. Geben Sie beim Beispielfilter oben z. B. 1 an, ist der Filter »Alle Dateien« aktiv. 0 ist die Voreinstellung.
15
gibt die Dateierweiterung an, die Dateinamen angehängt wird, die ohne Dateierweiterung eingegeben werden. Die Dateierweiterung müssen Sie inklusive Punkt angeben.
16
InitialDirectory gibt den Ordner an, den der Dialog beim Öffnen anzeigt. CheckFileExists
definiert, ob der Dialog bei der Betätigung des OK-Schalters überprüft, ob die eingegebene Datei existiert. Ist diese Eigenschaft true (Voreinstellung beim Öffnen-Dialog) und existiert die ausgewählte oder eingegebene Datei nicht, erhält der Anwender bei der Betätigung des OK-Schalters eine entsprechende Meldung. Der Dialog wird dann nicht geschlossen. Beim Speichern-Dialog ist CheckFileExists per Voreinstellung auf false eingestellt.
CheckPathExists
definiert, ob der Dialog bei der Betätigung des OK-Schalters überprüft, ob der Pfad der ausgewählten Datei existiert. Steht für beide Dialoge per Voreinstellung auf true.
OverwritePrompt
gibt beim Speichern-Dialog an, ob der Dialog bei der Betätigung des OK-Schalters eine Meldung ausgibt, dass die ausgewählte Datei bereits existiert und den Anwender fragt, ob er diese überschreiben will. Der Anwender kann die Meldung mit JA bestätigen, worauf der Dialog geschlossen wird. Betätigt der Anwender Nein, weil er die Datei nicht überschreiben will, wird der Dialog nicht geschlossen.
Multiselect
ShowReadOnly
FileName
FileNames
17
18
19
20
gibt für den Öffnen-Dialog an, ob eine Mehrfachauswahl erlaubt ist. Die Voreinstellung ist false. Wenn Sie eine Mehrfachauswahl erlauben, lesen Sie die ausgewählten Dateien aus der Filenames-Auflistung aus.
21
gibt für den OpenFileDialog an, ob der Dialog eine CheckBox anzeigt, die der Anwender einschalten kann, um anzuzeigen, dass die Datei schreibgeschützt geöffnet werden soll. Diese Einstellung können Sie nach dem Öffnen aus der Eigenschaft ReadOnlyChecked auslesen.
22
gibt den Dateinamen an. Diese Eigenschaft können Sie vor dem Öffnen des Dialogs beschreiben, um einen Dateinamen voreinzustellen. Nach dem Öffnen des Dialogs lesen Sie den vom Benutzer ausgewählten Dateinamen hier aus.
23
gibt die ausgewählten Dateinamen an, wenn eine Mehrfachauswahl erlaubt ist.
791
WPF-Anwendungen
ShowDialog öffnet den Dialog
Datei-Öffnen- und -Speichern-Dialoge öffnen Sie wie bei Dialogen üblich über die ShowDialog-Methode. Diese Methode gibt einen Nullable-Boolean-Wert zurück. true sagt aus, dass der Anwender den Dialog mit OK bestätigt hat. Bei der Rückgabe von false hat der Anwender den Abbrechen-Schalter betätigt. null ist für andere Dialoge vorgesehen und wird beim Datei-Öffnen- und -Speichern-Dialog nicht verwendet. Wenn ShowDialog true zurückgibt, können Sie den ausgewählten Dateinamen aus der Filename-Eigenschaft auslesen. Bei einem Öffnen-Dialog mit Mehrfachauswahl lesen Sie die ausgewählten Dateinamen aus der Filenames-Auflistung aus. Listing 13.3 zeigt, wie Sie einen Öffnen-Dialog verwenden, um den Anwender den Namen einer zu öffnenden Bilddatei angeben zu lassen. Der Ordner, in dem der Dialog startet, wird dabei auf den Standard-Ordner für Bilder eingestellt. Listing 13.3:
Typische Anwendung des Datei-Öffnen-Dialogs
// Den Dialog erzeugen und initialisieren Microsoft.Win32.OpenFileDialog dialog = new Microsoft.Win32.OpenFileDialog(); dialog.Filter = "Bilddateien|*.bmp;*.gif;*.jpg;*.png|Alle Dateien|*.*"; dialog.FilterIndex = 0; dialog.DefaultExt = ".bmp"; dialog.InitialDirectory = Environment.GetFolderPath( Environment.SpecialFolder.MyPictures); // Den Dialog anzeigen und auswerten if (dialog.ShowDialog() == true) { MessageBox.Show("Ausgewählte Datei: " + dialog.FileName); }
Abbildung 13.1 zeigt den Dialog unter Vista. Abbildung 13.1: Der Standard-DateiÖffnen-Dialog unter Vista
792
Grundlegendes zur Anwendungsentwicklung unter WPF
Wie Sie sehen, entspricht der Datei-Öffnen-Dialog (wie auch der Datei-SchließenDialog) unter Vista unverständlicherweise nicht dem eigentlichen Vista-Standarddialog. Um diesen zu erhalten, können Sie einige Klassen aus dem Microsoft-Beispiel VistaBrigde verwenden. Sie finden dieses leider nur in den Beispielen des aktuellen Windows SDK im Ordner Samples\CrossTechnologySamples\VistaBridge. Die in diesem Beispiel implementierten Klassen sind aber zum einen komplex und laufen zum anderen nur unter Vista.
TIPP
12
Ich habe auch versucht, basierend auf einer Klasse aus dem Blog-Eintrag blogs. vertigo.com/personal/ralph/Blog/Lists/Posts/Post.aspx?ID=15 Klassen für DateiDialoge zu entwickeln, die einfacher sind als das Microsoft-Beispiel und die auch unter XP laufen. Leider hatte ich damit erhebliche Probleme: Beim mehrfachen Aufruf der Dialoge hintereinander trat eine Ausnahme mit der Meldung auf, dass in den geschützten Speicher geschrieben wurde. Falls ich eine Lösung finde, veröffentliche ich diese in dem Blog zum Buch. Die Methoden OpenFile (für Einfachauswahl-Öffnen-Dialoge) und OpenFiles (für Mehrfachauswahl-Öffnen-Dialoge) erlauben bei den WPF-Dialogen alternativ das direkte Öffnen der ausgewählten Datei(en) nachdem der Anwender den Dialog bestätigt hat. Diese Methoden geben Streams zurück, die Sie dann beliebig weiterverwenden.
13 14 Über OpenFile und OpenFiles können Sie Dateien direkt öffnen
16
Prinzipiell wäre es besser, im Programm den oder die ausgewählten Dateinamen auszuwerten und damit ggf. selbst einen FileStream oder StreamReader zu erzeugen. Damit können Sie z. B. using verwenden, um den Aufruf von Dispose sicherzustellen. OpenFile und OpenFiles haben aber ihren Sinn für Assemblys, die über die Codezugriffssicherheit (CAS) speziell eingeschränkt sind. Ich will hier nicht näher darauf eingehen, weil Codezugriffssicherheit in Kapitel 23 behandelt wird. In der .NET-Voreinstellung ist es Assemblys, die aus dem Intranet oder Internet geladen werden, aber z. B. untersagt, Dateien direkt (über einen FileStream oder StreamReader) zu öffnen. Diese Assemblys dürfen allerdings einen OpenFileDialog verwenden und Dateien über dessen OpenFile- oder OpenFiles-Methode öffnen.
15
17 OpenFile und OpenFiles machen Sinn bei eingeschränkten Assemblys
18
19
Der Sinn dieser Einschränkung ist übrigens, dass damit eine Assembly aus dem Intranet oder Internet keinen Schaden anrichten kann, indem sie ungefragt Dateien liest oder schreibt. Bei Dateien, die der Anwender explizit über einen Dialog auswählt, kann aber angenommen werden, dass das Öffnen sicher ist.
20
Der Ordnerauswahl-Dialog Wie gesagt bietet WPF (noch) keinen Dialog zur Auswahl eines Ordners. Wenn Sie einen Ordnerauswahl-Dialog benötigen, können Sie in den .NET-Klassen lediglich auf die Windows.Forms-Klasse FolderBrowserDialog zurückgreifen. Dazu müssen Sie die Assembly System.Windows.Forms.dll referenzieren.
FolderBrowserDialog zeigt einen Ordner-Dialog an
21
22
Der Dialog an sich ist einfach. Tabelle 13.5 beschreibt die wichtigen Eigenschaften.
23
793
WPF-Anwendungen
Tabelle 13.5: Die wichtigen Eigenschaften der FolderBrowserDialog-Klasse
Eigenschaft
Beschreibung
RootFolder
definiert den Wurzel-Ordner der im Auswahldialog angezeigten Ordner. Diesen können Sie z. B. auf den Ordner für eigene Dateien setzen, um den Benutzer auf diesen Ordner (und dessen Unterordner) einzuschränken.
SelectedPath
In dieser Eigenschaft können Sie vor dem Öffnen des Dialogs den Ordner eintragen, der voreingestellt sein soll. Nach dem Öffnen des Dialogs lesen Sie hier den vom Anwender ausgewählten Ordner aus.
Description
bestimmt den Text, der im oberen Bereich des Dialogs als Beschreibung für den Anwender erscheint. Den Titel des Dialogs können Sie allerdings scheinbar nicht (direkt) ändern.
ShowNewFolderButton
bestimmt, ob der Dialog einen Schalter anzeigt, über den der Anwender einen neuen Unterordner erzeugen kann.
Einen FolderBrowserDialog öffnen Sie wie bei WPF über die ShowDialog-Methode. Da es sich hier aber um einen Windows.Forms-Dialog handelt, ist die Rückgabe eine andere, nämlich ein Wert der DialogResult-Aufzählung. DialogResult.OK sagt aus, dass der Anwender den OK-Schalter betätigt hat. Listing 13.4 zeigt eine typische Anwendung. Der Dialog wird in diesem Beispiel auf den Windows-Ordner gesetzt, der den Arbeitsplatz darstellt. Als voreingestellter Ordner wird der Ordner für eigene Bilder verwendet. Dem Anwender wird über ShowNewFolderButton ermöglicht, einen Unterordner anzulegen (was ich bei Ordnerauswahl-Dialogen immer sehr wichtig finde). Listing 13.4:
Typische Verwendung des Ordnerauswahl-Dialogs
// Den Dialog erzeugen und initialisieren System.Windows.Forms.FolderBrowserDialog dialog = new System.Windows.Forms.FolderBrowserDialog(); dialog.RootFolder = Environment.SpecialFolder.MyComputer; dialog.SelectedPath = Environment.GetFolderPath( Environment.SpecialFolder.MyPictures); dialog.Description = "Geben Sie den Ordner an, in dem die " + "Dateien gespeichert werden sollen"; dialog.ShowNewFolderButton = true; // Den Dialog öffnen und auswerten if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { MessageBox.Show("Ausgewählter Ordner: " + dialog.SelectedPath); }
794
Grundlegendes zur Anwendungsentwicklung unter WPF
Abbildung 13.2: Der Ordner-Dialog unter XP
12
13 14 Der Farbauswahl-Dialog Da WPF zurzeit noch keinen eigenen Farbauswahl-Dialog bietet, müssen Sie auf das Windows.Forms-Pendant zurückgreifen. Wie schon bei dem Dialog FolderBrowserDialog müssen Sie dazu die Assembly System.Windows.Forms.dll referenzieren. Um die in Windows.Forms verwendete Color-Struktur verwenden zu können, müssen Sie daneben auch System.Drawing.dll referenzieren.
15
16
Ich verzichte hier auf die Beschreibung der wichtigen Eigenschaften. Im folgenden Beispiel finden Sie entsprechende Kommentare. Bei der Arbeit mit dem Windows.Forms-Dialog müssen Sie die in den unterschiedlichen Typen verwalteten Farbwerte transformieren. Das Beispiel zeigt, wie dies möglich ist: Listing 13.5:
17
Typische Verwendung des Farbauswahl-Dialogs
18
// Die voreingestellte Farbe als WPF-Farbe definieren System.Windows.Media.Color wpfColor = System.Windows.Media.Colors.Red; // Die WPF-Farbe in eine Windows.Forms-Farbe transformieren System.Drawing.Color winformsColor = System.Drawing.Color.FromArgb( wpfColor.A, wpfColor.R, wpfColor.G, wpfColor.B);
19
// Den Dialog erzeugen und initialisieren System.Windows.Forms.ColorDialog dialog = new System.Windows.Forms.ColorDialog(); dialog.AllowFullOpen = true; // Benutzerdefinierte Farben erlauben dialog.Color = winformsColor; // Farbe voreinstellen
20
// Den Dialog öffnen und auswerten if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { // Die ausgewählte Windows.Forms-Farbe in eine WPF-Farbe konvertieren wpfColor = System.Windows.Media.Color.FromArgb(dialog.Color.A, dialog.Color.R, dialog.Color.G, dialog.Color.B);
21
22
// Den Hintergrund des Fensters entsprechend einstellen this.Background = new System.Windows.Media.SolidColorBrush(wpfColor); }
23
795
WPF-Anwendungen
Abbildung 13.3: Der FarbauswahlDialog unter Vista
Die weiteren Standarddialoge
DISC
Die weiteren Standarddialoge beschreibe ich nicht. In dem Beispiel zu diesem Abschnitt auf der Buch-DVD zeige ich allerdings, wie Sie diese anwenden. Beim Schriftauswahldialog müssen Sie wie bereits beim Farbauswahl-Dialog beachten, dass dieser mit Windows.Forms-Typen arbeitet, die in die äquivalenten WPF-Typen transformiert werden müssen. Das Beispiel auf der DVD zeigt, wie das geht.
13.1.4
Systemparameter auslesen
Die Klasse SystemParameters aus dem Namensraum System.Windows liefert Zugriff auf Systemwerte von WPF und Windows über eine Vielzahl an statischen Eigenschaften. Tabelle 13.6 zeigt eine Auswahl. Tabelle 13.6: Auswahl aus den Eigenschaften der SystemParametersKlasse
796
Eigenschaft
Beschreibung
FixedFrameVerticalBorderWidth FixedFrameHorizontalBorderHeight
gibt die Breite des Rahmens eines Fensters mit unveränderbarem Rahmen zurück.
ResizeFrameVerticalBorderWidth ResizeFrameHorizontalBorderHeight
gibt die Breite des Rahmens eines Fensters mit veränderbarem Rahmen zurück.
IconHeight IconWidth
gibt die Breite bzw. Höhe eines System-Icons zurück.
IsMousePresent
gibt an, ob eine Maus vorhanden ist.
IsMouseWheelPresent
gibt an, ob ein Mausrad vorhanden ist.
WheelScrollLines
gibt die im System voreingestellte Anzahl der Zeilen beim Scrollen mit dem Mausrad an.
PrimaryScreenWidth PrimaryScreenHeight
gibt die Breite bzw. Höhe des primären Bildschirms zurück.
WorkArea
gibt den Arbeitsbereich auf dem primären Bildschirm zurück.
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
13.2
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
Fenster und Steuerelemente haben viele Gemeinsamkeiten. So besitzen sie z. B. eine Eigenschaft Background, die den Hintergrund des Fensters bzw. Steuerelements beschreibt. Auf den folgenden Seiten beschreibe ich deswegen die wichtigen gemeinsamen Grundlagen von Fenstern und Steuerelementen.
13.2.1
12
Die Einheit von Positions- und Größenangaben
Eine Besonderheit von WPF ist, dass numerische Positions- und Größenangaben in einer spezifischen Einheit als Double-Wert angegeben werden. Klassische Systeme verwenden dazu Integer-Werte, die entweder in der Einheit Pixel oder in einer Längeneinheit (wie cm) angegeben sind.
Positions- und Größenangaben verwenden eine 1/96-Zoll-Einheit
14
WPF verwendet für Positions- und Größenangaben eine geräteunabhängige Einheit. Microsoft-Mitarbeiter nennen diese intern DIU (Device Independent Unit). Ein DIU entspricht 1/96 Zoll. Diese Einheit korrespondiert mit der Auflösung eines normalen Bildschirms von 96 DPI, was aber eigentlich ein unwichtiges Detail ist, das die Arbeit auf 96-DPI-Bildschirmen erleichtert.
15
DPI steht für »Dots Per Inch« (Punkte pro Zoll) und wird auf Bildschirmen auch als PPI (Point Per Inch) bezeichnet. Ein Steuerelement, das 96 DIUs breit ist, sollte also auf einem korrekt eingestellten System genau einen Zoll bzw. 2,54 cm breit sein (was meistens nicht so ganz passt, weil die Einstellungen nicht ganz korrekt sind). WPF-Anwendungen sind damit geräteunabhängig. Wird eine Anwendung auf einem Bildschirm mit einer höheren Auflösung (z. B. 120 DPI) ausgeführt, sollte diese genauso aussehen wie auf einem normalen Bildschirm. Dazu muss natürlich der Bildschirm wirklich eine Auflösung von 120 DPI besitzen und diese muss dem Betriebssystem auch mitgeteilt werden.
Geräteunabhängigkeit ist ein komplexes Thema. Damit ein Zoll auf einem Bildschirm wirklich ein Zoll ist, müssen zwei Voraussetzungen gegeben sein: Zum einen muss die aktuell in Windows eingestellte Auflösung der physikalischen Auflösung des Bildschirms entsprechen. Wenn Sie einen Bildschirm mit physikalisch 1280 Pixeln Breite z. B. auf 1024 Pixel einstellen, wird ein Zoll prinzipiell schon einmal größer dargestellt.
13
16 WPF-Anwendungen sind geräteunabhängig
17
18
EXKURS
19
20
Zum anderen muss der in Windows eingestellte DPI-Wert zu dem physikalischen DPI-Wert des Bildschirms passen. Viele »normale« Bildschirme besitzen z. B. nicht genau 96 DPI, sondern einen mehr oder weniger abweichenden Wert. Das können Sie gut an einem Notebook nachvollziehen. Dessen Bildschirm besitzt in der Regel eine recht hohe Auflösung (z. B. 1280 * 1024), ist aber wesentlich kleiner als ein normaler Bildschirm. Deswegen besitzt ein Notebook-Bildschirm einen höheren DPI-Wert als ein normaler Bildschirm. Mein Notebook mit einer Auflösung von 1680 * 1050 Pixel hat bei einer Breite (nicht Diagonale!) von 13 Zoll z. B. einen DPI-Wert von 1680 / 13 = ca. 129. Bei Notebooks ist in Windows aber meist der Standardwert eingestellt (96 DPI), weswegen alle Fenster, Steuerelemente und Schriften kleiner erscheinen als auf einem normalen Bildschirm.
21
22
23
797
WPF-Anwendungen
Die DPI-Einstellung können Sie unter Windows anpassen: Unter XP wählen Sie dazu in den Eigenschaften des Desktop im EINSTELLUNGEN-Register den ERWEITERT-Schalter. Im erscheinenden Dialog wählen Sie im Register ALLGEMEIN in der DPI-EINSTELLUNG den Eintrag BENUTZERDEFINIERTE EINSTELLUNG. Unter Vista erreichen Sie diesen Dialog, indem Sie im Kontextmenü des Desktop den Befehl ANPASSEN wählen und im Anpassung-Dialog in den Aufgaben den Link SCHRIFTGRAD ANPASSEN (DPI) wählen. Der Schalter BENUTZERDEFINIERTE DPI… führt Sie zu dem Einstelldialog. Daraufhin erscheint ein Dialog mit einem Messband, auf dem Sie die DPI-Einstellung korrigieren können. Vergleichen Sie das dargestellte Messband dazu einfach mit einem richtigen Messband oder Gliedermaßstab (der für diesen Zweck auch ein richtiger »Zollstock« sein darf ☺).
INFO
Um einem häufigen Missverständnis vorzubeugen: Falls Sie nun versuchen, in den Einstellungen des Windows-Desktop die Auflösung Ihres Bildschirms auf einen anderen Wert als den Standard zu setzen, um die Geräteunabhängigkeit auszuprobieren: Das funktioniert so nicht, denn die physikalische Auflösung des Bildschirms wird damit nicht geändert. Sie teilen Windows lediglich mit, Sie hätten einen Bildschirm mit dem angegebenen DPI-Wert. Ein zu hoch angegebener DPI-Wert führt z. B. dazu, dass WPF-Fenster und -Steuerelemente kleiner dargestellt werden.
Warum sind Position- und Größenangaben Double-Werte? Positions- und Größenangaben sind doubleWerte, um eine hohe Genauigkeit zu ermöglichen
Eine Frage ist nun noch offen: Warum werden DIUs als double-Wert angegeben? Die Antwort auf diese Frage habe ich weder in der Dokumentation noch im Internet gefunden. Aber sie ist eigentlich logisch: Damit können Sie eine höhere Genauigkeit erreichen, als mit 96 Ganzzahlwerten ansonsten möglich ist. OK, auf einem 96-DPIBildschirm bringt eine solche Genauigkeit nicht viel. Wenn Sie für Systeme mit einer anderen Auflösung programmieren, sind Dezimalzahlen unumgänglich. Für hochauflösende Zeichnungen auf einem 1200-DPI-Drucker wären 96 Ganzzahlwerte pro Zoll doc etwas zu wenig. Wollen Sie eine Linie von der X-Position 1 cm zur X-Position 2,5 cm zeichnen, sind die DIU-Werte dazu 37,795275 ((96 / 2,54) * 1) und 94,488187. WPF verwendet den für den aktuellen DPI-Wert am besten passenden Wert, sodass auch hochgenaue Angaben für weniger genaue Ausgabegeräte möglich sind.
Positions- und Größenangaben in XAML In XAML können Sie Zahlen, optional mit px, cm oder in am Ende, und die speziellen Strings »Infinity», »NaN« und »Auto« verwenden
XAML-Größenangaben werden von der (schlecht dokumentierten) Klasse LengthConverter konvertiert. Positionsangaben sind in WPF nur auf einem Canvas-Element direkt möglich. Alle anderen Layout-Container verwenden eine spezifische Positionierung. Die zur absoluten Positionierung zumeist verwendete Margin-Eigenschaft wird von der ThicknessConverter-Klasse konvertiert. Diese Konverter erlauben prinzipiell Strings in der folgenden Form: ■ ■ ■ ■
798
Ein Zahlwert im englischen Format gibt einen DIU-Wert an. Alternativ können Sie den Suffix px hinter den Zahlwert setzen. Damit sind dann aber verwirrenderweise nicht physikalische Pixel gemeint, sondern DIUs! Über den Suffix cm können Sie Angaben in cm machen. Beispiel: »2cm«. Der Suffix in steht für Angaben in Zoll (Inch).
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
■
■
■ ■
Wenn Sie den Suffix pt an die Zahlangabe anhängen, meinen Sie damit Punkte. 1 Punkt ist 96/72 DIUs. Punkte werden üblicherweise für Schriftgrößen verwendet. Wenn Sie nichts angeben, wird der Standardwert verwendet. Dieser ist häufig Double.NaN oder Double.PositiveInfinity, was dafür steht, dass kein spezieller Wert angegeben ist. Die Breite eines WPF-Elements steht z. B. per Voreinstellung auf Double.NaN, was dazu führt, dass der Container die Breite bestimmt. Der String »Infinity« stellt Double.PositiveInfinity ein. Die Strings »NaN« und »Auto« stellen für Größenangaben Double.NaN ein.
12
Wenn Sie Suffixe verwenden, sollten Sie beachten, dass diese bei einigen Eigenschaften wie z. B. Margin direkt an die Zahl angehängt werden müssen.
13.2.2
13
Farbangaben
Eine Farbe ist in WPF eine Instanz der Struktur System.Windows.Media.Color. Diese Struktur beschreibt eine Farbe mit vier Werten: einem Alphawert, der die Transparenz bestimmt, und Werten für den Rot-, Grün- und Blauwert der Farbe.
Color definiert eine Farbe im sRGB- oder ScRGBFormat
Color kann eine Farbe in zwei verschiedenen Farbräumen verwalten, was ein wenig verwirrend erscheint: dem sRGB-Farbraum und dem ScRGB-Farbraum. Der sRGBFarbraum (der auch als RGB-Farbraum bezeichnet wird) ist der Standard-Farbraum, der auch auf anderen Systemen verwendet wird. In diesem werden Farben mit Bytewerten zwischen 0 und 255 für die einzelnen Teilwerte (Alphawert, Rot, Grün, Blau) verwaltet.
14
15
16
Den ScRGB-Farbraum hat Microsoft für Vista erfunden. In diesem Farbraum werden Farben mit float-Werten verwaltet. Der Bereich der einzelnen Teilwerte einer Farbe im ScRGB-Farbraum ist prinzipiell 0 bis 1. Die Werte des sRGB-Farbraums werden aber auf den ScRGB-Farbraum nicht linear abgebildet (z. B. ergibt 0 in sRGB den Wert 0 in ScRGB, 255 ergibt den Wert 1.0, aber 128 ergibt 0,2158605). Außerdem erlaubt sRGB auch Werte größer als 1, was das Ganze noch komplizierter macht. Mit ScRGB sind prinzipiell feinere Abstufungen möglich als bei sRGB. Ich kann hier aber nicht näher darauf eingehen.
17
18
19 Einen Artikel über den ScRGB-Farbraum finden Sie im englischsprachigen Wikipedia: en.wikipedia.org/wiki/ScRGB_color_space. REF
20
Die sRGB-Werte einer Farbe werden in den Eigenschaften A (Alphawert), R (Rot), G (Grün) und B (Blau) verwaltet, die ScRGB-Werte in den Eigenschaften ScA, ScR, ScG und ScB. Die jeweils korrespondierenden Eigenschaften sind miteinander synchronisiert. Wenn Sie z. B. in die Eigenschaft R den Wert 128 schreiben, wird ScR auf 0,2158605 gesetzt. Schreiben Sie den Wert 0,2 in ScR, wird R auf den konvertierten und ggf. gerundeten Wert 124 gesetzt.
21
Das Verhalten von Farben erläutere ich nicht am ScRGB-, sondern am sRGB-Farbraum: Eine solide (nicht transparente) Farbe besitzt in diesem Farbraum einen Alphawert von 255. Eine halbtransparente Farbe einen Alphawert von 128, volle Transparenz erreichen Sie mit einem Alphawert von 0.
Der Alphawert definiert die Transparenz
22
Aus den Rot-, Grün- und Blauwerten, die im sRGB-Farbraum ebenfalls im Bereich von 0 bis 255 angegeben werden, wird die Farbe gemischt. Üblicherweise werden Farbangaben aber nicht im Dezimal-, sondern im Hexadezimal-System angeben. Der gültige Bereich reicht also von 0x00 bis 0xFF. Ich will hier nicht auf das additive
R, G und B definieren die Farbe
23
799
WPF-Anwendungen
Mischen von Farben auf dem Bildschirm eingehen. Sie können sich vorstellen, dass die Werte 0x0, 0x0, 0x0 Schwarz, die Werte 0xFF, 0xFF, 0xFF Weiß, die Werte 0xFF, 0, 0 ein kräftiges Rot und die Werte 0xFF, 0xA0, 0x0 ein kräftiges Orange ergeben. Oder Sie probieren dies einfach aus, indem Sie die Eigenschaft Background eines Steuerelements auf einen String in der folgenden Form setzen: #RRGGBB, wobei RR, GG und BB jeweils Hexadezimalwerte für den Rot-, Grün- und Blauwert sind. Sie können aber auch bei Wikipedia nachlesen: de.wikipedia.org/wiki/RGB-Farbraum. REF
Farben können Sie über verschiedene Methoden ermitteln
Wenn Sie eine Farbe angeben, können Sie im Programmcode die statischen Methoden FromRgb, FromArgb und FromScRgb der Color-Struktur aufrufen, denen Sie die einzelnen Farbwerte übergeben. Sie können aber auch vordefinierte Farben verwenden, die Sie aus den statischen Eigenschaften der Colors- und der SystemColors-Klasse auslesen. Colors.Red ergibt z. B. Rot (was sonst ☺).
SystemColors verwaltet Systemfarben
SystemColors gibt in ihren statischen Eigenschaften Farben zurück, die im Betriebssystem voreingestellt sind. SystemColors.WindowColor gibt z. B. eine Color-Instanz mit der Farbe zurück, die in Windows für den Hintergrund von Fenstern eingestellt ist, SystemColors.ControlColor liefert die Hintergrundfarbe von normalen Steuerelementen wie einem Schalter.
In XAML verwenden Sie in der Regel keine direkten Farbangaben. Die Eigenschaften von Fenstern und Steuerelementen, die etwas mit Farben zu tun haben, arbeiten in der Regel mit einer Brush- oder Pen-Instanz. Näheres dazu finden Sie in den folgenden Abschnitten.
13.2.3 Foreground definiert den Vordergrund, Background den Hintergrund über einen Pinsel
Der Vorder- und Hintergrund eines Fensters oder Steuerelements: Die Brush-Klasse
Der Vordergrund eines Fensters oder Steuerelements wird üblicherweise in der Eigenschaft Foreground verwaltet, der Hintergrund in der Eigenschaft Background (bei Steuerelementen gilt dies nur für die Steuerelemente, die von Control abgeleitet sind, siehe »Die Basisklassen der Steuerelemente«, Seite 825). Der Vordergrund ist übrigens alles, was auf dem Fenster oder Steuerelement gezeichnet wird (wie z. B. Beschriftungen). Foreground und Background verwalten eine Instanz einer von System.Windows. Media.Brush abgeleiteten Klasse. Ein Brush ist ein »Pinsel«, der verschiedene Formen aufweisen kann. Im Namensraum System.Windows.Media stehen die folgenden von Brush abgeleiteten Klassen zur Verfügung: ■ ■ ■ ■ ■
800
SolidColorBrush: Definiert einen Pinsel, der den Hintergrund mit einer Farbe einfärbt. LinearGradientBrush: Zeichnet einen linearen Farbverlauf. RadialGradientBrush: Zeichnet einen radialen Farbverlauf. ImageBrush: Zeichnet ein Bild. DrawingBrush und VisualBrush: Diese speziellen Pinsel sind für das eigene zweidimensionale Zeichnen vorgesehen (das in diesem Buch nicht behandelt wird).
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
So können Sie z. B. in XAML den Hintergrund eines Fensters verlaufend darstellen: Listing 13.6:
Verlaufender Hintergrund eines Fensters
12
13 14
Abbildung 13.4 zeigt das Ergebnis. Abbildung 13.4: Das linear verlaufend gefüllte Fenster unter Vista
15
16
17
Auf das etwas komplexe verlaufende Füllen eines Fensters kann ich hier nicht vertieft eingehen. Eine kurze Erläuterung muss reichen: Ein LinearGradientBrush besitzt einen Start- und einen Endpunkt. Diese werden mit logischen Koordinaten im Bereich von 0 bis 1 angegeben. 0 steht dabei für links bzw. oben, 1 für rechts bzw. unten. 0,5 ist demnach auf der X- und Y-Achse die Mitte. Im obigen Beispiel wird der Verlauf so definiert, dass er horizontal in der Mitte liegt.
18
19
Ein linearer Verlauf besitzt zumindest zwei Stopps, die über GradiantStop-Instanzen definiert werden. Im Beispiel werden diese auf den Anfang und das Ende des Verlaufs gesetzt. Dabei besitzt jeder Stopp eine Farbe, die dann bis zum nächsten Stopp passend verlaufend dargestellt wird.
20
Farbangaben in XAML Instanzen der SolidColor- oder ImageBrush-Klasse können Sie in XAML über die Elements-Syntax erzeugen. Unkomplizierter ist für einfache Farbangaben aber die Angabe als Attribut in Form eines Strings. Der im Falle einer Eigenschaft, die einen Brush erwartet, verwendete BrushConverter erlaubt Angaben in den folgenden Formaten: ■ ■
21 XAML erlaubt Brushes- und RGB-Werte als String
als Name der statischen Eigenschaften der Brushes-Klasse, die im Wesentlichen die Standardfarben abbilden (die auch über Colors erreichbar sind), als sRGB-Wert in der Form "#RGB", "#ARGB", "#RRGGBB" oder "#AARRGGBB", wobei die Werte hexadezimal angegeben werden,
22
23
801
WPF-Anwendungen
■
als ScRGB-Wert in der Form "sc#scA,scR,scG,scB"
■
und in der speziellen Form "ContextColor profileUri alphaValue,colorValue".
Die statischen Eigenschaften der Brushes-Klasse schlägt IntelliSense bereits vor, weswegen ich diese nicht weiter erläutern muss. Das folgende Beispiel zeigt, wie Sie ein Rectangle-Objekt (in dessen Fill-Eigenschaft) mit einem selbst definierten Orange füllen: Listing 13.7:
Angabe eines RGB-Werts als Füllfarbe eines Rechtecks
Systemfarben müssen über Static gesetzt werden
Wollen Sie eine der Systemfarben in XAML setzen, müssen Sie die Static-Markuperweiterung verwenden, die ja Zugriff auf statische Elemente von Klassen gibt. Dabei können Sie zum einen die Tatsache nutzen, dass SystemColors nicht nur ColorInstanzen zurückgibt, sondern über entsprechende Eigenschaften auch gleich einen SolidColorBrush. Für die Farbe SystemColors.ControlColor liefert SystemColors. ControlBrush z. B. den passenden Pinsel. So können Sie z. B. die Hintergrundfarbe eines Fensters auf die Farbe setzen, die für Fenster mit Steuerelementen (also keine Dokumenten-Fenster) eigentlich Standard ist: Listing 13.8:
(Statisches) Setzen der Hintergrundfarbe eines Fensters auf den Standard für Fenster mit Steuerelementen
en Sie z. B. d
Systemfarben sollten über eine dynamische Ressource gesetzt werden
Wenn Sie Systemfarben auf diese Weise setzen, erfolgt allerdings keine automatische Aktualisierung, wenn die Systemfarben geändert werden, während das Fenster geöffnet ist. Um dies zu erreichen, müssen Sie die DynamicResource-Markuperweiterung einsetzen, die in Kapitel 14 näher erläutert wird. Dieser Markuperweiterung, die dynamische Ressourcen ausliest, übergeben Sie den Schlüssel der Ressource, die die entsprechende Farbe verwaltet. Den Wert des Schlüssels lesen Sie aus der Eigenschaft aus, deren Name so beginnt wie die Systemfarbe, aber mit Key endet. Für unser Beispiel sieht das Ganze dann folgendermaßen aus: Listing 13.9:
Korrektes Setzen der Hintergrundfarbe eines Fensters auf den Standard für Fenster mit Steuerelementen
802
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
Abbildung 13.5 auf Seite 813 zeigt ein Fenster mit dem im Beispiel definierten Hintergrund.
12
Wenn Sie ein solches Fenster nun geöffnet haben und in Windows die Hintergrundfarbe für Steuerelemente ändern, ändert sich automatisch auch die Hintergrundfarbe des Fensters.
13
Unter Windows XP können Sie die Hintergrundfarbe für Steuerelemente folgendermaßen ändern: In den Eigenschaften des Desktop wählen Sie das DARSTELLUNGRegister und betätigen dort den ERWEITERT-Schalter. Wählen Sie aus der ELEMENTListe den Eintrag 3D-OBJEKTE, stellen Sie eine Farbe ein und bestätigen Sie alle Dialoge.
14
EXKURS
15
Unter Vista wählen Sie im Kontextmenü des Desktop den Befehl ANPASSEN. Klicken Sie auf den Link FENSTERFARBE UND -DARSTELLUNG, dann auf EIGENSCHAFTEN FÜR KLASSISCHE DARSTELLUNG ÖFFNEN, UM WEITERE OPTIONEN ANZUZEIGEN. Im erscheinenden Dialog betätigen Sie den ERWEITERT-Schalter. Schließlich wählen Sie aus der ELEMENT-Liste den Eintrag 3D-OBJEKTE, stellen eine Farbe ein und bestätigen den Dialog und den davorliegenden.
16
17
13.2.4
Schriftangaben: Verschiedene Schriftart-Klassen
In WPF-Fenstern und -Steuerelementen werden Schriftarten in mehreren Eigenschaften angegeben. Tabelle 13.7 stellt diese vor. Eigenschaft
Beschreibung
FontFamily
gibt die Schriftartfamilie in Form einer Instanz der Klasse System.Windows.Media.FontFamily an. Wenn Sie die Schriftartfamilie im Programm angeben wollen, erzeugen Sie eine neue Instanz der FontFamily-Klasse, der Sie am Konstruktor den Namen der Schriftartfamilie übergeben. In XAML können Sie den Namen direkt angeben (z. B. "Arial", "Tahoma", "Times New Roman").
FontSize
FontStretch
18 Tabelle 13.7: Die Eigenschaften für Schriftarten
19
20
gibt die Schriftgröße als double-Wert an. Die Default-Einheit ist die geräteunabhängige Einheit von WPF. Sie können an die Zahl auch die Suffixe cm, in oder pt hängen, um Angaben in cm, Zoll oder Punkten zu machen.
21
gibt in Form einer Instanz der FontStretch-Struktur aus dem Namensraum System.Windows den Grad an, um den die Schrift im Vergleich zu einer normalen gestreckt wird. Im Programm verwenden Sie die statischen Eigenschaften der FontStretches-Klasse (z. B. Condensed, Expanded, Medium, Normal, UltraCondensed, UltraExpanded). In XAML können Sie deren Namen als String angeben.
22
FontStretch wird nicht von allen Steuerelementen unterstützt. Ein Button ignoriert z. B. eine Einstellung seiner (von Control geerbten) FontStretch-Eigenschaft.
23
803
WPF-Anwendungen
Tabelle 13.7: Die Eigenschaften für Schriftarten (Forts.)
Eigenschaft
Beschreibung
FontStyle
definiert in Form einer Instanz der FontStyle-Struktur aus dem Namensraum System.Windows, ob die Schriftart normal, kursiv oder schräg dargestellt wird. Im Programm verwenden Sie die statischen Eigenschaften der FontStyles-Klasse. In XAML können Sie die Namen dieser Eigenschaften als String verwenden: Normal, Italic (Kursiv) und Oblique (schräggestellt, etwas schräger als Kursiv).
FontWeight
bestimmt als System.Windows.FontWeight-Instanz, wie fett eine Schrift dargestellt wird. Im Programm verwenden Sie die Instanzen, die die FontWeights-Klasse in ihren statischen Eigenschaften zurückgibt. FontWeights.Bold gibt z. B. ein FontWeight-Objekt für eine fette Schrift zurück. In XAML können Sie die Namen der statischen Eigenschaften der FontWeights-Klasse als String verwenden. FontWeights definiert die folgenden Schriftauszeichnungen: Thin, ExtraLight, UltraLight, Light, Normal, Regular, Medium, DemiBold, SemiBold, Bold, ExtraBold, UltraBold, Black, Heavy, ExtraBlack, UltraBlack (wer braucht so viele Unterscheidungen der Auszeichnung einer Schrift?). FontStretch wird nicht von allen Steuerelementen unterstützt oder auch nicht komplett unterstützt. Ein Button unterstützt z. B. laut meinen Versuchen nur die Einstellungen Normal und Bold. Auszeichnungen unterhalb von Normal werden normal ausgegeben, Auszeichnungen oberhalb von Bold werden fett ausgegeben.
13.2.5
Wichtige allgemeine Eigenschaften der WPFFenster und Steuerelemente
WPF-Fenster und Steuerelemente besitzen in der Regel eine Vielzahl an Eigenschaften, die auf den ersten Blick sehr verwirrend erscheint. Das hängt auch damit zusammen, dass WPF-Elemente nicht nur Eigenschaften besitzen, die die direkte Darstellung auf dem Bildschirm beeinflussen, sondern auch solche, die für spezielle WWFFeatures wie Transformationen und Befehle verwendet werden. Ich kann in diesem Kapitel nicht auf alle Eigenschaften eingehen. Bei der Beschreibung von Fenstern und der wichtigen Steuerelemente gehe ich auf die Eigenschaften ein, die für die direkte Arbeit mit diesen Objekten wichtig sind. Einige Eigenschaften kommen bei Fenstern und vielen Steuerelementen immer wieder vor. Damit ich diese nicht immer wieder neu beschreiben muss, finden Sie im Folgenden eine kurze Beschreibung dieser wichtigen Eigenschaften und einige Grundlagen.
Übersicht über die wichtigen allgemeinen Eigenschaften Die WPF-Steuerelemente und WPF-Fenster sind von der Basisklasse Control abgeleitet und erben von dieser eine Menge an Eigenschaften (und Methoden und Ereignisse). Tabelle 13.8 stellt zunächst die wichtigen Control-Eigenschaften vor, bevor einige dieser Eigenschaften näher erläutert werden. Tabelle 13.8: Die wichtigen allgemeinen Eigenschaften
804
Eigenschaft
Beschreibung
Background
bestimmt den Hintergrund. Näheres dazu finden Sie ab Seite 800.
BorderBrush
definiert den Pinsel, mit dem der Rahmen gezeichnet wird. Pinsel wurden bereits prinzipiell im Abschnitt »Der Vorder- und Hintergrund eines Fensters oder Steuerelements: Die Brush-Klasse« (Seite 800) behandelt.
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
Eigenschaft
Beschreibung
BorderThickness
bestimmt die Breite des Rahmens.
ContextMenu
bestimmt das Kontextmenü des Fensters oder Steuerelements. Kontextmenüs werden ab Seite 856 behandelt.
Cursor
bestimmt den Mauscursor, der angezeigt wird, wenn die Maus über dem Fenster bzw. Steuerelement liegt. Über System.Windows. Input.Cursors.Wait können Sie z. B. bei länger dauernden Aktionen einen Stundenglas-Cursor anzeigen. Der Wert null (Voreinstellung) steht für den Default-Cursor.
Focusable
Tabelle 13.8: Die wichtigen allgemeinen Eigenschaften (Forts.)
12
13
gibt an, ob ein Steuerelement den Eingabefokus erhalten kann.
FontFamily, FontSize, FontDiese Eigenschaften bestimmen die Schrift. Näheres dazu finden Sie Stretch, FontStyle, FontWeight ab Seite 803. Foreground
bestimmt den Vordergrund (siehe Seite 800).
IsEnabled
gibt an, ob das Steuerelement oder Fenster aktiviert ist. Deaktivierte Steuerelemente können vom Anwender nicht verwendet werden. Das Deaktivieren eines Fensters oder eines übergeordneten Steuerelements deaktiviert automatisch alle enthaltenen Steuerelemente.
14
15
IsFocused
gibt an, ob ein Steuerelement gerade den Eingabefokus besitzt.
IsMouseOver
gibt an, ob sich die Maus gerade über dem Fenster oder Steuerelement befindet.
IsTabStop
definiert, ob das Steuerelement mit der (Tab)-Taste angesprungen werden kann. Die Voreinstellung ist für Steuerelemente, die den Eingabefokus empfangen können, true.
17
IsVisible
definiert, ob ein Steuerelement oder Fenster sichtbar ist. IsVisible gibt auch dann false zurück, wenn ein Steuerelement oder Fenster zwar grundsätzlich als sichtbar eingestellt ist, aber zurzeit nicht sichtbar ist, weil z. B. das Fenster verkleinert wurde oder im Hintergrund liegt.
18
16
19
Die Sichtbarkeit von Steuerelementen stellen Sie über die Visibility-Eigenschaft ein. Left, Top, Height, Width, HorizontalAlignment, VerticalAlignment
bestimmen die Position und die Größe. Näheres dazu finden Sie im Abschnitt »Die Größe und Position von Steuerelementen« auf Seite 830.
20
TabIndex
definiert mit aufsteigenden Werten die Reihenfolge, in der die einzelnen Steuerelemente eines Fensters über die (Tab)-Taste angesprungen werden. Wenn TabIndex nicht angegeben ist, wird die Tabulatorreihenfolge über die Reihenfolge der Elemente im XAMLCode bestimmt. Über TabIndex können Sie eine davon unabhängige Tabulatorreihenfolge einstellen. Ein wichtiger Praxis-Trick ist, dass Sie TabIndex nicht von 1 an hochzählen, sondern bei logisch zusammengehörenden Gruppen bei 10, 20, 100, 200 o. Ä. beginnen.
21
Tag
22
23
In dieser Object-Eigenschaft können Sie einen beliebigen Wert für eigene Zwecke ablegen. Diese Eigenschaft wird häufig verwendet, wenn Steuerelemente dynamisch erzeugt werden.
805
WPF-Anwendungen
Tabelle 13.8: Die wichtigen allgemeinen Eigenschaften (Forts.)
Eigenschaft
Beschreibung
ToolTip
bestimmt einen Tooltipp, der angezeigt wird, wenn der Anwender die Maus einen Moment auf dem Steuerelement oder Fenster ruhen lässt. Sie können in diese Eigenschaft einen einfachen String schreiben. Alternativ können Sie auch komplexe WPF-Elemente zuweisen, um eigene Tooltipps (z. B. mit Bild) zu definieren.
Visibility
bestimmt mit den Werten der Visibility-Aufzählung die Sichtbarkeit eines Steuerelements oder Fensters: – Visible: Das Element ist sichtbar. – Hidden: Das Element ist nicht sichtbar. Bei Steuerelementen bleibt jedoch der Platz reserviert, den das Steuerelement einnehmen würde. – Collapsed: Das Element ist nicht sichtbar und es wird auch kein Platz reserviert. Der Unterschied zwischen Hidden und Collapsed hat Auswirkungen auf Layouts, die nicht auf einer festen Positionierung, sondern auf einem Fluss der Steuerelemente basieren. Werden drei Steuerelemente z. B. in einem Fluss untereinander dargestellt (z. B. über ein StackPanel) und die Sichtbarkeit des mittleren wird auf Hidden gesetzt, bleibt das untere an seinem Platz. Wird die Sichtbarkeit des mittleren aber auf Collapsed gesetzt, rückt das untere nach oben.
HALT
Margin
bestimmt den äußeren Rand um das Steuerelement.
Name
gibt den Namen des Steuerelements oder Fensters an.
Padding
bestimmt den inneren Rand eines Steuerelements.
Beachten Sie bei Eigenschaften, die das Aussehen des Steuerelements verändern, dass es vorkommen kann, dass ein Dekorator das Aussehen des Steuerelements nach anderen Kriterien bestimmt. Auf Dekoratoren gehe ich in Kapitel 14 ein. Beim ButtonSteuerelement wird z. B. der Hintergrund des Schalters für den Fall, dass die Maus darüber liegt oder der Schalter betätigt ist, z. B. unabhängig vom gesetzten Hintergrund mit den im System eingestellten Farben gezeichnet.
13.2.6 Die wichtigen allgemeinen Ereignisse der Steuerelemente und Fenster Steuerelemente und Fenster, die von der gemeinsamen Basisklasse Control abgeleitet sind, besitzen neben der fast unüberschaubaren Anzahl an Eigenschaften auch eine Vielzahl von Ereignissen. Ich habe die allerwichtigsten (ohne die für Drag&Drop verwendeten) in Tabelle 13.9 zusammengefasst. Tabelle 13.9: Die wichtigsten Ereignisse der Control-Klasse
806
Ereignis
Beschreibung
GotFocus, LostFocus
wird aufgerufen, wenn das Steuerelement oder Fenster den Eingabefokus erhält bzw. verliert.
IsEnabledChanged
wird aufgerufen, wenn der Wert der IsEnabled-Eigenschaft geändert wird.
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
Ereignis
Beschreibung
IsVisibleChanged
wird aufgerufen, wenn der Wert der IsVisible-Eigenschaft geändert wird.
KeyDown, KeyUp, PreviewKeyDown, PreviewKeyUp
Diese Ereignisse werden aufgerufen, wenn eine Taste auf dem Steuerelement oder Fenster heruntergedrückt bzw. losgelassen wird. Über die Eigenschaft Key des Ereignisargument-Objekts erhalten Sie eine Information über die betätigte Taste. Leider liefert das Ereignisargument-Objekt nicht direkt eine Information darüber, ob gleichzeitig eine der so genannten Modifiziertasten ((CTRL), (ALT), (SHIFT), Windows-Taste) betätigt ist. Dies können Sie aber über die statische Eigenschaft Modifiers der Keyboard-Klasse abfragen, die eine Kombination der Werte der ModifierKeys-Aufzählung verwalten kann.
Tabelle 13.9: Die wichtigsten Ereignisse der Control-Klasse (Forts.)
12
13
In den Preview-Ereignissen können Sie die Tastenbetätigung über das Setzen der Handled-Eigenschaft des Ereignisargument-Objekts auch abbrechen. [Preview]MouseDoubleClick, [Preview]MouseDown, [Preview]MouseLeftButtonDown, [Preview]MouseLeftButtonUp, [Preview]MouseRightButtonDown, [Preview]MouseRightButtonUp, [Preview]MouseRightButtonUp, [Preview]MouseUp
14
Diese Ereignisse werden bei dem Namen entsprechenden Betätigungen einer Maustaste aufgerufen. Über die Eigenschaften LeftButton, RightButton und MiddleButton des EreignisargumentObjekts erhalten Sie in Form der MouseButtonState-Aufzählung eine Information darüber, ob die jeweilige Taste betätigt ist.
15
16
Die Ereignisse, die sich auf eine spezielle Maustaste beziehen, werden nur dann aufgerufen, wenn diese Taste betätigt bzw. losgelassen wird. Allerdings kann bei der gleichzeitigen Betätigung einer anderen Taste auch eine der Ereignisargument-Eigenschaften auf MouseButtonState.Pressed stehen.
17
Die Ereignisse, deren Name mit »Preview« beginnt, werden vor der Auswertung der entsprechenden Mausbetätigung durch das Steuerelement aufgerufen. Über die Handled-Eigenschaft des Ereignisargument-Objekts können Sie die Verarbeitung verhindern.
18
Steuerelemente wie die TextBox, die die Betätigung der Maus intern verarbeiten, rufen nur die Preview-Ereignisse auf.
19
Die mittlere Maustaste können Sie übrigens nur in den Ereignissen MouseDown und MouseUp und den äquivalenten Preview-Ereignissen abfragen. Informationen über eine gleichzeitige Betätigung einer der Modifiziertasten erhalten Sie über die statische Eigenschaft Modifiers der Keyboard-Klasse. Über die Methode Keyboard.IsKeyDown können Sie abfragen, ob gleichzeitig eine der »normalen« Tasten betätigt ist. [Preview]MouseWheel
20
21
Diese Ereignisse werden bei einer Betätigung des Mausrads aufgerufen. Über die Eigenschaft Delta des Ereignisargument-Objekts erhalten Sie eine Information darüber, wie viele Einheiten eine Bewegung des Mausrads ausmacht. Ein positiver Wert steht für eine Bewegung nach oben, ein negativer für eine Bewegung nach unten.
MouseEnter, MouseLeave
Diese Ereignisse werden aufgerufen, wenn die Maus in das Steuerelement hinein- bzw. herausbewegt wird.
[Preview]MouseMove
Diese Ereignisse werden aufgerufen, wenn die Maus auf dem Steuerelement oder Fenster bewegt wird.
22
23
807
WPF-Anwendungen
Das folgende Beispiel setzt einige dieser Ereignisse in einer praxisorientierten Form ein. Das Beispiel enthält eine TextBox, ein Label und einen Schalter. Die Ereignisse GotFocus, LostFocus, PreviewKeyDown und KeyDown der TextBox sind zugewiesen: Listing 13.10: Die XAML-Datei des Beispiels Schließen
Der Ereignishandler für das GotFocus-Ereignis setzt die Hintergrundfarbe der TextBox auf AliceBlue. Der Ereignishandler für LostFocus setzt die Farbe wieder auf die Systemeinstellung zurück. Im Handler für PreviewKeyDown werden nur Ziffern und für die Arbeit mit der TextBox notwendige Tasten zugelassen. Der Handler für das KeyDown-Ereignis fängt die (¢)-Taste ab und gibt in diesem Fall den Inhalt der TextBox im Label aus. Listing 13.11: Die Ereignishandler der Demo-Ereignisse /* Setzt die Hintergrundfarbe der TextBox auf AliceBlue, wenn diese den Fokus erhält */ private void txtDemo_GotFocus(object sender, RoutedEventArgs e) { ((TextBox)sender).Background = Brushes.AliceBlue; } /* Setzt die Hintergrundfarbe der TextBox auf die Default-Fensterfarbe zurück, wenn diese den Fokus verliert */ private void txtDemo_LostFocus(object sender, RoutedEventArgs e) { ((TextBox)sender).Background = SystemColors.WindowBrush; } /* Überprüft die Betätigung einer Taste auf eine Ziffer oder die Lösch- und Bewegungstasten und verhindert die Eingabe anderer Tasten */ private void txtDemo_PreviewKeyDown(object sender, KeyEventArgs e) { if ((e.Key < Key.D0 || e.Key > Key.D9) && e.Key != Key.Back && e.Key != Key.Left && e.Key != Key.Right && e.Key != Key.Delete && e.Key != Key.Home && e.Key != Key.End && e.Key != Key.Tab && e.Key != Key.Enter)
808
Grundlegendes zur Arbeit mit Fenstern und Steuerelementen
{ // Die Eingabe abbrechen e.Handled = true; } } /* Überprüft die Betätigung einer Taste und schreibt den Wert der TextBox in das Label, wenn die Return-Taste betätigt wurde */ private void txtDemo_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { this.lblInfo.Content = ((TextBox)sender).Text; } }
12
13
/* Schließt das Fenster */ private void btnClose_Click(object sender, RoutedEventArgs e) { this.Close(); }
Beachten Sie, dass Sie viele Dinge, die Sie auf traditionelle Weise (wie im Beispiel) über Ereignisse programmieren würden, auch über WPF-Features erreichen können. Den Wechsel der Hintergrundfarbe einer TextBox beim Eintritt können Sie besser so entwickeln, dass Sie in XAML einen Eigenschaftentrigger einsetzen, wie ich es bereits in Kapitel 12 gezeigt habe. Der Trigger reagiert dann auf eine Änderung der Eigenschaft IsFocused:
14
15 INFO
16
17
18
19
Das Ganze können Sie über Stile zum einen global für alle Instanzen einer Steuerelement-Klasse einstellen und zum anderen so steuern, dass Sie die Farben später problemlos und global an einer Stelle ändern können. In Kapitel 14 erhalten Sie eine Übersicht über die Möglichkeiten.
20
21
13.2.7
Die wichtigen allgemeinen Methoden der Steuerelemente und Fenster 22
Die Klasse Control definiert einige Methoden, von denen allerdings nur wenige für die tägliche Arbeit mit Steuerelementen und Fenstern wichtig sind. Tabelle 13.10 fasst diese zusammen.
23
809
WPF-Anwendungen
Tabelle 13.10: Die wichtigen allgemeinen Methoden der Steuerelemente und Fenster
Methode
Beschreibung
bool Focus()
setzt den Fokus auf das Steuerelement, auf dem diese Methode aufgerufen wurde. Das ist natürlich nur dann möglich, wenn das Steuerelement den Fokus erhalten kann (es muss ein generell fokussierbares Steuerelement sein, die Eigenschaften IsEnabled und Focusable müssen true sein). Focus gibt true zurück, wenn der Fokus auf das Steuerelement gesetzt werden konnte.
Verschiedene Methoden, deren Name mit »On« beginnt (z. B. OnGotFocus).
Diese geschützten Methoden sind für Fenster interessant und wenn Sie eigene Steuerelemente von vorhandenen ableiten. Die »On«Methoden werden nämlich nach dem Microsoft-Ereignisaufrufmuster intern im Steuerelement aufgerufen, wenn ein Ereignis eintritt. Die Methoden machen in der Regel nichts weiter, als das Ereignis aufzurufen. Über ein Überschreiben der Methoden können Sie aber Ereignisse abfangen, ohne die eigentlichen Ereignisse zuweisen zu müssen.
DependencyObject PredictFocus( FocusNavigationDirection direction)
gibt eine Referenz auf das nächste Steuerelement in der Fokusreihenfolge zurück.
13.3
Der Umgang mit WPF-Fenstern
In einer fensterbasierten Anwendung sind Fenster natürlich der Basisbaustein. Kaum eine Anwendung kommt nur mit einem Fenster aus. Außerdem existieren häufig spezielle Anforderungen an Fenster, z. B. dass diese vom Anwender nicht in der Größe verändert werden können oder dass ein Fenster so geöffnet wird, dass der Anwender dieses schließen muss, um mit dem Rest der Anwendung weiterarbeiten zu können. Dieser Abschnitt erläutert deswegen zunächst die wichtigsten Eigenschaften eines WPF-Fensters, zeigt dann, wie Sie Anwendungen mit mehreren Fenstern entwickeln, und endet schließlich mit einigen für die Praxis wichtigen Techniken, wie z. B. dem Initialisieren eines Fensters.
13.3.1 Die Klasse Window liefert eine Menge an Eigenschaften
810
Die wichtigen Eigenschaften eines Fensters
Die Klasse Window, von der alle WPF-Fenster abgeleitet sind, bietet eine Vielzahl an Eigenschaften, über die Sie das Fenster beeinflussen können. So können Sie z. B. den Rahmen eines Fensters so einstellen, dass das Fenster nicht in der Größe verändert werden kann. Damit Sie eine Übersicht über die Möglichkeit erhalten, beschreibt Tabelle 13.11 die wichtigen Eigenschaften der Window-Klasse. Diese Tabelle lässt allerdings die meisten der Eigenschaften aus, die bereits erläutert wurden. Nur die Eigenschaften, die bei Window eine besondere Bedeutung besitzen, werden noch einmal erwähnt.
Der Umgang mit WPF-Fenstern
Eigenschaft
Beschreibung
Background
verwaltet den Hintergrund des Fensters in Form eines Brush (siehe Seite 800). Per Voreinstellung steht diese Eigenschaft auf der dynamischen Ressource, die den Schlüssel SystemColors.WindowColorKey besitzt, also auf einem SolidColorBrush, der die im System eingestellte Farbe für Fenster definiert (normalerweise Weiß). Auf Seite 802 habe ich gezeigt, wie Sie die Hintergrundfarbe auf die Systemfarbe für Dialoge setzen.
DialogResult
Diese Eigenschaft wird für eigene Dialoge verwendet (siehe Seite 819).
Foreground
verwaltet den Vordergrund des Fensters. Die Voreinstellung ist der Wert der dynamischen Ressource, die den Schlüssel SystemColors.WindowTextBrushKey besitzt, also ein SolidColorBrush, der die im System eingestellte Farbe für Texte auf Fenstern definiert (normalerweise Schwarz).
Icon
definiert das Icon, das in der Titelzeile angezeigt wird. Wenn Sie kein eigenes Icon angeben, verwendet ein Fenster das Icon, das der Assembly zugewiesen ist. Ein solches weisen Sie über die Eigenschaften des Projekts im Register ALLGEMEIN unterhalb von RESSOURCEN zu. Ist dort auch keines zugewiesen, wird ein (langweiliges) Standard-Icon angezeigt. (unter der Tabelle) zeigt, wie Sie ein eigenes Icon zuweisen.
IsEnabled
IsVisible
Left
MaxHeight MaxWidth MinHeight MinWidth ResizeMode
Tabelle 13.11: Die wichtigen Eigenschaften der Klasse Window
12
13 14
15
gibt an, ob das Fenster aktiviert ist. Über IsEnabled können Sie den kompletten Inhalt eines Fensters deaktivieren, sodass der Anwender mit dem Fenster nicht mehr arbeiten kann.
16
gibt zurück, ob das Fenster sichtbar ist. Über die Methode Hide können Sie ein Fenster verstecken und über Show wieder anzeigen. Der aktuelle Status des Fensters (die Eingaben) bleibt dabei erhalten.
17
definiert die linke Position des Fensters, wenn WindowStartupLocation auf Manual steht. Der Wert Double.NaN steht dafür, dass Windows die Position bestimmt.
18
gibt die maximale Höhe bzw. Breite eines in der Größe veränderbaren Fensters an. Die Voreinstellung Double.PositiveInfinity schränkt das Fenster nicht in der maximalen Höhe bzw. Breite ein.
19
gibt die minimale Höhe bzw. Breite eines in der Größe veränderbaren Fensters an. Die Voreinstellung 0 schränkt das Fenster nicht in der minimalen Höhe bzw. Breite ein.
20
Bestimmt, ob und wie das Fenster in der Größe verändert werden kann. Sie können die folgenden Werte der gleichnamigen Aufzählung verwenden: – NoResize: Das Fenster kann nicht in der Größe verändert werden. Die Schaltflächen zum Minimieren und Maximieren werden in der Titelleiste nicht angezeigt.
21
– CanMinimize: Das Fenster kann nicht in der Größe verändert, aber minimiert und wiederhergestellt werden.
22
– CanResize: Das Fenster kann in der Größe verändert, minimiert und maximiert werden. Dieser Wert ist die Voreinstellung. – CanResizeWithGrip: Wie CanResize, das Fenster zeigt in der unteren rechten Ecke aber einen zusätzlichen Zielpunkt an. Title
23
Der Titel des Fensters
811
WPF-Anwendungen
Tabelle 13.11: Die wichtigen Eigenschaften der Klasse Window (Forts.)
Eigenschaft
Beschreibung
Topmost
gibt an, ob es sich bei dem Fenster um eines handelt, das immer oben angezeigt wird, auch wenn es in den Hintergrund gesetzt wird. Ein solches Fenster ist innerhalb von Windows immer sichtbar (außer wenn ein anderes Topmost-Fenster darüberliegt).
Width
Die Breite des Fensters
WindowState
gibt mit den Werten der gleichnamigen Aufzählung an, ob das Fenster gerade normal angezeigt wird (Normal), minimiert (Minimized) oder maximiert ist (Maximized). Sie können diese Eigenschaft auch setzen.
WindowStyle
definiert die Rahmenart mit den folgenden Werten der gleichnamigen Aufzählung: – None: Das Fenster besitzt keinen Rahmen und keine Titelleiste. Unter Vista wird allerdings ein einfacher Rahmen um das Fenster gezeichnet. – SingleBorderWindow: Das Fenster besitzt einen einfachen Rahmen. Dieser Wert ist die Voreinstellung. – ThreeDBorderWindow: Das Fenster besitzt einen (unter XP unschönen) 3D-Rahmen. – ToolWindow: Das Fenster wird mit einer verkleinerten Titelleiste und ohne Minimieren- und Maximieren-Schalter angezeigt.
ShowInTaskbar
gibt an, ob das Fenster in der Windows-Taskbar angezeigt wird. Die Voreinstellung ist true.
WindowStartupLocation gibt an, wo das Fenster beim Öffnen angezeigt wird. Sie können die folgenden Werte der WindowStartupLocation-Aufzählung einstellen: – Manual: Die Startposition liegt an der an Left und Top angegebenen Position, wenn diese einen numerischen Wert (ungleich Double.NaN) aufweisen. Ist Left und/oder Top Double.NaN (Voreinstellung), bestimmt Windows die Position. – CenterScreen: Das Fenster wird auf dem primären Bildschirm zentriert geöffnet. – CenterOwner: Diese Einstellung gilt für Fenster, die mit einem Besitzer (Owner) geöffnet werden. Das Fenster wird dann bezogen auf sein BesitzerFenster zentriert dargestellt. Top
gibt die obere Position des Fensters an, wenn WindowStartupLocation auf Manual steht. Der Wert Double.NaN steht dafür, dass Windows die Position bestimmt.
Listing 13.12 zeigt die Definition eines Fensters mit einem unveränderbaren Rahmen, das aber minimiert werden kann. Das Fenster ist zudem so eingestellt, dass es in der Mitte des Bildschirms geöffnet wird. Außerdem wird das Icon des Fensters definiert. Dieses Icon wurde dem Projekt hinzugefügt, wobei die Eigenschaft BUILDVORGANG auf den Wert Resource gesetzt wurde.
812
Der Umgang mit WPF-Fenstern
Listing 13.12: Fenster mit unveränderbarem, aber minimierbarem Rahmen, dem Default-Hintergrund für Dialoge und einem eigenen Icon, das beim Öffnen auf dem Bildschirm zentriert erscheint
12
13 14
Abbildung 13.5: Das veränderte Fenster in Aktion (unter XP)
15
16
17
Die wichtigen Ereignisse eines Fensters
18
Neben den von Control geerbten Ereignissen besitzt ein Fenster noch einige spezifische. Tabelle 13.12 stellt die wichtigsten (in der Reihenfolge ihrer Ausführung) vor. Ereignis
Beschreibung
SizeChanged
Dieses Ereignis wird aufgerufen, wenn die Größe des Fensters geändert wird.
Activated
wird aufgerufen, wenn ein Fenster in den Vordergrund geholt wird.
Loaded
wird aufgerufen, nachdem ein Fenster komplett geladen wurde. Dieses Ereignis können Sie, wie ich auf Seite 822 zeige, zum Initialisieren eines Fensters verwenden.
Deactivated
wird aufgerufen, wenn ein Fenster in den Hintergrund gesetzt oder verkleinert wird.
Closing
Dieses Ereignis wird aufgerufen, bevor ein Fenster geschlossen wird. Über Closing können Sie das Schließen verhindern. Näheres dazu finden Sie auf Seite 818.
Unloaded
wird aufgerufen, nachdem ein Fenster aus dem Speicher entladen wurde. Näheres dazu finden Sie auf Seite 818.
Closed
wird aufgerufen, nachdem ein Fenster geschlossen wurde.
Tabelle 13.12: Die wichtigsten im Vergleich zu Control zusätzlichen Ereignisse der Window-Klasse
19
20
21
22
23
813
WPF-Anwendungen
INFO
Beachten Sie, dass ein Fenster auch alle Tastatur- und Mausereignisse besitzt und dass Sie darüber die Tastatur bzw. die Maus global für das Fenster behandeln können.
13.3.2 Die Tabulatorreihenfolge wird über die Reihenfolge in XAML oder über TabIndex bestimmt
TIPP
Die Tabulatorreihenfolge
Die Reihenfolge, in der die fokussierbaren Steuerelemente eines Fensters angesprungen werden, wenn der Anwender die (ÿ)-Taste betätigt, sollten Sie beim Design von Fenstern immer beachten. In WPF wird diese Reihenfolge von der Reihenfolge bestimmt, in der die einzelnen Steuerelemente in der XAML-Datei angelegt sind, sofern die TabIndex-Eigenschaft nicht gesetzt ist. Über die Eigenschaft TabIndex können Sie allerdings auch eine alternative, von der XAML-Reihenfolge unabhängige Tabulatorreihenfolge einstellen. Ob Sie nun die XAML-Reihenfolge verwenden oder TabIndex, ist Ihre Entscheidung. Auf größeren Fenstern wird die Reihenfolge der Elemente in XAML möglicherweise unübersichtlich. TabIndex kann dann wesentlich einfacher gesetzt werden. Das Setzen von TabIndex ist in der Praxis in der Regel eine aufwändige Angelegenheit. Der folgende Tipp hilft Ihnen, die Tabulatorreihenfolge mit möglichst wenig Aufwand einzustellen: Beginnen Sie TabIndex nicht bei 0, sondern bei 10 oder 100. Zählen Sie TabIndex nicht mit 1 hoch, sondern vielleicht mit 5. Beginnen Sie eine neue logische Gruppe von Steuerelementen mit einem höheren TabIndex (z. B. 200, 300 etc.). Dann ist es später wesentlich einfacher, Steuerelemente dazwischenzuschieben.
Der Start-Fokus Nach dem Start eines Fensters besitzt unter WPF kein Steuerelement den Fokus. Das ist Absicht in WPF, weil es damit dem Programmierer überlassen bleibt, den Fokus auf ein Steuerelement zu setzen oder nicht. Für Fenster, die fokussierbare Steuerelemente enthalten, ist das aber für den Anwender etwas eigenartig. Dieser muss nach dem Öffnen des Fensters nämlich einmal die (ÿ)-Taste betätigen, um den Fokus auf das erste Steuerelement zu legen, das den Fokus erhalten kann (oder mit der Maus klicken).
TIPP
Um dieses Problem zu lösen, können Sie im Handler des Loaded-Ereignisses des Fensters die folgende Anweisung unterbringen: this.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
Diese Anweisung führt dazu, dass der Fokus auf das nächste Steuerelement in der Tabulatorreihenfolge gesetzt wird, simuliert also im Prinzip die Betätigung der (ÿ)Taste.
13.3.3
(Modales und unmodales) Öffnen weiterer Fenster
Eine fensterbasierte WPF-Anwendung besitzt (im Gegensatz zu einer navigationsbasierten Anwendung, die mit Seiten arbeitet) zumindest ein Hauptfenster, das automatisch mit der Anwendung gestartet wird. In der Praxis kommen Anwendungen aber in den seltensten Fällen mit nur einem Fenster aus.
814
Der Umgang mit WPF-Fenstern
Fenster fügen Sie einem Visual-Studio-WPF-Anwendungsprojekt über das Kontextmenü des Projekt-Eintrags im Projektmappen-Explorer hinzu. Wählen Sie den Eintrag HINZUFÜGEN / NEUES ELEMENT und im Hinzufügen-Dialog den Eintrag FENSTER (WPF). Benennen Sie die Datei gleich sinnvoll, damit das spätere Umbenennen entfällt. Für das einfache Beispiel dieses Abschnitts nenne ich das zweite Fenster einfach SecondWindow. Um ein Fenster anzuzeigen, erzeugen Sie eine Instanz des Fensters und rufen die Methode ShowModal oder Show auf. ShowModal öffnet das Fenster modal, Show öffnet das Fenster unmodal. Ein modal geöffnetes Fenster verhindert, dass der Anwender innerhalb der Anwendung zu einem anderen Fenster wechseln kann (außer zu einem unmodalen Fenster, das von dem modalen aus geöffnet wurde). Der Anwender muss ein modales Fenster zunächst schließen, bevor er zu einem anderen Fenster wechseln kann. Ein Beispiel für ein modernes Fenster ist der Öffnen-Dialog fast jeder Anwendung. Wenn Sie diesen öffnen, können Sie so lange nicht zu dem Rest der Anwendung zurückkehren, bis Sie den Dialog wieder geschlossen haben. Der Begriff »Dialog« wird übrigens auch für modale Fenster verwendet.
12
Modale Fenster verhindern den Wechsel zu anderen Fenstern, unmodale verhindern einen Wechsel nicht
13 14
15
Bei unmodal geöffneten Fenstern ist das anders. Ein unmodales Fenster verhindert nicht, dass der Anwender zu anderen Fenstern der Anwendung wechselt. Die vielen Fenster von Visual Studio (außer den Dialogen wie z. B. dem Projekt-öffnen-Dialog) sind Beispiele für unmodale Fenster (die allerdings noch zusätzlich an das Hauptfenster andocken).
16
Das folgende Beispiel öffnet das Fenster SecondWindow in der Ereignisbehandlungsmethode je eines Schalters (des Hauptfensters der Anwendung) einmal unmodal und einmal modal:
17
Listing 13.13: Unmodales und modales Öffnen eines Fensters
18
private void btnOpenSecondWindowUnmodal_Click(object sender, RoutedEventArgs e) { // Instanz der Fenster-Klasse erzeugen SecondWindow secondWindow = new SecondWindow();
19
// Das Fenster unmodal öffnen secondWindow.Show(); }
20
private void btnOpenSecondWindowModal_Click(object sender, RoutedEventArgs e) { // Instanz der Fenster-Klasse erzeugen SecondWindow secondWindow = new SecondWindow();
21
// Das Fenster modal öffnen secondWindow.ShowDialog();
22
}
Probieren Sie einfach das Beispiel, das Sie auf der DVD finden, aus, um ein Gefühl für unmodale und modale Fenster zu erhalten. Das modale und unmodale Öffnen hat noch eine Auswirkung, die sich auf das Programm bezieht: Die ShowModal-Methode blockiert das Programm, die Show-Methode allerdings nicht. Das bedeutet, dass Programmcode, der nach ShowModal angegeben
Das modale Öffnen blockiert das Programm
815
23
WPF-Anwendungen
ist, erst dann ausgeführt wird, nachdem das Fenster geschlossen wurde. Programmcode, der der Show-Methode folgt, wird allerdings quasi sofort nach deren Aufruf ausgeführt (Show wird asynchron ausgeführt). Das folgende Beispiel beweist dies: Listing 13.14: Beweis für die Tatsache, dass Show asynchron ausgeführt wird private void btnOpenSecondWindowUnmodal_Click(object sender, RoutedEventArgs e) { // Instanz der Fenster-Klasse erzeugen SecondWindow secondWindow = new SecondWindow(); // Das Fenster unmodal öffnen secondWindow.Show(); // Eine Meldung ausgeben MessageBox.Show("Nach dem Öffnen des Fensters"); } private void btnOpenSecondWindowModal_Click(object sender, RoutedEventArgs e) { // Instanz der Fenster-Klasse erzeugen SecondWindow secondWindow = new SecondWindow(); // Das Fenster modal öffnen secondWindow.ShowDialog(); // Eine Meldung ausgeben MessageBox.Show("Nach dem Öffnen des Fensters"); }
Abbildung 13.6: Die Beispielanwendung nach dem unmodalen Öffnen des Fensters
816
Der Umgang mit WPF-Fenstern
Das Ganze mag verwirrend sein, wenn Sie sich mit modalen und unmodalen Fenster noch nicht auskennen. Die folgenden Punkte klären das Ganze ein wenig auf: –
Dass Programmcode nach dem Öffnen unmodaler Fenster weiter ausgeführt wird, ist logisch. Ansonsten könnte das Fenster, von dem aus das unmodale Fenster geöffnet wurde, z. B. gar nicht mehr auf Benutzereingaben reagieren.
–
Dass bei modal geöffneten Fenstern Programmcode, der der ShowDialogMethode folgt, erst nach dem Schließen des Fensters ausgeführt wird, ist erstens logisch und zweitens absolut notwendig. Logisch ist dies deswegen, weil das Betriebssystem die Ausführung des Programmcodes des anderen Fensters blockiert, damit dieses z. B. nicht auf Benutzereingaben reagieren kann. Notwendig ist dieses Blockieren, da ShowDialog für entsprechend programmierte Fenster eine Information darüber zurückgibt, wie das Fenster geschlossen wurde. Ein Dialog mit einem OK- und einem Abbrechen-Schalter kann z. B. so programmiert werden, dass ShowDialog bei der Betätigung des OK-Schalters true zurückgibt und bei der Betätigung des Abbrechen-Schalters false. Dazu erfahren Sie mehr im Abschnitt »Dialoge«. Würde das Programm nach dem Aufruf von ShowDialog weiterlaufen, wäre eine Auswertung der Rückgabe unmöglich.
INFO
12
13 14
15
Wenn Sie die folgenden Punkte beachten, vermeiden Sie Probleme bei der Arbeit mit unmodalen Fenstern: ■
■
16
Beachten Sie beim unmodalen Öffnen immer, dass Programmcode, der nach dem Aufruf der Show-Methode angegeben ist, nach dem Öffnen ausgeführt wird Für die Praxis bedeutet dies, dass Sie eigentlich niemals nach dem Aufruf der Show-Methode weitere Anweisungen programmieren sollten. Der Show-Aufruf sollte immer in der letzten Anweisung einer Methode stehen.
17
Wenn Sie Fenster unmodal öffnen, sollten Sie zudem immer beachten, dass der Anwender auch zu anderen Fenstern der Anwendung wechseln und mit diesen weiterarbeiten kann. Deswegen sollte ein unmodal geöffnetes Fenster immer relativ unabhängig von den Daten der Anwendung sein. Es sollte nicht vorkommen können, dass eine Änderung der Daten der Anwendung über ein anderes Fenster zu Problemen in einem unmodal geöffneten Fenster führt.
18
19
Ich hoffe, das reicht aus, um den Unterschied zwischen unmodalen und modalen Fenstern zu erläutern.
20
In der Praxis werden bei »normalen« Anwendungen fast ausschließlich modale Fenster eingesetzt. Damit wird u. a. auch verhindert, dass ungewollte Seiteneffekte entstehen (dadurch dass der Anwender gleichzeitig in mehreren Fenstern arbeitet).
21
In Anwendungen, die eher dokumentenorientiert sind (wie z. B. Word, Excel) oder die zur Arbeit mit einem Hauptfenster mehrere Tool-Fenster anbieten (wie die Fenster von Visual Studio), werden hingegen auch unmodale Fenster eingesetzt.
22
23
817
WPF-Anwendungen
13.3.4 Schließen eines Fensters Fenster können per Voreinstellung über die Titelleiste und über (ALT) + (F4) und explizit über Close geschlossen werden
INFO
Ein Fenster kann vom Anwender auf verschiedene, vordefinierte Weise geschlossen werden. Ein normales Fenster besitzt z. B. in der Titelleiste links ein Systemmenü und rechts einen Schließen-Schalter. Außerdem wird die Tastenkombination (ALT) + (F4) von der Window-Klasse verarbeitet und führt ebenfalls zu einem Schließen. Wie ich im Abschnitt »Reaktion auf das Schließen eines Fensters« noch zeige, können Sie das Schließen aber auch abfangen. Normalerweise wird ein Fenster aber auch mit einem Schalter oder/und mit einem Menübefehl ausgestattet, über den dieses geschlossen werden kann. In dem Ereignishandler dieses Schalters rufen Sie die Close-Methode des Fensters auf, um dieses zu schließen (es sei denn, Sie entwickeln einen Dialog, siehe Seite 819). Zum »Schließen« eines Fensters können Sie auch die Hide-Methode verwenden. In diesem Fall wird das Fenster zwar für den Anwender geschlossen, bleibt aber im Speicher geladen. Die Ereignisse Closing, Unloaded und Closed werden nicht aufgerufen. Da das Fenster im Speicher verbleibt, bleibt der aktuelle Status erhalten, und damit natürlich auch alle Eingaben des Benutzers. Wenn Sie das Fenster später wieder über die Show-Methode öffnen, ist der Status derselbe wie zuvor. Dazu benötigen Sie natürlich eine Referenz auf das Window-Objekt, das das Fenster darstellt. Diese Technik sollten Sie nur im absoluten Sonderfall einsetzen, z. B. wenn Sie Steuerelemente entwickeln, die ein separates Fenster anzeigen sollen, das seinen Status beinhaltet (ähnlich wie eine ComboBox ein separates Fenster besitzt, das eine ListBox beinhaltet). Schließen Sie ansonsten Fenster prinzipiell immer über deren CloseMethode, um Probleme mit Fenstern zu vermeiden, die nicht wirklich geschlossen sind. Wenn Sie Dialoge entwickeln, können Sie nach dem Schließen noch auf die Eingaben im Fenster zugreifen. Für das einfache Verstecken eines Fensters gibt es also in normalen Anwendungen keinen Grund.
13.3.5 Reaktion auf das Schließen eines Fensters Wenn Sie darauf reagieren wollen, dass der Anwender (oder das Programm oder das Betriebssystem) ein Fenster schließen will oder geschlossen hat, sollten Sie dazu die Ereignisse Closing und Closed verwenden. Closing wird vor dem Schließen aufgerufen, Closed danach
Closing wird aufgerufen, bevor das Fenster geschlossen wurde, Closed wird nach dem Schließen aufgerufen. Der wesentliche Unterschied zwischen diesen Ereignissen ist, dass Sie das Schließen in Closing abbrechen können, indem Sie die Eigenschaft Cancel des Ereignisargument-Objekts auf true setzen.
Damit können Sie z. B. nachfragen, ob der Anwender wirklich schließen will (was meiner Ansicht nach für die meisten Anwendungen aber eine schlechte Praxis ist): Listing 13.15: Abbrechen des Schließens eines Fensters im Closing-Ereignis private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { if (MessageBox.Show("Wollen Sie das Fenster wirklich schließen?", "Schließen", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.No) {
818
Der Umgang mit WPF-Fenstern
// Das Schließen abbrechen e.Cancel = true; } }
Der wesentliche Punkt der Verwendung der Ereignisse Closing und Closed ist, dass diese immer dann aufgerufen werden, wenn das Fenster irgendwie geschlossen wird. Wenn Sie eine Reaktion auf das Schließen eines Fensters nur in dem Ereignishandler eines Schließen-Schalters (den Sie auf dem Fenster platziert haben) programmieren, wird dieses Programm nicht aufgerufen, wenn das Fenster über die Titelzeile, das System-Menü, über (ALT) + (F4) oder sonstwie geschlossen wird (z. B. weil Windows heruntergefahren wird).
Closing und Closed werden immer dann aufgerufen, wenn ein Fenster geschlossen wird
13
In der Praxis wird das Schließen eines Fensters immer dann über Closing angefangen, wenn noch irgendetwas abgeschlossen werden muss. Eine Textverarbeitung oder ein Editor fragt Sie z. B. beim Schließen, ob Sie speichern wollen, aber noch ungespeicherte Änderungen vorliegen. Nutzen Sie Closing für solche Fälle. Closed wird immer dann verwendet, wenn zwar Abschlussarbeiten ausgeführt werden müssen, dafür aber nicht der Anwender gefragt werden muss. In Fällen, wo Abschlussarbeiten aber auch zu einer Ausnahme führen können und Sie diese abfangen, sollten Sie lieber das Closing-Ereignis verwenden und im Falle einer Ausnahme das Schließen abbrechen.
14
15
13.3.6 Dialoge Dialoge sind Fenster, die modal geöffnet werden und die in der Regel zumindest einen OK- und einen Abbrechen-Schalter besitzen. Ein Dialog wird üblicherweise in einem »normalen« Fenster dazu verwendet, den Anwender etwas eingeben oder auswählen zu lassen.
12
16 Für Dialoge bietet WPF spezielle Features
17
Wenn Sie ein Fenster, das Sie als Dialog vorgesehen haben, öffnen, wollen Sie nach dem Öffnen natürlich ermitteln, ob der Anwender den Dialog mit OK bestätigt hat. Und dafür gibt es eine sehr einfache Lösung.
18
Die ShowDialog-Methode gibt dazu nämlich einen bool?-Wert zurück. Dieser Wert wird durch die DialogResult-Eigenschaft des Fensters bestimmt. Sie können nun beim Schließen des Dialogs DialogResult explizit auf true setzen, was für »OK« steht, auf false, was für »Abbrechen« steht, oder auf null. Die Bedeutung von null ist nicht definiert. Sie können diese dritte Rückgabe für eigene Zwecke einsetzen (z. B. bei einem Dialog, der die Schalter JA, NEIN und ABBRECHEN enthält). Wenn Sie DialogResult auf true oder false setzen, wird das Fenster damit automatisch geschlossen. Für die Rückgabe von null müssen Sie DialogResult auf null setzen (was allerdings die Voreinstellung ist) und das Fenster explizit über Close schließen.
DialogResult bestimmt die Rückgabe von ShowDialog
Hinzu kommt, dass die Eigenschaften IsDefault und IsCancel bei Schaltern eine besondere Bedeutung haben. Ein Schalter mit IsDefault == true ist der Defaultschalter des Dialogs, der automatisch betätigt wird, wenn der Anwender die (¢)Taste betätigt. Ein Schalter mit IsCancel == true wird automatisch betätigt, wenn die (Esc)-Taste betätigt wird.
IsDefault und IsCancel haben bei Buttons eine besondere Bedeutung
19
20
Um das Ganze noch ein wenig komplizierter zu machen, muss bei einem Schalter mit IsCancel == true keine Ereignisbehandlungsmethode programmiert werden, um den Dialog zu schließen. Eine Betätigung der (Esc)-Taste oder des Schalters führt automatisch zu einem Schließen. ShowDialog gibt dann false zurück. ShowDialog gibt übrigens auch dann false zurück, wenn der Anwender das Fenster über (ALT) + (F4) oder über die Befehle in der Titelzeile schließt.
21
22
23
819
WPF-Anwendungen
Für den Abbrechen-Schalter müssen Sie also normalerweise nichts programmieren (Sie können jedoch, wenn Sie wollen). Im Ereignishandler des OK-Schalters muss aber meistens programmiert werden. Üblicherweise wird hier die Eingabe überprüft und nur dann geschlossen, wenn diese in Ordnung ist. Deswegen führt IsDefault bei einem Schalter nicht zu einem automatischen Schließen. Sie müssen DialogResult explizit auf true setzen, damit das Fenster geschlossen wird und ShowDialog true zurückgibt. Listing 13.16 zeigt das am Beispiel eines einfachen Dialogs, in dem der Anwender seinen Namen eingeben soll. Listing 13.16: Ein einfacher Dialog zur Eingabe eines Namens Ihr Name OK Abbrechen
Listing 13.17 zeigt den Programmcode des Ereignishandlers für den OK-Schalter. Listing 13.17: Der Ereignishandler des Namenseingabedialogs private void btnOK_Click(object sender, RoutedEventArgs e) { // Die Eingabe überprüfen if (String.IsNullOrEmpty(this.txtName.Text) == false) { // DialogResult setzen und das Fenster damit automatisch schließen this.DialogResult = true; } else { // Meldung ausgeben MessageBox.Show("Sie haben Ihren Namen nicht eingegeben", this.Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); } }
In Listing 13.18 wird der Dialog (in einem anderen Fenster) geöffnet und ausgewertet: Listing 13.18: Öffnen und Auswerten des Dialogs NameDialog dialog = new NameDialog(); if (dialog.ShowDialog() == true) {
820
Der Umgang mit WPF-Fenstern
this.tbInfo.Text = "Der Dialog wurde bestätigt. " + "Der eingegebene Name war: " + dialog.txtName.Text; } else { this.tbInfo.Text = "Der Dialog wurde abgebrochen"; }
Wie Sie sehen, können Sie nach dem Öffnen des Dialogs auf die Eingaben zugreifen, obwohl der Dialog je bereits geschlossen wurde. Das Fenster ist zu dieser Zeit aber noch im Speicher. Über die Referenz auf das Window-Objekt können Sie natürlich noch darauf zugreifen. Besonders elegant ist dieses Vorgehen übrigens nicht. In der Praxis sollten Sie es nicht ermöglichen, dass von außen direkt auf die Steuerelemente eines Fensters zugegriffen werden kann. Damit wahren Sie die in komplexen Anwendungen sehr wichtige Kapselung der Daten eines Objekts (des Fensters). Der Zugriff auf die Steuerelemente eines Fensters sollte immer über speziell dafür vorgesehene Eigenschaften erfolgen. Der Dialog müsste also eine Eigenschaft haben, die den Zugriff auf die Namens-TextBox erlaubt:
12
13 INFO
14
15 Listing 13.19: Eigenschaft zum Zugriff auf ein Steuerelement public string Input { get { return this.txtName.Text; } set { this.txtName.Text = value; } }
16
17
18
Das Beispiel kann dann so geschrieben werden: Listing 13.20: Öffnen und Auswerten des Dialogs mit einer speziellen Eigenschaft zur Auswertung der Eingabe
19
NameDialog dialog = new NameDialog(); if (dialog.ShowDialog() == true) { this.tbInfo.Text = "Der Dialog wurde bestätigt. " + "Der eingegebene Name war: " + dialog.Input; } else { this.tbInfo.Text = "Der Dialog wurde abgebrochen"; }
20
21
Das einzige Problem ist, dass Sie dem benutzerdefinierten Tool, das unter WPF die generierte Teilklasse für das Fenster erzeugt, nun noch mitteilen müssten, dass die erzeugten Steuerelement-Referenzen nicht internal, sondern private sein sollten. Ich habe leider nicht herausgefunden ob und wie das möglich ist.
22
23
821
WPF-Anwendungen
13.3.7 Fenster werden im Konstruktor oder in Loaded initialisiert
Initialisieren eines Fensters
Das Initialisieren eines Fensters ist eine in der Praxis häufig vorkommende Aufgabe. Wenn ein Fenster sich selbst initialisieren soll, verwenden Sie dazu üblicherweise das Loaded-Ereignis. Wenn ein Fenster von außen initialisiert werden soll, verwenden Sie den Konstruktor oder entwickeln Sie Eigenschaften, über die auf die Steuerelemente des Fensters zugegriffen werden kann. Der obige Dialog soll z. B. auch erlauben, dass der Name voreingestellt wird. Dazu erweitere ich den Konstruktor: Listing 13.21: Den Dialog über den Konstruktor initialisieren public NameDialog(string defaultName) { InitializeComponent(); // Die Eingabe übernehmen this.Input = defaultName; }
HALT
Achten Sie darauf, dass Sie Ihre Initialisierung erst nach dem Aufruf von InitializeComponent vornehmen. Vor InitializeComponent sind die Felder in der Klasse, die die WPF-Elemente referenzieren, noch null und Sie erhalten beim Zugriff eine NullReferenceException.
13.3.8 Tipps und Tricks zu Fenstern Die Arbeit mit WPF-Fenstern beende ich mit ein paar kleinen Tipps und Tricks, die ich (wie schon im LINQ-Kapitel) nicht näher erläutere.
Aktualisieren eines Fensters bei prozessorlastigen Aktionen Das Aktualisieren eines Fensters bei prozessorlastigen Aktionen ist ein Trick, der in der Praxis häufiger benötigt wird. Eine prozessorlastige Aktion ist schon eine einfache Schleife, die ein Zwischenergebnis z. B. in ein Label schreibt (im Beispiel ist das lblInfo): for (int i = 0; i < 100; i++) { this.lblInfo.Content = i.ToString(); Thread.Sleep(100); // 100 ms Pause }
Dieses Beispiel führt dazu, dass während der Ausführung der Schleife das Label nicht aktualisiert wird, sondern erst am Ende. Das Problem liegt darin, dass zwar Nachrichten zum Neuzeichnen (die im Allgemeinen den Konstantennamen WM_PAINT besitzen) in die Nachrichten-Warteschlange des Fensters geschrieben werden, diese aber eine niedrige Priorität haben und deswegen nicht ausgewertet werden, solange das Programm ausgeführt wird. Erst wenn das Programm beendet ist, wertet die Fensterfunktion die WM_PAINT-Nachrichten aus und zeichnet das Fenster neu. Das Problem können Sie unter WPF lösen, indem Sie dafür sorgen, dass die Nachrichtenwarteschlange des Fensters explizit abgearbeitet wird. Und das geht folgendermaßen:
822
Der Umgang mit WPF-Fenstern
Listing 13.22: Aktualisieren eines Fensters in einer Schleife for (int i = 0; i < 100; i++) { // Fortschritt melden this.lblInfo.Content = i.ToString(); // Die Nachrichten-Warteschlange leeren if (Application.Current != null) { Application.Current.Dispatcher.Invoke( DispatcherPriority.Background, new Action(delegate { })); }
12
13
// 100 ms Pause als Demo für die Aktion Thread.Sleep(100); }
14
Dispatcher.Invoke ruft die am zweiten Argument übergebene Methode synchron auf (auch wenn diese Methode leer ist). Die am ersten Argument übergebene Priorität DispatcherPriority.Background sorgt dafür, dass die Methode erst dann ausgeführt wird, wenn alle im Leerlauf befindlichen Vorgänge abgeschlossen sind. Dieser kleine Trick sorgt also dafür, dass Invoke so lange wartet, bis die Nachrichtenwarteschlange des Fensters leer ist. Da die darin enthaltenen WM_PAINT-Nachrichten dann auch abgearbeitet wurden, werden alle Veränderungen der Oberfläche gezeichnet.
15
16 Die Abfrage auf Application.Current != null ist übrigens sehr wichtig. Damit verhindern Sie, dass der Aufruf der Invoke-Methode zu einer NullReferenceException führt, wenn die Anwendung während der Ausführung des Codes heruntergefahren wird. Wenn Sie mit diesem Trick arbeiten, müssen Sie aufpassen: Da das Fenster dann auch auf Eingaben reagiert, kann es vorkommen, dass der Benutzer den Programmcode noch einmal ausführt. Der erste Ablauf wird dann so lange unterbrochen, bis der zweite beendet ist. Das kann in der Praxis natürlich erhebliche Probleme verursachen. Deshalb sollten Sie verhindern, dass der Programmcode erneut ausgeführt werden kann, während er gerade ausgeführt wird, z. B. indem Sie das Steuerelement deaktivieren, über das er gestartet wird. Eine bessere Lösung ist, prozessorlastige Aktionen in einem Thread auszuführen. Idealerweise verwenden Sie dazu einen BackgroundWorker. Dieses etwas komplexere Thema finden Sie in Kapitel 20. Das Beispiel zu diesem Abschnitt, das Sie auf der Buch-DVD finden, setzt den BackgrundWorker allerdings alternativ zu der oben beschriebenen Methode ein.
HALT
17
18 HALT
19
20 INFO
21
Fenster in der Mitte eines anderen Fensters öffnen 22
Wollen Sie ein Fenster in der Mitte eines anderen Fensters öffnen, stellen Sie die Eigenschaft WindowStartupLocation auf CenterOwner ein. Vor dem Öffnen des Fensters übergeben Sie den Besitzer an die Owner-Eigenschaft:
23
823
WPF-Anwendungen
Listing 13.23: Auf dem Besitzer-Fenster zentriertes Öffnen eines Fensters // Fenster erzeugen CenterOwnerDemoWindow window = new CenterOwnerDemoWindow(); // Den Besitzer bestimmen window.Owner = this; // Das Fenster öffnen window.Show();
13.4
Die WPF-Steuerelemente
Nachdem Sie alles Grundlegende zu WPF-Anwendungen und WPF-Fenstern erfahren haben, geht es nun um die WPF-Steuerelemente. Ich kann in diesem Abschnitt aber nur die Grundlagen behandeln und verzichte aus Platzgründen zum einen auf die Behandlung spezieller Steuerelemente. Zum anderen stelle ich die wesentlichen Eigenschaften der behandelten Steuerelemente nicht mehr tabellenförmig dar, sondern nur noch im Fließtext und im jeweiligen Beispiel. Ansonsten würden die Steuerelemente im Buch viel zu viel Platz einnehmen. WPF unterscheidet fünf Steuerelementarten, die in diesem Abschnitt grundlegend besprochen werden: ■
■ ■
■
■
Inhalts-Steuerelemente (Seite 835): Inhalts-Steuerelemente zeigen einen einfachen Inhalt an, erlauben ggf. Eingaben, aber nicht die Änderung des Inhalts. Zu diesen Steuerelementen gehören z. B. der Button und das Label. Listen-Steuerelemente (Seite 842): Diese Steuerelemente verwalten nahezu beliebig viele Einträge. Die ListBox und die ComboBox sind typische Beispiele. Bereichssteuerelemente (Seite 849): Bereichssteuerelemente stellen einen Bereich (z. B. den Bereich zwischen 1 und 100) oder einen Wert in einem Bereich dar und ermöglichen in einigen Fällen auch das Einstellen eines Werts. Dazu gehören die ProgressBar, die einen Fortschritt anzeigt, der Slider, der die Einstellung eines Werts in einem Bereich zulässt, und die ScrollBar, die zum Scrollen eines Bereichs vorgesehen ist. Text-Steuerelemente (Seite 851): Diese Steuerelemente sind für die Eingabe von Text vorgesehen. Beispiele sind das TextBox- und das RichTextBox-, aber auch das InkCanvas-Steuerelement, das es erlaubt, eine Handschrift (über einen Stift oder über die Maus) in Text umzuwandeln. Layout-Steuerelemente (Seite 858): In den ersten Beispielen dieses Abschnitts werden die Steuerelemente noch wie in den bisherigen Beispielen auf einem Grid angelegt und absolut positioniert (wie Sie es in Kapitel 12 gelernt haben). Diese Art der Positionierung von Steuerelementen wird in den meisten Standardanwendungen verwendet. WPF bietet darüber hinaus aber über die Layout-Steuerelemente noch eine Vielzahl anderer Layout-Möglichkeiten. Der Abschnitt »Layout-Steuerelemente« setzt sich mit diesen auseinander.
Na dann: Auf zu den Steuerelementen ☺. Aber zuerst folgen ein paar Grundlagen:
824
Die WPF-Steuerelemente
13.4.1
Die Basisklassen der Steuerelemente
WPF-Steuerelemente sind mehr oder weniger direkt von der Basisklasse FrameworkElement abgeleitet. Diese Klasse stellt die Basisfunktionalität aller Steuerelemente zur Verfügung. Dazu gehören z. B. die Unterstützung von Stilen, Ressourcen, Datenbindung, Tooltipps und Kontextmenüs. Von FrameworkElement direkt sind nur wenige einfache Steuerelemente abgeleitet, wie z. B. das Image-Steuerelement, das Bilder anzeigt.
12
Die Klasse Control ist von FrameworkElement abgeleitet und stellt Funktionalität für komplexere Steuerelemente und solche, die Eingaben erlauben, zur Verfügung. Dazu gehören z. B. die Eigenschaften Background, Foreground, Schrift-Eigenschaften und die Unterstützung von Vorlagen. Von Control sind die meisten Steuerelemente (mehr oder weniger direkt) abgeleitet. In diesem Sinne wäre es eigentlich falsch, bei Steuerelementen, die nicht von Control abgeleitet sind, den Begriff »Steuerelement« zu verwenden. Eigentlich müsste ich diese mit »Framework-Element« bezeichnen. Im Allgemeinen werden aber auch die einfachen »Framework-Elemente« wie Image, MediaElement, Rectangle und Ellipse als Steuerelement bezeichnet. Ein Grund dafür ist, dass diese Steuerelemente eine Menge Ereignisse von FrameworkElement erben und deswegen z. B. auch auf die Betätigung der Maus reagieren können. Ich bleibe also dabei und bezeichne alle Framework-Elemente auch als Steuerelement.
13.4.2
13 14 INFO
15
16
Übersicht über die Steuerelemente
Damit Sie eine Übersicht über die in WPF verfügbaren Steuerelemente erhalten, fasse ich diese hier einmal zusammen. Die wichtigsten Steuerelemente werden ab Seite 835 behandelt. Beachten Sie bitte, dass nicht alle hier beschriebenen Steuerelemente in der Toolbar von Visual Studio enthalten sind.
17
Steuerelemente, die von FrameworkElement abgeleitet sind
18
Die folgenden Steuerelemente sind von der Basisklasse FrameworkElement abgeleitet, bieten also nur Basisfunktionalitäten und z. B. keinen Support für einen definierten Hintergrund, Vordergrund oder Vorlagen: ■
■
■ ■ ■
■ ■
19
Canvas, DockPanel, Grid, StackPanel, VirtualizingPanel, WrapPanel, TabPanel, ToolBarOverflowPanel, UniformGrid: Diese Steuerelemente sind Layout-Container, die auf eine vielfältige Weise die enthaltenen Steuerelemente platzieren. Die wichtigsten Layout-Container werden ab Seite 858 besprochen. DocumentPageView: Über dieses Steuerelement können Sie einzelne Seiten eines XPS-Dokuments (oder eines anderen von WPF unterstützten Dokumenttyps) anzeigen. Ellipse, Line, Path, Polygon, Polyline, Rectangle: Diese Steuerelemente stellen grafische Figuren dar. Image: Zeigt ein Bild an. InkCanvas: Erlaubt die Eingabe von Text als Handschrift über die Maus oder (auf einem entsprechend ausgestatteten Rechner wie einem Tablett-PC) über einen Stift. MediaElement: Über dieses Steuerelement können Sie Audio- und Videodaten wiedergeben. Popup: Stellt ein Popup-Fenster samt Inhalt dar.
20
21
22
23
825
WPF-Anwendungen
■ ■
INFO
TextBlock: Stellt einen mehrzeiligen Text dar, der optional auch automatisch umbrochen wird. Dieses Steuerelement wird auf Seite 851 behandelt. Viewport3D: Stellt eine Oberfläche bereit, auf der 3D-Inhalt ausgegeben werden kann.
Beachten Sie bitte, dass diese Aufzählung nur die wirklichen Steuerelemente beinhaltet und nicht spezielle Klassen wie z. B. Adorner, die ebenfalls von FrameworkElement abgeleitet sind.
Steuerelemente, die von Control abgeleitet sind Die folgenden Steuerelemente sind mehr oder weniger direkt von Control abgeleitet, unterstützen also neben einem Vorder- und Hintergrund z. B. auch Vorlagen: ■ ■
■ ■ ■
■
■ ■
■ ■ ■ ■ ■ ■ ■ ■
826
Button, ToggleButton, RepeatButton: Diese Steuerelemente stellen Schalter dar, die der Anwender betätigen kann. Schalter werden ab Seite 837 besprochen. ComboBox: Steuerelement, das die Auswahl eines Eintrags aus einer Liste erlaubt und optional die Eingabe in einer TextBox. Die Liste ist normalerweise zugeklappt und kann (über einen Schalter) aufgeklappt werden. Die ComboBox wird ab Seite 847 behandelt. ContextMenu: Über dieses Steuerelement können Sie Kontextmenüs erzeugen. Kontextmenüs werden ab Seite 856 behandelt. DocumentViewer: Erlaubt die Anzeige eines XPS-Dokuments (oder eines anderen, von WPF unterstützten Dokumenttyps). Frame: In diesem Steuerelement können Sie XAML-Code von anderem XAMLCode separiert darstellen. Frame ist auch in der Lage, HTML-Code anzuzeigen, und unterstützt die Navigation. Ein Frame ist damit so etwas wie ein HTML-Frame. ItemsControl: Die Basisklasse aller Listen-Steuerelemente ist in der Lage, eine Liste von Elementen darzustellen. ItemsControl wird grundsätzlich ab Seite 842 besprochen. Label: Stellt Text dar und dient prinzipiell der Beschriftung anderer Steuerelemente. Das Label wird ab Seite 840 behandelt. ListBox: Steuerelement, das die Auswahl eines Eintrags aus einer Liste erlaubt. Die Liste wird im Gegensatz zur ComboBox immer dargestellt. Eine Beschreibung der ListBox finden Sie ab Seite 845. Menu: Steuerelement zur Erstellung von Menüs (Seite 854). PasswordBox: Dient der Eingabe eines Passwortes. Näheres zu diesem Steuerelement finden Sie ab Seite 853. ProgressBar: Über dieses Steuerelement können Sie einen Fortschritt darstellen. Die ProgressBar wird ab Seite 850 behandelt. RichTextBox: Dieses Steuerelement stellt formatierbaren RTF-Text dar und ist damit so etwas wie eine kleine Textverarbeitung. ScrollBar: Stellt eine Scrollbar dar. Dank des ScrollViewer-Steuerelements benötigen Sie die ScrollBar nur in sehr speziellen Situationen. ScrollViewer: Über dieses Container-Steuerelement können Sie das Scrollen ermöglichen. ScrollViewer wird ab Seite 863 behandelt. Slider: Erlaubt die Einstellung eines Werks in einem Bereich über einen Schiebebalken. Dieses Steuerelement wird kurz ab Seite 850 vorgestellt. TabControl: Dieses Steuerelement kann mehrere Seiten (Tabs) darstellen, die der Anwender umschalten kann. Auf jeder Seite können unterschiedliche
Die WPF-Steuerelemente
■ ■ ■
■ ■
Steuerelemente angebracht werden. Das TabControl dient damit der übersichtlichen Darstellung auf WPF-Fenstern (oder -Seiten), die zu viele Steuerelemente beinhalten, als dass diese noch übersichtlich dargestellt werden können. TextBox: Stellt einen Text dar und erlaubt auch das Editieren des Textes. Die TextBox beschreibe ich ab Seite 852. ToolBar: Stellt eine Symbolleiste dar. Dieses Steuerelement wird auf Seite 857 behandelt. ToolTip: Zeigt einen Tooltipp an. Tooltipps können Sie allerdings auch einfach erreichen, indem Sie einen String oder ein komplexes WPF-Element in die von FrameworkElement geerbte ToolTip-Eigenschaft schreiben. TreeView: Stellt einen Baum von Einträgen dar, ähnlich der Ordner-BaumAnsicht im Windows-Explorer. UserControl: Erlaubt die Erstellung eigener, zusammengesetzter Steuerelemente.
12
13 14
Leider kann ich im Buch nicht alle Steuerelemente behandeln. Ich denke aber, dass die behandelten Steuerelemente in der Praxis am häufigsten verwendet werden. Mit den in diesem Kapitel vermittelten Grundlagen sollte auch die Anwendung der hier nicht behandelten Steuerelemente kein (großes) Problem darstellen.
15
13.4.3 Der Inhalt eines Steuerelements Viele WPF-Steuerelemente verhalten sich bezüglich ihres Inhalts vollkommen anders als klassische Steuerelemente. Ein Inhalts-, Listen- oder Container-Steuerelement kann prinzipiell alles in WPF Mögliche als Inhalt haben. So können Sie z. B. einen Schalter problemlos mit einem Bild und einem Text ausstatten oder eine ListBox erzeugen, die in ihrer Liste Bilder und weitere Informationen anzeigt. Dieses Feature ist, verglichen mit klassischen Systemen, bei denen ein Steuerelement nur den speziell für das Objekt definierten Inhalt besitzt, genial.
Inhalts-, Listenund ContainerSteuerelemente können einen beliebigen Inhalt besitzen
16
17
Sie können aber nicht alle Steuerelemente mit einem beliebigen Inhalt ausstatten. TextSteuerelemente und Bereichssteuerelemente, die einen speziellen Inhalt besitzen oder die ihren Inhalt selbst definieren, sind davon (logischerweise) ausgeschlossen.
18
Ein Button ist z. B. ein Inhalts-Steuerelement. Ein solches kann maximal ein direktes Kind-Element beinhalten. Das kann z. B. ein Text sein:
19
OK
Die bei Inhalts-Steuerelementen für den Inhalt verwendete Eigenschaft Content ist aber vom Typ Object. Ein Inhalts-Steuerelement kann deswegen ein beliebiges Objekt als Inhalt verwenden. Ein Button kann z. B. ein Label beinhalten (und das Label als Inhalt wieder Text):
20
21 Listing 13.24: Ein Inhalts-Steuerelement mit einem anderen Steuerelement als Inhalt OK
Die Einstellung der Padding-Eigenschaft im Beispiel-Label sorgt dafür, dass dieses in der Mitte des Schalters angezeigt wird. Näheres dazu finden Sie im Abschnitt »Die Größe und Position von Steuerelementen« (Seite 830).
22
23
INFO
827
WPF-Anwendungen
Sie können das natürlich noch weiter treiben, auch wenn das hier jetzt keinen Sinn macht: Listing 13.25: Ein Inhalts-Steuerelement mit einem anderen Steuerelement als Inhalt, das wieder ein Steuerelement als Inhalt besitzt OK
Weil ein Inhalts-Steuerelement zwar auf maximal einen Inhalt beschränkt ist, aber dieser auch ein WPF-Element sein kann, können Sie auch Steuerelemente als Inhalt verwenden, die mehr als einen Inhalt besitzen können. Über ein StackPanel, das die Darstellung beliebig vieler Steuerelemente in einem Fluss erlaubt, können Sie z. B. einen Schalter mit einem Bild und einem Text versehen: Listing 13.26: Inhalts-Steuerelement mit einem Container-Steuerelement als Inhalt, das mehrere andere Steuerelemente enthält WPF is not that hard to learn
Abbildung 13.7: Das zusammengesetzte InhaltsSteuerelement
INFO
Das Bild »DontPanic.gif« ist in diesem Beispiel als Ressource in die Anwendung integriert. In Visual Studio erreichen Sie dies, indem Sie das Bild dem Projekt hinzufügen. Ressourcen werden für WPF in Kapitel 14 behandelt. Bei Listen-Steuerelementen gilt Ähnliches. Der Inhalt eines Listen-Steuerelements ist eine Liste. Eine ListBox kann z. B. eine Liste von Strings anzeigen: Listing 13.27: Listen-Steuerelement mit einfachem Inhalt in den Elementen Windows Presentation Foundation Unleashed Windows Presentation Foundation Pro WPF
828
Die WPF-Steuerelemente
Die Klasse ListBoxItem ist aber wie Button z. B. auch von ContentControl abgeleitet und erlaubt deswegen einen beliebigen Inhalt. So können Sie in einer ListBox z. B. problemlos Informationen zu (WPF-)Büchern strukturiert darstellen (inklusive Bild): Listing 13.28: Listen-Steuerelement mit komplexem Inhalt in den Elementen Windows Presentation Foundation Unleashed Adam Nathan, Daniel Lehenbauer Windows Presentation Foundation Dirk Frischalowski
12
13 14
15
Dieses Beispiel ist noch nicht besonders schön formatiert, zeigt aber die Möglichkeiten ganz gut (Abbildung 13.8).
16 Abbildung 13.8: Die ListBox mit komplexem Inhalt. Der erste Eintrag ist selektiert.
17
18
19
20
Auf die Positionierung von Steuerelementen gehe ich im folgenden Abschnitt ein. Damit können Sie die Liste auch schöner gestalten. Die Möglichkeit, beliebige Elemente in einem Steuerelement unterzubringen, gibt auf jeden Fall einen enormen Spielraum. Probieren Sie es einfach aus.
21
22 In der Praxis werden Sie einen komplexen Inhalt eines Steuerelements eher selten direkt im Steuerelement definieren. Für komplexe Inhalte bieten viele Steuerelemente die Möglichkeit, eine Datenvorlage zu definieren. Bei der ListBox erreichen Sie diese über die ItemTemplate-Eigenschaft. In Kapitel 14 erfahren Sie beim Thema »Datenbindung« mehr darüber.
INFO
23
829
WPF-Anwendungen
13.4.4 Die Größe und Position von Steuerelementen Größe und Position werden von dem Containerund dem KindSteuerelement gemeinsam bestimmt
WPF ist sehr flexibel, was die Einstellung der Größe und der Position von Steuerelementen angeht. Leider ist diese Vielfalt auch etwas verwirrend und fehlerträchtig. Deswegen gehe ich hier grundlegend darauf ein. Die Größe und Position eines Steuerelements wird von WPF in Zusammenarbeit mit dem Steuerelement und seinem Container ermittelt. Der Container legt zwar fest, wo und wie das Kind-Steuerelement gerendert wird. Das Kind-Steuerelement kann aber definieren, welche Größe und Position es bevorzugen würde. Die Größe und Position eines Steuerelements wird von verschiedenen Faktoren beeinflusst: ■ ■ ■ ■ ■
den Eigenschaften Width und Height, den Eigenschaften MinWidth, MaxWidth, MinHeight und MaxHeight, den Eigenschaften Margin und Padding, den Eigenschaften HorizontalAlignment und VerticalAlignment und den Eigenschaften HorizontalContentAlignment und VerticalContentAlignment des Containers.
Das Ganze ist leider recht komplex und kann hier nicht vertieft behandelt werden. Gott sei Dank hilft Visual Studio enorm bei der Positionierung und Einstellung der Größe (siehe Kapitel 12). Ein wenig Grundlagenwissen ist aber schon sinnvoll:
Die Eigenschaften für Positions- und Größenangaben Tabelle 13.13 beschreibt zunächst die Eigenschaften von allgemeinen Steuerelementen. Diese Eigenschaften sind in der Basisklasse FrameworkElement definiert. Tabelle 13.13: Die zur Größeneinstellung und Positionsbestimmung verwendeten Eigenschaften von FrameworkElementen
830
Eigenschaft
Beschreibung
Width, Height
Wenn diese Eigenschaften nicht Double.NaN sind, bestimmen Sie die Breite bzw. Höhe des Steuerelements. In XAML sind Width oder Height (oder die anderen Breiten- und Größeneigenschaften) dann Double.NaN, wenn sie nicht angegeben sind. Bei Double.NaN bestimmt der Container die Breite bzw. Höhe.
MinWidth, MaxWidth, MinHeight, MaxHeight
Wenn diese Eigenschaften nicht auf Double.NaN eingestellt sind, bestimmen sie die minimale bzw. maximale Breite des Steuerelements.
Margin
bestimmt den äußeren Rand des Steuerelements. Der Typ dieser Eigenschaft ist Thickness, eine Struktur, die vier Werte für links, rechts, oben und unten verwaltet. Der für XAML verwendete Konverter erlaubt die Angabe eines String in einer der drei folgenden Formen: Eine einzeln angegebene Zahl setzt alle Eigenschaften auf denselben Wert, zwei durch Kommas getrennte Zahlen setzen die Werte für links/rechts und oben/unten getrennt, vier angegebene Werte setzen alle Eigenschaften individuell.
Die WPF-Steuerelemente
Eigenschaft
Beschreibung
HorizontalAlignment
bestimmt, wie das Steuerelement auf der horizontalen Achse ausgerichtet wird. Die gleichnamige Aufzählung erlaubt die folgenden Werte: – Left: Das Steuerelement wird an dem linken Rand seines Containers ausgerichtet. – Right: Das Steuerelement wird an dem rechten Rand seines Containers ausgerichtet.
Tabelle 13.13: Die zur Größeneinstellung und Positionsbestimmung verwendeten Eigenschaften von FrameworkElementen (Forts.)
12
– Center: Das Steuerelement wird in der horizontalen Mitte des Containers ausgerichtet. – Stretch: Das Steuerelement wird links und rechts am Rand seines Containers ausgerichtet, sodass es in der maximal möglichen Breite erscheint. VerticalAlignment
13
bestimmt die vertikale Ausrichtung des Steuerelements mit den folgenden Werten der gleichnamigen Aufzählung:
14
– Top: Das Steuerelement wird am oberen Rand seines Containers ausgerichtet. – Bottom: Das Steuerelement wird am unteren Rand seines Containers ausgerichtet.
15
– Center: Das Steuerelement wird in der vertikalen Mitte des Containers ausgerichtet
16
– Stretch: Das Steuerelement wird oben und unten am Rand seines Containers ausgerichtet, sodass es in der maximal möglichen Höhe erscheint.
17 Bei der Einstellung der Breite und der Höhe sollten Sie vorsichtig sein. Mit einer fest eingestellten Breite bzw. Höhe kann es sein, dass Teile des Inhalts eines Steuerelements nicht ausgegeben werden. Da WPF vielfältige Layout-Möglichkeiten besitzt, ist es in vielen Fällen auch gar nicht nötig, die Breite oder Höhe eines Steuerelements festzulegen. In Visual Studio, das per Voreinstellung ein Grid als Layout-Container verwendet, wird die Größe von Steuerelementen, die Sie aus der Toolbar ziehen, aber vom Designer eingestellt. Denken Sie jedoch daran, dass Sie das Layout auch ändern können. Grundlagen dazu beschreibe ich ab Seite 858.
HALT
18
19
Die Klasse Control stellt drei weitere Positionierungs- und Größen-Eigenschaften zur Verfügung (Tabelle 13.14). Eigenschaft
Beschreibung
Padding
bestimmt den inneren Rand des Steuerelements.
HorizontalContentAlignment
bestimmt die Ausrichtung des Inhalts des Steuerelements mit den Werten der HorizontalAlignment-Aufzählung. Obwohl dies so nicht dokumentiert ist, scheint HorizontalContentAlignment die HorizontalAlignment-Eigenschaft eines ggf. als Inhalt angegebenen WPF-Elements zu überschreiben. Die Voreinstellung ist HorizontalAlignment.Left, wird aber von verschiedenen Steuerelementen wie dem Button auch anders definiert (beim Button auf Center).
20 Tabelle 13.14: Die bei Steuerelementen zusätzlich zur Größeneinstellung und Positionsbestimmung verwendeten Eigenschaften
21
22
23
VerticalContentAlignment bestimmt ähnlich HorizontalContentAlignment die vertikale Ausrichtung des Inhalts des Steuerelements.
831
WPF-Anwendungen
INFO
Falls Sie in dieser Tabelle Eigenschaften für die linke und obere Position vermissen: Die gibt es grundsätzlich in WPF nicht. Nur der Canvas-Layout-Container erlaubt über die angefügten Eigenschaften Left, Top, Right und Bottom eine absolute Positionierung. Abbildung 13.9 stellt die Zusammenhänge grafisch dar. Das hier dargestellte Steuerelement hat prinzipiell die folgenden Einstellungen (nehmen Sie es aber bitte mit den Werten auf der Grafik nicht so genau ☺): ■ ■ ■ ■ ■ ■
Width: 100 Height: 50 HorizontalAlignment: Right VerticalAlignment: Bottom Margin: 20 Padding: 10
Abbildung 13.9: Die Eigenschaften, die die Größe und Position eines Steuerelements beeinflussen
Wenn Sie keine der hier gezeigten Eigenschaften einstellen, gelten die Voreinstellungen. Diese sind zwar ggf. bei den einzelnen Steuerelementen anders eingestellt, bei den meisten gelten jedoch die folgenden Werte: ■ ■ ■ ■ ■ ■
Width: Double.NaN Height: Double.NaN HorizontalAlignment: Center oder Stretch VerticalAlignment: Center oder Stetch Margin: 0 Padding: 0
Bei einem Label, dessen Aligment-Eigenschaften per Voreinstellung auf Center stehen, bewirkt ein Platzieren ohne weitere Angaben, dass dieses in der Mitte des Containers angezeigt wird. Bei einem Button, dessen Aligment-Eigenschaften auf Stretch stehen, bewirkt ein Platzieren ohne weitere Angaben, dass dieser den gesamten Platz des Containers ausfüllt. Das gilt aber nur dann, wenn der Container nicht von Control abgeleitet ist. In diesem Fall gelten dessen Einstellungen in HorizontalContentAlignment und VerticalContentAlignment.
832
Die WPF-Steuerelemente
Das folgende Beispiel stellt die Zusammenhänge am Beispiel eines Grid dar (das z. B auf einem Fenster platziert wird): Das Grid enthält drei Button-Steuerelemente, die wiederum je einen TextBlock mit Text enthalten. Ein TextBlock wird zur Darstellung von mehrzeiligem Text verwendet. TextWrap der TextBlock-Elemente ist auf Wrap gesetzt, damit der Text automatisch umbrochen wird. HorizontalAlignment ist auf Right gesetzt, um zu demonstrieren, dass diese Einstellung nichts bewirkt, da sie von der HorizontalContentAlignment-Eigenschaft der Button-Elemente überschrieben wird. Die TextBlock-Elemente sind etwas kleiner definiert als der jeweilige Button, um die Ausrichtung demonstrieren zu können.
12
Listing 13.29: Beispiel für die Positionierung und Größe von WPF-Framework-Elementen
13
Das ist TextBlock 1
14
15
Das ist TextBlock 2
16
Das ist TextBlock 3
17
18
Abbildung 13.10 zeigt das Ergebnis. Wie Sie sehen, bewirkt die Einstellung der horizontalen Ausrichtung der TextBlock-Elemente nichts, da diese von der HorizontalContentAlignment-Eigenschaft der Button-Steuerelemente überschrieben wird.
19
Abbildung 13.10: Beispiel für die Positionierung, Ausrichtung und Größe von WPFFrameworkElementen
20
21
22
23
833
WPF-Anwendungen
Die Zusammenhänge sind also schon ein wenig komplex. Mit ein wenig »Herumspielen« bekommen Sie aber ein Gefühl dafür. Außerdem hilft Visual Studio bei der Einstellung der Eigenschaften enorm. Trotzdem ist es in einigen Fällen schwer, herauszufinden, warum das eine oder andere gewünschte Feature beim besten Willen nicht funktioniert. Mit diesem Wissen können wir die ListBox aus dem vorhergehenden Abschnitt in der Mitte eines Grid anzeigen und ein wenig schöner gestalten: Listing 13.30: Eine über Layoutangaben verbesserte ListBox zur Anzeige von Buchdaten auf einem Grid Windows Presentation Foundation Unleashed Adam Nathan, Daniel Lehenbauer Windows Presentation Foundation Dirk Frischalowski
Abbildung 13.11 zeigt ein WPF-Fenster, das dieses Grid beinhaltet: Abbildung 13.11: Die im Layout verbesserte ListBox in Aktion
834
Die WPF-Steuerelemente
13.4.5 Inhalts-Steuerelemente Inhalts-Steuerelemente sind Steuerelemente, die von ContentControl abgeleitet sind. ContentControl liefert eine Content-Eigenschaft, die für XAML als Inhaltseigenschaft gekennzeichnet ist und die den Inhalt des Steuerelements verwaltet. In XAML darf der Inhalt des XML-Elements, das ein Inhalts-Steuerelement darstellt, also nur ein einfacher sein.
Inhalts-Steuerelemente haben maximal ein direktes InhaltsElement
12
.NET bietet in der Version 3.5 die folgenden Inhalts-Steuerelemente: ■
■ ■ ■ ■
■ ■ ■ ■
■
Button: Normaler Schalter, der über die Maus oder Tastatur betätigt werden kann und nach der Betätigung wieder in seinen Ursprungszustand zurückwechselt. RepeatButton: Wie Button, nur dass das Click-Ereignis permanent gefeuert wird, solange der Benutzer den Schalter betätigt. ToggleButton: Definiert einen Umschalter, dessen Zustand bei der Betätigung von Ein- in Ausgeschaltet wechselt (und umgekehrt). CheckBox: Definiert einen Umschalter, der aber nicht wie ein normaler Schalter dargestellt wird, sondern mit einem »Ankreuz«-Feld auf der linken Seite. RadioButton: Ähnlich einer CheckBox, nur mit einer anderen Darstellung und mit der Möglichkeit, dass mehrere RadioButton-Steuerelemente zu einer Gruppe zusammengefasst werden, sodass immer nur einer eingeschaltet sein kann (wie bei den Frequenz-Schaltern eines alten Radios – falls Sie das noch kennen …). Label: Steuerelement für Beschriftungen. GroupBox: Dient dem visuellen und logischen Gruppieren von anderen Steuerelementen. Zeichnet einen Rahmen und besitzt eine Überschrift. Expander: Steuerelement ähnlich GroupBox, mit dem Feature, seinen Inhalt zu expandieren oder zusammenzuklappen. ToolTip: Dieses Steuerelement zeigt seinen Inhalt in einem separaten Fenster an, das an der Maus positioniert wird. Dieses Steuerelement wird in diesem Kapitel nicht behandelt, weil es normalerweise nicht benötigt wird. Sie können einfach einen Tooltipp in die ToolTip-Eigenschaft eines Steuerelements schreiben. Frame: Dient dem Separieren von XAML-Inhalt von anderem Inhalt und der Anzeige von HTML-Dokumenten (wie ein HTML-Frame). Dieses Steuerelement wird in diesem Kapitel ebenfalls nicht behandelt.
13 14
15
16
17
18
19
Daneben sind noch die Klassen Window und Page von ContentControl abgeleitet. Ein Fenster oder eine Seite ist also im Wesentliche auch »nur« ein Inhalts-Steuerelement (und darf deswegen auch nur maximal ein Element als Inhalt besitzen).
20
Weiterhin sind noch Klassen wie ComboBoxItem, ListBoxItem und StatusBarItem, die Einträge von Listen-Steuerelementen darstellen, und weitere Klassen von ContentControl abgeleitet. Diese werden in diesem Abschnitt aber nicht behandelt.
21
ButtonBase
22
ButtonBase ist die Basisklasse der WPF-Schalter-Steuerelemente und definiert die Möglichkeit, dass der Schalter über die Maus oder die Tastatur (mit der Leertaste oder der (¢)-Taste, wenn der Schalter den Fokus besitzt) betätigt werden kann, und stellt das Ereignis Click zur Verfügung, das in diesem Fall aufgerufen wird. Die Eigenschaft IsPressed gibt an, ob der Schalter gerade betätigt ist.
ButtonBase ist die Basis aller Schalter
23
835
WPF-Anwendungen
Die Eigenschaft ClickMode definiert, wann das Click-Ereignis über die Maus aufgerufen wird. Die Werte der gleichnamigen Aufzählung sind:
INFO
■
Release: Das Click-Ereignis wird aufgerufen, nachdem die Maus auf dem Schalter betätigt und wieder losgelassen wurde. Dies ist die Voreinstellung.
■
Press: Das Click-Ereignis wird aufgerufen, wenn die Maus auf dem Schalter betätigt wird.
■
Hover: Das Click-Ereignis wird aufgerufen, wenn die Maus in den Schalterbereich hineinbewegt wird.
Beachten Sie, dass Sie bei den Schalter-Steuerelementen Button, RepeatButton und ToggleButton den Hintergrund nur eingeschränkt setzen können. Einen entsprechenden Hinweis finden Sie im Abschnitt »Übersicht über die wichtigen allgemeinen Eigenschaften« (Seite 804).
Schnellzugriffstasten ButtonBase ermöglicht die Betätigung des Schalters über (ALT) + Taste
Ein weiteres Feature von ButtonBase ist, dass die Beschriftung eine Schnellzugriffstasten-Definition enthalten kann. Unter Windows wird eine Schnellzugriffstaste in einer Beschriftung üblicherweise über einen Unterstrich dargestellt. Die Menüs von Standardanwendungen enthalten in der Regel solche Schnellzugriffstasten-Deklarationen. Wenn Sie die (ALT)-Taste und die in der Beschriftung unterstrichene Taste betätigen, wird der entsprechende Befehl ausgeführt. Eine Schnellzugriffstasten-Definition fügen Sie einer Beschriftung dadurch zu, dass Sie vor den entsprechenden Buchstaben einen Unterstrich setzen. Das folgende Beispiel zeigt dies für einen Button: _Demo
Abbildung 13.12: Button mit SchnellzugriffstastenDefinition
INFO
HALT
836
Setzen Sie Schnellzugriffstasten-Definitionen möglichst sparsam ein und achten Sie darauf, dass diese nicht mehrfach vorkommen. Windows unterstützt zwar mehrfach vorkommende Schnellzugriffstasten-Definitionen, der Benutzer muss dann aber ggf. mehrfach die Kombination aus (ALT) und der Schnellzugriffstaste betätigen. Sinnvoll sind Schnellzugriffstasten-Definitionen für OptionButton- und CheckBox-Instanzen, weniger für normale Schalter. Wenn Sie in der Beschriftung eines Schalters (oder eines Label, das auch Schnellzugriffstasten erlaubt) einen Unterstrich unterbringen wollen, müssen Sie zwei Unterstriche angeben.
Passen Sie auf, wenn Sie Schnellzugriffstasten definieren und die Höhe des Steuerelements einstellen. Bei einer zu geringen Höhe (z. B. 23 bei einem Label) wird u. U. zwar der Inhalt des Steuerelements angezeigt, aber nicht der Unterstrich für die Schnellzugriffstaste, wenn der Anwender die (ALT)-Taste betätigt. Verlassen Sie sich lieber auf die Höhe, die Visual Studio als Voreinstellung einstellt (beim Label 28). Ob der gesamte Text (inkl. Unterstrich) angezeigt wird, hängt natürlich auch von der definierten Schriftart ab. Probieren Sie dies ggf. aus und definieren Sie die Steu-
Die WPF-Steuerelemente
erelemente lieber ein wenig zu hoch. Diese Warnung hätte ich übrigens nicht gegeben, wenn ich nicht zwei Stunden nach der Ursache dafür gesucht hätte, dass ein Label den Unterstrich nicht anzeigte …
Button Die Klasse Button fügt ButtonBase lediglich die Eigenschaften IsCancel, IsDefault und IsDefaulted zu, die definieren, ob der Schalter automatisch betätigt wird, wenn die (Esc)- bzw. die (¢)-Taste betätigt wird. Die schreibgeschützte Eigenschaft IsDefaulted gibt nur dann true zurück, wenn IsDefault true ist und zurzeit kein anderes Steuerelement den Fokus besitzt, das die (¢)-Taste für sich in Anspruch nimmt.
Ansonsten ist ein Button sehr einfach. Werten Sie das Click-Ereignis aus, um auf die Betätigung des Schalters zu reagieren.
Button ist ein »normaler« Schalter
Abbildung 13.13: Ein Button in dem Default-Stil für Windows Vista
12
13 14
RepeatButton Die Klasse RepeatButton ähnelt einem Button, mit dem einen Unterschied, dass die Eigenschaften IsCancel, IsDefault und IsDefaulted nicht zur Verfügung stehen. Der andere Unterschied ist, dass das Click-Ereignis in definierten Abständen wiederholt aufgerufen wird, solange der Benutzer den Schalter betätigt. Die Frequenz dieser Aufrufe wird durch die Eigenschaften Delay (Anzahl der Millisekunden zwischen dem ersten Click und dem zweiten) und Interval (Anzahl der Millisekunden zwischen den einzelnen Aufrufen) bestimmt. Die Voreinstellung dieser Eigenschaften werden über die Systemeinstellung SystemParameters.KeyboardDelay und SystemParameters.KeyboardSpeed bestimmt.
RepeatButton ruft Click wiederholt auf
16
17
Einen RepeatButton können Sie immer dann einsetzen, wenn Sie dem Anwender ermöglichen wollen, einen Wert dadurch hoch- oder herunterzuzählen, dass er einen Schalter dauerhaft betätigt. Ein solches Verhalten bieten z. B. die Schalter, die am Ende einer ScrollBar liegen.
18
ToggleButton Ein ToggleButton ist ein Schalter, dessen Zustand umgeschaltet werden kann. Wenn Sie auf einem solchen Schalter klicken und er ist zurzeit ausgeschaltet, wird er eingeschaltet. Umgekehrt gilt das natürlich auch.
15
19 Ein ToggleButton ist ein Umschalter mit Zwischenzustand
Den Zustand des Schalters können Sie im Programm über die IsChecked-Eigenschaft ermitteln. Diese Eigenschaft ist ein Nullable-Boolean. true steht dafür, dass der Schalter eingeschaltet ist, bei false ist der Schalter ausgeschaltet. Der dritte Wert, null, legt einen Zwischenzustand fest (so etwas wie »halb eingeschaltet«). Wenn die (unpassend benannte) Eigenschaft IsThreeState true ist, kann auch der Anwender den Zwischenzustand einschalten. Ist IsThreeState false (was die Voreinstellung ist), kann der Anwender den Schalter nur zwischen dem ein- und ausgeschalteten Zustand umschalten. Im Programm (oder in XAML) können Sie aber trotzdem den Zwischenzustand einschalten, indem Sie den Wert null in IsChecked schreiben. Solch ein Verhalten können Sie z. B. bei Installationsanwendungen beobachten, bei denen eine durch eine CheckBox auswählbare Option im Zwischenzustand angezeigt wird, wenn der Anwender nur Teile der Unteroptionen ausgewählt hat.
20
21
22
23
837
WPF-Anwendungen
Der dritte Status unterscheidet sich bei einem ToggleButton in der Ansicht allerdings nicht von einem ausgeschalteten Schalter. Abbildung 13.14: ToggleButtons in den drei möglichen Zuständen mit der originalen Ansicht für den Zwischenzustand unter Vista
Sie müssen den ToggleButton über einen Eigenschaftentrigger (angedeutet in Kapitel 12 und behandelt in Kapitel 14) anpassen, damit der dritte Zustand erkennbar wird: Listing 13.31: Anpassen eines ToggleButton mit IsThreeState==true, sodass der Zwischenzustand erkennbar ist. Demo
INFO
Dieses Beispiel ist noch nicht komplett, weil Sie auch darauf reagieren müssen, dass gerade die Maus auf dem Schalter liegt etc. In der Praxis haben Sie bei der Anpassung also (leider) noch ein wenig mehr Arbeit. Unverständlich, dass Microsoft keine spezielle Ansicht für den Zwischenzustand vorgesehen hat … Abbildung 13.15 zeigt den veränderten ToggleButton in den drei möglichen Zuständen.
Abbildung 13.15: ToggleButtons in den drei möglichen Zuständen mit angepasster Ansicht für den Zwischenzustand
Der Namensraum des ToggleButton ist übrigens System.Windows.Controls.Primitives, was darauf hindeutet, dass dieses Steuerelement nicht für den direkten Einsatz gedacht ist, sondern dazu, angepasst zu werden. Zur Auswertung der Betätigung eines ToggleButton stehen Ihnen neben dem ClickEreignis und der IsChecked-Eigenschaft die Ereignisse Checked, Unchecked und Indeterminate zur Verfügung, die dann aufgerufen werden, wenn der Schalter eingeschaltet, ausgeschaltet bzw. in den Zwischenzustand geschaltet wird.
838
Die WPF-Steuerelemente
CheckBox Eine CheckBox ist nichts anderes als ein ToggleButton in einer anderen Ansicht. Sie bietet dieselben Features, was auch daran liegt, dass sie von ToggleButton abgeleitet ist. CheckBox überschreibt allerdings die HorizontalAlignment-Eigenschaft mit dem Wert HorizontalAlignment.Left (bei ToggleButton steht diese auf Center). Ein weiterer Unterschied ist, dass eine CheckBox den Zwischenzustand anzeigt (Abbildung 13.16). Außerdem kann eine CheckBox auch über die (+)-Taste ein- und über die (-)-Taste ausgeschaltet werden (was aber kaum bekannt ist). Ansonsten ist das Verhalten mit dem von ToggleButton identisch.
RadioButton
CheckBox ist ein ToggleButton mit anderem Layout
12
Abbildung 13.16: CheckBox-Instanzen in den drei möglichen Zuständen
Die Klasse RadioButton ist wie CheckBox ebenfalls von ToggleButton abgeleitet. Sie bietet deswegen unsinnigerweise auch die Eigenschaft IsThreeState und lässt in IsChecked den Wert null zu. Abgesehen davon besitzt ein RadioButton ein besonderes Verhalten beim Einschalten: Er lässt zum einem kein Ausschalten zu und verhindert zum anderen, dass bei mehreren in einer Gruppe angelegten RadioButton-Instanzen mehr als einer eingeschaltet sein kann. Mit anderen Worten: Wird ein RadioButton eingeschaltet, wird ein ggf. eingeschalteter anderer RadioButton in derselben Gruppe automatisch ausgeschaltet. Eine RadioButton-Gruppe dient also der Auswahl einer von mehreren Optionen.
13 14
15 Ein RadioButton kann in einer Gruppe nur exklusiv eingeschaltet werden
16
17
Per Voreinstellung teilen sich alle RadioButton-Instanzen, die auf demselben Container angelegt sind, eine Gruppe:
18
Listing 13.32: RadioButtons, die auf demselben Container angelegt sind, teilen sich eine Gruppe Option 1 Option 2 Option 3
19
20
In dem Beispiel habe ich die OptionButton-Instanzen auf einem GroupBox-Element angelegt, um der Gruppe eine Beschriftung geben zu können. GroupBox ist ein Inhalts-Steuerelement, darf also nur einen einfachen Inhalt besitzen. Um die RadioButton-Instanzen untereinander anlegen zu können, habe ich deswegen ein StackPanel verwendet. Das StackPanel definiert damit die RadioButton-Gruppe. Abbildung 13.17 zeigt das Ergebnis.
21
22 Abbildung 13.17: Eine RadioButtonGruppe
839
23
WPF-Anwendungen
INFO
GroupName definiert optional die Gruppe
Was für die Praxis wichtig ist, ist, dass in einer RadioButton-Gruppe immer einer der Schalter per Voreinstellung eingeschaltet sein sollte. Wenn Sie nicht darauf achten und davon ausgehen, dass der Anwender eine Auswahl trifft, kann es ansonsten passieren, dass der Anwender das nicht macht und keiner der RadioButton-Steuerelemente eingeschaltet ist. Im Beispiel habe ich deswegen IsChecked des ersten RadioButton auf true gesetzt. Wenn Sie mehrere RadioButton-Gruppen auf demselben Container anlegen wollen, können Sie auch alternativ die GroupName-Eigenschaft mit einem Gruppennamen belegen. Der Name ist eigentlich unerheblich, er sollte nur für die RadioButton-Elemente einer Gruppe gleich (und im Sinne einer qualitativ hochwertigen Programmierung auch sprechend) sein. Listing 13.33: RadioButton, die über die GroupName-Eigenschaft zu zwei Gruppen zusammengefasst werden Lieblingssport: _Windsurfen _Snowboarden _Anderer Level: _Experte _Mittelgut _Anfänger
Im Programm lesen Sie dann die IsChecked-Eigenschaft der einzelnen RadioButtonInstanzen aus, um zu ermitteln, ob diese eingeschaltet sind. Üblicherweise geschieht das (wie bei CheckBox-Steuerelementen) bei der Auswertung aller Eingaben aus dem Fenster (also in der Regel in dem Ereignishandler des OK-Schalters). Sie können zur Auswertung aber natürlich auch die Ereignisse Checked, Unchecked und Indeterminate einsetzen, die von ToggleButton geerbt wurden.
Label Über ein Label beschriften Sie andere Steuerelemente und definieren Schnellzugriffstasten
Ein Label dient prinzipiell der Beschriftung eines anderen Steuerelements, kann aber, weil es ein Inhalts-Steuerelement ist, jeden beliebigen Inhalt besitzen. Ein anderer Inhalt als ein String macht aber in den wenigsten Fällen Sinn. Eine Besonderheit der Label-Klasse ist, dass die Beschriftung eine Schnellzugriffstaste beinhalten kann (die wie bei Schaltern durch einen Unterstrich vor dem entsprechenden Zeichen angelegt wird). In der Eigenschaft Target geben Sie dann das Steuerelement an, das den Fokus erhalten soll, wenn die Schnellzugriffstaste betätigt wird. So können Sie z. B. eine TextBox über ein Label beschriften und dafür sorgen, dass der Anwender über (ALT) + (V) in TextBox wechseln kann:
840
Die WPF-Steuerelemente
Listing 13.34: Ein Label mit Schnellzugriffstasten-Definition
Das Ziel wird in XAML über Datenbindung angegeben, indem Sie über die BindingMarkuperweiterung das Element angeben, das das Ziel ist. Datenbindung wird in Kapitel 14 grundlegend behandelt. Im C#-Quellcode können Sie das Ziel auch direkt setzen:
12
this.lblFirstName.Target = this.txtFirstName;
13 Passen Sie auf, wenn Sie Schnellzugriffstasten definieren und die Höhe des Label einstellen. Bei einer zu geringen Höhe (z. B. 23) wird u. U. zwar der Inhalt des Label angezeigt, aber nicht der Unterstrich für die Schnellzugriffstaste, wenn der Anwender die (ALT)-Taste betätigt. Verlassen Sie sich lieber auf die Höhe, die Visual Studio als Voreinstellung einstellt (beim Label 28), oder verwenden Sie ein Layout, bei dem Sie keine Höhe angeben müssen. Ob der gesamte Text (inkl. Unterstrich) angezeigt wird, hängt natürlich auch von der definierten Schriftart ab.
HALT
14
15
Ein Label hat per Voreinstellung die Einschränkung, dass es nur einen einzeiligen Text beinhalten kann. Wollen Sie mehrzeiligen Text erlauben, können Sie entweder direkt ein TextBlock-Element mit der Einstellung TextWrapping = Wrap verwenden (das aber keine Schnellzugriffstaste zulässt) oder ein solches als Inhalt des Label angeben:
16
Listing 13.35: Label mit TextBlock als Inhalt, um mehrzeiligen Text zu ermöglichen
17
Das ist ein mehrzeiliger Text im Label
18
GroupBox und Expander Die Steuerelemente GroupBox und Expander dienen dem visuellen Separieren von Steuerelementen. Beide lassen als typische Inhalts-Steuerelemente nur einen einzigen Inhalt zu und werden deshalb häufig mit einem StackPanel oder Grid versehen, um auch mehrere Steuerelemente unterbringen zu können.
GroupBox und Expander dienen dem visuellen und logischen Gruppieren
Beide besitzen die Eigenschaft Header, die die Beschriftung festlegt. Der Unterschied zwischen GroupBox und Expander ist, dass GroupBox per Voreinstellung mit einem Rahmen gezeichnet wird und Expander dafür das Zusammenklappen erlaubt. Die IsExpanded- Eigenschaft gibt an, ob der Expander expandiert ist oder nicht. Die Voreinstellung ist false.
19
20
21
Das folgende Beispiel demonstriert den Einsatz beider Steuerelemente:
22
Listing 13.36: GroupBox und Expander auf einem StackPanel C# Java Andere
23
841
WPF-Anwendungen
Webentwicklung Datenbanken Andere
Abbildung 13.18 zeigt das Ergebnis. Im rechten Fenster ist der Expander zugeklappt. Abbildung 13.18: GroupBox und Expander in einem Fenster unter Windows Vista mit dem Basis-Thema
Beim Expander können Sie über die Ereignisse Expanded und Collapsed ermitteln, wann dieser expandiert bzw. zusammengeklappt wird. Außerdem können Sie über die ExpandDirection-Eigenschaft bestimmen, in welche Richtung expandiert wird.
13.4.6 Listen-Steuerelemente Die WPF-Listen-Steuerelemente sind von der Basisklasse ItemsControl abgeleitet und haben eine Liste von (WPF-)Elementen als Inhalt. WPF enthält die folgenden, sehr mächtigen Listen-Steuerelemente: ■
■ ■ ■ ■ ■
842
ItemsControl: Basisklasse aller Listen-Steuerelemente. Stellt selbst bereits eine einfache Liste dar, die allerdings noch keine Selektionen erlaubt. Die Liste wird per Voreinstellung in einer sehr einfachen Form dargestellt. Sie können aber (wie bei allen WPF-Elementen) über die Template-Eigenschaft das Aussehen und damit auch die Liste flexibel verändern. ListBox: Zeigt die Liste per Voreinstellung in einer vertikalen Ausrichtung an und ermöglicht die Selektion einzelner oder mehrerer Elemente. ComboBox: Kombination aus einer TextBox mit angefügter ListBox. TreeView: Ermöglicht die Anzeige einer Baumstruktur wie im Windows-Explorer, wenn dieser die Ordnerstruktur anzeigt. ListView: Spezielle ListBox, die die Anzeige einer Liste mit mehreren Spalten ermöglicht, ähnlich der Liste des Windows-Explorers in der Detailansicht. TabControl, ContextMenu, Menu und StatusBar: Auch diese Steuerelemente zählen zu den Listen-Steuerelementen, weil sie mehr oder weniger direkt von ItemsControl abgeleitet sind.
Die WPF-Steuerelemente
Aus Platzgründen verzichte ich in diesem Buch auf die Behandlung der Steuerelemente TreeView, ListView und TabControl. Die Steuerelemente ContextMenu, Menu und StatusBar werden im Abschnitt »Menüs, Symbolleisten und Statuszeilen« (Seite 854) grundlegend behandelt. Und selbst die einfacheren Listen-Steuerelemente ListBox und ComboBox kann ich leider nur grundlegend behandeln, obwohl diese sehr interessant sind. Aber eben auch sehr mächtig und komplex …
INFO
12 Die Liste eines Listen-Steuerelements Die Liste eines Listen-Steuerelements steht über die Items-Auflistung zur Verfügung, deren Elemente vom Typ Object sind, also alle möglichen Objekte beinhalten können. Beim Rendern der Liste unterscheidet ItemsControl, ob es sich um ein WPF-Element oder ein normales .NET-Objekt handelt. Ein WPF-Element wird so gerendert, dass es innerhalb der Liste wie gewohnt angezeigt wird. Ein Beispiel dafür haben Sie bereits im Abschnitt »Der Inhalt eines Steuerelements« (Seite 827) gesehen.
Items verwaltet beliebige Objekte
Ist ein Element allerdings ein normales .NET-Objekt, wird per Voreinstellung die Rückgabe der ToString-Methode ausgegeben. Über die Eigenschaft DisplayMemberPath können Sie allerdings einen »Pfad« zu einer Eigenschaft angeben, die ausgegeben werden soll. Der Pfad ist die Angabe einer Eigenschaft relativ vom Objekt aus gesehen. Die Eigenschaft FirstName eines Person-Objekts geben Sie z. B. als »FirstName« an, wollen Sie die Eigenschaft City des Address-Objekts ausgeben, das in der Address-Eigenschaft verwaltet ist, verwenden Sie den String »Address.City«. Dabei muss es sich aber zwingend um Eigenschaften handeln (die nicht lesegeschützt sind). Felder sind nicht möglich.
.NET-Objekte werden über ToString oder die über die in DisplayMemberPath angegebene Eigenschaft ausgegeben
13 14
Listen werden normalerweise nicht in XAML gefüllt (es sei denn, sie sind statisch). Deswegen zeigt das folgende Beispiel, wie Sie eine ItemsControl-Instanz im Programm mit den Instanzen einer Person-Klasse füllen. Die Person-Klasse besitzt neben den Eigenschaften FirstName und LastName eine schreibgeschützte Eigenschaft FullName:
15
16
17
18
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get { return this.FirstName + " " + this.LastName; } } }
19
20
Die Beispiel-Liste ist folgendermaßen deklariert:
21 Listing 13.37: Listen-Steuerelement mit DisplayMemberPath-Angabe
22
Das Füllen (im Konstruktor oder im Loaded-Ereignis des Fensters) sieht dann so aus: Listing 13.38: Füllen eines Listen-Steuerelements mit Instanzen einer Klasse
23
this.lstDemo.Items.Add(new Person { FirstName = "Zaphod", LastName = "Beeblebrox" }); this.lstDemo.Items.Add(new Person { FirstName = "Ford", LastName = "Prefect" });
843
WPF-Anwendungen
this.lstDemo.Items.Add(new Person { FirstName = "Arthur", LastName = "Dent" }); this.lstDemo.Items.Add(new Person { FirstName = "Tricia", LastName = "McMillan" });
Die ItemsControl-Instanz zeigt dann auch wie erwartet den vollen Namen der verwalteten Personen an (Abbildung 13.19). Abbildung 13.19: Ein WPF-Fenster mit einem einfachen ItemsControl
INFO
XAML erlaubt das Füllen der Liste über den Inhalt
Falls Sie ItemsControl ausprobieren und sich wundern, dass dieses Steuerelement keine Scrollbars besitzt für den Fall, dass der Inhalt nicht passt: Eine ItemsControlInstanz können Sie über einen ScrollViewer scrollbar machen. Diesen behandle ich ab Seite 863. In XAML können Sie die Einträge eines Listen-Steuerelements als Inhalt des Elements angeben (weil Items als Inhaltseigenschaft deklariert ist). Der Inhalt des Elements wird automatisch in die Items-Auflistung geschrieben. Einfache Strings werden allerdings nicht als einzelne Elemente ausgewertet, sondern als ein einziges. Deswegen sollten Sie immer WPF-Elemente oder .NET-Objekte als Inhalt angeben (wenn Sie die Liste im Programm füllen, sind einfache Strings allerdings problemlos möglich): Listing 13.39: Ein Listen-Steuerelement, das in XAML gefüllt wird Eintrag 1 Eintrag 2 Eintrag 3
ItemsSource ermöglicht die Übergabe einer beliebigen Auflistung
Neben der Items-Eigenschaft, die übrigens schreibgeschützt ist, bietet ItemsControl die Eigenschaft ItemsSource, in die Sie auch eine beliebige Auflistung (die IEnumerable implementiert) schreiben können. Diese Eigenschaft ist sehr nützlich, wenn Sie Objekte bereits in einer Auflistung verwalten und diese lediglich in einem ListenSteuerelement darstellen wollen. Daneben bietet ItemsControl noch weitere Möglichkeiten, wie z. B. eine Gruppierung und die Eigenschaft HasItems (über die Sie in XAML herausfinden können, ob die Liste Einträge enthält), die ich hier nicht weiter besprechen kann.
Selektor-Steuerelemente Selektor-Steuerelemente sind solche, die ein Selektieren des Inhalts zulassen. Die Klasse Selector liefert dazu die Grundfunktionalität. Die Klassen ListBox, ComboBox, ListView und TabControl sind von Selector abgeleitet und bieten demnach ein ähnliches Verhalten. Die wesentlichen Eigenschaften der Selector-Klasse sind: ■
844
SelectedIndex: Gibt den Index des aktuell selektierten Elements an. Der gültige Bereich ist -1 bis Items.Count – 1. -1 steht dafür, dass zurzeit kein Element ausgewählt ist.
Die WPF-Steuerelemente
■ ■
SelectedItem: Diese Eigenschaft verwaltet das Objekt, das aktuell selektiert ist. SelectedValue: Per Voreinstellung verwaltet diese Eigenschaft denselben Wert wie SelectedItem. Sie können jedoch über die Eigenschaft SelectedValuePath den Pfad zu einer Eigenschaft der Listen-Objekte angeben, die dann als »Wert« des Objekts verwaltet wird und über SelectedValue zur Verfügung steht.
Selector besitzt zwei angefügte Eigenschaften, die für die einzelnen Einträge verwendet werden können: ■ ■
12
IsSelected: Gibt an, ob der Eintrag aktuell selektiert ist. Um einen Eintrag explizit zu selektieren, können Sie IsSelected auf true setzen. IsSelectionActive: Gibt an, ob die Selektion gerade den Fokus besitzt.
13
Über das Ereignis SelectionChanged können Sie ermitteln, wann die Selektion geändert wurde.
14
ListBox Eine ListBox zeigt die Einträge ähnlich einem ItemsControl in einer Liste an, besitzt allerdings eine automatisch bei Bedarf angezeigte horizontale und vertikale Scrollbar und ermöglicht die Selektion eines einzelnen oder mehrerer ihrer Einträge.
Eine Listbox stellt eine selektierbare Liste dar
15
Das Selektions-Verhalten wird über die Eigenschaft SelectionMode gesteuert. Sie können die folgenden Werte einstellen: Single: Die ListBox erlaubt lediglich die Auswahl eines einzelnen Eintrags. Dies ist die Voreinstellung. Multiple: Die ListBox erlaubt die Auswahl mehrerer Einträge über ein einfaches Anklicken. Extended: Die ListBox erlaubt ebenfalls die Auswahl mehrerer Einträge, allerdings ist die Form der Auswahl eine andere. Der Benutzer kann einzelne Einträge selektieren, indem er beim Klicken gleichzeitig die (STRG)-Taste betätigt, und eine Liste von Einträgen, indem er gleichzeitig die (ª)-Taste betätigt. Ein einfacher Klick auf einen Eintrag selektiert diesen, hebt aber die Selektion ggf. selektierter anderer Einträge auf.
16
ListBox besitzt neben den von Selector geerbten Eigenschaften die Eigenschaft SelectedItems, über die Sie auf alle zurzeit selektierten Einträge zugreifen können. Dies macht die Auswertung gerade einer Mehrfachauswahl-ListBox besonders einfach. Für Einfachauswahl-ListBox-Instanzen können Sie aber auch die Eigenschaften SelectedIndex, SelectedItem und SelectedValue zur Auswertung und zum Setzen der Selektion verwenden.
19
■ ■ ■
Im Programm füllen Sie eine ListBox wie bei jedem Listen-Steuerelement über die Items-Auflistung. In XAML können Sie die Einträge ebenso wie bei jedem ListenSteuerelement direkt als Inhalt angeben, wobei Sie natürlich wieder alle möglichen Objekte als Inhalt verwenden können. Ich verzichte hier allerdings auf ein entsprechendes Beispiel, weil ich diese Technik zum einen bereits gezeigt habe und zum anderen in »normalen« Anwendungen in einer ListBox meistens einfache Strings ausgegeben werden.
17
18
20 Füllen können Sie wieder mit beliebigen Objekten oder über ListBoxItem-Instanzen
21
22
Wenn Sie einfache Strings in der Liste ausgeben wollen, können Sie dazu Instanzen der Klasse ListBoxItem verwenden, die die Möglichkeit bieten, dass Sie über die Eigenschaft IsSelected bestimmen (und auslesen) können, ob die einzelnen Einträge selektiert sind. ListBoxItem besitzt zudem den Vorteil, dass Sie über die Eigenschaft IsEnabled angeben können, ob ein Element aktiviert ist. Deaktivierte Elemente wer-
23
845
WPF-Anwendungen
den zwar angezeigt, können aber vom Anwender nicht ausgewählt werden. Der Inhalt eines ListBoxItem-Objekts kann wieder ein beliebiger sein, sodass Sie auch mit diesen komplexe Designs erzeugen können. Das folgende Beispiel zeigt eine einfache ListBox mit einer Mehrfachauswahl, bei der die ersten zwei Einträge bereits vorselektiert sind und der dritte deaktiviert ist. Listing 13.40: Mehrfachauswahl-ListBox mit vorselektierten und deaktivierten String-Einträgen Eintrag 1 Eintrag 2 Eintrag 3 Eintrag 4 Eintrag 5
Abbildung 13.20 zeigt das Ergebnis auf einem Fenster, das so verkleinert wurde, dass die ListBox ihre vertikale Scrollbar anzeigt. Abbildung 13.20: Eine Mehrfachauswahl-ListBox mit einfachen Strings
Listing 13.41 zeigt, wie Sie die Auswahl auswerten. Das Beispiel schreibt die aktuell ausgewählten Einträge in ein TextBlock-Element mit Namen tbResult: Listing 13.41: Auswerten der in einer Mehrfachauswahl-ListBox aktuell ausgewählten Einträge string result = null; if (this.lstDemo.SelectedItems.Count > 0) { foreach (ListBoxItem item in this.lstDemo.SelectedItems) { if (result != null) { result += Environment.NewLine; } result += item.Content.ToString(); } this.tbResult.Text = result; } else { this.tbResult.Text = "Zurzeit ist kein Eintrag ausgewählt"; }
Das Beispiel zeigt, dass Sie die Objekte, die Sie aus SelectedItems (oder aus Items) auslesen, vor der Weiterverarbeitung in den Typ umwandeln müssen, der eigentlich in der Liste verwaltet wird. Das Beispiel gibt deswegen explizit den ListBoxItem-Typ in der foreach-Schleife an und greift über dessen Content-Eigenschaft auf den Inhalt zu (der ja in unserem Fall ein einfacher String ist).
846
Die WPF-Steuerelemente
Für weitere Features der ListBox habe ich hier leider keinen Platz mehr. Dazu gehören u. a.: –
Die Eigenschaften RemovedItems und AddedItems des Ereignisargument-Objekts des SelectionChanged-Ereignisses, über das Sie die aus der Auswahl entfernten und die zur Auswahl hinzugefügten Elemente auslesen können,
–
das komplette Umdefinieren der Oberfläche über Vorlagen (das ja bei allen WPF-Elementen möglich ist).
–
die ItemsPanel-Eigenschaft, die auf eine einfachere Weise als mit einer Vorlage die Umgestaltung der Darstellung der Einträge erlaubt (ein Beispiel finden Sie allerdings im Abschnitt »WrapPanel« auf Seite 860),
–
die ItemTemplate-Eigenschaft, die es ermöglicht, eine neue (Daten-)Vorlage für die einzelnen Einträge zu definieren (diese behandle ich allerdings in Kapitel 14 bei der Datenbindung)
–
INFO
12
13 14
und die Sortierung der Einträge über die SortDescriptions-Eigenschaft der Items-Auflistung.
15
ComboBox Eine ComboBox ist im Prinzip eine ListBox, bei Bedarf kombiniert mit einer TextBox. Die ListBox ist zunächst (per Voreinstellung) unsichtbar. Die ComboBox zeigt auf der rechten Seite einen Schalter an, über den der Anwender die ListBox öffnen kann. In der Standardeinstellung zeigt die ComboBox den ausgewählten Eintrag lediglich an (Abbildung 13.21) und ermöglicht die Auswahl aus der Liste.
Alternativ können Sie die ComboBox aber auch mit einer TextBox ausstatten (Abbildung 13.22).
Eine ComboBox stellt eine Liste raumsparend dar, bei Bedarf mit Texteingabe
16
17 Abbildung 13.21: Default-ComboBox in der zu- und der aufgeklappten Version. Die zugeklappte ComboBox besitzt zurzeit keine Auswahl.
Abbildung 13.22: Aufgeklappte ComboBox mit einer TextBox
18
19
20
21 Die Ansicht der ComboBox wird über die nicht besonders sprechend benannten Eigenschaften IsEditable und IsReadOnly bestimmt. IsEditable sagt aus, ob die TextBox angezeigt wird, IsReadOnly bestimmt, ob die TextBox (nicht die gesamte ComboBox!) schreibgeschützt ist.
IsEditable und IsReadOnly bestimmen die Ansicht
So können Sie z. B. in XAML eine ComboBox mit drei Einträgen erzeugen, die in der TextBox eine Eingabe erlaubt:
22
23
847
WPF-Anwendungen
Listing 13.42: ComboBox mit TextBox, die eine Eingabe erlaubt Eintrag 1 Eintrag 2 Eintrag 3
IsReadOnly ist in diesem Beispiel nicht gesetzt, weil die Voreinstellung (false) in Ordnung ist. Die Liste wird im Beispiel über ComboBoxItem-Objekte gefüllt, die zum einen in XAML das Anfügen einfacher Strings ermöglichen und zum anderen über die IsEnabled-Eigenschaft auch, dass Elemente deaktiviert werden (ähnlich den ListBoxItem-Objekten bei der ListBox).
Wird eine TextBox angezeigt und ist diese nicht schreibgeschützt, führt per Voreinstellung eine Eingabe in der TextBox dazu, dass der erste passende Eintrag in der Liste selektiert und der Text automatisch so zum Listeneintrag passend erweitert wird, dass eine weitere Eingabe möglich ist (Abbildung 13.23). Abbildung 13.23: Automatische Selektion der ComboBox
Dieses Verhalten können Sie über die Eigenschaft IsTextSearchEnabled auch abschalten. Für die Auswertung einer Änderung der Auswahl in der Liste steht wie bei der ListBox das Ereignis SelectionChanged zur Verfügung.
HALT
In SelectionChanged können Sie zwar den Index der aktuellen Selektion ermitteln, aber nicht den aktuellen Text. Dieser wird erst nach dem Aufruf von SelectionChanged aktualisiert (was ein wenig eigenartig ist). Auf eine Änderung des Textes in einer ComboBox mit editierbarer TextBox können Sie dummerweise nicht reagieren, da die ComboBox dazu (noch) kein Ereignis zur Verfügung stellt.
DISC
Eine Lösung dieses Problems habe ich für mein Codebook entwickelt. Sie finden diese in Form des Projekts »Das TextChanged-Ereignis bei der ComboBox abfangen« neben den Beispielen zu diesem Abschnitt auf der Buch-DVD. Die aktuelle Selektion werten Sie, wie bei einem Listen-Steuerelement üblich, über die Eigenschaften SelectedIndex, SelectedItem und SelectedValue aus. Den separat dargestellten Text können Sie zudem über die Eigenschaft Text separat auswerten. Lässt die ComboBox eine Eingabe zu, kann das natürlich ein vollkommen anderer Text sein, als in den Einträgen vorkommt. Listing 13.43 zeigt eine Auswertung, bei der das Ergebnis in ein TextBlock-Element (mit Namen tbResult) geschrieben wird.
848
Die WPF-Steuerelemente
Listing 13.43: Auswertung einer ComboBox, die gleichzeitig eine Texteingabe und eine Listenauswahl erlaubt // Auswerten des Textes in der TextBox this.tbResult.Text = "Text: " + this.cboDemo.Text + Environment.NewLine; // Auswerten der Auswahl in der Liste if (this.cboDemo.SelectedIndex > -1) { this.tbResult.Text += "Eintrag: " + this.cboDemo.SelectedIndex; } else { this.tbResult.Text += "Eintrag: Kein Eintrag ausgewählt"; }
Für weitere Features bleibt leider einmal wieder kein Platz. Dazu gehören die folgenden: –
ComboBox-Elemente mit komplexem Inhalt,
–
die Eigenschaften RemovedItems und AddedItems des Ereignisargument-Objekts des SelectionChanged-Ereignisses, über das Sie das aus der Auswahl entfernte und das zur Auswahl hinzugefügte Element auslesen können,
–
ComboBoxen, die komplexe Objekte beinhalten, aber trotzdem eine TextBox darstellen (deren Darstellung in der TextBox über die angefügten Eigenschaften TextSearch.TextPath und TextSearch.Text definiert wird),
–
die Eigenschaft StaysOpenOnEdit, die bestimmt, ob die Liste geöffnet bleibt, nachdem der Benutzer diese geöffnet hat,
–
die Eigenschaft MaxDropDownHeight, die die maximale Höhe der Drop-DownListe bestimmt,
–
die Ereignisse DropDownOpened und DropDownClosed, die aufgerufen werden, wenn die Drop-Down-Liste geöffnet bzw. geschlossen wird.
13.4.7
12
13 14
INFO
15
16
17
18
Bereichssteuerelemente
Die WPF-Bereichssteuerelemente sind von der Basisklasse RangeBase abgeleitet. RangeBase stellt die Eigenschaften Minimum und Maximum zur Verfügung, die den Bereich festlegen. Die Eigenschaft Value verwaltet den aktuellen Wert. SmallChange definiert bei Bereichssteuerelementen, die eine Änderung des Werts durch den Anwender zulassen, den Wert, um den Value für eine »kleine Änderung« erhöht oder vermindert wird. Bei einer ScrollBar wird SmallChange z. B. dann angewendet, wenn der Benutzer auf die Pfeil-Schalter am jeweiligen Ende klickt. LargeChange definiert den Wert einer »großen« Änderung. Bei der ScrollBar wird LargeChange dann angewendet, wenn der Benutzer auf den Bereich zwischen dem Verschiebebalken und dem Schalter am Ende klickt. Alle genannten Eigenschaften sind vom Typ double.
19 Bereichssteuerelemente sind von RangeBase abgeleitet
21
22
Das wichtigste Ereignis, das RangeBase zur Verfügung stellt, ist ValueChanged. Dieses Ereignis wird aufgerufen, wenn der in Value verwaltete Wert geändert wird. Die folgenden Klassen sind von RangeBase abgeleitet: ■
20
23
ProgressBar: Zeigt den in Value angegebenen Wert als Balken an und ist damit als Fortschrittanzeige vorgesehen.
849
WPF-Anwendungen
■ ■
Slider: Ermöglicht die Einstellung des Werts über einen Verschiebe-Balken (oder »Thumb«). ScrollBar: Ermöglicht die Einstellung des Werts über einen Verschiebe-Balken und Schalter am Ende der ScrollBar. Dieses Steuerelement wird in diesem Abschnitt nicht behandelt, weil der ScrollViewer zum Scrollen in den meisten Fällen ausreicht.
ProgressBar ProgressBar zeigt einen Fortschritt an
Die ProgressBar ist ein sehr einfaches Steuerelement. Sie zeigt per Voreinstellung lediglich einen Balken an, der den Wert von Value relativ zu den in Minimum und Maximum angegebenen Werten angibt. Minimum ist bei der ProgressBar per Voreinstellung auf 0 und Maximum auf 100 gesetzt. ProgressBar fügt den von RangeBase geerbten Eigenschaften lediglich zwei hinzu: ■
■
IsIndeterminate: Wenn Sie diese Eigenschaft auf true setzen, zeigt die ProgressBar nicht den aktuellen Wert von Value an, sondern eine allgemeine Animation, die einen laufenden Vorgang symbolisiert, dessen Ende nicht absehbar ist. Solche nicht informativen »Fortschritts«-Anzeigen sollten Sie nur dann verwenden, wenn Sie den Bereich oder den aktuellen Wert nicht ermitteln können, z. B. bei einem Download aus dem Internet. Orientation: Gibt die Ausrichtung des Balkens an. Per Voreinstellung läuft dieser horizontal (von links nach rechts). In der vertikalen Darstellung wird der Balken von unten nach oben gefüllt.
Abbildung 13.24 zeigt eine ProgressBar in der horizontalen Darstellung mit einem Wert um die 50%. Abbildung 13.24: Eine ProgressBar unter Vista
Slider Das Slider-Steuerelement erlaubt dem Benutzer die Einstellung eines Werts über einen Verschiebe-Balken. Die Eigenschaften Minimum und Maximum sind beim Slider per Voreinstellung auf 0 bzw. 10 gesetzt. Er besitzt ähnlich der ProgressBar auch eine Orientation-Eigenschaft, die die Ausrichtung bestimmt. Sie können das Aussehen und Verhalten einer Slider-Instanz über verschiedene Eigenschaften beeinflussen: ■ ■ ■
■
TickPlacement: Gibt an, ob und wie der Slider eine zusätzliche Leiste mit Strichen anzeigt, die einzelne Werte darstellen. TickFrequency: Gibt den Abstand der Striche auf der Werte-Leiste an. LargeChange: Definiert die Änderung des Werts für den Fall, dass der Anwender auf die Verschiebe-Linie des Slider klickt (SmallChange hat beim Slider scheinbar keine Bedeutung). IsSelectionRangeEnabled: Gibt an, ob der Slider einen Unterbereich darstellt. Die Eigenschaften SelectionStart und SelectionEnd geben dann den Anfang und das Ende des Unterbereichs an.
Über das Ereignis ValueChanged können Sie auf eine Änderung des Werts reagieren.
850
Die WPF-Steuerelemente
Das folgende Beispiel zeigt einen Slider, der alle Möglichkeiten (außer der Ausrichtung) nutzt: Listing 13.44: Slider mit Wertleiste und Unterbereich
12
13 Abbildung 13.25: Slider mit Wertleiste und Unterbereich
14
13.4.8 Text-Steuerelemente 15
Text-Steuerelemente ermöglichen die Anzeige und die Eingabe von Text (was auch sonst ☺). WPF enthält die folgenden Steuerelemente dieser Art: ■ ■ ■
■ ■
TextBlock: Gibt ein- oder mehrzeiligen Text aus, der bei Bedarf automatisch umbrochen wird. TextBox: Ermöglicht die Aus- und Eingabe eines ein- oder mehrzeiligen Textes. RichTextBox: Dieses Steuerelement kann formatierten (RTF-)Text darstellen und ermöglicht damit eine individuelle Formatierung einzelner Zeichen oder Absätze. PasswordBox: Eine Art TextBox, die zur Eingabe von Passwörtern vorgesehen ist. InkCanvas: Sehr interessantes Steuerelement, das die Eingabe von handgeschriebenem Text (über die Maus oder auf einem entsprechend ausgestatteten Rechner über einen Stift) ermöglicht.
16
17
18
In diesem Abschnitt behandle ich lediglich die Standard-Steuerelemente TextBlock, TextBox und PasswordBox.
19
TextBlock Ein TextBlock-Element ist ein Steuerelement, das lediglich Text darstellt (allerdings auch in einer formatierten Form). Der darzustellende Text wird prinzipiell in der Text-Eigenschaft verwaltet. Das Besondere am TextBlock im Vergleich zu einem Label ist, dass ein TextBlock auch mehrzeiligen Text darstellen kann. Außerdem ist ein TextBlock in der Lage, den dargestellten Text automatisch zu umbrechen, was über die TextWrapping-Eigenschaft gesteuert wird. Daneben kann ein TextBlock auch formatierten Text darstellen.
Ein TextBlock stellt Text mehrzeilig und bei Bedarf autom. umbrochen und formatiert dar
■ ■
21
22
Die Eigenschaft TextWrapping kann auf die folgenden Werte gesetzt werden: ■
20
NoWrap: Der Text wird nicht automatisch umbrochen (Voreinstellung). Wrap: Der Text wird automatisch umbrochen. Ist ein Wort zu lang für einen Umbruch, erfolgt der Umbruch mitten im Wort. WrapWithOverflow: Der Text wird automatisch umbrochen. Ist ein Wort zu lang für einen Umbruch, wird das Wort nicht umbrochen.
23
851
WPF-Anwendungen
So können Sie recht einfach einen mehrzeiligen Text mit einem automatischen Umbruch ausgeben: Listing 13.45: Ausgabe eines mehrzeiligen Textes über ein TextBlock-Element Das ist ein TextBlock, dessen Inhalt automatisch umbrochen wird.
Abbildung 13.26: Automatischer Umbruch in einem TextBlock-Steuerelement
Ein besonderes Feature der TextBlock-Klasse ist die Inlines-Eigenschaft. Auf diese kann ich hier nicht näher eingehen. Sie haben damit aber die Möglichkeit, Text auch formatiert auszugeben: Listing 13.46: Verwenden von »Inlines« zur Formatierung eines Textes in einem TextBlock this.demoTextBlock2.Inlines.Add(new Run("Das ist normaler Text\r\n")); this.demoTextBlock2.Inlines.Add(new Bold(new Run( "Das ist fetter Text\r\n"))); this.demoTextBlock2.Inlines.Add(new Italic(new Run( "Das ist kursiver Text\r\n"))); this.demoTextBlock2.Inlines.Add(new Bold(new Italic(new Run( "Das ist fett-kursiver Text\r\n"))));
Abbildung 13.27: Der formatierte Demo-TextBlock
TextBox TextBox ist zur Eingabe von Text vorgesehen
Eine TextBox erlaubt die Darstellung und Eingabe von Text. Die TextBox-Klasse stellt bereits die Möglichkeiten, Text in die Zwischenablage zu kopieren oder daraus einzufügen, die letzten Änderungen (über (STRG) + (Z)) rückgängig zu machen und die zuletzt rückgängig gemachten Änderungen (über (STRG) + (Y)) wiederherzustellen, zur Verfügung. Außerdem enthält die TextBox-Klasse ein Kontextmenü mit Befehlen zum Ausschneiden, Kopieren und Einfügen (leider nicht für die Rückgängig- und Wiederherstellen-Befehle, deren Tastenkombination nicht alle Anwender kennen).
TextBox unterstützt mehrzeiligen Text per Default nur über das Programm
Der Inhalt einer TextBox-Instanz wird in der Eigenschaft Text verwaltet. Mehrzeiligen Text können Sie per Voreinstellung nur über das Programm schreiben (indem Sie z. B. Environment.NewLine an die einzelnen Zeilen anhängen). Um auch dem Benutzer zu ermöglichen, Text mehrzeilig einzugeben, müssen Sie die Eigenschaft AcceptsReturn auf true einstellen. Dann behandelt die TextBox die (¢)-Taste, indem ein Zeilenumbruch eingefügt wird. Leider verhindert eine TextBox-Instanz damit leider auch, dass ein auf demselben WPF-Fenster angelegter Default-Schalter (mit IsDefault = true) mit (¢) betätigt
852
Die WPF-Steuerelemente
werden kann, wenn sie den Fokus besitzt. In früheren Versionen der TextBox war es für den Anwender auch ohne AcceptsReturn möglich, über (ª) + (¢) Zeilenumbrüche einzugeben. Das ist jetzt scheinbar (leider) nicht mehr möglich. Die Eigenschaft TextWrapping bestimmt wie beim TextBlock, ob und wie der dargestellte Text automatisch umbrochen wird. Zusätzlich dazu können Sie über die Eigenschaften HorizontalScrollBarVisibility und VerticalScrollBarVisibility bestimmen, ob und wann die TextBox eine horizontale bzw. vertikale Scrollbar anzeigt. Die möglichen Werte hier sind: ■ ■ ■ ■
TextWrapping bestimmt den Umbruch
12
Auto: Die Scrollbar wird bei Bedarf automatisch angezeigt. Visible: Die Scrollbar wird immer angezeigt, ist aber deaktiviert, wenn sie nicht benötigt wird. Hidden: Die Scrollbar wird nicht angezeigt, existiert aber auf der logischen Ebene. Ein Scrollen ist deswegen über die Cursortasten möglich. Disabled: Die Scrollbar wird nicht angezeigt und existiert auch nicht auf der logischen Ebene. Ein Scrollen ist deswegen nicht möglich.
13 14
Für die Praxis macht es Sinn, TextWrapping auf Wrap zu setzen und VerticalScrollBarVisibility auf Auto oder Visible (Abbildung 13.28). Wenn Sie keinen automatischen Umbruch wünschen, setzen Sie TextWrapping auf NoWrap und VerticalScrollBarVisibility und HorizontalScrollBarVisibility auf Auto oder Visible. Damit ermöglichen Sie dem Anwender, mehrzeiligen Text bei Bedarf in den sichtbaren Bereich zu scrollen.
15
16 Abbildung 13.28: TextBox mit TextWrapping = Wrap und VerticalScrollBarVisibility = Auto
Weitere wichtige Eigenschaften der TextBox sind: ■
■
18
IsReadOnly: Bestimmt, ob die TextBox schreibgeschützt ist. Schreibgeschützte TextBox-Instanzen besitzen gegenüber dem TextBlock den Vorteil, dass der Benutzer den enthaltenen Text in die Zwischenablage kopieren kann. AcceptsTab: Definiert, ob die TextBox die (ÿ)-Taste berücksichtigt. Wenn Sie hier true angeben, kann der Benutzer innerhalb des Textes Tabulatoren anlegen. In diesem Fall kann der Benutzer aber mit (ÿ) nicht mehr in das nächste Steuerelement wechseln.
Über das Ereignis TextChanged können Sie auf eine Änderung des Texts reagieren. Weitere Features der TextBox, wie eine automatische Rechtschreibprüfung über die Eigenschaft SpellCheck.IsEnabled (OK, das ist nicht so schwer, diese Eigenschaft zu setzen ☺), verschiedene Methoden zum Ermitteln von Informationen, das Selektieren von Teilen des Textes (SelectionStart, SelectionLength, Select-Methode) und Eigenschaften wie MaxLength (maximale Eingabelänge) können hier nicht behandelt werden.
17
19
20 TextChanged wird bei einer Änderung des Textes aufgerufen
21
22
PasswordBox Das PasswordBox-Steuerelement ist eine Art TextBox, die zur Eingabe eines Passworts vorgesehen ist. Sie stellt an Stelle der eingegebenen Zeichen das Zeichen dar, das in der PasswordChar-Eigenschaft eingestellt ist.
PasswordBox dient der Eingabe von Passwörtern
853
23
WPF-Anwendungen
Abbildung 13.29: Eine PasswortTextBox
PasswordBox unterstützt bei den Zwischenablage-Operationen aus Sicherheitsgründen nur das Einfügen, erlaubt keine mehrzeiligen Texte und auch keine erweiterten Features wie eine automatische Rechtschreibprüfung.
Das Passwort wird in der Password-Eigenschaft verwaltet. Diese Eigenschaft ist vom Typ System.Security.SecureString. SecureString verwaltet einen String innerhalb einer Anwendung verschlüsselt, sodass dieser von außen nicht ausgelesen werden kann. SecureString-Instanzen werden außerdem bei Nichtbenutzung automatisch unabhängig vom Garbage Collector zerstört, sodass keine Überreste eines eingegebenen Passworts im Speicher verbleiben können. Im Programm können Sie Password allerdings wie eine normale String-Eigenschaft setzen und abfragen. Dabei sollten Sie in der Praxis aber auch vorsichtig sein:
INFO
Wenn Sie Passwörter selbst in String-Instanzen verwalten, können diese von einem Hacker ausgelesen werden. Sie sollten Passwörter grundsätzlich nur über einen Hashcode (z. B. eine MD5-Hashcode) verwalten und den Hashcode des in der PasswordBox eingegebenen Passworts zum Vergleich oder zum Speichern einsetzen. Hashcodes werden in Kapitel 23 behandelt.
13.4.9 Menüs, Symbolleisten und Statuszeilen Über die WPF-Steuerelemente Menu, ContextMenu, ToolBarPanel und StatusBar können Sie Menüs, Kontextmenüs, Symbolleisten und Statuszeilen auf einem Fenster (oder einer Seite) anlegen. Dieser Abschnitt zeigt (kurz) die wesentlichen Dinge dazu.
Normale Menüs Menu definiert mit MenuItemElementen ein Menü
Normale Menüs erzeugen Sie über das Menu-Steuerelement. Diesem ordnen Sie MenuItem-Elemente unter, die beliebig geschachtelt werden können, um eine hierarchische Menüstruktur aufzubauen. Die Eigenschaft Header der MenuItem-Elemente bestimmt deren Beschriftung. Über einem Unterstrich vor einem Zeichen in der Beschriftung können Sie eine Schnellzugriffstaste definieren. Das Click-Ereignis eines MenuItem-Elements weisen Sie zu, um eine Betätigung des Menüeintrags auszuwerten. Alternativ können Sie auch Befehle verwenden, die Sie in die CommandEigenschaft schreiben. Befehle werden in Kapitel 14 behandelt. Über Separator-Elemente können Sie Separatoren einbauen, die im Menü als Striche angezeigt werden. Menu arbeitet im Vergleich zu den Menüs von Windows.Forms etwas anders, da Menu in WPF ein ganz normales Listen-Steuerelement ist. Der Unterschied wird schnell sichtbar, da Sie das Steuerelement frei auf einem Formular platzieren können und dieses nicht automatisch Vorrang vor anderen Steuerelementen besitzt. Prinzipiell können Sie damit auch mehrere Menüs auf einem Fenster unterbringen, z. B. um ein untergeordnetes StackPanel mit einem separaten Menü auszustatten (was aber wohl für den Anwender eher verwirrend ist).
Die Eigenschaft IsMainMenu eines Menüs bestimmt deswegen, ob es sich um ein Hauptmenü handelt. Ist diese Eigenschaft true (was die Voreinstellung ist), kann der Benutzer das Menü über (ALT) oder (F10) öffnen.
854
Die WPF-Steuerelemente
Listing 13.47 zeigt die XAML-Definition eines typischen Hauptmenüs. Listing 13.47: Ein typisches Hauptmenü
12
13 14
15
Abbildung 13.30: Das Beispiel-Hauptmenü in Aktion
16
17
18
Zusätzlich zu den im Beispiel beschriebenen Möglichkeiten können Sie noch (u. a.) die folgenden Features einsetzen: ■ ■
■
19
Die Eigenschaft Icon der MenuItem-Elemente verwaltet ein Icon, das neben dem Text dargestellt wird. Die Eigenschaft IsCheckable der MenuItem-Elemente gibt an, ob die Menüeinträge umgeschaltet werden können. Ein eingeschalteter Menüeintrag zeigt per Voreinstellung ein Haken-Symbol an. Die Eigenschaft IsChecked bestimmt, ob der Menüeintrag eingeschaltet ist. Über die Ereignisse Checked und Unchecked können Sie auf das Ein- bzw. Ausschalten eines Menüeintrags reagieren. Über die Eigenschaft InputGestureText der MenuItem-Elemente können Sie neben dem Menüeintrag einen Text für eine Tastenkombination anzeigen (z. B. »STRG + M«. InputGestureText definiert aber nur den dargestellten Text und bewirkt keine automatische Zuweisung der Tastenkombination. Sie müssten die Tastenkombination selbst auswerten. Wenn Sie dies machen wollen, sind an Stelle von Ereignishandlern Befehle besser geeignet, die mit Tastenkombinationen assoziiert werden können. Ist ein Befehl über die Command-Eigenschaft mit einem Menüeintrag verknüpft, wird dessen Tastenkombination automatisch angezeigt. Befehle werden in Kapitel 14 behandelt.
20
21
22
23
855
WPF-Anwendungen
Kontextmenüs ContextMenu definiert ein Kontextmenü
Kontextmenüs werden über ContextMenu-Elemente definiert und gleichen im Aufbau einem Menü. Ein Kontextmenü kann jedem von FrameworkElement abgeleiteten Steuerelement über die ContextMenu-Eigenschaft zugeordnet werden. Listing 13.48 zeigt, wie Sie einer ListBox ein Kontextmenü zuweisen. Listing 13.48: ListBox mit Kontextmenü Eintrag 1 Eintrag 2 Eintrag 3
Listing 13.49 zeigt der Vollständigkeit halber die Ereignishandler. Das Beispiel verdeutlicht auch, wie Sie prinzipiell mit der Zwischenablage umgehen. Listing 13.49: Die Ereignishandler des Beispiel-Kontextmenüs private void mnuListDelete_Click(object sender, RoutedEventArgs e) { if (this.lstDemo.SelectedIndex > -1) { // Eintrag löschen this.lstDemo.Items.RemoveAt(this.lstDemo.SelectedIndex); } else { MessageBox.Show("Kein Eintrag selektiert"); } } private void mnuListCopy_Click(object sender, RoutedEventArgs e) { if (this.lstDemo.SelectedIndex > -1) { // Eintrag in die Zwischenablage kopieren string itemText = ((ListBoxItem)this.lstDemo.SelectedItem).Content.ToString(); Clipboard.SetText(itemText); } else { MessageBox.Show("Kein Eintrag selektiert"); } } private void mnuListPaste_Click(object sender, RoutedEventArgs e) { // Eintrag aus der Zwischenablage hinzufügen if (Clipboard.ContainsText())
856
Die WPF-Steuerelemente
{ this.lstDemo.Items.Add(Clipboard.GetText()); } else { MessageBox.Show("Die Zwischenablage enthält keinen Text"); } }
12
Die Klasse ContextMenu besitzt noch weitere Features, die hier nicht weiter besprochen werden: ■ ■ ■
Über die Eigenschaft Placement können Sie die Position des Menüs bestimmen. HorizontalOffset und VerticalOffset bestimmen den horizontalen und vertikalen Offset bezogen auf die aktuelle Mausposition. Die angefügten Eigenschaften der Klasse ContextMenuService können WPF-Elementen mit Kontextmenü angefügt werden, um das Verhalten des Kontextmenüs vom übergeordneten Steuerelement aus zu kontrollieren.
13 14
Symbolleisten Symbolleisten werden über das ToolBar-Steuerelement definiert. Diesem Steuerelement können Sie (wie bei allen WPF-Steuerelementen) beliebige Elemente unterordnen, was die ToolBar extrem flexibel macht. Das folgende Beispiel zeigt eine typische Anwendung:
ToolBar definiert eine Symbolleiste
15
16 Listing 13.50: ToolBar mit zwei Schaltern (mit Bild) und einer ComboBox Zoom 10% 50% 100% 200%
17
18
19
20
21
Gerade bei Symbolleisten sind Tooltipps sehr wichtig, weswegen das Beispiel diese über die ToolTip-Eigenschaft der in der ToolBar enthaltenen Steuerelemente zuweist.
22 Abbildung 13.31: Die Beispiel-ToolBar
23
Wie Sie in Abbildung 13.31 sehen, werden WPF-Steuerelemente in einer ToolBar anders dargestellt als normalerweise. Auch der Separator wird nicht wie in einem Menü horizontal, sondern vertikal dargestellt.
857
WPF-Anwendungen
Weitere Features der ToolBar sind (u. a.): ■ ■
Die Möglichkeit, mehrere ToolBar-Instanzen in einem ToolBarTray anzuordnen, wie es in den meisten modernen Windows-Anwendungen üblich ist, die Möglichkeit, den Überlauf-Modus der Einträge über die angefügte Eigenschaft OverflowMode zu bestimmen. Mit Überlauf ist gemeint. dass die ToolBar automatisch am rechten Rand einen Überlauf-Bereich anzeigt, wenn nicht alle Elemente in den sichtbaren Bereich passen.
Statuszeilen Statuszeilen werden über StatusBarInstanzen dargestellt
Statuszeilen werden über das StatusBar-Steuerelement dargestellt. Dieses verhält sich prinzipiell wie eine ToolBar, mit dem Unterschied, dass eine StatusBar anders angezeigt wird und (logischerweise) nicht auf einem ToolBarTray angeordnet werden kann. Außerdem können Sie einem StatusBar-Element StatusBarItem-Elemente unterordnen, um einfachen Text anzuzeigen: Listing 13.51: Eine Demo-StatusBar Info ... Demo-Text: Copyright 2008 Addison Wesley
Abbildung 13.32: Die Demo-StatusBar
TIPP
Wenn Sie erreichen wollen, dass die StatusBar ihren Inhalt proportional zu der Größe des Fensters darstellt (z. B. drei Bereiche mit jeweils 1/3 des verfügbaren Platzes), können Sie dazu ein Grid-Steuerelement verwenden, das Sie so definieren, dass dieses entsprechend viele Spalten angezeigt. Das Grid-Steuerelement wird grundlegend ab Seite 861 behandelt.
13.4.10 Layout-Steuerelemente WPF enthält eine Vielzahl an Layout-Containern
Das Layout ist unter WPF ein vielfältiges Thema, das Ihnen eine Menge an Möglichkeiten bietet. So können Sie wie in den vorhergehenden Beispielen des Buchs alle Steuerelemente auf einem Grid mehr oder weniger absolut positionieren und links, oben, rechts und/oder unten »anheften« (was ja in Wirklichkeit über die Margin- und Alignment-Eigenschaften der Steuerelemente geschieht). Sie können aber auch andere Layout-Steuerelemente wie z. B. das StackPanel verwenden, das die enthaltenen Elemente in einem Fluss darstellt. Dieser Abschnitt bietet nun eine Übersicht über diese Layout-Steuerelemente. Eine vertiefte Behandlung der vielen Möglichkeiten einiger Layout-Container würde den Rahmen dieses Buchs sprengen (der sowieso schon zu platzen droht …). Layout-Steuerelemente sind auch deswegen interessant, weil sie als Unterelement eines anderen WPF-Elements verwendet werden können. So können Sie z. B. in einer ListBox über ein WrapPanel erreichen, dass die Liste ihren Inhalt in mehreren Spalten darstellt.
858
Die WPF-Steuerelemente
Dieser Abschnitt behandelt deswegen die wichtigen Layout-Steuerelemente Canvas, StackPanel, WrapPanel, Grid und ScrollViewer, aufgrund der Mächtigkeit einiger dieser Steuerelemente aber nur grundlegend. Die weiteren Layout-Container DockPanel, TabPanel, ToolBarOverflowPanel, ToolBarTray und UniformGrid werden nicht behandelt.
Canvas Canvas2 ist ein einfaches Layout-Steuerelement, das über seine angefügten Eigenschaften Left, Top, Right und Bottom eine absolute Positionierung der enthaltenen WPFElemente erlaubt. Über ein Setzen dieser Eigenschaften (in C# auf einen Wert ungleich Double.NaN) können Sie Elemente auf eine eher klassische Art positionieren.
Canvas erlaubt das absolute Positionieren
12
13
Listing 13.52 zeigt ein einfaches Beispiel. Listing 13.52: Einfaches Beispiel für die Verwendung des Canvas-Elements
14
Links ausgerichteter Button Rechts ausgerichteter Button Unten ausgerichteter Button
15
16
Abbildung 13.33: Das Canvas-Beispiel in Aktion
17
18
19 Ein automatisches Vergrößern enthaltener Steuerelemente (wie z. B. einer TextBox) bei einer Größenänderung des Fensters ist mit einem Canvas nicht möglich. Wenn Sie gleichzeitig Left und Right angeben, wird Right ignoriert. Geben Sie gleichzeitig Top und Bottom an, wird Bottom ignoriert.
20
Canvas eignet sich aber immer dann, wenn Sie Elemente präzise absolut positionieren wollen, und ist außerdem der Layout-Container, der die wenigsten System-Ressourcen verwendet.
21
22
2
23
Der Name »Canvas« (Leinwand) ist ein wenig verwirrend, da es sich ja nicht um eine Leinwand handelt, sondern um einen Container für Steuerelemente. Canvas macht aber Sinn in Anwendungen, die grafische Elemente wie Rechtecke und Kreise frei platziert ausgeben, und hat in diesen dann so etwas wie eine Leinwand-Funktion.
859
WPF-Anwendungen
StackPanel StackPanel ordnet seine KindElemente stapelweise an
Ein StackPanel stellt die enthaltenen Elemente in einem Stapel dar, der horizontal oder vertikal ausgerichtet sein kann. Die Ausrichtung wird über die OrientationEigenschaft bestimmt, die standardmäßig auf Vertical steht. Die Position der Elemente können Sie über deren Margin-Eigenschaft beeinflussen. Die Eigenschaften HorizontalAlignment und VerticalAlignment der Elemente auf dem StackPanel werden (natürlich) ignoriert.
Abbildung 13.34: WPF-Fenster mit einem StackPanel in der vertikalen Ausrichtung
Abbildung 13.35: WPF-Fenster mit einem Fenster mit einem StackPanel in der horizontalen Ausrichtung
WrapPanel WrapPanel ist ein StackPanel mit autom. Umbruch
Das Steuerelement WrapPanel arbeitet prinzipiell wie ein StackPanel. Es fügt aber mehrere Spalten oder Zeilen hinzu, wenn der Platz für den Stapel nicht ausreicht. Wie beim StackPanel bestimmen Sie die Ausrichtung über die Orientation-Eigenschaft. Die Eigenschaften ItemHeight und ItemWidth geben die Breite der Elemente im Panel an. Elemente, deren Größe diese Angabe überschreitet, werden automatisch abgeschnitten. Per Voreinstellung stehen ItemHeight und ItemWidth auf Double.NaN, was bewirkt, dass das WrapPanel die Größe der Spalten bzw. Zeilen nach dem größten Element berechnet. Abbildung 13.36 zeigt ein WrapPanel in der horizontalen Ausrichtung mit fünf Button-Elementen in verschiedenen Größen des enthaltenen WPF-Fensters.
Abbildung 13.36: Ein WrapPanel in der horizontalen Ausrichtung in verschiedenen Größen
860
Die WPF-Steuerelemente
Im letzten Fenster in Abbildung 13.36 reicht der Platz nicht aus, um alle Schalter darzustellen. Das WrapPanel besitzt aber keine Scrollbars. Wenn Sie ein Scrollen ermöglichen wollen, müssen Sie das WrapPanel in ein ScrollViewer-Element einbetten (siehe Seite 863).
INFO
Ein WrapPanel können Sie sehr gut in einem Listen-Steuerelement einsetzen, wenn Sie ein automatisches Umbrechen der Elemente erreichen wollen, ähnlich der Listen-Ansicht im Windows Explorer. Dazu ersetzen Sie die Vorlage des EintragsPanels der Listen-Steuerelements durch ein WrapPanel:
12
Listing 13.53: Eine ListBox mit WrapPanel für die Einträge
13
14
15
Der Trick dabei ist, dass Sie gleichzeitig die jeweilige Scrollbar der ListBox abschalten, wie dies im Beispiel gezeigt ist. Ansonsten führt die ScrollBar dazu, dass das WrapPanel seinen Inhalt nicht umbricht. Abbildung 13.37: Eine ListBox mit WrapPanel
16
17
18 Grid Das Grid-Steuerelement ist ein relativ mächtiger Layout-Container, der es erlaubt, die enthaltenen Elemente in Zeilen und Spalten zu organisieren. Per Voreinstellung verwendet das Grid nur eine einzige Zeile und Spalte, weswegen es sehr gut für einfache Layouts eingesetzt werden kann, bei denen die Position der Elemente über Margin, HorizontalAlignment und VerticalAlignment definiert wird. Visual Studio und Expression Blend nutzen deswegen das Grid als Default-Layout-Container für neue Fenster und Seiten.
Ein Grid erlaubt eine sehr flexible Gestaltung einer Tabelle mit HTMLähnlichen Möglichkeiten
Sie können ein Grid aber auch mit beliebig vielen Zeilen und Spalten ausstatten und diese vielfältig in der Höhe bzw. Breite beeinflussen. Da eine vollständige Behandlung des Grid den Rahmen dieses Buchs sprengen würde, geht dieser Abschnitt allerdings nur auf die Grundlagen ein.
19
20
21
22
Wenn Sie ein Grid mit mehreren Zeilen oder Spalten definieren wollen, fügen Sie der RowDefinitions-Auflistung eine entsprechende Anzahl RowDefinition-Instanzen hinzu und der ColumnDefinitions- Auflistung eine entsprechende Anzahl ColumnDefinition-Objekte. Diese Objekte können Sie vielfältig einstellen, worauf ich allerdings nicht näher eingehen kann. Über die Eigenschaften Height bzw. Width können Sie die Höhe der Zeile bzw. Breite der Spalte einstellen. Steht diese auf Double.NaN (Voreinstellung), wird sie vom Grid nach dem verfügbaren Platz automatisch
23
861
WPF-Anwendungen
berechnet. Damit (und mit den üblichen Margin- und Ausrichtungs-Einstellungen der enthaltenen Elemente) können Sie bereits sehr flexible Layouts gestalten, ohne dass Sie die Position der einzelnen Elemente explizit angeben müssen. Um Elemente in den einzelnen Zellen zu platzieren, geben Sie in diesen die angefügten Eigenschaften Row und Column an. Das folgende Beispiel erzeugt ein Grid zur Eingabe von Personendaten. Die oberen beiden und die letzte Zeile werden mit einer definierten Höhe angegeben, die dritte Zeile allerdings nicht. Bei einer Größenänderung des Grid wird die dritte Zeile deswegen automatisch in der Höhe angepasst: Listing 13.54: Grid zur Eingabe von Personendaten Vorname Nachname Bemerkungen OK Abbrechen
Abbildung 13.38 zeigt ein Fenster, das dieses Grid enthält. Abbildung 13.38: Das Fenster mit dem Beispiel-Grid
862
Die WPF-Steuerelemente
Wie gesagt, auf weitere Features des Grid kann ich leider nicht eingehen (dieses Kapitel hat auch ohne diese schon viel zu viele Seiten …). Zu den interessanten, hier nicht behandelten Features zählen: ■ ■ ■ ■ ■ ■
WPF-Elemente über mehrere Spalten oder Zeilen über die angefügten Eigenschaften RowSpan und ColumnSpan, Grid-Zellen mit unterschiedlicher Hintergrundfarbe (über den Trick, dass im Grid ein Rectangle-Objekt angelegt wird), die ShowGridLines-Eigenschaft, die dazu führt dass das Grid (per Voreinstellung unschöne) Linien zwischen den Zeilen und Spalten anzeigt, proportionale Zeilen- und Spalten-Größen über die Angabe von »*« als Wert von Heigth bzw. Width, die Möglichkeit, dem Benutzer über GridSplitter-Elemente eine interaktive Größenveränderung zu ermöglichen, und das Teilen derselben Zeilenhöhe und/oder Spaltenbreite über mehrere Zeilen bzw. Spalten über die Eigenschaft SharedSizeGroup der RowDefinitionund ColumnDefinition-Objekte.
12
13 14
15
Das Scrollen Wenn der Inhalt eines Layout-Containers nicht komplett dargestellt werden kann, wird dieser per Voreinstellung abgeschnitten. Wenn Sie ein Scrollen ermöglichen wollen (was in normalen Anwendungen Standard sein sollte), müssen Sie den Layout-Container in einem ScrollViewer-Element unterbringen. Die Sichtbarkeit der ScrollBars steuern Sie wie bei der TextBox (Seite 852) über die Eigenschaften HorizontalScrollBarVisibility und VerticalScrollBarVisibility:
ScrollViewer ermöglicht das Scrollen
16
17
Listing 13.55: Beispiel für die Anwendung eines ScrollViewer für das Scrollen in beide Richtungen Button 1 Button 2 Button 3 Button 4 Button 5
18
19
Abbildung 13.39: Das ScrollViewerBeispiel
20
21
22 HorizontalScrollBarVisibility und VerticalScrollBarVisibility können Sie auf die folgenden Werte setzen: ■ ■
Auto: Die Scrollbar wird bei Bedarf automatisch angezeigt. Visible: Die Scrollbar wird immer angezeigt, ist aber deaktiviert, wenn sie nicht benötigt wird.
23
863
WPF-Anwendungen
■
■
Hidden: Die Scrollbar wird nicht angezeigt, existiert aber auf der logischen Ebene. Ein Scrollen ist deswegen über die Cursortasten möglich. Weil das Scrollen möglich ist, erhält der Inhalt des ScrollViewer die gesamte angeforderte Breite bzw. Höhe. Disabled: Die Scrollbar wird nicht angezeigt und existiert auch nicht auf der logischen Ebene. Ein Scrollen ist deswegen nicht möglich. Der Inhalt des ScrollViewer erhält nur so viel Breite bzw. Höhe, wie zur Verfügung steht.
Der Unterschied zwischen Hidden und Disabled ist je nach Inhalt des ScrollViewer erheblich. Wird z. B. ein WrapPanel mit vertikaler Ausrichtung als Inhalt eingesetzt und VerticalScrollBarVisibility steht auf Hidden, erhält das WrapPanel so viel Höhe, wie es beansprucht, und die Elemente werden nicht umbrochen (Abbildung 13.40): Button 1 Button 2 Button 3 Button 4 Button 5
Abbildung 13.40: Fenster mit ScrollViewer mit VerticalScrollBarVisibility = Hidden, der ein WrapPanel mit Orientation = Vertical enthält
Bei der Einstellung Disabled erfolgt allerdings ein Umbruch (Abbildung 13.41 ). Abbildung 13.41: Dasselbe Fenster mit ScrollViewer mit VerticalScrollBarVisibility = Disabled
Beachten Sie, dass in Abbildung 13.41 die horizontale Scrollbar angezeigt wird, weil in dem Beispiel HorizontalScrollBarVisibility auf Auto gesetzt ist.
864
Einige abschließende Tipps
13.5
Einige abschließende Tipps
Das WPF-Anwendungs-Kapitel schließe ich mit ein paar für die Praxis wichtigen Tipps ab, die bisher keinen Platz gefunden haben.
13.5.1
Abfrage der Tastatur außerhalb von Tastaturereignissen
12
Außerhalb von Tastaturereignissen können Sie die Tastatur über die Keyboard-Klasse (aus dem Namensraum System.Windows.Input) abfragen. Die Methode IsKeyDown liefert eine Information darüber, ob die übergebene Taste betätigt ist. Über die Eigenschaft Modifiers können Sie abfragen, ob gerade eine der Modifiziertasten ((ALT), (CTRL), (ª), ()) betätigt ist.
13.5.2
13
Umgang mit der Zwischenablage
14
Die Zwischenablage können Sie über die Klasse System.Windows.Clipboard abfragen. Außerdem erlaubt Clipboard das Schreiben in die Zwischenablage. Die Methoden ContainsText, ContainsImage, ContainsFileDropList und ContainsAudio ermitteln, ob die Zwischenablage Text, ein Bild, eine Dateinamens-Liste oder Audio-Daten enthält. Über GetText, GetImage, GetFileDropList bzw. GetAudioStream können Sie diese auslesen. Die Methoden SetText, SetImage, SetFileDropList und SetAudio erlauben das Schreiben entsprechender Daten in die Zwischenablage.
15
16
So, mit den Tipps ist dieses Kapitel nun (endlich) zu Ende. Ich hoffe, Sie haben einen guten Überblick über die Gestaltung von Oberflächen mit WPF enthalten. Wenn Sie allerdings tiefer in WPF einsteigen wollen, kommen Sie wahrscheinlich um den Erwerb eines speziellen Buchs nicht herum. Im nächsten Kapitel, mit dem WPF dann abgeschlossen wird, beschreibe ich die Grundlagen der wichtigen speziellen WPFTechniken.
17
18
19
20
21
22
23
865
Inhalt
14
Wichtige WPF-Techniken 12
WPF bietet neben den grundlegenden Techniken einige erweiterte, die in vielen Fällen hilfreich zur Seite stehen. Bei der Vielfalt an Möglichkeiten ist es aber auch schwer, den Überblick zu behalten.
13
Dieses Kapitel setzt sich nun mit den wichtigsten der bisher noch nicht besprochenen WPF-Themen grundlegend (!) auseinander. Für eine vertiefte Besprechung der behandelten Themen fehlt in diesem Buch der Platz, genau wie für die Behandlung spezieller Themen wie Animationen, Transformationen, 2D- und 3D-Grafiken.
14 15
Dieses Kapitel behandelt die folgenden Themen grundlegend: ■ ■ ■ ■ ■ ■ ■ ■
WPF-Ressourcen (allgemeine Ressourcen werden in Kapitel 15 behandelt) Befehle Trigger Stile Vorlagen Dekoratoren Skins und Themen Datenbindung
16
17
18
Am Beispiel zeige ich: ■ ■
Transformationen Animationen
19
Die folgenden Themen werden in diesem Kapitel nicht behandelt:
■ ■ ■ ■
Die Integration von Audio und Video über die Klassen SoundPlayer, MediaPlayer und MediaElement 2D- und 3D-Grafiken Bildbearbeitung unter WPF über die Klassen BitmapSource, ColorConvertedBitmap, CroppedBitmap, TransformedBitmap etc. Die Integration von Sprache über die Klassen SpeechSynthesizer (Sprachausgabe) und SpeechRecognizer (Spracherkennung) Die Arbeit mit (XPS-)Dokumenten über die Klassen FlowDocument, FixedDocument, DocumentViewer u. a.
Was in diesem Kapitel sehr wichtig ist, ist die Priorität der Ermittlung des Werts einer Abhängigkeitseigenschaft. Ich habe diese bereits in Kapitel 12 beschrieben. Erst in Kapitel 14 wird aber wahrscheinlich klarer, was damit (bzw. mit den meisten der Begriffe) gemeint ist. Hier ist noch einmal die Priorität:
20
21
22
23
INFO
867
Index
■
Wichtige WPF-Techniken
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12.
Eigenschaftensystemkoersion (Erzwingung oder Veränderung des Werts über eine Callback-Methode in der Abhängigkeitseigenschaft) Animationen Lokaler Wert Auslesen von TemplatedParent-Vorlageneigenschaften für WPF-Elemente, die Teil der Vorlage von Steuerelementen sind Impliziter Stil (nur für die Style-Eigenschaft) Trigger in einem Stil in einer Ressource Trigger in einer Vorlage in einer Ressource Stil-Setter eines Stils in einer Ressource Trigger im Standardstil Stil-Setter im Standardstil Wert-Vererbung Standardwert der Abhängigkeitseigenschaft
Im Verlauf dieses Kapitels wird die Bedeutung der meisten dieser Punkte klarer. So sollten Sie z. B. immer beachten, dass der lokal gesetzte (in die Eigenschaft geschriebene) Wert Vorrang vor Stilen und Triggern hat, ein Stil-Trigger Vorrang vor einem Vorlagen-Trigger und ein Vorlagen-Trigger Vorrang vor einem Stil-Setter.
INFO
Und noch ein Hinweis: Die in diesem Kapitel behandelten Themen sind nicht trivial und haben mir bei der Recherche teilweise echte Kopfschmerzen bereitet. Obwohl die Beispiele im Buch natürlich alle funktionieren (hoffentlich …), soll nicht der Eindruck entstehen, alles wäre kinderleicht. Während der Arbeit an diesem Kapitel habe ich mich so manches Mal gefragt, warum das eine oder andere Feature einfach nicht funktioniert. Für viele dieser Fragen habe ich Antworten gefunden, die ich an den entsprechenden Stellen auch beschreibe. Einige Dinge sind aber leider im Dunklen geblieben. Auf diese Probleme weise ich ebenfalls hin. Falls ich damit neue Erfahrungen mache, erfahren Sie die natürlich sofort über den Blog zu diesem Buch.
14.1
Ressourcen in WPF
Ressourcen sind Daten, die im Programm verwendet werden, aber nicht zum eigentlichen Programmcode gehören. Das können z. B. Bilder sein, die in einer Anwendung angezeigt werden, oder Audio-Dateien oder auch einfache Dinge, wie SchriftartObjekte oder Strings. Ressourcen sind absolut nicht WPF-spezifisch, jede andere Art von Anwendungen benötigt ebenfalls Ressourcen. Eine sehr häufig angewendete Technik ist z. B., dass alle Strings, die in einer Anwendung ausgegeben werden, in separaten RessourcenAssemblys gespeichert werden. Über die .NET-Lokalisierungs-Techniken ist es sehr einfach, Ressourcen-Assemblys für mehrere Sprachen zu entwickeln und diese im Programm bei Bedarf zu verwenden. Auf diese eher allgemein verwendeten Ressourcen und auf die Lokalisierung gehe ich in Kapitel 15 ein. WPF besitzt daneben aber eigene Möglichkeiten, Ressourcen zu verwalten. Und diese laufen Ihnen bei der Arbeit mit WPF schon relativ schnell über den Weg. Deswegen beschreibe ich diese speziellen WPF-Ressourcen bereits in diesem Kapitel.
868
Ressourcen in WPF
14.1.1
Die unterschiedlichen Ressourcenarten
In einer WPF-Anwendung werden verschiedene Ressourcen unterschieden: ■ ■ ■ ■
Binäre Daten, die in externen Dateien verwaltet werden (z. B. Bilder, die während der Laufzeit eingelesen werden), binäre Daten, die in die Anwendungs-Assembly eingebettet sind, Wörterbuch-Ressourcen, und Text-Ressourcen.
WPF unterscheidet binäre, Wörterbuch- und TextRessourcen
12
Wörterbuch-Ressourcen (für diese Art Ressourcen gibt es keinen offiziellen Begriff) sind WPF-Objekte, die in einem ResourceDictionary verwaltet werden. Alle von FrameworkElement und FrameworkContentElement abgeleiteten Klassen stellen ein solches über die Resources-Eigenschaft zur Verfügung. Wörterbuch-Ressourcen werden u. a. zur Verwaltung von Stilen, Vorlagen und Daten-Providern verwendet, können aber auch für eigene Zwecke, zur Verwaltung beliebiger Objekte, eingesetzt werden.
13
14
Text-Ressourcen sind für die Lokalisierung von Anwendungen wichtig. Auf diese Art von Ressourcen gehe ich in Kapitel 15 ein.
14.1.2
Binäre Ressourcen
Binäre Ressourcen können in drei verschiedenen Formen verwaltet werden: ■ ■ ■
Eingebettet in einer Assembly, als separate Datei, die aber beim Kompilieren bekannt ist und als separate Datei, die beim Kompilieren nicht bekannt ist.
15 Binäre Ressourcen können eingebettet sein oder in separaten Dateien vorliegen
Die erste Form macht Sinn, wenn die Daten sich in der Laufzeit der Anwendung nicht mehr ändern. Dabei kann es sich z. B. um Bilder handeln, die in der Anwendung angezeigt werden, oder um Audio-Dateien.
17
Separate Dateien machen dann Sinn, wenn Sie die Assembly schlank halten wollen oder/und die Möglichkeit haben wollen, die Dateien später relativ einfach auszutauschen, ohne die Anwendung neu kompilieren zu müssen. Das kann schon dann sinnvoll sein, wenn die Anwendung ein Logo anzeigt und Sie die Vermutung haben, dass dieses sich in der nächsten Zeit ändern könnte.
18
19
Alle diese Formen können Sie sehr einfach über Visual Studio in ein Projekt integrieren. Fügen Sie dem Projekt dazu die binäre Datei hinzu, die die Ressource darstellt, wählen Sie diese ggf. im Projektmappen-Explorer aus und stellen Sie im Eigenschaftenfenster die Eigenschaften BUILDVORGANG und IN AUSGABEVERZEICHNIS KOPIEREN ein.
20
Die Einstellung BUILDVORGANG steuert das allgemeine Vorgehen des C#-Compilers für die in ein Projekt integrierten Dateien. Für Ressourcen unter WPF sind ausschließlich die folgenden Optionen interessant: ■
■
16
21
INHALT: Steht für Dateien, die ein »Inhalt« der Anwendung sind, aber nicht in die Anwendungs-Assembly eingebettet werden. Diese Option führt dazu, dass die Datei dem Laufzeitsystem bekannt ist. Wenn Sie die Datei z. B. in den Anwendungsordner kopieren (lassen), können Sie über einen URI mit einer relativen Angabe darauf zugreifen. Dies ist nicht möglich, wenn die Build-Aktion auf KEINE (oder einen der anderen Werte) steht. Außerdem sorgt die Build-Aktion INHALT dafür, dass die Datei mit in eine Installationsanwendung integriert wird, zumindest wenn Sie diese über Visual Studio für den Windows Installer erzeugen. Bei dem Buildvorgang KEINE wäre dies nicht der Fall. RESOURCE: Die Datei wird in die Assembly eingebettet.
22
23
869
Wichtige WPF-Techniken
HALT
Die Einstellungen KEINE und EINGEBETTETE RESSOURCE sollten Sie für Ressourcen nicht verwenden (genau wie die anderen noch möglichen Einstellungen). Die BuildAktion KEINE steht für Dateien, die in das Projekt integriert sind, aber mit dem Programm nichts zu tun haben. Das kann z. B. ein Word-Dokument sein, in dem Sie die Programmierung dokumentieren. Die Build-Aktion EINGEBETTETE RESSOURCE ist die Vor-WPF-Art, Ressourcen in eine Anwendung einzubetten. Wenn Sie Ressourcen auf diese Weise einbetten, ist das Auslesen sehr schwierig und nicht direkt in XAML möglich. Diese Option verwendet Visual Studio für die allgemeine Möglichkeit, Ressourcen einzubinden, die ich in Kapitel 15 beschreibe. Nur damit Sie wissen, was ich meine: In den Eigenschaften eines Projekts finden Sie das Register RESSOURCEN, über das Sie beliebige Ressourcen in eine Anwendung einbetten können. Visual Studio erzeugt für solche Ressourcen Einträge in der Datei Properties\Resources.resx und Proxy-Eigenschaften in der Klasse Resources in der Datei Settings.Designer.cs. Über diese Klasse können Sie diese allgemeinen Ressourcen im Programm recht einfach auslesen. In Kapitel 15 erfahren Sie mehr darüber.
Sie können Ressourcen automatisch in das Ausgabeverzeichnis kopieren lassen
In der Einstellung IN AUSGABEVERZEICHNIS KOPIEREN können Sie für Ressourcen, deren BUILDVORGANG auf KEINE oder INHALT steht, bestimmen, ob diese beim Kompilieren in das Ausgabeverzeichnis kopiert werden (für eingebettete Ressourcen macht ein Kopieren nicht wirklich Sinn). Wenn Sie binäre Ressourcen in externen Dateien verwalten wollen und diese im Anwendungsordner (oder einem Unterordner) erwarten, sollten Sie diese Option auf IMMER KOPIEREN oder KOPIEREN WENN NEUER stellen (je nach Bedarf). Abbildung 14.1 zeigen meine entsprechenden Einstellungen für das Beispielprojekt dieses Abschnitts. Um die Ressourcen-Dateien zusammenzuhalten habe ich in Visual Studio für das Projekt den Unterordner Images angelegt und einige Bilddateien dort hineinkopiert. Die Eigenschaft BUILDVORGANG der Datei DontPanic.gif habe ich auf INHALT gesetzt, die Eigenschaft IN AUSGABEVERZEICHNIS KOPIEREN auf IMMER KOPIEREN.
Abbildung 14.1: Das Beispielprojekt (mit einigen zusätzlichen Dateien im Ordner Images)
870
Ressourcen in WPF
Visual Studio erzeugt beim Kompilieren automatisch den Unterordner Images im Ausgabeordner und kopiert die Bilddatei(en), deren Eigenschaft IN AUSGABEVERZEICHNIS KOPIEREN auf IMMER KOPIEREN oder KOPIEREN WENN NEUER steht, dort hinein.
14.1.3
Ressourcenangaben: Normale URIs und Paket-URIs
Wenn Sie in einer WPF-Anwendung in XAML oder im Programm auf binäre Ressourcen zugreifen wollen, verwenden Sie dazu einen URI (Uniform Resource Identifier) in Form einer Instanz der Klasse System.Uri.
Ein URI lokalisiert eine Ressource, die nicht unbedingt auch physikalisch vorhanden sein muss. Beispiele für URIs sind Internetadressen (die dann die spezialisierte Form URL = Uniform Resource Locator sind) oder E-Mail-Adressen. Wenn zwar eine Internetadresse (ein URL) eine physikalisch vorhandene Ressource lokalisiert, steht eine E-Mail-Adresse nicht für eine physikalisch vorhandene Ressource. Dieser feine Unterschied ist allerdings bei der Arbeit mit WPF-URIs nicht allzu wichtig.
Ressourcen werden in WPF über einen URI angegeben
13 EXKURS
14 15
Die Klasse Uri Die Klasse Uri besitzt einige Konstruktoren, über die Sie einen URI erzeugen können. Im Wesentlichen geben Sie dabei einen URI-String und einen eventuellen URIArt an. Die beiden wichtigen Konstruktoren sind folgendermaßen deklariert: ■ ■
12
Uri kann mit klassischen URI-Strings arbeiten
16
public Uri(string uriString) public Uri(string uriString, UriKind uriKind)
17
Der URI-String kann prinzipiell alle für einen URI standardmäßig definierten Formate beinhalten. Beginnt ein URI z. B. mit http://, handelt es sich um einen URI, der eine Ressource über das HTTP-Protokoll lokalisiert. file:// steht für einen Datei-URI, ftp:// für das FTP-Protokoll.
18
Da Sie URIs in Eigenschaften, die einen solchen erwarten, auch in XAML als URIString angeben können, ist es z. B. möglich, eine Bilddatei aus dem Internet zu laden und in einem Image-Steuerelemente anzuzeigen: Listing 14.1:
19
Laden einer Ressource aus dem Internet
Obwohl es möglich ist, ist ein Laden von Ressourcen aus dem Internet nicht unbedingt zu empfehlen. Besonders wenn Sie diese direkt in XAML verwenden, kann es zu Problemen kommen, wenn die Ladezeit der Ressource sehr groß ist oder die Ressource nicht geladen werden kann. Das Problem beim Einbinden von Ressourcen in XAML ist nämlich, dass diese synchron geladen werden. Das merken Sie bereits, wenn Sie in Visual Studio die Quelle für ein Image-Steuerelement angeben und der Designer beim Tippen immer wieder eine kurze Pause macht (weil er versucht den URI aufzulösen). Ressourcen, die aus dem Internet stammen, sollten Sie besser im Programmcode einlesen, wobei Sie über Threads (Kapitel 20) auch asynchron programmieren können.
20
INFO
21
22
23
Bei der Auflösung des URI-Strings in eine Uri-Instanz ist natürlich wieder ein Konverter am Werk. Beim Image-Steuerelement geschieht dies allerdings über einen ImageSourceConverter, der den String in ein Bild »konvertiert«.
871
Wichtige WPF-Techniken
URIs im Programm verwenden Wenn Sie eine Ressource im Programm einlesen wollen, erzeugen Sie eine Instanz der Uri-Klasse, übergeben den URI-String am Konstruktor und verwenden diesen dann weiter. Für das Image-Steuerelement ist das allerdings etwas komplexer, da Source in Wirklichkeit vom Typ ImageSource ist, der die Basisklasse für alle BildKlassen ist. Sie müssen das Bild (über einen URI) laden und können diesen der Source-Eigenschaft zuweisen: Listing 14.2:
Dynamisches Laden eines Bildes aus dem Internet
// Das Bild laden BitmapImage image = new BitmapImage(); image.BeginInit(); image.UriSource = new Uri( "http://www.juergen-bayer.net/images/DontPanic.gif"); image.EndInit(); // Das Bild dem Image-Steuerelement zuweisen this.imgHitchhiker1.Source = image;
Die Aufrufe von BeginInit und EndInit sind bei der BitmapImage-Klasse übrigens notwendig, wenn UriSource dazu verwendet wird, eine Datei über einen URI zu laden.
Relative und absolute Angabe des URI Der zweite oben beschriebene Konstruktor der Uri-Klasse erwartet am Argument uriKind eine Angabe über die Art des URI. Die möglichen Werte der UriKind-Aufzählung sind: ■ ■ ■
RelativeOrAbsolute: Die Art des URI ist unbestimmt. Dies ist die Voreinstellung. Absolute: Es handelt sich um einen absolut angegebenen URI Relative: Es handelt sich um einen relativ angegebenen URI
Die letzte URI-Art wird häufig verwendet, um im Programm eine Datei relativ zu dem Ordner der Anwendung anzugeben. So könnten Sie die Bilddatei des Beispiels auch aus einem Ordner laden, der im Anwendungsordner gespeichert ist: Listing 14.3:
Laden einer Ressource relativ zum Anwendungsordner
BitmapImage image = new BitmapImage(); image.BeginInit(); image.UriSource = new Uri("Images/DontPanic.gif", UriKind.Relative); image.EndInit(); // Das Bild dem Image-Steuerelement zuweisen this.imgHitchhiker1.Source = image;
In diesem Beispiel wird die Datei in einem Unterordner Images unterhalb des Anwendungsordners erwartet. In einem URI werden solche Ordnerangaben durch normale Schrägstriche voneinander getrennt.
HALT
872
Die Angabe der URI-Art ist in diesem Beispiel essenziell: Ohne diese würden Sie den folgenden Fehler erhalten: »Ungültiger URI: Das URI-Format konnte nicht bestimmt werden«.
Ressourcen in WPF
URIs für relativ angegebene Dateien in XAML Wenn Sie in XAML auf Dateien zugreifen wollten, die relativ zum Anwendungsordner gespeichert sind, können Sie diese als einfachen URI angeben (ohne URI-Art): Listing 14.4:
Angabe einer Ressource relativ zum Anwendungsordner in XAML
Für die relative Angabe in der bisher verwendeten einfachen Form muss der Buildvorgang der Datei in Visual Studio auf INHALT stehen. Ansonsten ist die Datei dem Laufzeitsystem nicht bekannt und kann (über einen URI mit einer einfachen, relativen Angabe) nicht eingelesen werden (weder über XAML noch über das Programm). Dummerweise erhalten Sie dabei in den meisten Fällen noch nicht einmal einen Fehler (sofern Sie nicht im Programm auf das nicht existierende Bild zugreifen). Wenn Sie das Bild z. B. in einem Image-Steuerelement darstellen wollen, wird dieses einfach nicht angezeigt. Sie können zum Einlesen von Ressourcen, die zur Kompilierzeit nicht bekannt sind, aber auch zwei andere Wege gehen: Über eine absolute Referenzierung oder über einen speziellen »Paket«-URI.
12
13 HALT
14 15
16
Einlesen von Ressourcen, die zur Kompilierzeit nicht bekannt sind Wenn Sie Ressourcen einlesen wollen, die zur Kompilierzeit nicht bekannt sind, können Sie zwei Wege gehen: Der eine Weg ist, dass Sie die Ressource absolut referenzieren. In XAML macht das in der Regel keinen Sinn (auch wenn es funktioniert). Im Programm können Sie aber den Ordner der Anwendung ermitteln und darüber den absoluten Dateinamen zusammensetzen: Listing 14.5:
Lokale Ressourcen können Sie auch absolut referenzieren
18
Einlesen einer losen Ressource im Programm über den absoluten Dateinamen
// Den Anwendungsordner und den Dateinamen ermitteln string appPath = System.IO.Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location); string fileName = System.IO.Path.Combine(appPath, "Images\\Marvin.jpg");
19
// Das Bild über den absoluten Dateinamen einlesen BitmapImage image3 = new BitmapImage(); image3.BeginInit(); image3.UriSource = new Uri(fileName, UriKind.Absolute); image3.EndInit(); this.imgHitchhiker3.Source = image3;
Die Ermittlung des Ordners, in dem die Anwendung ausgeführt wird, ist ein kleiner Trick. Über die Methode GetEntryAssembly der Klasse Assembly ermittelt das Programm die Assembly, die die eigentliche Anwendungs-Assembly ist. Das ist pure Reflektion, die in Kapitel 22 behandelt wird. Die Eigenschaft Location eines AssemblyObjekts liefert den vollen Dateinamen. Deswegen wird über GetDirectoryName der Ordnerpfad extrahiert. Der Rest ist ein einfaches Zusammensetzen des Anwendungsordners mit dem Namen der Datei (die ja in dem Unterordner Images verwaltet wird).
17
20
21 TIPP
22
23
873
Wichtige WPF-Techniken
DISC
Zur Referenzierung lokaler Ressourcen können Sie auch einen Paket-URI verwenden
Auf der Buch-DVD finden Sie im Ordner Kompendium-Code-Snippets den CodeSchnipsel appPath.snippet. Dieser Code-Schnipsel trägt die Ermittlung des Anwendungsordners in Ihren Programmcode ein. Falls Sie die Kompendium-Code-Schnipsel nicht bereits in Visual Studio integriert haben, ist jetzt der richtige Moment: Kopieren Sie die Code-Schnipsel-Dateien des Ordners auf der DVD in den Ordner Visual Studio 2008\Code Snippets\Visual C#\My Code Snippets in Ihren Dokumente-Ordner (Eigene Dateien unter XP). Eine andere Möglichkeit, lose, zur Kompilierzeit unbekannte Dateien zu verwenden, ist ein spezielles URI-Format: das Paket-Schema. Auf dieses komme ich ab Seite 875 zurück. Hier ist der etwas seltsam aussehende URI für die relative Angabe einer losen, zur Kompilierzeit unbekannten Datei über einen Paket-URI: pack://siteOfOrigin:,,,/Images/Marvin.jpg
Einen solchen URI können Sie auch in XAML einsetzen. Wundern Sie sich aber nicht, wenn Visual Studio 2008 (erstes Release ohne Service Pack) mit Paket-URIs noch so seine Probleme hat. In der Laufzeit funktionieren diese auf jeden Fall.
Einlesen eingebetteter Ressourcen Eingebettete Ressourcen können ebenfalls mit einem einfachen URI angegeben werden
Wenn Sie eine Ressource in der Assembly eingebettet haben, können Sie diese ebenfalls über ihren relativen Namen angeben. Für den Fall, dass die Ressource in dem Projekt in einem Unterordner verwaltet wird (wie es im Beispielprojekt der Fall ist), erzeugt Visual Studio eine eingebettete Ressource, die in einem gleichnamigen logischen Unterordner verwaltet wird. Die Ressource Zaphod.jpg, die im Unterordner Images verwaltet wird (vgl. Abbildung 14.1, Seite 870) und deren BUILDVORGANG auf RESSOURCE steht, wird also innerhalb der Ressourcen der Anwendung in den logischen Unterordner Images gespeichert. Beim Abruf der Ressource müssen Sie diesen angeben:
Bei solchen Angaben im Programm ist natürlich unklar, ob es sich um eine eingebettete oder eine lokale Ressource handelt. WPF ist bei solchen Ressourcen (die in Wirklichkeit Ressourcen vom Typ pack://application:,,, sind) sehr flexibel: Zuerst wird in den Anwendungsressourcen nach einer Ressource in dem angegebenen (logischen) Ordner mit dem angegebenen Namen gesucht. Wird dort keine gefunden, wird relativ gesehen vom Ordner der Anwendung aus nach einer entsprechenden Datei gesucht. Das können Sie beweisen, indem Sie eine gleichnamige, aber andere Datei in den Anwendungsordner kopieren: Angezeigt wird die Datei, die in die Assembly eingebettet ist. Setzen Sie BUILDVORGANG auf INHALT, wird stattdessen die lokale Datei angezeigt.
Ressourcen in anderen Assemblys Ressourcen können in externen Assemblys gespeichert sein
In der Praxis werden Ressourcen häufig in separaten Assemblys verwaltet, die manchmal nichts anderes als Ressourcen speichern. In Visual Studio können Sie solche Assemblys sehr einfach über ein Klassenbibliothek-Projekt erzeugen, dem Sie lediglich eingebettete Ressourcen hinzufügen. Um solche Ressourcen referenzieren zu können, muss die Assembly natürlich geladen sein. In den meisten Fällen wird diese dazu referenziert (es ist jedoch auch möglich, eine Assembly dynamisch zu laden, was hier allerdings nicht besprochen wird).
874
Ressourcen in WPF
Über die in .NET eingebaute automatische Lokalisierung können Sie sogar erreichen, dass je nach eingestellter Kultur eine andere Assembly geladen wird. Dieses Thema wird in Kapitel 15 behandelt. Der URI für den Zugriff auf eine Ressource in einer separaten Assembly hat dann (natürlich) ein spezielles Format: /Ressourcen-Assembly;component/Ressourcen-Pfad
12
Ressourcen-Assembly bezeichnet die Assembly. Hier können Sie den einfachen Name der Assembly angeben, diesen um die Version erweitern und (bei Assembly mit starken Namen, siehe Kapitel 22) auch um den Token des öffentlichen Schlüssels des Herstellers. Damit haben Sie vier Möglichkeiten: ■ ■ ■ ■
13
/Assembly-Name /Assembly-Name;vVersions-Nummer /Assembly-Name;Token des öffentlichen Schlüssels /Assembly-Name;vVersions-Nummer;Token des öffentlichen Schlüssels
Der Schrägstrich am Anfang ist ein wichtiger Bestandteil, ohne den die Referenzierung nicht funktioniert. Beachten Sie auch, dass vor der Versionsnummer ein v angegeben ist.
14 15 INFO
16
Die Angabe einer Version macht nur dann Sinn, wenn mehrere Assemblys mit demselben Namen in unterschiedlichen Versionen geladen sind und Sie speziell auf eine Version verweisen wollen. Ist die Assembly nur in einer Version geladen (was in normalen Anwendungen die Regel ist), wird die Angabe der Version ignoriert.
17
Handelt es sich um eine Assembly mit einem starken Namen, müssen Sie den Token des öffentlichen Schlüssels für diese Assembly angeben, damit die Referenzierung möglich ist. Informationen dazu finden Sie in Kapitel 22.
18
So können Sie z. B. in einem Image-Steuerelement ein Bild anzeigen, das im der (einfach benannten) Assembly Ressourcen-Assembly.dll verwaltet wird:
19
Listing 14.6: Referenzierung einer Ressourcen aus einer separaten Assembly
20
Paket-URIs Paket-URIs sind eine alternative Art, URIs anzugeben. Wenn Sie einen relativen URI in einem String angeben, verwenden Sie in Wirklichkeit intern bereits einen PaketURI, der automatisch aus Ihrer einfachen Angabe erzeugt wird. Wenn Sie z. B. im StartupUri-Attribut der Application-Instanz den einfachen Namen des Startfensters der Anwendung angeben:
21
22
...
23
wird daraus implizit ein etwas komplexer aussehender URI: StartupUri="pack://application:,,,/MainWindow.xaml"
875
Wichtige WPF-Techniken
Paket-URIs können Ressourcen genauer spezifizieren
Dieser URI ist ein so genannter Paket-URI (Pack URI). Paket-URIs sind ein Teil der Open Packaging Conventions (OPC), die ein Modell zum Beschreiben und Organisieren von Inhalt definieren. Ein Paket-URI beschreibt demnach den Pfad einer Ressource in einem »Paket«, wobei ein Paket ein logischer Container für eine oder mehrere Paket-Teile (Package parts) ist. Dieses Modell wird z. B. in der von Microsoft entwickelten XML Paper Specification (XPS) eingesetzt. XPS definiert, wie Dokumente in XML dargestellt werden, und ist damit ein neues Format für Dokumente, das u. a. dem PDF-Format Konkurrenz macht. OK, soweit die etwas verwirrende Theorie. Paket-URIs haben gegenüber einfachen URIs den Vorteil, dass sie genauer sind und als Zusatzfeature das Referenzieren von losen, zur Kompilierzeit unbekannten binären Ressourcen ermöglichen. Sie sehen aber auch leider etwas seltsam aus. Ein Paket-URI besitzt das folgende Grundformat: pack://Autorität/Teil-Pfad
Der etwas verwirrende Name Autorität (Authority) steht für das Paket. Teil-Pfad bezeichnet den Pfad zu dem Teil des Pakets, das angesprochen werden soll. application:,,, definiert Ressourcen innerhalb der Anwendung
Die Autorität ist selbst auch ein URI und gibt den Pfad zum Paket an. In WPF kann ein Paket entweder die Anwendung sein oder der »Ausgangspunkt der Anwendung«. Dazu verwenden Sie die folgenden Autoritäts-URIs: ■
■
application:///: Steht für Ressourcen, die zur Anwendung gehören. Das sind zum einen eingebettete Ressourcen, aber auch Ressourcen, deren BUILDVORGANG auf INHALT steht. Ein solcher Paket-URI entspricht im Wesentlichen einer einfachen URI-Angabe. siteOfOrigin:///: Diese besondere Autorität gibt den »Ausgangspunkt« der Anwendung an, für den Fall, dass diese Ressource nahe dem Ort gespeichert ist, von dem die Anwendung stammt.
Da die Autorität ein URI in einem URI ist, müssen die drei Schrägstriche, die ja Syntaxelemente eines URIs sind, maskiert werden. Ein Paket-URI verwendet deswegen Kommas an Stelle der Schrägstriche des inneren URI. Wenn Sie eine Ressource angeben wollen, die innerhalb der Anwendung gespeichert ist und die Logo.jpg heißt, verwenden Sie also den folgenden Paket-URI: pack://application:,,,/Logo.jpg
Anwendungsressourcen fügen Sie über Visual Studio einfach einem Projekt hinzu und setzen danach die Eigenschaft BUILDVORGANG auf RESSOURCE (bzw. lassen diese Einstellung stehen). Versuchen Sie dies einmal mit einer Bilddatei, die Sie einem WPF-Anwendung-Projekt hinzufügen. siteOfOrigin:,,, steht für den Ausgangspunkt der Anwendung
Der Ausgangspunkt der Anwendung (Site of origin) ist etwas komplexer: ■
■
■
■
876
Für Anwendungen, denen voll vertraut wird (was über die Codezugriffssicherheit eingestellt wird, siehe Kapitel 23) und die von einem lokal erreichbaren Ordner aus gestartet wurden, ist siteOfOrigin der Anwendungsordner. Für Anwendungen, denen voll vertraut wird, und die über ClickOnce installiert wurden, ist siteOfOrigin der URL oder UNC, von dem aus die Anwendung verteilt wurde. Für XAML-Browser-Anwendungen oder ClickOnce-Anwendungen, denen nur partiell vertraut wird, ist siteOfOrigin der URL oder UNC, der die Anwendung hosted. Für lose XAML-Seiten, die in einem Browser angezeigt werden, existiert keine siteOfOrigin.
Ressourcen in WPF
OK. Das ist etwas komplexer. Ich wollte dieses für die Praxis wichtige und fehlerträchtige Thema aber nicht verschweigen. ClickOnce wird in Kapitel 16 behandelt. Dort erfahren Sie auch, wie Sie Anwendungen lokal installieren. Codezugriffssicherheit wird in Kapitel 23 grundlegend behandelt. Für »normale« WPF-Anwendungen, die auf dem Rechner, auf dem sie ausgeführt werden, gespeichert sind, steht siteOfOrigin für den Anwendungsordner. Damit können Sie z. B. eine lose, zur Kompilierzeit unbekannte Bilddatei referenzieren, die im Unterordner Images im Anwendungsordner gespeichert ist:
12
pack://siteOfOrigin:,,,/Images/Logo.jpg
Der WPF-Designer von Visual Studio scheint mit Ressourcen, die über siteOfOrigin referenziert werden, noch massive Probleme zu haben. Er meldet für solche Angaben im XAML-Code den Fehler »Die Datei "pack://siteOfOrigin:,,,/Dateiname" ist nicht Bestandteil des Projekts, oder ihre "Build Action"-Eigenschaft ist nicht auf "Resource" festgelegt«. Schlimmer ist, dass das WPF-Fenster dann auch nicht mehr im FensterDesigner angezeigt wird. Das scheint ein großer Bug zu sein. Im Beispiel, das Sie auf der DVD finden, werden beide Bilddateien (die als Ressource integrierte und die im Anwendungsordner gespeicherte) korrekt angezeigt, wenn das Beispiel ausgeführt wird. Um das Problem zu lösen, können Sie Ressourcen verwenden, die der Anwendung bekannt sind (deren Buildvorgang also auf INHALT oder RESSOURCE steht). Wenn Sie allerdings explizit Ressourcen einlesen müssen, die als Datei gespeichert sind und die zur Kompilierzeit nicht bekannt sind, können Sie diese beim Laden des Fensters dynamisch einlesen und zuweisen. Paket-URIs können noch ein wenig mehr, als ich hier beschreiben kann. Lesen Sie ggf. in der Dokumentation nach. Das Paket-URI-Thema finden Sie am schnellsten, wenn Sie im Index der Hilfe nach »Paket-URI-Schema« suchen.
14.1.4
13 INFO
14 15
16
17 REF
18
Wörterbuch-Ressourcen
Neben binären Ressourcen können Sie in WPF-Anwendungen auch die von mir als »Wörterbuch-Ressourcen« bezeichneten Ressourcen einsetzen, die in der Auflistung Resources verwaltet werden, die WPF-Elemente von FrameworkElement und FrameworkContentElement erben. Solche Ressourcen werden vorwiegend in XAML definiert (als Unter-Elemente der Resources- Eigenschaft) und können alle WPF-Elemente und auch normale .NET-Objekte beinhalten. Wörterbuch-Ressourcen enthalten in vielen Fällen Stile (Seite 905), Vorlagen (Seite 913) oder Daten-Provider, werden aber auch für normale WPF-Elemente verwendet.
19
20
Die Resources-Auflistung Wörterbuch-Ressourcen können in die Resources-Auflistung jedes von FrameworkElement oder FrameworkContentElement abgeleiteten Elements und der ApplicationKlasse geschrieben werden. Resources ist ein Dictionary vom Typ ResourceDictionary, das beliebige Objekte verwalten kann, die mit einem Object-Schlüssel assoziiert werden.
21 WörterbuchRessourcen werden der Resources-Eigenschaft zugewiesen
In XAML können Sie der Resources-Eigenschaft also beliebig viele Objekte unterordnen, denen Sie über x:Key einen Schlüssel zuordnen. Das folgende Beispiel legt auf diese Weise zwei SolidColorBrush-Objekte in den Ressourcen eines Fensters an:
22
23
877
Wichtige WPF-Techniken
Listing 14.7:
Wörterbuch-Ressourcen in einem Fenster
...
Auf diese Weise können Sie Ressourcen, die innerhalb einer XAML-Datei ggf. mehrfach verwendet werden, zentral an einer Stelle speichern und haben damit schon einmal die Möglichkeit, Änderungen relativ schnell vornehmen zu können.
INFO
Aber nicht, dass Sie denken, allein mit Ressourcen würden Sie das Design einer Anwendung gestalten, z. B., indem Sie wie im Beispiel alle verwendeten BrushObjekte in den Fenster-(oder Anwendungs-)Ressourcen speichern. Für das Design von Anwendungen stellt WPF die höheren Konzepte Stile und Vorlagen zur Verfügung (siehe ab Seite 905). Stile und Vorlagen haben aber etwas mit Ressourcen zu tun, weil sie in diesen gespeichert werden.
StaticResource und DynamicResource StaticResource und DynamicResource ermöglichen den Zugriff
In den WPF-Elementen, aus denen Sie die Oberfläche aufbauen, können Sie über die Markuperweiterungen StaticResource und DynamicResource auf Ressourcen zugreifen. Den Schlüssel der Ressource übergeben Sie am Konstruktor: Listing 14.8: Zugriff auf Wörterbuch-Ressourcen in XAML
DynamicResource reagiert auch auf Änderungen von Ressourcen
Der wesentliche Unterschied zwischen StaticResource und DynamicResource ist, dass StaticResource die Ressource nur einmal einliest. DynamicResource liest die Ressource aber dynamisch immer wieder dann neu ein, wenn diese geändert wird. Sie können dies nachvollziehen, indem Sie im Programm auf die Ressourcen des Fensters zugreifen (die ja über die Resources-Eigenschaft erreichbar sind) und diese ändern. Die folgende Methode ist an das Click-Ereignis eines Schalter gebunden (der in Listing 14.8 noch nicht enthalten ist):
878
Ressourcen in WPF
Listing 14.9: Dynamisches Ändern von Ressourcen private void btnChangeResources_Click(object sender, RoutedEventArgs e) { this.Resources["backgroundBrush"] = new SolidColorBrush(Colors.Navy); this.Resources["foregroundBrush"] = new SolidColorBrush(Colors.White); }
Abbildung 14.2 zeigt die (um zwei Beschriftungs-Label erweiterte) Beispiel-Anwendung nach der Betätigung des Schalters.
12 Abbildung 14.2: Die Beispielanwendung nach der dynamischen Änderung der Ressourcen
13
14 15 DynamicResource sollten Sie also immer dann verwenden, wenn es vorkommen kann, dass die referenzierten Ressourcen (oder die Daten, auf denen diese basieren) in der Laufzeit des Programms geändert werden können. Die folgenden Punkte, die die weiteren Unterschiede zwischen StaticResource und DynamicResource auflisten, können weitere Entscheidungskriterien sein:
16
■
17
■ ■
■
DynamicResource kann nur auf Abhängigkeitseigenschaften angewendet werden, StaticResource hingegen auf alle. StaticResource verursacht weniger Overhead, da die Überwachung der Ressource in der Laufzeit entfällt. Mit DynamicResource referenzierte Ressourcen werden erst dann wirklich eingelesen, wenn diese benötigt werden. Mit StaticResource referenzierte Ressourcen werden allerdings bereits beim Rendern eingelesen. Wenn die Ressourcen nicht sofort beim Öffnen eines Fensters sichtbar sind (z. B. weil diese in zur Zeit unsichtbaren Steuerelementen verwendet werden), kann DynamicResource die Ladezeit eines Fensters verbessern. Mit DynamicResource können Sie so genannte Vorwärts-Referenzen verwenden. Dabei wird die Ressource bereits eingesetzt, bevor diese überhaupt deklariert wurde. Das ist z. B. der Fall, wenn Sie einen SolidColorBrush als Ressource speichern und diese in die Background-Eigenschaft des Fensters schreiben wollen. Mit StaticResource wäre das nicht möglich.
18
19
20
21
Die Suche nach Wörterbuch-Ressourcen Wenn Sie in einem WPF-Element eine Ressourcen referenzieren, geht WPF bei der Suche nach Ressourcen von unten nach oben vor. Zuerst werden die Ressourcen des Elements selbst durchsucht, dann die seines Patent-Elements usw., bis WPF zunächst bei den Ressourcen des Fensters (oder der Seite) angelangt ist. Wird die Ressource auch dort nicht gefunden, sucht WPF in den Ressourcen, die der Anwendung (dem Application-Objekt) zugeordnet sind. Für den Fall, dass die gesuchte Ressource auch nicht in den Anwendungsressourcen gefunden wird, sucht WPF schließlich in den Themen-Ressourcen.
WPF durchsucht die Elemente von unten nach oben nach einer Ressource
22
23
879
Wichtige WPF-Techniken
EXKURS
Themen-Ressourcen sind Ressourcen in referenzierten Assemblys, die in dem logischen Unterordner Themes verwaltet werden. In diesem Ordner ist optional für alle Windows-Themen eine Ressourcen-Datei gespeichert. Dabei handelt es sich zurzeit um die folgenden Dateien: –
Aero.NormalColor.xaml: Ressourcen für das Aero-Thema von Vista
–
Classic.xaml: Ressourcen für das klassische Windows-Thema
–
Luna.Homestead.xaml: Ressourcen für das oliv-grüne Thema von XP
–
Luna.Metallic.xaml: Ressourcen für das silberne Thema von XP
–
Luna.NormalColor.xaml: Ressourcen für das normale Thema von XP
–
Royale.NormalColor.xaml: Ressourcen für das Thema von XP Media Center Edition 2005 und Windows XP Tablet PC Edition
–
Zune.NormalColor.xaml: Ressourcen für das Zune-Thema von XP
–
Generic.xaml: Allgemeine Ressourcen für alle Themen
Die Datei Generic.xaml, auf die WPF zurückgreift, wenn in der evtl. vorhandenen themenspezifischen Datei nichts gefunden wird, sollte immer vorhanden sein. Wird die Ressource auch nicht in den Themen-Ressourcen gefunden, resultiert eine ResourceReferenceKeyNotFoundException. So können Sie Ressourcen lokal für einzelne Elemente (oder eine Gruppe von Elementen), für ein Fenster, global für die gesamte Anwendung oder sogar themenspezifisch (Grundlagen dazu finden Sie im Abschnitt »Ressourcen in anderen Assemblys«) verwalten. Da Ressourcen auf verschiedenen Ebenen auch mit demselben Schlüssel gespeichert werden können (da es sich ja um separate Resources-Auflistungen handelt), können Sie sogar Ressourcen, die auf einer höheren Ebene definiert sind, auf einer niedrigeren Ebene überschreiben. In der Praxis wird es wahrscheinlich aber eher sinnvoll sein, globale Ressourcen im Application-Objekt (also in der Resources-Eigenschaft der App-Klasse in der App.xaml-Datei) zu verwalten und im Fenster nur Ressourcen, die sich speziell auf das Fenster beziehen. Ressourcen, die einzelnen WPF-Elementen zugeordnet sind, machen wohl nur bei der Entwicklung eigener WPF-Steuerelemente Sinn.
Ungeteilte Wörterbuch-Ressourcen Wörterbuch-Ressourcen existieren normalerweise nur einmal. Dies kann zu Problemen führen, wenn Sie in XAML Ressourcen-Typen referenzieren, die prinzipiell nicht geteilt werden können. Im folgenden Listing wird z. B. ein Image-Steuerelement als Ressource angelegt und im Fenster mehrfach (über eine direkte Verwendung der StaticResource-Markuperweiterung) referenziert: Listing 14.10: Versuch, ein WPF-Steuerelement als Ressource zu verwalten und in XAML einzusetzen
Dieser Versuch führt aber zu dem Fehler »Das angegebene Visual-Objekt ist bereits ein untergeordnetes Element eines anderen Visual-Objekts oder der Stamm von "CompositionTarget"«. Der Grund dafür ist, dass eine Instanz einer von Visual abgeleiteten Klasse im logischen Baum nur einmal vorkommen darf. Im Beispiel wird hingegen versucht, dasselbe Objekt an mehreren Stellen hinzuzufügen. Die Lösung des Problems ist die Verwendung der Markuperweiterung x:Shared, über die Sie festlegen können, dass die Ressource nicht geteilt wird:
12
13 x:Shared kann das Teilen verbieten
14
Eigenartigerweise hat Visual Studio 2008 (Release ohne Service Pack) damit (wenigstens in meinem Fall) noch Probleme und zeigt den genannten Fehler trotzdem an. Da die Anwendung aber problemlos läuft, muss dies ein Bug im WPFDesigner von Visual Studio sein.
15 INFO
16 Ich denke aber auch, dass das Verwalten ganzer UI-Elemente in Ressourcen wohl nur in den wenigsten Fällen Sinn macht. Im Beispiel wäre es besser, nur das eigentliche Bild (über ein BitmapImage-Objekt) in der Ressource zu verwalten und die Darstellung der Anwendung zu überlassen. Freigegebene Ressourcen funktionieren übrigens nur dann, wenn die XAML-Datei kompiliert wird. Kompiliert wird eine XAML-Datei nur dann, wenn ihr über x:Class eine partielle Klasse zugewiesen ist.
17
18 HALT
Auf Wörterbuch-Ressourcen im Programm zugreifen
19
Auf Wörterbuch-Ressourcen können Sie natürlich auch im Programm zugreifen, weil es sich ja lediglich um Objekte handelt, die in einer Auflistung verwaltet werden. So können Sie z. B. im Konstruktor eines Fensters Ressourcen hinzufügen (auch wenn das in der Praxis wahrscheinlich eher selten benötigt wird):
20
Listing 14.11: Hinzufügen von Ressourcen im Konstruktor eines Fensters public MainWindow() { this.Resources.Add("backgroundBrush", new SolidColorBrush(Colors.AliceBlue)); this.Resources.Add("foregroundBrush", new SolidColorBrush(Colors.DarkBlue));
21
22
InitializeComponent(); }
23
881
Wichtige WPF-Techniken
INFO
Über FindResource können Sie Ressourcen suchen
Der Grund dafür, dass ich die Ressourcen im Konstruktor des Fensters hinzugefügt habe, ist, dass dies ermöglicht, in XAML auf die Ressourcen zuzugreifen. Dabei ist auch wichtig, dass das Hinzufügen der Ressourcen vor dem Aufruf von InitializeComponent erfolgt. Visual Studio hat aber noch Probleme damit, wenn Sie Ressourcen, die Sie im Konstruktor einfügen, in XAML einsetzen. Visual Studio zeigt in diesem Fall einen Fehler an und ist nicht in der Lage, den Fenster-Designer zu öffnen. Was in der Praxis wahrscheinlich häufiger vorkommt, ist das Suchen von Ressourcen. Sie können dazu zwar auf die Ressourcen eines Elements oder Fensters bzw. der Anwendung über den Indexer der jeweiligen Resources-Auflistung zugreifen, wie ich es bereits auf Seite 879 zur Demonstration gezeigt habe. Da WPF aber bei der Suche nach einer Ressource von unten nach oben vorgeht, sollten Sie dasselbe machen und zur Suche die FindResource-Methode des Elements aufrufen, von dem aus die Ressource gesucht werden soll. Da FindResource eine Object-Referenz zurückgibt, müssen Sie diese in den erwarteten Typ umwandeln: Listing 14.12: Statisches Zuweisen von Ressourcen im Programmcode this.txtDemo1.Background = (Brush)this.txtDemo1.FindResource("backgroundBrush"); this.txtDemo1.Foreground = (Brush)this.txtDemo1.FindResource("foregroundBrush");
FindResource wirft eine ResourceReferenceKeyNotFoundException, wenn die Ressource nicht gefunden wird. Sie können alternativ auch TryFindResource verwenden, die in diesem Fall null zurückgibt. Dann können Sie allerdings nicht unterscheiden, ob die Ressource leer oder nicht vorhanden ist.
Im Wesentlichen entspricht die Verwendung von FindResource der Anwendung der StaticResource-Markuperweiterung in XAML. Wenn Sie Ressourcen im Programmcode dynamisch zuweisen wollen, rufen Sie die SetResourceReference-Methode auf, der Sie eine Referenz auf die Abhängigkeitseigenschaft und den Schlüssel der Ressource übergeben: Listing 14.13: Dynamisches Zuweisen von Ressourcen im Programmcode this.txtDemo2.SetResourceReference(TextBox.BackgroundProperty, "backgroundBrush"); this.txtDemo2.SetResourceReference(TextBox.ForegroundProperty, "foregroundBrush");
Wörterbuch-Ressourcen in XAML-Dateien auslagern In komplexen Anwendungen macht es häufig Sinn, bestimmte oder alle Ressourcen in separate XAML-Dateien auszulagern. Eine entsprechende XAML-Datei hat ein ResourceDictionary-Objekt als Wurzel-Element: Listing 14.14: Eine Ressourcen-XAML-Datei
882
Ressourcen in WPF
In Visual Studio erzeugen Sie eine Ressourcenwörterbuch-XAML-Datei über den entsprechenden Befehl im HINZUFÜGEN-Menü. In XAML können Sie separate Wörterbuch-Ressourcen-Dateien hinzufügen, indem Sie in die Resources-Eigenschaft eine neue ResourceDictionary-Instanz schreiben und in deren MergedDictionaries-Auflistung ResourceDictionary-Objekte für die einzelnen XAML-Dateien. Diese referenzieren Sie über die Source-Eigenschaft:
In XAML können Sie externe RessourcenDateien mergen
12
Listing 14.15: Hinzufügen von Ressourcen, die in separaten XAML-Dateien verwaltet werden
13
14
Wörterbuch-Ressourcen in separaten Assemblys Wörterbuch-Ressourcen werden auch häufig in separaten Assemblys verwaltet, was den späteren Austausch von Ressourcen oder die Lokalisierung einer Anwendung wesentlich erleichtert. Solche Ressourcen-Assemblys können Sie über ein Klassenbibliothek-Projekt erzeugen, indem Sie diesem entsprechende XAML-Dateien hinzufügen.
15
16
Das Klassenbibliothek-Projekt benötigt Referenzen auf die Assemblys PresentationCore.dll, PresentationFramework.dll und WindowsBase.dll.
17
Beim Auslagern von Ressourcen in separate Assemblys haben Sie prinzipiell zwei Möglichkeiten: ■
■
Sie können in der Ressourcen-Assembly einfache XAML-Dateien mit Ressourcen speichern und diese mit den Ressourcen der Anwendung oder eines Fensters (oder eines anderen WPF-Elements) zusammenführen (mergen). Alternativ können Sie in Anwendungen auch direkt auf die Ressourcen zugreifen, wozu diese allerdings mit einem ComponentResourceKey benannt und weitere Voraussetzungen erfüllt sein müssen.
Wenn Sie in einer separaten Assembly verwaltete Ressourcen lediglich mit den Ressourcen der Anwendung zusammenführen wollen, speichern Sie in der Ressourcen-Assembly einfach eine oder mehrere XAML-Dateien wie im vorhergehenden Beispiel. In der WPF-Anwendung fügen Sie die Ressourcen den Ressourcen der Anwendung oder eines Fensters hinzu, indem Sie diese mit ggf. anderen Ressourcen zusammenführen:
18
19 WörterbuchRessourcen in separaten Assemblys können zusammengeführt werden
20
21
Listing 14.16: Mergen der Anwendungsressourcen mit Ressourcen in einer separaten Assembly
INFO
ComponentResourceKey ermöglicht den direkten Zugriff
Das Zusammenführen von Ressourcen mit einfachem Namen aus verschiedenen Assemblys ist nicht ganz unproblematisch: Falls Ressourcen in mehreren Assemblys denselben Namen aufweisen, würde WPF zwar zunächst nicht unbedingt einen Fehler generieren. WPF verwendet einfach eine der gleichnamigen Ressourcen. Welche das ist, ist aber schwer zu sagen. Passt die Ressource nicht zum erwarteten Typ, resultiert allerdings eine InvalidOperationException. Sie können aber auch den direkten Zugriff auf Ressourcen in einer separaten Assembly ermöglichen. Dazu müssen einige Voraussetzungen erfüllt sein, die allerdings nicht besonders gut (eigentlich gar nicht) dokumentiert sind (weswegen ich auch nicht sicher bin, ob dies der einzige Weg ist). Das etwas komplexe Vorgehen sichert aber zum einen die Eindeutigkeit der Ressourcen-Schlüssel und ermöglicht zum anderen die Verwaltung von themenspezifischen Ressourcen. Halten Sie sich einfach an die folgenden Voraussetzungen: ■
■
■
■
■
Die Assembly muss eine öffentliche Klasse beinhalten, die als Typ für den ComponentResourceKey verwendet wird, der den Schlüssel der Ressourcen darstellt. Diese Klasse kann leer sein. Die Ressourcen müssen zumindest in einer Datei mit Namen Generic.xaml in dem Unterordner Themes angelegt werden. Auf der Suche nach einer Ressource sucht WPF am Ende in den Ressourcen des aktuellen Windows-Themas (die entsprechend des Themas benannt sind). Werden dort keine Ressourcen gefunden, sucht WPF in den allgemeinen Ressourcen, die in der Datei Generic.xaml verwaltet werden. Falls Sie mehrere Ressourcen-Dateien verwalten wollen, müssen Sie diese in Generic.xaml mergen. Zusätzlich zu Generic.xaml könnten Sie natürlich auch noch Ressourcen-Dateien für die einzelnen Windows-Themen hinzufügen, wenn Sie Ihre Ressourcen themenspezifisch anlegen wollen. Die Eigenschaft BUILDVORGANG der Ressourcen-XAML-Dateien muss auf PAGE stehen (nicht auf RESSOURCE!). Wenn BUILDVORGANG auf RESSOURCE steht, funktioniert das Ganze einfach nicht. Der Schlüssel der Ressourcen muss vom Typ ComponentResourceKey sein. ComponentResourceKey verbindet den Namen des Schlüssels mit dem Namen eines Typs und dem Namen der Assembly. Als Typ geben Sie (in der Eigenschaft TypeInTargetAssembly) die oben erstellte Klasse an. Der Eigenschaft ResourceId weisen Sie eine String-ID zu. In der Datei AssemblyInfo.cs müssen Sie das Assembly-Attribut ThemeInfo angeben, um WPF mitzuteilen, wo die Ressourcen gefunden werden können. Wenn Sie nur eine generische Ressourcen-Datei haben, sieht dieses Attribut folgendermaßen aus: [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
884
Ressourcen in WPF
Beachten Sie diese Voraussetzungen auf jeden Fall. Ich habe mehrere Stunden damit verbracht, diese Voraussetzungen herauszufinden. Leider half mir die Dokumentation dabei gar nicht und WPF-Bücher und das Internet nur wenig. In der letzten Stufe meiner Versuche fehlte nur noch das ThemeInfo-Attribut und ich hatte den BUILDVORGANG nicht auf PAGE stehen, was dazu führte, dass die Ressourcen einfach nicht ausgelesen wurden (aber auch kein Fehler oder Einträge im Trace-Protokoll erschienen).
HALT
12
Bei der Arbeit mit Ressourcen in externen Assemblys werden Sie sich wahrscheinlich so einige Male die Haare raufen. In meinen Versuchen reichte manchmal nur eine kleine Veränderung, dass das Ganze nicht mehr funktionierte. Da WPF leider absolut keine Informationen darüber ausgibt, was schief gelaufen ist, ist die Fehlersuche sehr langwierig und nervtötend.
13
14
Abbildung 14.3 zeigt den Projektmappen-Explorer eines Klassenbibliothek-Projekts, das eine Ressourcen-Assembly darstellt. Abbildung 14.3: KlassenbibliothekProjekt einer Ressourcen-Assembly
15
16
17
18
Die Datei generic.xaml sieht folgendermaßen aus:
19
Listing 14.17: Ressourcen-Datei mit Ressourcen, die mit ComponentResourceKey benannt sind
20
21
22
23
885
Wichtige WPF-Techniken
HALT
Beachten Sie auch die UriSource des BitmapImage, die nur mit einer absoluten Referenzierung wie im Beispiel funktioniert. Eine relative Referenzierung (von der XAMLDatei aus mit UriSource="../Smilie.gif") funktioniert nicht. Eigenartigerweise zeigt Visual Studio 2008 (ohne Service Pack) für die absolute Referenzierung einen Fehler an und für die (nicht funktionierende) relative nicht. Die Klasse Resources.cs ist zunächst noch leer: public class Resources { }
In einer WPF-Anwendung, die die Ressourcen-Assembly referenziert, können Sie sich über den ComponentResourceKey-Schlüssel nun (mit StaticResource oder besser mit DynamicResource) direkt auf die Ressourcen beziehen: Listing 14.18: Direkter Bezug auf Ressourcen in einer separaten Assembly
Da der Schlüssel relativ aufwändig ist, stellen Ressourcen-Assemblys diesen üblicherweise als statische Eigenschaft zur Verfügung, deren Name dem Namen der Ressourcen entspricht, mit einem angehängten »Key«. Diese Eigenschaften können Sie in der noch leeren Klasse implementieren: Listing 14.19: Statische Eigenschaft, die den Komponenten-Schlüssel einer Ressource zurückgibt public class Resources { /* Gibt den Schlüssel für den Hintergrund-Brush zurück */ public static ComponentResourceKey BackgroundBrushKey { get { return new ComponentResourceKey(typeof(Resources), "backgroundBrush"); } } ...
886
Befehle
In der Ressourcen-XAML-Datei können Sie sich darauf beziehen: ... ...
Genau wie in der Anwendung: ... ...
12
13
Und damit reicht es jetzt zum Thema Ressourcen (das aber noch nicht ausgeschöpft ist). Interessant wäre auf jeden Fall noch das themenspezifische Verwalten eigener Ressourcen. Die Basis dafür haben Sie: Legen Sie in der Ressourcen-Assembly einfach noch weitere XAML-Dateien an, die Sie entsprechend der Themen benennen (siehe Seite 879).
14.2
14 15
Befehle
Befehle (Commands) sind ein wichtiges Konzept für WPF-Anwendungen. Sie sind so etwas wie Ereignisse, sind aber lose gekoppelt und besitzen interessante Möglichkeiten für die Entwicklung von Anwendungen.
14.2.1
16
Was ist ein Befehl?
17
Aus der logischen Sicht ist ein Befehl ein Teilprogramm, das eine Aktion ausführt, die innerhalb einer Anwendung von verschiedenen Stellen aus aufgerufen werden soll. Microsoft Word z. B. enthält eine große Anzahl an logischen Befehlen. Der Befehl File.Save, der die aktuelle Datei speichert, kann innerhalb von Word z. B. über das Menü, über die Symbolleiste, über eine Tastenkombination ((STRG) + (S)), in einem Makro oder in einem Add-In aufgerufen werden. Viele Programmteile rufen ein und denselben logischen Befehl auf. In einer Anwendung ist es von Vorteil, wenn solche Befehle auch auf der physikalischen Ebene nur einmal existieren (was allerdings bei Word nicht direkt der Fall ist).
Befehle sind Teilprogramme, die von mehreren Stellen aus aufgerufen werden können
Ein anderes wichtiges Prinzip ist, dass Befehle in einer Anwendung üblicherweise nicht immer zur Verfügung stehen. Der Datei-Speichern-Befehl kann in Word z. B. nur dann aufgerufen werden, wenn ein Dokument geöffnet ist. Ist kein Dokument geöffnet, stehen weder der Menüeintrag noch der Symbolleisten-Befehl oder die anderen Möglichkeiten zur Verfügung. Gut, wenn die Verfügbarkeit des Befehls ohne viel Aufwand zentral gesteuert werden kann. WPF-Befehle unterstützen dieses Feature natürlich (sonst hätte ich darüber ja nichts geschrieben).
Befehle können deaktiviert werden
14.2.2
■
19
Die Methode Execute ist dazu vorgesehen, den Befehl auszuführen. Das dieser Methode übergebene object-Argument dient der Übergabe von Parametern an
20
21
Ein eigener Befehl
Aus der technischen Sicht ist ein Befehl (in WPF) ein Objekt, das die ICommandSchnittstelle implementiert. ICommand beschreibt die folgenden Member:
18
22 Befehle implementieren ICommand
887
23
Wichtige WPF-Techniken
■
■
den Befehl. Wenn Sie den Befehl (z. B. an ein WPF-Steuerelement) binden, können Sie diese Parameter in die CommandParameter-Eigenschaft des Steuerelements schreiben. Die Eigenschaft CanExecute soll zurückgeben, ob der Befehl zurzeit ausgeführt werden kann. Das dieser Methode übergebene object-Argument enthält wieder die Parameter, die bei der Bindung des Befehls angegeben wurden. Das Ereignis CanExecuteChanged soll aufgerufen werden, wenn der boolesche Wert geändert wird, den CanExecute zurückgibt. Darüber erfährt die WPF-Laufzeitumgebung, dass die Verfügbarkeit des Befehls geändert wurde. Sie ruft daraufhin CanExecute auf, um die Verfügbarkeit zu ermitteln, und deaktiviert oder aktiviert automatisch mit dem Befehl verknüpfte WPF-Steuerelemente.
Sie können also eigene Befehle erzeugen, indem Sie eine Klasse entwickeln, die ICommand implementiert. Listing 14.20 zeigt dies an einem einfachen Demo-Befehl. In die Implementierung der Klasse habe ich bereits ein paar praxisrelevante Dinge integriert.
INFO
Dieses Beispiel soll nur demonstrieren, wie ein Befehl prinzipiell aufgebaut ist. In der Praxis werden Sie wahrscheinlich selten eigene Befehls-Klassen implementieren, weil die Implementierung eigener gerouteter Befehle wesentlich einfacher ist. Im Abschnitt »Befehlsbindungen für eigene Befehle« ab Seite 896 erfahren Sie mehr dazu. Listing 14.20: Ein einfacher Demo-Befehl public class DemoCommand : ICommand { private bool canExecute = true; /* Gibt an, ob der Befehl ausgeführt werden kann */ public bool CanExecute(object parameter) { return this.canExecute; } /* Wird aufgerufen, wenn CanExecute geändert wird */ public event EventHandler CanExecuteChanged; /* Führt den Befehl aus */ public void Execute(object parameter) { System.Windows.MessageBox.Show("Der Demo-Befehl wurde " + "von '" + (parameter != null ? parameter.ToString() : "Unbekannt") + "' ausgeführt"); } /* Setzt das Feld, das definiert, ob der Befehl zurzeit ausgeführt werden kann */ public void SetCanExecute(bool value) { if (this.canExecute != value) { // Den Wert übergeben this.canExecute = value; // Das CanExecuteChanged-Ereignis aufrufen if (this.CanExecuteChanged != null) {
888
Befehle
this.CanExecuteChanged(this, new EventArgs()); } } } /* Privater Konstruktor. Verhindert die Instanzierung von außen. */ private DemoCommand() { }
12
private static DemoCommand instance; /* Liefert eine Instanz des Demo-Befehls */ public static DemoCommand Instance { get { if (DemoCommand.instance == null) { DemoCommand.instance = new DemoCommand(); } return DemoCommand.instance; } }
13
14 15
}
Die Klasse DemoCommand implementiert zunächst einmal ICommand. Die CanExecuteMethode gibt lediglich das zurück, was im privaten Feld canExecute gespeichert ist.
16
Die Execute-Methode führt in diesem Demo-Befehl nicht allzu viel aus (es ist ja auch nur eine Demo …). Diese Methode zeigt lediglich die Meldung an, dass der Befehl ausgeführt wurde. Dabei wird allerdings das Argument parameter ausgewertet, wobei angenommen wird, dass hier ein String übergeben wird, der einen Namen enthält. Diese einfache Parameterübergabe habe ich nur zur Demonstration implementiert. In der Praxis würden Sie hier natürlich »richtige« Parameter übergeben, um den Befehl von der Stelle aus steuern zu können, an der Sie diesen binden. Das ist natürlich nur dann notwendig, wenn Sie Befehle gesteuert aufrufen müssen.
17
18
Die zusätzliche Methode SetCanExecute habe ich implementiert, um das Feld, das den Wert für CanExecute verwaltet, von außen setzen zu können. Damit kann ich von außen definieren, ob CanExecute true oder false zurückgibt. Diese Methode setze ich im Beispiel später ein, um den Befehl zu deaktivieren und wieder zu aktivieren. Um die WPF-Laufzeitumgebung davon zu informieren, dass die Verfügbarkeit geändert wurde, ruft SetCanExecute in diesem Fall das CanExecuteChangedEreignis auf. Damit werden dann automatisch alle verknüpften WPF-Elemente und andere Möglichkeiten, den Befehl auszuführen, ebenfalls je nach Verfügbarkeit deaktiviert oder aktiviert. DemoCommand folgt zudem dem Singleton-Entwurfsmuster. Nach diesem darf es von der betreffenden Klasse nur eine Instanz geben. Deswegen ist zum einen der Konstruktor privat deklariert, damit die Klasse nicht von außen instanziert werden kann. Die statische Eigenschaft Instance (deren Name lediglich eine Konvention ist) gibt zum anderen eine Instanz der Klasse zurück, die im privaten Feld instance verwaltet wird. Um diese nur bei Bedarf zu erzeugen, prüft Instance, ob das Feld null verwaltet, und erzeugt die Instanz nur in diesem Fall. Das Singleton-Muster macht die Verwendung eines eigenen Befehls einfacher und sicherer, da Sie im Programm lediglich die Instance-Eigenschaft verwenden müssen, um das Singleton-Objekt zu erreichen.
19
20
21 INFO
22
23
889
Wichtige WPF-Techniken
Ein Befehl kann nun auf verschiedene Weise ausführbar gemacht werden: ■ ■ ■ ■
über eine Bindung an die Command-Eigenschaft eines WPF-Elements, über Eingabe-Bindungen (die einen Befehl z. B. mit einer Tastenkombination verknüpfen), über eine Bindung an Eingabe-Gesten, und (natürlich) über den direkten Aufruf der Execute-Methode.
Die Verknüpfung eines Befehls mit der Command-Eigenschaft von WPF-Elementen wird im folgenden Abschnitt behandelt, Eingabe-Bindungen und Eingabe-Gesten behandle ich ab Seite 898. Damit bietet sich eine Möglichkeit, die Befehle einer Anwendung als Klassen zu implementieren, die die ICommand-Schnittstelle implementieren und diese dann auf die verschiedenen Arten, die in einer Anwendung üblich sind, aufzurufen. Der Befehl zum Öffnen eines Fensters, das Produktdaten anzeigt, kann so z. B. mit einem Menüpunkt, einem Schalter und einer Tastenkombination verknüpft werden.
INFO
Befehle stehen damit in Konkurrenz zu Ereignissen. Welche Variante Sie für Ihre Anwendungen verwenden, bleibt Ihnen überlassen. Über Ereignisse können Sie eine Anwendung in der Regel genauso elegant implementieren wie über Befehle. Eine konsequente Verwendung von Befehlen macht eine Anwendung aber u. U. übersichtlicher, was besonders dann gilt, wenn Sie auch die in WPF vordefinierten Befehle verwenden. Sie sollten aber auch im Auge erhalten, dass die Verwendung eines Befehls im Vergleich zum Aufruf eines Ereignisses zumindest im Programm nicht so einfach erkennbar ist. Ein Programmierer, der sich das Programm eines anderen Programmierers anschaut, kann dadurch Probleme haben, das Programm zu verstehen. Wenn Sie dieser Programmierer sind, sollten Sie also immer auch berücksichtigen, dass ein WPFSteuerelement (oder eine Tastenkombination oder Geste) mit einem Befehl verknüpft sein kann.
14.2.3 Command verbindet einen Befehl mit einem Steuerelement
Verknüpfen eines Befehls mit WPF-Steuerelementen
Einen Befehl können Sie mit beliebig vielen WPF-Steuerelementen verknüpfen, die Befehle unterstützen. Steuerelemente wie Button, CheckBox und MenuItem bieten dazu die Eigenschaft Command, die vom Typ ICommand ist. Diese Eigenschaft können Sie mit einer Instanz eines Befehls-Objekts belegen. Der Eigenschaft CommandParameter können Sie Parameter für den Befehl übergeben (die von diesem natürlich ausgewertet werden müssen). Das folgende Beispiel, das den Demo-Befehl mit einer Menu-Instanz und einen Schalter verbindet, übergibt in dieser Eigenschaft lediglich einen Namen: Listing 14.21: Verknüpfen eines Befehls mit mehreren WPF-Elementen
890
Befehle
12
Demo
Neben Command und CommandParameter können Sie noch die Eigenschaft CommandTarget definieren. Diese Eigenschaft bestimmt das Ziel eines Befehls, der sich auf einzelne Elemente bezieht. Ein Beispiel dafür finden Sie im Abschnitt »Vordefinierte Befehle mit Steuerelementen einsetzen« (Seite 893). Wie Sie CommandTarget mit eigenen Befehlen verwenden, ist mir allerdings unklar.
13
14 INFO
15
Wird nun der Menüeintrag oder der Schalter betätigt, wird der Befehl ausgeführt.
16
Um das Deaktivieren und Aktivieren von Befehlen zu demonstrieren, habe ich auf dem WPF-Fenster zusätzlich eine CheckBox untergebracht: CanExecute
17
Der Ereignishandler, der mit dem Click-Ereignis verknüpft ist, ist folgendermaßen definiert:
18
Listing 14.22: Der Ereignishandler des Click-Ereignisses der CheckBox definiert die Verfügbarkeit des Demo-Befehls
19
private void chkCanExecute_Click(object sender, RoutedEventArgs e) { DemoCommand.Instance.SetCanExecute( this.chkCanExecute.IsChecked == true); }
20
Wird die CheckBox nun ausgeschaltet, werden implizit auch die mit dem Befehl verknüpften Steuerelemente deaktiviert (Abbildung 14.4).
21 Abbildung 14.4: Das Befehls-Beispiel in Aktion
22
23
891
Wichtige WPF-Techniken
14.2.4 Vordefinierte Befehle RoutedCommand ist die Basis
WPF enthält eine Vielzahl an bereits vordefinierten Befehlen. Diese basieren auf der gemeinsamen Basisklasse System.Windows.Input.RoutedCommand, die auch zurzeit die einzige ist, die ICommand implementiert1. RoutedCommand bietet einige spezielle Eigenschaften (Tabelle 14.1).
Tabelle 14.1: Eigenschaften von RoutedCommand
Eigenschaft
Beschreibung
Name
verwaltet den Namen des Befehls.
InputGestures
verwaltet eine Liste von Eingabe-Gesten (InputGesture-Objekten), die mit dem Befehl verknüpft sind. Eingabe-Gesten werden ab Seite 898 behandelt.
OwnerType
gibt den Typ an, für den der Befehl registriert wurde.
Wie der Name der RoutedCommand-Klasse bereits andeutet, handelt es sich bei einer Instanz einer dieser Klasse um einen gerouteten Befehl. Der Sinn ist prinzipiell derselbe wie bei gerouteten Ereignissen: Stellt ein WPF-Element keinen Handler für einen gerouteten Befehl zur Verfügung, wandert dieser den visuellen Baum hoch, bis ein WPF-Element gefunden wurde, das den Befehl behandelt. RoutedUICommand wird für UI-Elemente verwendet
Die Klasse RoutedUICommand ist von RoutedCommand abgeleitet. Sie erweitert RoutedCommand um die Eigenschaft Text. Diese Eigenschaft wird von WPF-Steuerelementen dazu verwendet, ihre Beschriftung einzustellen. RoutedUICommand besitzt außerdem die Fähigkeit, dass Befehle auch geroutet werden können. Über RoutedUICommand können Sie sehr einfach die Beschriftung auch mehrerer WPFElemente über den Befehl definieren, mit denen diese verknüpft sind. Eine Änderung der Text-Eigenschaft wird von WPF sofort an die verknüpften Elemente weitergegeben, womit Sie z. B. relativ einfach eine Umschaltung der Sprache implementieren können, in der Ihre Anwendung angezeigt wird. Das Routing von Befehlen ist an dieser Stelle noch nicht wichtig. Im Abschnitt »Befehlsbindungen an Ereignishandler« (Seite 895) finden Sie ein Beispiel dafür.
Die meisten vordefinierten Befehle sind von RoutedUICommand abgeleitet
Die meisten vordefinierten Befehle in WPF sind von RoutedUICommand abgeleitet. Diese Befehle werden über statische Eigenschaften entsprechender Klassen repräsentiert: ■ ■ ■ ■ ■
1
892
ApplicationCommands: Enthält Anwendungs-Standardbefehle wie Cut, Copy, Paste, Undo, Redo, Open, SaveAs, Close etc. ComponentCommands: Definiert Befehle, die häufig von Elementen der Benutzeroberfläche verwendet werden, wie z. B. MoveDown, MoveLeft, ScrollPageDown etc. MediaCommands: Enthält Befehle, die für Multimedia-Anwendungen verwendet werden, wie Play, Pause, Stop, IncreaseVolume, DecreaseVolume, BoostBass etc. NavigationCommands: Definiert Befehle für die Seiten-Navigation wie NextPage, PreviousPage, FirstPage, LastPage, BrowseBack, BrowseHome, Favorites etc. EditingCommands: Enthält Befehle, die beim Bearbeiten von Dokumenten Verwendung finden, wie Delete, MoveDownByLine, MoveToLineEnd, ToggleBold etc. Was ich herausgefunden habe, indem ich den Quellcode der Assemblys PresentationCore.dll und PresentationFramework.dll mit dem Add-In File Disassembler im .NET Reflector disassembliert und in den resultierenden Dateien einfach nach ICommand gesucht habe
Befehle
Ich kann auf die Vielzahl von Befehlen hier nicht näher eingehen und zeige lediglich, wie Sie prinzipiell damit arbeiten. Eine (gute) Übersicht der WPF-Standardbefehle finden Sie leider auch nicht in der Visual-Studio-Dokumentation. In der Dokumentation der Member der oben beschriebenen Klassen werden aber wenigstens alle verfügbaren Befehle aufgelistet. Die Beschreibung der Befehle bedarf aber noch einer massiven Nachbearbeitung (diese ist immer »Ruft den Wert ab, der den xyz-Befehl darstellt«). Sie müssen in die Dokumentation des jeweiligen Befehls wechseln, um in den Hinweisen zu erfahren, was dieser bewirkt. Viele der vordefinierten Befehle werden bereits intern von WPF-Steuerelementen eingesetzt. Eine TextBox hat z. B. die Befehle Cut, Copy, Paste, Undo und Redo der ApplicationCommands-Klasse mit entsprechenden Tastenkombinationen (die Sie hier als versierte Entwickler wahrscheinlich kennen) und/oder Kontextmenü-Einträgen verknüpft.
REF
12 Viele WPF-Steuerelemente setzen Befehle intern bereits ein
13
14
Sie können vordefinierte Befehle aber natürlich auch selbst einsetzen. Wie schon bei eigenen Befehlen, können Sie vordefinierte Befehle z. B. der Command-Eigenschaft von Steuerelementen zuweisen.
15
14.2.5 Vordefinierte Befehle mit Steuerelementen einsetzen Um vordefinierte Befehle mit Steuerelementen einzusetzen, belegen Sie die CommandEigenschaft der Steuerelement-Instanz mit dem Namen des Befehls. Dabei müssen Sie die Klasse, die die jeweilige Befehls-Eigenschaft verwaltet, nicht angeben.
Über die Command-Eigenschaft können Sie Befehle binden
17
Wenn Sie nichts weiter angeben, wird der Befehl global angewendet. Wenn Sie z. B. einen Menüeintrag mit dem Copy-Befehl versehen, wirkt sich dieser auf alle WPFSteuerelemente aus, die ein Kopieren unterstützen (die den Copy-Befehl also intern behandeln). Der Befehl steht automatisch nur dann zur Verfügung, wenn ein entsprechendes Steuerelement den Fokus besitzt. Der Befehl wirkt sich dann natürlich auch nur auf dieses Steuerelement aus. Sie können aber auch über die CommandTarget-Eigenschaft bestimmen, welches Element das Ziel des Befehls ist. Das ist z. B. dann notwendig, wenn Sie den Copy-Befehl mit einem Button-Steuerelement verknüpfen. Eine Verknüpfung ohne CommandTarget würde nicht funktionieren, da im Fall der Betätigung des Schalters dieser den Fokus besitzt (und der Befehl sich folglich auf den Schalter beziehen würde).
16
18 CommandTarget gibt optional das Ziel des Befehls an
19
20
CommandTarget wird als Datenbindung über die Markuperweiterung Binding angegeben. Datenbindung wird erst ab Seite 922 behandelt. Ich muss hier also ein wenig vorgreifen.
21
Im Wesentlichen teilen Sie der Binding-Markuperweiterung mit, woher der Wert stammt, der zurückgegeben werden soll. Im Falle der CommandTarget-Eigenschaft schreiben Sie den Namen des Steuerelements, für das der Befehl gilt, in die Eigenschaft ElementName.
22
Das folgende Beispiel implementiert auf diese Weise eine kleine Demo-Anwendung, die die Befehle Cut, Copy, Paste, Undo und Redo auf entsprechende Schalter legt und als Ziel eine TextBox angibt. Um die Verwendung der Befehle komplett zu machen, habe ich zusätzlich die Eigenschaft Content der Schalter an die Eigenschaft Text der Befehle gebunden. Die Schalter zeigen damit automatisch den Text an, der in der jeweiligen RoutedUICommand-Instanz (dem Befehl) verwaltet wird.
23
893
Wichtige WPF-Techniken
Listing 14.23: Demo für die Verwendung von Standardbefehlen
Abbildung 14.5 zeigt das Ergebnis. Die Schalter zum Ausschneiden und zum Kopieren sind deswegen (von WPF automatisch) deaktiviert, weil die TextBox nicht den Fokus besitzt.
INFO
894
Für eigene Befehle wäre eine von RoutedUICommand abgeleitete Klasse wesentlich sinnvoller als eine, die lediglich ICommand implementiert. Damit könnten Sie, besonders in größeren Anwendungen, z. B. von der automatischen Lokalisierung Gebrauch machen. Oder Sie könnten das Routing von Befehlen nutzen. Mir ist allerdings unklar, wie Sie eine eigene, von RoutedUICommand abgeleitete Klasse korrekt implementieren.
Befehle
Abbildung 14.5: Das Beispielprogramm nach der Betätigung des Einfügen-Schalters
12
13
14.2.6 Befehlsbindungen an Ereignishandler Viele der vordefinierten Befehle beziehen sich auf spezifische WPF-Elemente (die den Befehl empfangen und auswerten). Der Copy-Befehl ist ein Beispiel dafür. Er wird von WPF-Elementen ausgewertet, die das Kopieren ihres Inhalts erlauben. Es gibt aber auch Befehle, die nicht von WPF-Elementen ausgewertet werden. Ein Beispiel ist der Close-Befehl, der dazu vorgesehen ist, irgendetwas zu schließen, z. B. eine Datei oder ein Fenster.
Die CommandBinding-Eigenschaft ermöglicht ein Binden an Ereignishandler
Solche Befehle können Sie über die CommandBinding-Eigenschaft, die alle von UIElement abgeleiteten Klassen besitzen, an einen Ereignishandler binden. Den CloseBefehl können Sie so z. B. für ein Fenster an eine Methode binden, die die zum Schließen erforderliche Aktion ausführt. Dabei geben Sie die folgenden Eigenschaften bzw. Ereignisse an: ■ ■ ■
14 15
16
17
Command: Der Befehl Executed: Die Ereignisbehandlungsmethode, die aufgerufen werden soll, wenn der Befehl ausgeführt wird CanExecute: Eine Ereignisbehandlungsmethode, die aufgerufen wird, wenn die WPF-Laufzeitumgebung ermitteln muss, ob der Befehl zur Verfügung steht. Bei Befehlen, die immer zur Verfügung stehen sollen, müssen Sie dieses Ereignis nicht programmieren.
18
19
Da Visual Studio Sie bei der Erzeugung der Ereignishandler unterstützt, verzichte ich auf eine Erläuterung und verweise auf das Beispiel. Dieses verknüpft den CloseBefehl auf einem WPF-Fenster mit einem Menüpunkt und einem Schalter:
20
Listing 14.24: Binden eines Befehls an einen Ereignishandler
21
22
23
895
Wichtige WPF-Techniken
Die Ereignisbehandlungsmethode sieht folgendermaßen aus: Listing 14.25: Ereignishandler für den Close-Befehl private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) { this.Close(); }
INFO
INFO
In diesem Beispiel ist übrigens Befehls-Routing im Spiel. Wird z. B. der Schalter betätigt, wird der Close-Befehl nach oben durchgeroutet, bis er am Window-Objekt ankommt. Dieses erkennt, dass der Close-Befehl in seinen Befehlsbindungen enthalten ist, und ruft die entsprechende Ereignisbehandlungsmethode auf. Gegenüber der herkömmlichen Programmierung mit Ereignissen haben Sie mit Befehlsbindungen nicht mehr allzu viele Vorteile, da Sie ja auch einen Ereignishandler zur Verfügung stellen müssen. Ein Programm mit Befehlsbindungen ist nicht unbedingt sehr übersichtlich. Ein Vorteil ist aber, dass Sie wie im Beispiel gezeigt die automatische Lokalisierung der Beschriftungen der WPF-Elemente nutzen können.
14.2.7 Befehlsbindungen können Sie für eigene Befehle einsetzen
Befehlsbindungen für eigene Befehle
Befehlsbindungen an Ereignishandler können Sie auch für eigene Befehle einsetzen. Dazu benötigen Sie ein statisches Feld vom Typ RoutedCommand oder RoutedUICommand, das auf eine neue Instanz dieser Klasse gesetzt wird. Dem RoutedCommand-Konstruktor übergeben Sie den Namen des Befehls und den Typ, für den der Befehl registriert ist (also in der Regel der Typ des Fensters, in dem Sie den Befehl einsetzen). Dem RoutedUICommand-Konstruktor übergeben Sie am ersten Argument zusätzlich den Text, der auf der Benutzeroberfläche dargestellt werden soll. RoutedUICommand hat wie bereits gesagt für das Binden an Elemente der Benutzeroberfläche den Vorteil, dass diese (über Datenbindung) den Text des Befehls auslesen können (was ich im Beispiel nutze). Der Rest ist dann ein einfaches Binden des Befehls an WPF-Elemente (oder Tastenkombinationen oder Maus-Gesten) und an Ereignishandler, die Sie zur Auswertung zur Verfügung stellen. Das Binden des neuen Befehls kann im XAML-Code erfolgen (wie im vorhergehenden Beispiel) oder auch im Programm. Da der Befehl im Programm erzeugt wird, würde ich ein Binden im Programm bevorzugen. Dazu fügen Sie im Konstruktor des jeweili-
896
Befehle
gen Fensters dessen CommandBindings-Auflistung ein neues CommandBinding-Objekt hinzu, das mit dem Befehl und einer Callback-Methode initialisiert wird. In der Callback-Methode schließlich implementieren Sie den Programmcode des Befehls. Listing 14.26 zeigt das am Beispiel eines einfachen WPF-Fensters, das einen DemoBefehl definiert. Listing 14.26: Ein eigener Befehl über die Bindung einer RoutedCommand-Instanz an einen Ereignishandler
12
/* Interaktions-Logik für MainWindow.xaml public partial class MainWindow : Window { /* Verwaltet einen eigenen Demo-Befehl public static readonly RoutedUICommand new RoutedUICommand("Demo", "Demo",
13
*/
*/ DemoCommand = typeof(MainWindow));
14
/* Konstruktor. Erzeugt die Steuerelemente und Komponenten. */ public MainWindow() { InitializeComponent();
15
// Den Demo-Befehl der CommandBindings-Eigenschaft hinzufügen this.CommandBindings.Add(new CommandBinding(MainWindow.DemoCommand, this.HandleDemo)); }
16
/* Ereignishandler für den Demo-Befehl */ private void HandleDemo(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("Demo"); }
17
}
Einen solchen Befehl können Sie prinzipiell wie jeden anderen an WPF-Elemente (und anderes) binden:
18 Listing 14.27: Binden des eigenen Befehls an einen Schalter
Ob diese Technik im Vergleich zu der traditionellen Technik über Ereignisbehandlungsmethoden, die den Ereignissen der WPF-Elemente zugewiesen sind, Vorteile besitzt, müssen Sie selbst entscheiden. Eine Anwendung, die bereits mit den vordefinierten Befehlen arbeitet, sollte in meinen Augen für die selbst programmierten Teilprogramme auch (eigene) Befehle einsetzen, um die Kontinuität zu wahren. Eigene geroutete Befehle bieten zudem den Vorteil, dass der Text der gebundenen WPF-Elemente zentral verwaltet wird.
19
20
21
22 INFO
23
897
Wichtige WPF-Techniken
14.2.8 Verknüpfen von Befehlen mit Tastenkombinationen oder mit der Maus Befehle können mit der Tastatur und der Maus verknüpft werden
Befehle können (natürlich) auch mit Tastenkombinationen verknüpft werden, aber auch mit der Maus. Microsoft unterscheidet dabei die folgenden Begriffe: ■
■
Eingabe-Gesten (Input Gestures): Eingabe-Gesten sind vordefinierte Tastenkombinationen oder Mausbetätigungen, die mit einem gerouteten Befehl direkt verknüpft sind. Der Befehl Copy ist z. B. mit den Tastenkombinationen (STRG) + (C) und (STRG) + (EINFG) verknüpft. Eingabe-Bindungen (Input Bindings): Eingabe-Bindungen sind Bindungen zwischen der Tastatur oder der Maus (und später ggf. auch mit Sprache) und einem Befehl, die von außen (in XAML oder im Programm) definiert werden können. Damit können Sie eigene Tastenkombinationen mit einem Befehl verknüpfen.
Eingabe-Gesten sind bei den meisten Befehlen bereits vordefiniert. Sie können einem Befehl aber auch neue Eingabe-Gesten hinzufügen. Die Klasse RoutedCommand stellt dazu die Auflistung InputGestures zur Verfügung. Über die Add-Methode können Sie eine Instanz einer von InputGesture abgeleitete Klasse übergeben. WPF stellt dazu die Klassen KeyGesture und MouseGesture zur Verfügung. Das folgende Beispiel nutzt alle damit gegebenen Möglichkeiten, um dem PasteBefehl eine neue Tastenkombination und eine Maus-Geste zuzuweisen: Listing 14.28: Hinzufügen zweier Eingabe-Gesten zu dem Paste-Befehl /* Dem Paste-Befehl die neue Tastenkombination SHIFT + CTRL + F2 hinzufügen */ ApplicationCommands.Paste.InputGestures.Add( new KeyGesture(Key.F2, ModifierKeys.Shift | ModifierKeys.Control)); /* Dem Paste-Befehl eine Kombination aus dem Klicken der linken Maustatste mit gleichzeitiger Betätigung von SHIFT + CTRL hinzufügen */ ApplicationCommands.Paste.InputGestures.Add( new MouseGesture(MouseAction.LeftClick, ModifierKeys.Shift | ModifierKeys.Control));
KeyGesture definiert eine Tastenkombination
Am ersten Argument des KeyGesture-Konstruktors geben Sie einen Wert der Key-Aufzählung (aus System.Windows.Input) an. Am optionalen zweiten Argument geben Sie Modifiziertasten an (als Wert der ModifierKeys-Aufzählung). Die möglichen Werte sind None, Alt, Control, Shift und Windows. Sie können mehrere Modifiziertasten miteinander kombinieren, leider aber nicht mehrere Key-Werte. Eingabe-Bindungen definieren Sie in XAML über die InputBindings-Auflistung eines WPF-Elements. Das folgende Beispiel bindet auf diese Weise den Demo-Befehl (Seite 887) an die Tastenkombination (ª) +(STRG) + (F2): Listing 14.29: Binden eines Befehls an eine Tastenkombination
In Key geben Sie zur Definition der Tasten wieder die Werte der Key-Aufzählung an. Modifiers gibt wie bei Eingabe-Gesten die optionalen Modifiziertasten an. Sie können die Werte der ModifierKeys-Aufzählung in XAML über ein Plus-Zeichen miteinander kombinieren. Leider können Sie auch hier nicht mehrere Werte für die KeyEigenschaft angeben.
12
13
Sie können Befehlsbindungen (natürlich) auch im Programmcode definieren. In diesem Fall können Sie auch einen zusammengesetzten Modifizierer angeben: Listing 14.30: Binden eines Befehls im Programm
14
public partial class MainWindow : Window { /* Konstruktor. Initialisiert das Fenster. */ public MainWindow() { InitializeComponent();
15
// Binden des Demo-Befehls an die Tastenkombination // SHIFT + STRG + F3 this.InputBindings.Add(new System.Windows.Input.InputBinding( DemoCommand.Instance, new KeyGesture(Key.F3, ModifierKeys.Shift | ModifierKeys.Control)));
16
}
17
}
Damit haben Sie schon eine Menge an Möglichkeiten, den Anwendern Ihrer Programme die Arbeit zu erleichtern. Was zurzeit noch fehlt, ist die Möglichkeit, Befehle über Sprache zu steuern. WPF bietet zwar Sprachausgabe und auch Spracherkennung, aber leider keine direkte Möglichkeit, einen Befehl mit einer Sprach»Geste« zu verknüpfen.
14.3
18
19
Trigger
Trigger werden direkt in XAML eingesetzt und ermöglichen die deklarative Reaktion auf die Änderung einer Eigenschaft oder den Aufruf eines Ereignisses. Ein Trigger kann z. B. die Eigenschaft IsEnabed eines Schalters überwachen und dann, wenn der Wert true in diese geschrieben wird, ein anderes Bild im Schalter darstellen, als wenn false geschrieben wird. Das Ganze erfordert keine einzige Zeile Programmcode, sondern wird komplett (deklarativ) in XAML definiert. Trigger sind deswegen auch für Designer interessant, die selbst nicht programmieren können oder wollen.
20
21
XAML unterscheidet drei Arten von Triggern: ■ ■ ■
22
Eigenschaftentrigger reagieren auf die Änderung einer Abhängigkeitseigenschaft (sind also auf WPF-Objekte eingeschränkt) Datentrigger reagieren auf die Änderung einer beliebigen .NET-Eigenschaft Ereignistrigger reagieren auf den Aufruf eines Ereignisses und führen eine Animation aus oder spielen einen Sound ab
23
Im Folgenden gehe ich grundlegend auf diese drei Arten von Triggern ein.
899
Wichtige WPF-Techniken
14.3.1 Trigger sind normale Klassen
Trigger sind in Vorlagen enthalten, können in einem Stil vorliegen oder in Triggers verwaltet werden
Grundlagen zu Triggern
Trigger sind Instanzen von normalen WPF-Klassen. Ein Eigenschaftentrigger ist z. B. eine Instanz der Klasse Trigger. Prinzipiell können Sie diese auch im Programmcode hinzufügen. Trigger sind aber dafür vorgesehen, direkt in XAML verwendet zu werden. Das Anfügen eines Triggers kann prinzipiell auf drei Arten geschehen: ■ ■ ■
Als Teil einer Vorlage, als Teil eines Stils und (eingeschränkt) über das Anfügen eines Triggers an die Triggers-Auflistung, die alle WPF-Steuerelemente von FrameworkElement erben.
Als Teil einer Vorlage definiert ein Trigger das Basisverhalten eines Steuerelements. In der Vorlage des Button-Steuerelements sind z. B. drei Trigger enthalten, die das Aussehen des Schalters je nach Zustand (Aktiviert, betätigt) ändern. Vorlagen werden ab Seite 913 behandelt. In einem Stil, der das Aussehen und grundsätzliche Verhalten eines Steuerelements verändern kann, machen Trigger in vielen Fällen Sinn. Stile haben zunächst einmal den Vorteil, dass sie allgemein allen Instanzen einer Steuerelement-Klasse zugewiesen werden können. Da Stile in Form von Ressourcen auch so gespeichert werden können, dass diese austauschbar sind, können Sie mit Triggern in Stilen erreichen, dass Sie nicht nur das Aussehen, sondern auch das Verhalten eines Steuerelements verändern. Das Anfügen eines Triggers an die Triggers-Auflistung ist leider in .NET 3.5 nur für Ereignistrigger möglich. Eigenschaften- und Datentrigger werden von der TriggersAuflistung nicht unterstützt (angeblich deswegen, weil das WPF-Team einfach keine Zeit hatte, diese Unterstützung zu integrieren). Trigger nehmen ihre Änderungen automatisch zurück
Eine wichtige Besonderheit von Triggern ist, dass die Änderungen, die ein Trigger vornimmt, automatisch zurückgenommen werden, nachdem die Bedingung des Triggers nicht mehr erfüllt ist. Wenn Sie z. B. über einen Eigenschaftentrigger eine Eigenschaft ändern, wird deren Wert automatisch zurückgesetzt, wenn die Eigenschaft, die der Trigger überwacht, nicht mehr den Wert aufweist, die den Trigger auslöst. Dieses Verhalten ist sehr nützlich, da Sie sich keine Gedanken darüber machen müssen, nach der Ausführung eines Triggers den Ursprungszustand wiederherzustellen.
Trigger-Einschränkungen Trigger können lokal gesetzte Werte nicht überschreiben
INFO
900
Trigger haben eine wesentliche Einschränkung, die sehr wichtig zu wissen ist: Trigger können lokal gesetzte Werte (also Werte, die Eigenschaften des Steuerelements direkt zugewiesen sind) nicht überschreiben. Wenn Sie also z. B. die Hintergrundfarbe eines Steuerelements über die Eigenschaft Background auf einen definierten Wert setzen, kann kein Trigger diese Farbe ändern. Dieses Verhalten ist zu erwarten. Wenn ich den Wert einer Eigenschaft explizit setze, möchte ich nicht, dass ein Trigger meine Werte einfach überschreibt. So weit, so gut. In einigen Fällen ist es aber notwendig, dass ein Eigenschafts-Wert explizit überschrieben wird. In diesem Fall kann auch ein Dekorator verwendet werden (in Form einer von Decorator abgeleiteten Klasse). Das Button-Steuerelement verwendet einen solchen in Form der ButtonChrome-Instanz. Ich gehe auf Dekoratoren kurz ab Seite 904
Trigger
ein. Ein Dekorator kann die Ansicht eines Steuerelements bedingungsabhängig verändern (oder komplett neu zeichnen). Im Falle des Button wird dessen Ansicht vom ButtonChrome-Dekorator (je nach eingestelltem Thema) komplett gezeichnet. Nur in normalem Zustand berücksichtigt dieser Dekorator die in Background gesetzte Hintergrundfarbe. Wenn die Maus auf dem Schalter liegt, der Schalter deaktiviert oder betätigt ist, verwendet der Dekorator einen für das jeweilige Windows-Thema definierten Hintergrund. Dieses Verhalten ist notwendig für solche Fälle, in denen gesetzte Eigenschaften überschrieben werden sollen (was ein Trigger ja nicht darf). Es macht das Umdefinieren des Designs eines Steuerelements aber auch sehr schwierig, wenn der Dekorator wie im Falle des ButtonChrome-Dekorators die gesetzten Eigenschaftswerte nicht in allen Fällen berücksichtigt.
14.3.2
12
13
Eigenschaftentrigger
Eigenschaftentrigger, die auf die Änderung einer Abhängigkeitseigenschaft reagieren, profitieren unter WPF von der Tatsache, dass Steuerelemente in der Regel eine Vielzahl von Eigenschaften besitzen, deren Name mit »Is« beginnt und die eine Aussage über einen Status machen. So sagt die Eigenschaft IsMouseOver z. B. aus, ob sich die Maus gerade über einem Steuerelement befindet. Über einen Eigenschaftentrigger können Sie auf die Änderung dieser (und beliebiger anderer Abhängigkeitseigenschaften) reagieren und andere Eigenschaften des Objekts mit einem neuen Wert versehen, wenn die überwachte Eigenschaft einen bestimmten Wert aufweist.
Eigenschaftentrigger reagieren auf die Änderung einer Eigenschaft
Eigenschaftentrigger sind Instanzen der Klasse System.Windows.Trigger. Diese Klasse stellt u. a. die folgenden Eigenschaften zur Verfügung:
Die Klasse Trigger stellt die Funktionalität zur Verfügung
■
Property: Gibt die zu überwachende Eigenschaft an.
■
Value: Gibt den Wert an, der dazu führt, dass der Trigger ausgelöst wird.
■
Setters: Auflistung von Setter-Objekten. Ein Setter-Objekt bestimmt ggf. ein Unterobjekt des Steuerelements (auf dem der Trigger arbeitet) und eine Eigenschaft, die vom Eigenschaftentrigger gesetzt wird, wenn er ausgelöst wird. Die Setter-Klasse stellt dazu die folgenden Eigenschaften zur Verfügung:
14 15
16
17
18
■
Property: Der Name der Zieleigenschaft
■
Value: Der Wert, der in die Zieleigenschaft geschrieben werden soll) zur Verfügung
■
TargetName: Name des Zielobjekts, falls dies ein Unterelement im visuellen Baum des Steuerelements ist (kann nur für Trigger in Vorlagen angewendet werden)
20
Eigenschaftentrigger können Sie leider (zurzeit) nicht in die Triggers-Auflistung der Steuerelemente schreiben. Um einen Eigenschaftentrigger zu definieren, müssen Sie diesen in einem Stil oder in einer Vorlage angeben. Stile werden ab Seite 905 behandelt. Ich muss hier also ein wenig vorgreifen.
21
Wenn Sie einen Eigenschaftentrigger in einem Stil definieren, kann es allerdings sein, dass er (teilweise) keine Wirkung hat. Das liegt zum einen daran, dass lokal gesetzte Werte Vorrang vor Triggern haben. Zum anderen verwenden einige Steuerelemente wie z. B. der Button zum Zeichnen einen Dekorator, der in bestimmten Fällen die Eigenschaften des Steuerelements nicht berücksichtigt. Wenn Sie z. B. einen Eigenschaftentrigger für einen Schalter definieren, der den Hintergrund des Schalters (über die Eigenschaft Background) neu definiert, wenn die Maus auf dem Schalter liegt, ist
19
22 HALT
23
901
Wichtige WPF-Techniken
dieser unter Vista unwirksam, weil der ButtonChrome-Dekorator in dem Fall die Background-Eigenschaft ignoriert und den Vista-Standard-Hintergrund für Schalter verwendet. Um dieses Problem zu lösen, könnten Sie die Vorlage des Steuerelements neu definieren, was aber etwas mehr Arbeit ist. Ab Seite 913 zeige ich, wie das geht. Den Stil eines Steuerelements können Sie direkt ändern, indem Sie dessen StyleEigenschaft mit einem neuen Style-Objekt versehen. So können Sie z. B. eine TextBox definieren, die beim Fokuserhalt Ihre Vorder- und Hintergrundfarbe ändert: Listing 14.31: TextBox, die beim Fokuserhalt ihre Vorder- und Hintergrundfarbe ändert
In der Praxis würden Sie den Stil nicht innerhalb der TextBox, sondern separat definieren. Stile werden ab Seite 905 behandelt. INFO MultiTrigger erlaubt die Definition eines Triggers, der mehrere Eigenschaften überwacht
14.3.3 Multi-Eigenschaftentrigger Ein Multi-Trigger, der vom Typ MultiTrigger ist, erlaubt die Definition eines Triggers, der in der Lage ist, mehr als eine Eigenschaft zu überwachen. Die wesentliche Eigenschaft der Klasse ist Conditions. Conditions ist eine Auflistung von ConditionObjekten, die einzelne Bedingungen definieren. Eine Bedingung wird wie bei einem einfachen Datentrigger über die Eigenschaften Property und Value definiert. So können Sie z. B. eine TextBox definieren, die ihren Hintergrund rot darstellt, wenn sie den Fokus besitzt und keinen Text enthält: Listing 14.32: TextBox mit einem MultiTrigger
902
Trigger
14.3.4 Datentrigger und Multi-Datentrigger Datentrigger überwachen wie Eigenschaftentrigger die Änderung von Eigenschaften. Sie sind aber nicht für Abhängigkeitseigenschaften vorgesehen, sondern können auf allen (normalen) Eigenschaften angewendet werden. Die Eigenschaft, in die ein solcher Trigger den neuen Wert schreibt, muss allerdings eine Abhängigkeitseigenschaft sein.
Datentrigger überwachen normale Eigenschaften
12
Datentrigger werden normalerweise im Zusammenhang mit Daten-Vorlagen eingesetzt. Diese behandle ich grundlegend ab Seite 929. Im Prinzip arbeitet ein Datentrigger wie ein normaler Eigenschaftentrigger. Der wesentliche Unterschied ist, dass die zu Grunde liegende Klassen DataTrigger und MultiDataTrigger keine Eigenschaft Property zur Verfügung stellen. Die Bindung an die zu überwachende Eigenschaft erfolgt bei einem Datentrigger über Datenbindung in Form der Binding-Markuperweiterung. Über Datenbindung, die ab Seite 922 behandelt wird, sind wesentlich komplexere Bindungen möglich, als mit einem Eigenschaftentrigger.
13
14
Das Beispiel aus dem Eigenschaftentrigger-Abschnitt können Sie über einen Datentrigger so programmieren (was hier nicht viel Sinn macht, aber zeigt, wie Datentrigger verwendet werden):
15
Listing 14.33: Ein einfacher Datentrigger, der einen Eigenschaftentrigger emuliert
16
17
18
14.3.5 Ereignistrigger Ereignistrigger reagieren auf geroutete Ereignisse (nicht auf normale). Sie werden ausschließlich in Zusammenhang mit Animationen und der Ausgabe von Sound verwendet. Ein Ereignistrigger kann beim Eintreten eines Ereignisses z. B. über das SoundPlayer-Element eine Wave-Datei (.wav) abspielen.
19 Ereignistrigger reagieren auf Ereignisse und steuern Animationen oder Sound
Ereignistrigger werden in EventTrigger-Elementen definiert. Die Eigenschaft RoutedEvent gibt das Ereignis an, das der Trigger überwacht. Die Auflistung Actions kann mit Aktionen belegt werden, die der Trigger ausführen soll, wenn das Ereignis eintritt. Die hier einsetzbaren Typen sind von der Basisklasse System.Windows.TriggerAction abgeleitet. In .NET 3.5 stehen einige vordefinierte Aktions-Klassen wie z. B. SoundPlayerAction (Abspielen einer Wave-Datei), BeginStoryboard (Starten einer Animation), PauseStoryboard (Anhalten einer Animation), ResumeStoryboard (WeiterAusführen einer Animation) und StopStoryboard (Beenden einer Animation) zur Verfügung.
20
21
22
23
Im Abschnitt »Animationen« (Seite 933) finden Sie ein einfaches Beispiel, das Ereignistrigger zum Starten von Animationen einsetzt. Das folgende Beispiel spielt beim Betreten eines Schalters mit der Maus und bei der Betätigung jeweils einen Sound ab:
903
Wichtige WPF-Techniken
Listing 14.34: Button mit Ereignistriggern, die bei Mauskontakt und beim Klicken Sounds abspielen
INFO
Falls Sie sich fragen, ob Sie auch MP3-Dateien abspielen können: Ja, prinzipiell schon (über eine MediaPlayer-Instanz, die Sie im Programm erzeugen). Ein Ereignistrigger unterstützt aber nur den SoundPlayer, der nur Wave-Dateien abspielen kann. Um MP3-Dateien abzuspielen, können Sie klassisch programmieren, indem Sie die entsprechenden Ereignisse abfangen, im Ereignishandler eine MediaPlayer-Instanz erzeugen und diese zum Abspielen des Sounds verwenden. Alternativ können Sie einen Ereignis-Setter in einem Stil verwenden. Einen solchen setze ich im Abschnitt »Ereignis-Setter« (Seite 912) genau für diesen Zweck ein.
14.4 Dekoratoren zeichnen Objekte oder statten diese mit neuen Effekten aus
Dekoratoren
Dekoratoren sind ein weiteres Feature, das WPF nutzt, um das Aussehen oder Verhalten eines Steuerelements dynamisch zu ändern. Ein Dekorator »dekoriert« ein Steuerelement mit neuen Effekten. Beim Button sorgt z. B. der ButtonChrome-Dekorator für die unterschiedliche Ansicht des Schalters unter den verschiedenen Windows-Themen. Der ViewBox-Dekorator ist in der Lage, das dekorierte Element zu skalieren: Demo
Wird eine Viewbox auf einem Fenster angelegt und so definiert, dass sie sich immer mit dem Fenster in der Größe verändert (was beim obigen Beispiel keine Änderung erfordert), wird der Inhalt nun automatisch skaliert. Dieses Skalieren geschieht aber eben nach dem WPF-Dekorator-Muster: Ein Dekorator fängt (zumindest) die OnRender-Methode des enthaltenen Steuerelements ab und ist darüber in der Lage, den Inhalt des Steuerelements komplett neu zu zeichnen. Beim Viewbox-Dekorator wird der Inhalt entsprechend den in der Viewbox-Instanz eingestellten Eigenschaften skaliert ausgegeben. ButtonChrome zeichnet einen Button je nach Thema unterschiedlich
904
Der ButtonChrome-Dekorator arbeitet im Prinzip genauso. Er fängt OnRender ab und zeichnet den Inhalt des Schalters. Dabei berücksichtigt er Eigenschaften des Objekts, das er zeichnet, wie z. B. IsEnabled, IsPressed und IsMouseOver, und zeichnet den Schalterinhalt je nach Zustand anders.
Stile, Vorlagen, Skins und Themen
Wenn Sie sich jetzt fragen, warum der Schalter (wie viele andere Steuerelemente auch) einen Dekorator einsetzt und sich nicht einfach selbst zeichnet: Die Klasse ButtonChrome existiert mehrfach in verschiedenen Assemblys. Jede dieser Assembly steht für ein Windows-Thema. Die Assembly PresentationFramework.Aero.dll steht z. B. für das Aero-Thema von Vista, PresentationFramework.Royale.dll für das XPThema. Jede dieser ButtonChrome-Klassen zeichnet den Schalter anders. Die Grundfunktionalität des Schalters ist aber in der allgemeinen Assembly PresentationFramework.dll definiert. Auf diese Weise muss nur die Themen-Assembly ausgetauscht werden, um den Steuerelementen eine andere Ansicht zu geben.
12
Prinzipiell könnte eine Änderung der Ansicht auch über einen Trigger (in der Vorlage des Steuerelements) implementiert werden. Ein Trigger besitzt aber keinen Vorrang vor lokal gesetzten Eigenschaften. Ein Dekorator wertet diese Eigenschaften aus und kann selbst entscheiden, ob er sie anwendet. Der ButtonChrome-Dekorator ignoriert z. B. (je nach Thema) die Background-Eigenschaft, wenn die Eigenschaft IsPressed true oder die Eigenschaft IsEnabled false ist, und zeichnet stattdessen den für das jeweilige Thema definierten Hintergrund.
13
14
Dekoratoren sind aber in der Praxis noch ein wenig komplexer. Ich kann hier leider nicht weiter darauf eingehen. Schauen Sie sich z. B. den ButtonChrome-Dekorator im .NET Reflector an, um ein Gefühl dafür zu bekommen. Sie können natürlich auch eigene Dekoratoren implementieren. Ein Grund dafür kann sein, dass Sie eine neue Vorlage für ein Steuerelement erzeugen und das Steuerelement dann speziell ausgeben wollen, wenn bestimmte Bedingungen erfüllt sind. Das ist z. B. dann der Fall, wenn ein deaktiviertes Steuerelement so ausgegeben werden soll, dass es auch deaktiviert erscheint. Mit einem Trigger können Sie nicht viel ausrichten, da ein solcher keinen Vorrang vor lokal gesetzten Eigenschaftswerten besitzt. Ein Dekorator kann aber das Zeichnen abfangen und ist somit in der Lage, das Steuerelement komplett anders zu zeichnen.
14.5
15 Eigene Dekoratoren werden hauptsächlich in Vorlagen eingesetzt
16
17
18
Stile, Vorlagen, Skins und Themen
Stile, Vorlagen, Skins und Themen sind Begriffe, die in WPF immer wieder vorkommen. Das liegt wohl daran, dass WPF selbst massiv auf diesen Techniken aufsetzt. In Kapitel 12 habe ich bereits eine grundlegende Einführung in diese Begriffe gegeben. Lesen Sie ggf. dort noch einmal nach.
19
In diesem Abschnitt geht es nun darum, eigene Stile, Vorlagen und Skins zu erzeugen und anzuwenden.
20
14.5.1
Stile
Stile definieren das Aussehen eines oder mehrerer Steuerelemente dadurch, dass in ihnen die Werte von Eigenschaften angegeben sind. Außerdem können Stile Trigger beinhalten, die das Verhalten eines Steuerelements in gewissen Grenzen bestimmen (eben in den Grenzen, die einem Trigger auferlegt sind). In WPF werden mehrere Stile zu Skins zusammengefasst, die das Aussehen aller (bzw. mehrerer) Elemente einer Oberfläche bestimmen. WPF enthält Skins für alle Windows-Themen des klassischen Windows, von XP und Vista und stellt den zum jeweiligen Windows-Thema passenden Skin automatisch ein.
Stile definieren das Aussehen und/oder Verhalten von Steuerelementen
21
22
23
905
Wichtige WPF-Techniken
Stile können Sie aber auch selbst definieren (und ggf. zu Skins zusammenfassen). Einen Stil können Sie der Style-Eigenschaft eines Steuerelements direkt zuweisen, wie Sie es in den vorhergehenden WPF-Beispielen dieses Buchs schon mehrfach gesehen haben. Üblicherweise werden Stile aber nicht einem Steuerelement direkt untergeordnet, sondern in Ressourcen definiert. Insgesamt gibt es vier Möglichkeiten, die die folgende Auflistung der Wichtigkeit nach ordnet: ■
■
■
■
Typisierte, unbenannte Stile sind in Ressourcen gespeichert und sind mit einer Typangabe versehen. Solche Stile werden nur auf die dem angegebenen Typ entsprechenden Steuerelemente angewendet. Typisierte, benannte Stile sind ebenfalls in Ressourcen gespeichert und mit einer Typangabe versehen, besitzen aber zusätzlich einen Namen. Solche Stile werden nur auf Steuerelemente angewendet, die dem angegebenen Typ entsprechen und denen der Stil über die Style-Eigenschaft explizit zugewiesen ist. Untypisierte, benannte Stile sind in Ressourcen gespeichert und besitzen lediglich einen Namen. In einem Steuerelement kann ein solcher Stil darüber angewendet werden, dass der Style-Eigenschaft die entsprechende Ressource zugeordnet wird. Inline-Stile sind direkt der Style-Eigenschaft eines Steuerelements untergeordnet.
Inline-Stile behandle ich hier nicht mehr, da Sie in den vorhergehenden Abschnitten genügende Beispiele dazu finden (z. B. im Abschnitt »Trigger«, Seite 899). Die weiteren Möglichkeiten werden in den folgenden Abschnitten besprochen.
Der Inhalt eines Stils Stile können die folgenden Inhalte besitzen: ■ ■
Eigenschafts-Setter, die den Wert von Eigenschaften definieren, und Trigger (in der Triggers-Auflistung).
Der erste Punkt hört sich harmlos an. In Wirklichkeit können Sie damit aber eine Menge ausrichten. Über die RenderTransform-Eigenschaft eines Steuerelements können Sie dieses z. B. rotieren, kippen, skalieren und verschieben. Und wenn eine solche Transformation in einem Stil definiert ist, wird sie automatisch für alle Steuerelemente angewendet, für die der Stil gilt. Und auch der zweite Punkt, Trigger, birgt eine Menge an Potenzial. Aber ich halte das Ganze (wie immer) sehr einfach. Die Möglichkeiten auszuloten ist dann Ihre Sache …
Die Deklaration einer Stil-Ressource Eine Stil-Ressource wird über die Style-Klasse deklariert. Die wichtigen Eigenschaften dieser Klasse sind: ■
■
906
TargetType: Wenn Sie in dieser Eigenschaft einen Zieltyp angeben, gilt der Stil nur für diesen und ggf. alle abgeleiteten Typen (dazu erfahren Sie mehr im Abschnitt »Stile für alle Instanzen eines Typs«). x:Key: Dieses XAML-Schlüsselwort bestimmt den Namen der Ressource und definiert damit einen benannten Stil.
Stile, Vorlagen, Skins und Themen
■
■
Setters: Dieser Auflistung (die als Inhaltseigenschaft deklariert ist und deswegen in XAML nicht angegeben werden muss) weisen Sie Setter-Instanzen zu, deren Eigenschaft Property die jeweils zu setzenden Eigenschaft bestimmt. Die Eigenschaft Value bestimmt den zu setzenden Wert. Triggers: In diese Auflistung können Sie Trigger schreiben, so wie Sie dies bereits im Abschnitt »Trigger« gesehen haben.
Der Bereich, für den der Stil angewendet wird, hängt nun von mehreren Faktoren ab: ■
■
■
12
Ist nur TargetType angegeben, gilt der Stil automatisch für alle Steuerelemente vom angegebenen Typ (bei der Angabe von Basistypen werden abgeleitete Typen aber in den meisten Fällen nicht berücksichtigt!). Ist nur x:Key angegeben, gilt der Stil nur für Steuerelemente, deren Style-Eigenschaft über StaticResource (oder DynamicResource) explizit auf diesen Stil gesetzt wird. Sind gleichzeitig TargetType und x:Key angegeben, gilt der Stil nur für Steuerelemente, deren Style-Eigenschaft auf die Stil-Ressource gesetzt ist und deren Typ in TargetType angegeben ist.
13
14 15
Da das Ganze ein wenig komplex ist, beschreibe ich in den folgenden Abschnitten die einzelnen Möglichkeiten.
Stile für alle Instanzen eines Typs
16
Die häufigste Anwendung von Stilen ist die Definition für eine ganze Klasse von Steuerelementen. Dazu versehen Sie den Stil lediglich mit der TargetType-Eigenschaft, die den Typ festlegt. So können Sie z. B. in der Ressource eines Fensters einen Stil für alle Button-Steuerelemente definieren:
17
Listing 14.35: Ein Stil für alle Instanzen eines Typs auf einem Fenster
19
20
21 ...
Listing 14.42: Ableiten von Stilen von einem typisierten Stil einer Basisklasse
22
...
23
911
Wichtige WPF-Techniken
Ereignis-Setter Ereignis-Setter behandeln Ereignisse in einem Stil
Das letzte in diesem Buch behandelte Stil-Thema sind Ereignis-Setter. Ein EreignisSetter kann einem Stil direkt untergeordnet werden. Er reagiert auf ein Ereignis mit dem Aufruf eines Ereignishandlers. Ereignis-Setter geben Sie in Form von Instanzen der EventSetter-Klasse an, wobei Sie die Eigenschaften Event und Handler definieren. Ich denke, die Namen sprechen für sich. Handler weisen Sie einen Ereignishandler zu. Dieser wird immer dann automatisch aufgerufen, wenn das Ereignis, das in Event angegeben ist, auftritt. Sind im Programm Ereignishandler zugewiesen, werden diese allerdings vorher aufgerufen.
Über Ereignis-Setter können Sie eine allgemeine Reaktion auf Ereignisse in einem Stil für alle Steuerelemente definieren, denen der Stil zugewiesen wird. Ich halte allerdings nicht allzu viel von dieser Technik, da sie einen Stil mit einem Programm verknüpft. Außerdem müssen Sie beachten, dass die in den Ereignis-Setter definierten Ereignisse auch ganz normal an Ereignishandler gebunden sein können, die das Ereignis auch abbrechen können. Für hochentwickelte Stile machen Ereignis-Setter aber ggf. Sinn. So können Sie damit z. B. das Problem umgehen, dass Ereignistrigger keine MP3-Dateien abspielen können: Listing 14.43: Stil mit der Deklaration von zwei Ereignis-Settern
Listing 14.44: Die partielle Klasse des Fensters, das den Programmcode für die Ereignishandler enthält public partial class MainWindow : Window { /* MediaPlayer-Instanz für die Ereignis-Setter */ MediaPlayer mediaPlayer; /* Konstruktor. Initialisiert das Fenster. */ public MainWindow() { InitializeComponent(); this.mediaPlayer = new MediaPlayer(); this.mediaPlayer.Open(new Uri("3kStatic-Recon.mp3", UriKind.Relative)); } /* Behandelt das MouseEnter-Ereignis für Schalter */ private void Button_MouseEnter(object sender, RoutedEventArgs e) { this.mediaPlayer.Stop(); this.mediaPlayer.Play();
912
Stile, Vorlagen, Skins und Themen
} /* Behandelt das MouseLeave-Ereignis für Schalter */ private void Button_MouseLeave(object sender, RoutedEventArgs e) { this.mediaPlayer.Stop(); } }
In der Praxis wäre es natürlich sinnvoll, solche Stile inklusive Programmcode in Assemblys auszulagern, diese zu referenzieren und die Ressourcen der Anwendung zuzuweisen (damit die Stile angewendet werden). Das Zuweisen von Vorlagen (mit integrierten Stilen) aus externen Dateien behandelt der Abschnitt »Skins und Themen« (Seite 919).
12 INFO
13
14.5.2 Vorlagen WPF-Steuerelemente sind, wie Sie ja bereits wissen, im visuellen Baum meist recht komplex aufgebaut. Ein Button besteht z. B. im visuellen Baum aus einem ButtonChrome-Objekt, das eine ContentPresenter-Instanz enthält (die den Inhalt darstellt).
Vorlagen definieren den visuellen Baum eines Steuerelements
Der visuelle Baum eines Steuerelements wird nun über eine Vorlage (Template) definiert. Eine Vorlage enthält üblicherweise aber nicht nur die Definition des visuellen Baums, sondern auch den Default-Stil des Steuerelements und Trigger.
14 15
16
Die Default-Vorlagen der Steuerelemente sind in Ressourcen als Teil eines Skins gespeichert. Ein Skin ist im Wesentlichen nur eine Zusammenfassung aller Ressourcen, die das Aussehen der Steuerelemente beeinflussen. WPF enthält Skins für die Default-Windows-Themen. Darauf komme ich kurz im Abschnitt »Skins und Themen« (Seite 919) zurück.
17
Auslesen einer Vorlage Sie können die einem Steuerelement zugewiesene Vorlage über die Eigenschaft Template auslesen. Das folgende Listing zeigt, wie Sie die Vorlage einer ButtonInstanz (btnDemo) formatiert auslesen und in eine TextBox (txtResult) schreiben:
Template gibt Zugriff auf die Vorlage
18
19
Listing 14.45: Auslesen der Vorlage eines Steuerelements // StringBuilder und mit diesem einen TextWriter erzeugen StringBuilder stringBuilder = new StringBuilder(); using (TextWriter textWriter = new StringWriter(stringBuilder)) { // Mit dem TextWriter einen XmlTextWriter erzeugen using (XmlTextWriter xmlTextWriter = new XmlTextWriter(textWriter)) { // Die Formatierung einstellen xmlTextWriter.Formatting = Formatting.Indented; xmlTextWriter.Indentation = 3;
20
21
// Über einen XamlWriter die Vorlage über den XmlTextWriter // formatiert ausgeben System.Windows.Markup.XamlWriter.Save( this.btnDemo.Template, xmlTextWriter); this.txtResult.Text = stringBuilder.ToString();
22
}
23
}
913
Wichtige WPF-Techniken
INFO
Leider erfordert diese Technik, dass Sie eine Instanz des Steuerelements besitzen, dessen Vorlage gelesen werden soll. Alle meine Versuche, Vorlagen für beliebige Steuerelement-Klassen auszulesen, sind bisher gescheitert. Falls ich eine Lösung finde, erfahren Sie dies über den Blog zum Buch. Abbildung 14.7 zeigt das (etwas nachformatierte) Ergebnis einer entsprechenden Anwendung, die Sie neben den Beispielen zu diesem Abschnitt finden.
Abbildung 14.7: Das BeispielProgramm hat die Vorlage des Button-Steuerelements unter Vista ausgelesen
Wie Sie sehen, enthält die Vorlage ein ButtonChrome-Objekt (welches ja ein Dekorator ist) und dieses einen ContentPresenter.
Eigene Vorlagen Vorlagen können neu definiert werden
914
Das Besondere an Vorlagen ist, dass Sie diese auch komplett neu definieren können. Damit können Sie die Ansicht und das Verhalten aller Steuerelemente in WPF verändern. Das ist ein sehr interessantes Feature für den Fall, dass eine Anwendung in einem speziellen Design erscheinen soll, z. B. um die Corporate Identity der Firma darzustellen, die diese Anwendung an ihre Kunden verteilt. Leider ist die Definition von Vorlagen eine sehr aufwändige Angelegenheit. Dies hat zumindest bereits eine Firma erkannt und stellt auf der Website www.xamltemplates.net kostenpflichtige Skins mit speziellen Vorlagen zur Verfügung. Schauen Sie einmal dort hinein, um eine Vorstellung davon zu bekommen, auf welch vielfältige Weise Sie das Design der WPF-Steuerelemente verändern können.
Stile, Vorlagen, Skins und Themen
Eigene Vorlagen müssen aber nicht unbedingt das Design aller Steuerelemente verändern. Vielleicht wollen Sie ja nur Schalter erzeugen, die anders aussehen als der normale Schalter. Sie können die Vorlage eines Steuerelements ähnlich des Stils direkt verändern, indem Sie eine neue Vorlage in die Template-Eigenschaft schreiben. Der übliche Weg ist aber, Vorlagen in einer Ressource zu definieren und diese den Steuerelementen zuzuweisen. Dabei können Sie zwei Wege gehen: ■
■
Vorlagen werden üblicherweise in Ressourcen definiert
Sie definieren die Vorlage als Instanz von ControlTemplate als direkte Ressource, geben der Ressource einen Schlüssel und weisen diese den Steuerelementen (in deren Template-Eigenschaft) über StaticResource (oder DynamicResource) zu, oder Sie definieren die Vorlage als Teil eines Stils in einer Ressource, indem Sie diese über einen Setter in die Template-Eigenschaft schreiben.
12
13
Der zweite Weg ist der flexiblere, weil Sie damit zum einen Vorlagen und Stile gleichzeitig verwenden und Sie zum anderen über typisierte, unbenannte Stile Vorlagen auch allen Instanzen einer Steuerelement-Klasse zuweisen können. Wegen des permanenten Platzmangels in diesem Buch zeige ich nur die zweite Variante. Ich denke, Sie können sich vorstellen, wie die erste implementiert wird.
14 15
Bei der Entwicklung von einfachen Vorlagen gelten zunächst die folgenden Dinge: ■
■
■
Eine Vorlage wird in einer ControlTemplate-Instanz definiert. Der Inhalt des ControlTemplate-Objekts ist in XAML eine XAML-Definition der WPF-Elemente, die das Steuerelement darstellen sollen. Hier sind Sie vollkommen frei und können alle Steuerelemente einsetzen. Wenn die Vorlage explizit auf Eigenschaften zugreifen muss, die im Steuerelement gesetzt sind, kann dies über die TemplateBinding-Markuperweiterung geschehen, der in der Eigenschaft Property der Name der Eigenschaft des Steuerelements übergeben wird, die ausgelesen werden soll. Wenn die Vorlage Inhalt darstellen soll, verwenden Sie dazu idealerweise die leichtgewichtige ContentPresenter-Klasse.
16
17
18
Das folgende Beispiel erzeugt einen benannten Stil für Button-Steuerelemente mit einer einfachen Vorlage. Die Vorlage enthält zwei einfache Ellipse-Objekte, die den Schalter als Kreis darstellen. Über Vorlagen-Bindung werden verschiedene Eigenschaften der Button- Klasse ausgewertet und in entsprechende Eigenschaften der Ellipse-Objekte geschrieben:
19
20
Listing 14.46: Einfacher, noch inkompletter Stil für einen runden Schalter
21
22
23
915
Wichtige WPF-Techniken
Ein wenig Erklärung ist wohl notwendig: Die erste Ellipse im Grid erhält als Füllung den Brush, der im Button in die Eigenschaft Foreground geschrieben wurde. Die zweite Ellipse erhält als Füllung den Wert der Background-Eigenschaft. Diese Ellipse wird zusätzlich über eine Transformation verkleinert. Die erste Ellipse dient damit quasi als Rahmen. Transformationen werden ab Seite 932 am Beispiel beschrieben. Der ContentPresenter dient der Darstellung des Inhaltes des Steuerelements. Im Beispiel wird dieser in einer Kurzform verwendet, bei der nur die horizontale und vertikale Ausrichtung eingestellt wird.
HALT
Diese Kurzform ist nur möglich, wenn im ControlTemplate-Element der Zieltyp angegeben ist. Wenn Sie Vorlagen ohne Zieltyp erstellen (was ich hier nicht behandle), müssen Sie die Eigenschaft Content des ContentPresenter und andere Eigenschaften wie die für die Schriftart explizit über Vorlagen-Bindung einstellen. Im Beispiel würden die Übergabe des Inhalts und der Schriftart an den ContentPresenter nicht funktionieren, wenn im ControlTemplate-Element der TargetType nicht angegeben wäre. Das ist in diesem Fall etwas verwirrend, weil im Beispiel ja bereits der Stil an den Button-Typ gebunden ist. Sie könnten statt ContentPresenter auch ein ContentControl verwenden. ContentPresenter ist aber auf die Verwendung in Vorlagen optimiert und benötigt weniger Ressourcen. Die Verwendung eines TextBlock würde sich übrigens nicht anbieten, da dieser lediglich Text darstellen kann, der Inhalt eines Steuerelements aber ein beliebiger sein kann. ContentPresenter ist z. B. auch in der Lage, Bilder und zusammengesetzten Inhalt anzuzeigen. In Listing 14.47 wird dieser Stil einem Schalter zugewiesen. Dabei setzen Sie die Eigenschaft Background auf einen LinearGradientBrush: Listing 14.47: Schalter, dem der Stil zugewiesen ist, der die Vorlage enthält
916
Stile, Vorlagen, Skins und Themen
Abbildung 14.8 zeigt das erste Ergebnis. Abbildung 14.8: Der mit der Vorlage veränderte Schalter
12 Das Beispiel habe ich übrigens bewusst einfach gehalten. Ich denke, Sie können sich vorstellen, dass Sie die Vorlage auch komplexer gestalten können. So können Sie z. B. den LinearGradientBrush bereits in der Vorlage zuweisen (evtl. auch für den äußeren Kreis) und damit einen Schalter erzeugen, der bereits wesentlich schöner aussieht als der Standard-Schalter. Dies überlasse ich aber ganz Ihnen …
13
INFO
14 Eine Frage hatte ich bei diesem Beispiel schon: Woher kennen die Ellipse-Objekte ihre Breite und Höhe? Und woher kennt der ContentPresenter seinen Inhalt und seine Schriftart? Alle diese Werte sind ja schließlich nur im Button eingestellt und werden nicht explizit per Vorlagenbindung weitergegeben.
EXKURS
15
Um ehrlich zu sein: Ich weiß nur auf die erste Frage eine direkte und auf die zweite eine teilweise Antwort. Width und Height von Ellipse-Objekten sind per Voreinstellung mit Double.NaN eingestellt, was bedeutet, dass der Container (das Grid im Beispiel) die Breite und Höhe bestimmt. Das Grid setzt die Breite und Höhe auf den maximal verfügbaren Wert. Für das Grid selbst gilt dasselbe, es wird automatisch auf die Breite und Höhe seines logischen Parent-Objekts (dem Button) gesetzt, weil seine Eigenschaften Width und Height per Voreinstellung auch auf Double.NaN stehen.
16
17
Die Schriftarteigenschaften werden dem ContentPresenter per Eigenschaftswertvererbung (die ja bei Abhängigkeitseigenschaften möglich ist) vererbt.
18
Warum der ContentPresenter aber die Eigenschaften Content von Button übernimmt, ist mir nicht ganz klar. Möglicherweise verwendet die ContentPresenterKlasse dazu eine Datenbindung an seinen Template-Parent, der über die Eigenschaft TemplatedParent erreichbar ist.
19
Die Vorlage für den Schalter ist noch nicht fertig, da dieser nicht visuell auf eine Betätigung mit der Maus oder auf ein Herüberfahren reagiert und nicht anzeigt, dass er deaktiviert ist. Um dies zu erreichen, können Sie Trigger in die Vorlage integrieren (oder/und Dekoratoren). Das Vorgehen ist an sich einfach: Wie bei Stilen fügen Sie Ihre Trigger der Triggers-Auflistung der Vorlage hinzu.
Vorlagen können auch Trigger enthalten
20
21
Listing 14.48 erweitert den Beispiel-Schalter um Trigger für die Reaktion darauf, dass die Maus auf ihm liegt und dass er betätigt ist.
22 Listing 14.48: Erweiterte Vorlage mit Triggern
23
917
Wichtige WPF-Techniken
Eine Besonderheit des ersten Triggers ist, dass dieser die Eigenschaft Fill des untergeordneten Ellipse-Objekts ändert, das den äußeren Kreis darstellt. Der Setter in dem Trigger bezieht sich deswegen über die TargetName-Eigenschaft auf dieses Objekt (das über seine Name-Eigenschaft benannt ist). Abbildung 14.9 zeigt den Schalter in den Zuständen Unbetätigt, Maus darüber und Betätigt. Abbildung 14.9: Der veränderte Schalter in den bisher definierten drei Zuständen
Was nun noch fehlt, ist die veränderte Darstellung für den Fall, dass IsEnabled false ist. Wenn Sie bei einem deaktivierten Schalter z. B. die Hintergrundfarbe verändern wollen, können Sie dazu keinen Trigger einsetzen, da ein Trigger die lokal gesetzten Eigenschaftswerte nicht überschreiben kann. Als einzige Lösung dieses Problems bleibt ein eigener Dekorator. Das geht alles über den Rahmen dieses Buchs weit hinaus (immerhin werden Dekoratoren selbst im Buch »Windows Presentation Foundation
918
Stile, Vorlagen, Skins und Themen
Unleashed« nur am Rande erwähnt …). Im Blog-Eintrag www.wiredprairie.us/ journal/2006/09/wpf_decorators_build_your_own.html finden Sie eine nähere Erläuterung und ein gutes Beispiel (die Vorlage für einen eigenen Schalter). Das muss dann auch zum Thema Vorlagen reichen. Weitere interessante Teilbereiche sind noch: ■ ■ ■ ■
Vorlagen, die nicht an einen Typ gebunden sind (was meiner Meinung nach fraglich ist), Eigene Dekoratoren (wie gesagt …), komplexere Bindungen der Eigenschaften der in der Vorlage enthaltenen Steuerelemente an die Eigenschaften des TemplateParent (über normale Datenbindung), und das »Entführen« von eigentlich ungenutzten Abhängigkeitseigenschaften des Template-Parent für andere Zwecke (indem diese an Eigenschaften der Steuerelemente im visuellen Baum gebunden werden).
12
13
14
14.5.3 Skins und Themen Skins und Themen sind Begriffe, die in WPF (und in anderen Umgebungen) immer wieder verwendet werden. In diesem Abschnitt kläre ich kurz, was es mit diesen Begriffen unter WPF auf sich hat. Skins sind im Allgemeinen eine Technik, die es erlaubt, die Oberfläche einer Anwendung dynamisch zu ändern. Ein gutes Beispiel für eine Anwendung, die mit Skins arbeitet, ist Winamp (www.winamp.com). Winamp können Sie über verschiedene Skins in sehr unterschiedlicher Darstellung verwenden (zu der Zeit, als ich dieses Buch schreibe, existieren mehr als 2400 verschiedene Skins für Winamp).
15 Skins erlauben die Änderung einer Oberfläche
17
WPF enthält keine spezielle Technik zur Berücksichtigung von Skins. Das ist auch gar nicht notwendig, denn die in WPF eingebauten Techniken reichen aus. Ein Skin ist in WPF einfach eine Sammlung aus Ressourcen, die Vorlagen und/oder Stile beinhalten. Da Ressourcen auch in externen Dateien (XAML-Dateien oder RessourcenAssemblys) verwaltet werden können, ist es ein Leichtes, die Oberfläche einer Anwendung zu verändern. Wenn Sie eigene Skins anwenden wollen, laden Sie lediglich die Ressourcen-Datei oder binden Sie die Ressourcen-Assembly ein und laden die Ressourcen in Application.Current.Resources. Ein Beispiel finden Sie aber auch auf der Buch-DVD in Form des Projekts Skin-Demo. In diesem habe ich die freien Vorlagen von www.xamltemplates.net (mit freundlicher Genehmigung) in eine XAML-Datei (xamltemplates.net.freeskins.xaml) kopiert, ein wenig nachbearbeitet und dynamisch geladen. Das Laden (in der partiellen Klasse App) sieht folgendermaßen aus:
16
18
19
20 DISC
21
Listing 14.49: Dynamisches Laden eines Skin public partial class App : Application { /* Lädt die Ressourcen, wenn die Anwendung startet */ protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e);
22
23
// Das ResourceDictionary einlesen ResourceDictionary resourceDictionary = null; using (FileStream fs =
919
Wichtige WPF-Techniken
new FileStream("xamltemplates.net.freeskins.xaml", FileMode.Open)) { // Das Wurzel-Element einlesen resourceDictionary = (ResourceDictionary)XamlReader.Load(fs); } // Das ResourceDictionary in die Ressourcen der Anwendung schreiben this.Resources = resourceDictionary; } }
Weil WPF auf der Suche nach Vorlagen- oder Stil-Ressourcen diese nun in den Anwendungsressourcen findet, werden diese verwendet, sofern keine gleichnamigen Ressourcen auf niedriger Ebene (im Fenster z. B.) gefunden werden. Abbildung 14.10 zeigt das Beispiel im Default-Vista-Stil, Abbildung 14.11 im neuen, dynamisch geladenen Stil. Abbildung 14.10: Das Beispielprogramm im VistaDefault-Stil
Abbildung 14.11: Das Beispielprogramm im Stil der freien Vorlagen von www.xamltemplates.net
920
Stile, Vorlagen, Skins und Themen
Leider sind freie Skins für WPF scheinbar noch sehr rar. Auf der Seite www. xamltemplates.net finden Sie einige kommerzielle Skins, aber auch die Sammlung freier Vorlagen, die ich im Beispiel verwendet habe. WPF macht im Prinzip Ähnliches, nur dass die Ressourcen, die den Skin ausmachen, nicht in den Anwendungsressourcen verwaltet werden. WPF lädt die Skins aus einer separaten Assembly. Für jedes Windows-Thema enthält WPF eine eigene Assembly: ■ ■ ■ ■
WPF verwaltet Themen-Skins in Assemblys
12
PresentationFramework.Classic.dll: Enthält einen Skin und Dekoratoren für das klassische Windows. PresentationFramework.Luna.dll: Enthält Skins und Dekoratoren für die XPThemen. PresentationFramework.Aero.dll: Enthält Skins und Dekoratoren für das VistaThema Aero in normaler Farbgebung. PresentationFramework.Royale.dll: Enthält einen Skin und Dekoratoren für Windows XP Media Center Edition 2005 und Windows XP Tablet PC Edition.
13
14
Die Skins sind jeweils in BAML-Ressourcen gespeichert und dem logischen Ordner Themes untergeordnet. Mit dem .NET Reflector können Sie diese Ressourcen auslesen, wenn Sie das Add-In BAML Viewer integrieren (das Sie über die Seite www.codeplex.com/reflectoraddins finden) Abbildung 14.12 zeigt den Reflector mit der Anzeige der Ressource für den Vista-Skin.
15
Abbildung 14.12: Der .NET Reflector mit integriertem BAML Viewer
16
17
18
19
20
21 Sie können auch separate eigene Skins für die einzelnen Windows-Themen definieren, die dann automatisch von WPF je nach Thema geladen werden. Diese Technik geht aber weit über den Rahmen dieses Buchs hinaus. Ich denke, zu Themen ist damit auch genug gesagt. Für die wichtige Datenbindung muss auch noch Platz bleiben …
22
23
921
Wichtige WPF-Techniken
14.6 Datenbindung verknüpft zwei Eigenschaften miteinander
Datenbindung
Datenbindung ist neben den bisher behandelten Techniken eines der zentralen Themen in WPF. Datenbindung basiert auf der Klasse System.Windows.Data.Binding, die im Prinzip nichts anderes macht, als zwei Eigenschaften (in der Regel von verschiedenen Objekten) zu verknüpfen. Eine Eigenschaft ist die Quelleigenschaft, die andere die Zieleigenschaft. Sind zwei Eigenschaften auf diese Weise miteinander verbunden, erhält die Zieleigenschaft immer automatisch den Wert der Quelleigenschaft, wenn diese sich ändert. Neben dieser Ein-Wege-Datenbindung gibt es auch noch eine ZweiWege-Bindung (die hier aber nicht behandelt wird), bei der auch Änderungen in der Zieleigenschaft in die Quelleigenschaft zurückgeschrieben werden. Damit können Sie sehr viele Datenquellen an WPF-Elemente binden: WPF-Elemente, normale .NET-Objekte, Auflistungen (sehr interessant, um diese z. B. in einer ListBox darzustellen), XML-Daten oder an die unterschiedlichsten externen Datenquellen, die über ADO.NET oder LINQ to SQL erreichbar sind (wie z. B. SQL-ServerDatenbanken). Eine Einschränkung der Datenbindung sollten Sie aber kennen: Die Zieleigenschaft muss eine Abhängigkeitseigenschaft sein. Das Binden an normale Eigenschaften ist nicht möglich.
14.6.1
Einfache Datenbindung
Ein zunächst einfaches Beispiel ist das Binden der Content-Eigenschaft eines Label an die SelectedItems.Count-Eigenschaft einer ListBox: Listing 14.50: Binden der Content-Eigenschaft eines Label an die die SelectedItems.Count-Eigenschaft einer ListBox Eintrag 1 Eintrag 2 Eintrag 3 Selektierte Einträge:
Wenn Sie dieses Beispiel ausführen, wird das letzte Label, das an die ListBox gebunden ist, immer automatisch dann aktualisiert, wenn die Selektion der ListBox (bzw. die Count-Eigenschaft der SelectedItems-Auflistung) geändert wird. Abbildung 14.13: Das einfache Datenbindungs-Beispiel in Aktion
922
Datenbindung
Im Beispiel wird die Datenbindung in XAML über die Binding-Markuperweiterung eingestellt. Sie können die Datenbindung aber auch im Programmcode erstellen. Dazu erzeugen Sie eine neue Instanz der Binding-Klasse, setzen deren Eigenschaften und übergeben diese neben einer Referenz auf die Abhängigkeitseigenschaft, die gebunden werden soll, an die SetBinding-Methode des Zielobjekts. Für das Beispiel sieht dies folgendermaßen aus:
Datenbindung erfolgt über die Binding-Klasse
12
Listing 14.51: Einstellen der Datenbindung im Programmcode Binding binding = new Binding(); // Das Quellobjekt definieren binding.Source = this.lstDemo;
13
// Den Pfad zur Quelleigenschaft definieren binding.Path = new PropertyPath("SelectedItems.Count");
14
// Das Binding-Objekt an das Label anfügen this.lblSelectedItemsCount.SetBinding(Label.ContentProperty, binding);
Eine Alternative zum Aufruf der SetBinding-Methode des Zielobjekts ist der Aufruf der gleichnamigen statischen Methode der BindingOperations-Klasse:
15
BindingOperations.SetBinding(this.lblSelectedItemsCount, Label.ContentProperty, binding);
Der Vorteil dieser Variante ist, dass das Zielobjekt, das am ersten Argument übergeben wird, in ein DependencyObject gewrappt wird. Damit sind dann auch Bindungen an Objekte möglich, deren Klassen nicht von FrameworkElement oder FrameworkContentElement angeleitet sind.
16
Die Datenbindung im Programmcode hat den Vorteil, dass Sie diese dynamisch ausführen können. In der Praxis wird aber wahrscheinlich der Datenbindung in XAML der Vorzug gegeben.
17
Binding besitzt die folgenden wichtigen Eigenschaften:
18
■ ■ ■
■
■ ■
■
ElementName: Gibt den Namen des Quellobjekts an. Source: Gibt alternativ das Quellobjekt direkt an. Path: Diese Eigenschaft, die vom Typ PropertyPath ist, verwaltet den Pfad zu der Eigenschaft des Quellobjekts, die gebunden werden soll. Am Konstruktor übergeben Sie einen String, der den Pfad definiert. Der Pfad ist im Prinzip der rechte Teil der Referenzierung einer Eigenschaft in C# ohne den Objektnamen. In XAML können Sie den Pfad einfach an Path übergeben. RelativeSource: Diese Eigenschaft (die vom Typ RelativeSource ist) kann alternativ zu Source verwendet werden. Sie gibt das Quellobjekt relativ vom Zielobjekt an. XPath: Gibt den Pfad zur Quelleigenschaft an, wenn die Quelle eine XML-Datenquelle ist. Mode: Bestimmt die Richtung des Datenflusses über die Werte der BindingModeAufzählung. BindingMode.OneWay (Standard) sorgt z. B. dafür, dass der Datenfluss nur in Richtung vom Quell- zum Zielobjekt läuft. TwoWay erlaubt auch die Gegenrichtung. OneWayToSource stellt einen Datenfluss in Richtung vom Ziel zur Quelle ein. Converter: Diese Eigenschaft verwaltet einen optionalen Konvertierer für den übertragenen Wert (der die Schnittstelle IValueConverter implementiert).
19
20
21
22
23
923
Wichtige WPF-Techniken
14.6.2 Fehler bei der Datenbindung auswerten Die Datenbindung ignoriert Fehler
Die WPF-Datenbindung ignoriert Fehler stillschweigend. Wenn Sie z. B. im vorhergehenden Beispiel in Path einen Pfad zu einer nicht existierenden Eigenschaft angeben, wird im Label nichts angezeigt und es tritt keine Ausnahme auf. Warum dies so ist, habe ich nicht herausgefunden. Ich finde dieses Verhalten sehr eigenartig und sehr fehlerträchtig … Sie (und ich) müssen bei der Entwicklung aber damit umgehen. Eine Möglichkeit, Datenbindungsfehler herauszufinden, ist das Trace-Protokoll von WPF, das Sie über die folgenden Anweisungen für die Datenbindung ohne Konfiguration in eine Datei schreiben können: Listing 14.52: Einschalten des WPF-Trace-Protokolls für die Datenbindung im Programm PresentationTraceSources.DataBindingSource.Listeners.Add( new TextWriterTraceListener("WPF-Databinding.log")); PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.All; // Alles ausgeben
Näheres dazu finden Sie in Kapitel 9 (Grundlagen des Tracing) und 12 (Fehler in WPF auswerten).
REF
Weitere interessante Hinweise zur Fehlersuche bei der Datenbindung finden Sie im Blog von Beatriz Costa: www.beacosta.com/blog/?p=52. Der entsprechende Eintrag ist mit »How can I debug WPF bindings?« überschrieben (falls der Link nicht mehr funktioniert).
14.6.3 Relative Datenbindung Wenn Sie bei der Datenbindung nicht ElementName bzw. Source angeben, sondern RelativeSource, geben Sie das Quellobjekt relativ gesehen vom Zielobjekt aus an. Die Klasse RelativeSource, die auch eine Markuperweiterung ist, besitzt dazu einige statische Eigenschaften, die Sie auch in XAML verwenden können. Einige typische Anwendungen von RelativeSource sind: ■
Angabe des Zielobjekts als Quelle: {Binding RelativeSource={RelativeSource Self}, Path=...}:
■
Angabe des Vorlagen-Parent-Objekts (in einer Vorlage, um auf Eigenschaften des Steuerelements zuzugreifen): {Binding RelativeSource={RelativeSource TemplatedParent}, Path=...}:
■
Angabe des Quellobjekts als ein Parent-Objekt, das am ehesten einem angegebenen Typ entspricht: {Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Typ}}, Path=...}
RelativeSource wird meist in Vorlagen verwendet, und dort besonders, um auf Eigenschaften des Steuerelements zuzugreifen (des Vorlagen-Parent). Sie können aber auch bei normalen Steuerelementen mit RelativeSource so manchen Trick erreichen. So können Sie z. B. bei einer ProgressBar erreichen, dass diese ihren aktuellen Wert in einem Tooltipp anzeigt:
924
Datenbindung
Abbildung 14.14: Die ProgressBar mit gebundenem Tooltipp
12
Falls Sie sich dabei dieselben Gedanken machen wie ich: Die ProgressBar besitzt leider keine Möglichkeit direkt Text auf ihr auszugeben. Deswegen verwende ich den Tooltipp. Eine andere Anforderung für die Praxis wäre noch, statt der einfachen Zahl einen formatierten String auszugeben (vielleicht mit einem Prozentzeichen am Ende). Dieses Problem löse ich im Abschnitt »Konvertieren der gebundenen Werte« (Seite 930).
13 INFO
14
14.6.4 Das Beispiel für die folgenden Themen 15
In den folgenden Abschnitten, die die Bindung an Auflistungen (oder Datenmengen) zum Thema haben, verwende ich die einfache Klasse Sitcom als Basis für die Beispiele. Sitcom verwaltet Informationen zu einer Sitcom (Situational comedy = Situations-Comedy-Serie im TV):
16
Listing 14.53: Beispiel-Klasse zur Verwaltung von Informationen zu einer Sitcom public class Sitcom { /* Der Name der Sitcom */ public string Name { get; set; }
17
/* */ public int StartYear { get; set; }
18
/* Gibt an, ob die Sitcom vorwiegend im Studio gedreht wird/wurde */ public bool IsStudioSitcom { get; set; }
19 /* Ein Bild zur Sitcom */ public BitmapSource Image { get; set; } }
Eine Besonderheit in dieser Klasse ist der Typ BitmapSource, der die Daten einer Bilddatei verwaltet.
20
In den Beispielen rufe ich die Methode GetSitcoms auf, die eine ObservableCollection-Instanz zurückgibt:
21
Listing 14.54: Methode, die Beispiel-Sitcom-Daten liefert public ObservableCollection GetSitcoms() { // Ergebnis-Auflistung erzeugen ObservableCollection sitcoms = new ObservableCollection();
22
23
// Sitcoms mit deren Darstellern erzeugen sitcoms.Add(new Sitcom { Name = "My Name Is Earl",
925
Wichtige WPF-Techniken
StartYear = 2005, IsStudioSitcom = false, Image = BitmapFrame.Create( new Uri("Images/My Name is Earl.jpg", UriKind.Relative)) }); sitcoms.Add(new Sitcom { Name = "The Big Bang Theory", StartYear = 2007, IsStudioSitcom = true, Image = BitmapFrame.Create( new Uri("Images/The Big Bang Theory.jpg", UriKind.Relative)) }); sitcoms.Add(new Sitcom { Name = "Scrubs", StartYear = 2001, IsStudioSitcom = true, Image = BitmapFrame.Create( new Uri("Images/Scrubs.jpg", UriKind.Relative)) }); // Das Ergebnis zurückgeben return sitcoms; }
Eine Besonderheit dieser Methode ist die Rückgabe einer Auflistung vom Typ ObservableCollection. Die Bedeutung dieses Typs kläre ich im folgenden Abschnitt. In der Praxis könnte diese Methode die Daten auch aus einer Datenbank einlesen (über LINQ to SQL z. B.). Und nur falls Sie sich wundern: Die Sitcoms werden alle in der englischen Originalfassung verwaltet, weil ich zum einen gerade in Dublin wohne und zum anderen die englischen Originals lieber mag als die (in vielen Fällen schrecklich) ins Deutsche übersetzten.
14.6.5 Bindung an Auflistungen ItemsSource erlaubt die Bindung einer Auflistung
Listen-Steuerelemente können Sie über deren Eigenschaft ItemsSource an Auflistungen binden, die IEnumerable implementieren. In ItemsSource können Sie jede Auflistung schreiben, die diese Voraussetzung erfüllt. Auf eine Änderung der Auflistung kann das WPF-Element aber nur dann reagieren, wenn die Auflistung die INotifyCollectionChanged-Schnittstelle implementiert. Bei der Klasse ObservableCollection (aus dem Namensraum System.Collections.ObjectModel) ist dies bereits der Fall. Solche Auflistungen werden üblicherweise im Programm gefüllt und an das ListenSteuerelement gebunden. Sie können eine Auflistung aber z. B. auch als Ressource in der Anwendung speichern und in XAML binden:
Weil es sich hier nicht um ein WPF-Element handelt, an das gebunden wird, sondern um ein normales, wird die Source-Eigenschaft des Binding-Objekts mit der Ressource belegt. Schlüssel ist übrigens der Schlüssel der Ressource (was sonst …).
926
Datenbindung
Ein Unterschied bei der Bindung von Listen ist zur Bindung von Einzeldaten, dass Sie keinen Pfad angeben. Sie binden immer die kompletten Objekte. Das ListenSteuerelement gibt per Voreinstellung das aus, was die ToString-Methode der gebundenen Objekte zurückgibt. Wenn das Listen-Steuerelement andere Daten ausgeben soll, können Sie dazu eine Eigenschaft der gebundenen Objekte in der Eigenschaft DisplayMemberPath angeben:
DisplayMemberPath erlaubt die Auswahl der darzustellenden Eigenschaft
12
Andere Möglichkeiten sind Datenvorlagen (Seite 929) und Wertkonverter (Seite 930). Im Programm sieht das Ganze dann so aus wie in Listing 14.55. Dieses Beispiel erzeugt eine ObservableCollection, füllt diese im Loaded-Ereignis des Fensters mit Sitcom-Instanzen und bindet diese an eine ListBox. Als DisplayMemberPath wird die Eigenschaft Name angegeben:
13
14
Listing 14.55: Binden einer ListBox an eine Auflistung
15
... private void Window_Loaded(object sender, RoutedEventArgs e) { // Sitcoms "einlesen" this.sitcoms = this.GetSitcoms();
16
// Die Sitcoms an die Liste binden this.lstSitcoms.ItemsSource = this.sitcoms;
17
}
Das Besondere an dieser Datenbindung ist nun, dass das Listen-Steuerelement dynamisch auf eine Änderung der Liste reagiert. Wenn Sie z. B. eine Sitcom hinzufügen, wird diese automatisch in der Liste angezeigt.
18
Den Selektion berücksichtigen Wenn Sie Auflistungen (oder XML-Daten) an eine von Selector abgeleitete Klasse (z. B. eine ListBox) binden, verfolgt das Binding-Objekt die aktuelle Auswahl mit. Diese Technik können Sie einsetzen, um mehrere Listen, die an dieselbe Datenquelle gebunden sind, automatisch synchron zu halten. In der Praxis eignet sich diese Technik hervorragend für Master-/Detail-Beziehungen: Eine ListBox stellt z. B. eine Artikel-Kategorie dar, eine andere ist mit dieser synchronisiert und listet die Artikel der jeweils ausgewählten Kategorie auf.
IsSynchronizedWithCurrentItem ermöglicht eine Synchronisation
19
20
21
Einer selektierbaren Liste teilen Sie über die IsSynchronizedWithCurrentItem-Eigenschaft mit, dass diese die aktuelle Selektion berücksichtigen soll. So könnten Sie zwei Listen (die vielleicht unterschiedliche Eigenschaften anzeigen) an dieselbe Datenquelle binden und miteinander synchronisieren:
22
Listing 14.56: Zwei miteinander synchronisierte ListBox-Instanzen
23
Binden Sie nun an beide ListBox-Instanzen dieselbe Auflistung (im Beispiel eine Auflistung von Sitcom-Objekten), verändert die eine implizit ihre Selektion, wenn die Selektion der anderen geändert wird. Abbildung 14.15: Die synchronisierten ListBox-Steuerelemente
Und falls Sie sich dasselbe fragen, wie ich: Nein, mit Listen-Steuerelementen, die eine Mehrfachselektion zulassen, ist die Synchronisierung nicht möglich. INFO DataContext verwaltet eine Datenquelle für mehrere Objekte
14.6.6 Der Datenkontext Das Binden einer Datenquelle an mehrere Steuerelemente wird in der Praxis relativ häufig angewendet. So ist es z. B. üblich, die Objekte einer Auflistung in einer ListBox anzuzeigen und die Detaildaten in separaten Steuerelementen (was allerdings nicht in diesem Kapitel behandelt wird). Solche Fälle unterstützt WPF über einen Datenkontext: Alle von FrameworkElement und FrameworkContentElement abgeleiteten Klassen besitzen eine DataContext-Eigenschaft, der Sie ein DatenQuellobjekt zuweisen können. Wenn dies der Fall ist, müssen Sie bei der Datenbindung das Quellobjekt nicht mehr angeben. Da alle KindElemente den Wert der DataContext-Eigenschaft erben, gilt dies auch für diese. Wenn Sie also der DataContext-Eigenschaft eines übergeordneten Elements ein DatenQuellobjekt zuweisen, können Sie sich in allen untergeordneten WPF-Elementen in der Datenbindung darauf beziehen. Dazu geben Sie als Datenbindung lediglich Binding an:
Sie müssen natürlich die Datenquelle definieren: Listing 14.57: Definieren eines Datenkontextes private void Window_Loaded(object sender, RoutedEventArgs e) { // Sitcoms "einlesen" this.sitcoms = this.GetSitcoms(); // Die Sitcoms an den gemeinsamen Datenkontext // der ListBoxen binden this.DataContext = this.sitcoms; }
928
Datenbindung
14.6.7 Datenvorlagen Mit der einfachen, bisher besprochenen Datenbindung können Sie komplexe Daten nur in einer einfachen Form darstellen. Datenvorlagen erlauben nun für solche Daten eine beliebige Darstellung. Viele WPF-Steuerelemente besitzen dazu eine Eigenschaft vom Typ DataTemplate, in die Sie eine Vorlage schreiben können (was nichts anderes ist, als ein Baum beliebiger Steuerelemente, die an Teildaten gebunden werden). Die Klasse ContentControl besitzt dazu z. B. die Eigenschaft ContentTemplate, ItemsControl die Eigenschaft ItemTemplate.
Datenvorlagen erlauben die flexible Gestaltung der Daten-Ansicht
12
13
Innerhalb der Vorlage binden Sie die einzelnen Steuerelemente über die BindingMarkuperweiterung, ohne eine Quelle anzugeben (denn die ist ja bereits in dem übergeordneten Steuerelement definiert). Da Sie sich in der Regel auf untergeordnete Eigenschaften beziehen, geben Sie diese über die Path-Eigenschaft an.
14
So können Sie in einer ListBox auch mehrere Sitcom-Daten angeben: Listing 14.58: ListBox mit einer Datenvorlage
15
16
17
18 Abbildung 14.16: Die ListBox mit einer Datenvorlage
19
20
21
22
23
929
Wichtige WPF-Techniken
TIPP
Datenvorlagen können Sie auch einsetzen, wenn Sie das Steuerelement nicht per Datenbindung an Daten binden. Bei der ListBox z. B. funktionieren Datenvorlagen auch, wenn Sie die Einträge über die Items-Auflistung hinzufügen. In der Praxis werden Datenvorlagen häufig nicht wie im Beispiel direkt zugewiesen, sondern in einer Ressource gespeichert. Damit haben Sie die Möglichkeit, Datenvorlagen wiederzuverwenden oder themenspezifisch zu definieren. Sie können Datenvorlagen sogar – ähnlich Stilen – so in einer Ressource verwalten, dass diese automatisch zugewiesen werden, wenn die Datenquelle aus einem in der Datenvorlage angegebenen Typen besteht. Dafür geben Sie statt des Schlüssels der Ressource (x:Key) die Eigenschaft DataType an. Beispiele dafür finden Sie im Beispielprojektordner zu diesem Abschnitt auf der Buch-DVD.
14.6.8 Konvertieren der gebundenen Werte Ein IValueConverter-Objekt ermöglicht die Konvertierung
Die Binding-Markuperweiterung besitzt eine Eigenschaft Converter, über die Sie den geschriebenen Wert konvertieren können. In diese Eigenschaft schreiben Sie eine Instanz eines Typs, der die Schnittstelle IValueConverter implementiert. IValueConverter definiert die beiden Methoden Convert und ConvertBack. Convert wird aufgerufen, wenn der Wert von der Quelle in das Ziel beschrieben wird. ConvertBack ist für die Zwei-Wege-Datenbindung vorgesehen, bei der ein Wert auch in die Datenquelle zurückgeschrieben werden kann. Beide Methoden erhalten am ersten Argument den zu schreibenden Wert, am zweiten den Ziel-Typ, am dritten optionale Parameter (die Sie der Binding-Markuperweiterung in der Eigenschaft ConverterParameter übergeben können) und am letzten Argument die aktuelle Kultur übergeben. Für einfache Konvertierungen müssen Sie nichts weiter machen, als den übergebenen Wert zu verarbeiten und den konvertierten Wert zurückzugeben. Dabei sind Sie natürlich vollkommen frei. Sie können den Wert (sofern es ein einfacher ist) einfach nur um zusätzliche Informationen erweitern, umrechnen oder in einen komplett anderen Typ transformieren. Ich löse mit der Konvertierung der gebundenen Werte das Problem aus dem Abschnitt »Relative Datenbindung« (Seite 924), bei dem im Tooltipp nur der aktuelle Wert der ProgressBar ausgegeben wurde. Mein Konverter fügt dem Wert noch ein Prozentzeichen an. Da für diesen einfachen Konverter ConvertBack nicht benötigt wird, wirft diese Methode lediglich eine NotSupportedException. Listing 14.59: Einfacher Konverter für die Datenbindung public class PercentConverter: IValueConverter { /* Konvertiert ein Objekt in Richtung Quelle => Ziel */ public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value != null) { return value.ToString() + "%"; } else { return String.Empty; }
930
Datenbindung
} /* Konvertiert ein Objekt in Richtung Ziel => Quelle */ public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotSupportedException(); } }
12
In einer XAML-Datei, in der dieser Konverter nun verwendet werden soll, muss eine Instanz dieser Klasse zunächst als Ressource gespeichert werden:
13
14
Dann können Sie den Konverter in die Converter-Eigenschaft der DataBinding-Markuperweiterung schreiben:
15
16
Und schon zeigt die ProgressBar in ihrem Tooltipp ihren aktuellen Wert mit einem Prozentzeichen an.
17
14.6.9 Nicht besprochene Features der Datenbindung Leider bleibt in diesem Kapitel kein Platz mehr für die restlichen, sehr interessanten Features der Datenbindung. Aber das Buch soll ja auch noch andere wichtige Themen wie Multithreading behandeln. Die folgenden Themen wären im Zusammenhang mit Datenbindung noch interessant: ■ ■ ■
■ ■ ■ ■ ■
18
19
Die Arbeit mit der Sicht auf die Daten über ICollectionView (Sortieren, Gruppieren, Filtern, Navigieren, mehrere Sichten auf dieselben Daten), Mater-Detail-Beziehungen, das Binden eigener (Business-)Objekte an WPF-Elemente über Klassen, die INotifyPropertyChanged implementieren, und über die Klasse ObjectDataProvider, die Instanzen der Business-Klassen in XAML erzeugt, das Binden an XML-Daten über einen XmlDataProvider, die Datenbindung an Methoden (über eine ObjectDataProvider-Instanz), die Anzeige hierarchischer Daten über die Klasse HierarchicalDataTemplate, die Bindung an externe Datenquellen (ADO, LINQ to SQL). LINQ to SQL wird allerdings in Kapitel 19 besprochen. die 2-Wege-Datenbindung, die es erlaubt, Änderungen an den gebundenen Daten auch zurückzuschreiben,
20
21
22
23
931
Wichtige WPF-Techniken
■ ■
Eingabevalidierungen bei der 2-Wege-Datenbindung (über von ValidationRule abgeleitete Klassen) und das Kombinieren mehrerer Datenquellen (über CompositeCollection, MultiBinding und PriorityBinding).
14.7
Einige weitere WPF-Techniken am Beispiel
WPF enthält neben den bisher vorgestellten Techniken noch einige weitere, die im Buch keinen Platz finden. Zwei davon stelle ich allerdings kurz am Beispiel vor: Transformationen und Animationen.
14.7.1
Transformationen
WPF-UI-Elemente erben von UIElement die Eigenschaft RenderTransform. In diese Eigenschaft können Sie eine Instanz einer von Transform abgeleiteten Klasse schreiben, die auf dem WPF-UI-Element eine Transformation ausführt. Dazu stehen Ihnen u. a. die folgenden Klassen zur Verfügung: ■ ■ ■ ■
ScaleTransform: Skaliert ein UI-Element RotateTransform: Rotiert ein UI-Element TranslateTransform: Verschiebt ein UI-Element TransformGroup: Ermöglicht mehrere Transformationen auf einem UI-Element
Transformationen sind besonders interessant in Zusammenhang mit Triggern und Animationen. Das folgende einfache Beispiel rotiert ein Rechteck um 45 Grad um die Mitte, wenn die Maus auf dem Rechteck liegt: Listing 14.60: Einfaches Beispiel für eine Transformation in einem Trigger
Abbildung 14.17: Das Transformations-Beispiel
932
Einige weitere WPF-Techniken am Beispiel
14.7.2
Animationen
Animationen sind eine sehr mächtige WPF-Technik, die es – besonders im Zusammenhang mit Ereignistriggern – erlaubt, einer Anwendung oder Komponente dynamische Effekte hinzuzufügen. Und das sogar, ohne eine Zeile Quellcode zu schreiben. Auf dieses spezielle Thema kann ich in diesem Buch leider nicht auch nur ansatzweise eingehen. Ein einfaches Beispiel soll aber zeigen, was eine Animation prinzipiell ist.
12
Listing 14.61 zeigt zwei einfache Animationen, die mit je einem Ereignistrigger in einem Stil für die Button-Klasse verknüpft sind. Beim Eintreten des MouseEnterEreignisses wird der Schalter über ein StoryBoard auf seine zweifache Größe vergrößert, beim Eintreten des MouseLeave-Ereignisses wird der Schalter über ein anderes StoryBoard wieder auf seine Normalgröße verkleinert:
13
Listing 14.61: Einfaches Beispiel für eine Animation, die von Ereignistriggern gesteuert wird
14
15
16
17
18
19
20
21
Demo
Dieses sehr einfache Beispiel ist bereits (auch für mich) ziemlich beeindruckend, weil die Animation flüssig und ohne Verlust ausgegeben wird. Eine Abbildung im Buch würde wohl aber nicht viel bringen ☺. Schauen Sie sich einfach das Beispiel auf der Buch-DVD an. Mit diesem sehr interessanten und Spaß machenden Thema möchte ich das Kapitel 14 und damit auch WPF abschließen. Viel Spaß beim Ausprobieren. Und möge der Fehlerteufel bei Ihnen nicht allzu oft seine dummen Späße ausführen …
934
Inhalt
15
Konfiguration, Ressourcen und Lokalisierung 12
Nahezu jede Anwendung benötigt eine anpassbare Konfiguration. .NET stellt dazu eine ältere, eine neuere Technik und viel Spielraum für eigene Lösungen zur Verfügung. Die grundlegende Konfiguration basiert aber auf einem Schema, das bei einer maschinenweiten Konfigurationsdatei anfängt und bei einer anwendungsspezifischen endet. Dieses Kapitel erläutert deswegen zunächst die Konfigurationsgrundlagen und geht dann auf die Konfiguration von Anwendungen ein.
13
14
Der zweite wichtige Punkt in diesem Kapitel sind Ressourcen. Die in Kapitel 14 behandelten, speziell zu WPF gehörenden Ressourcen werden hier aber nicht mehr weiter angesprochen. Kapitel 15 setzt sich mit allgemeinen, in allen .NET-Anwendungen verwendbaren Ressourcen auseinander. Diese bilden auch eine wichtige Basis für die Lokalisierung einer Anwendung, die im letzten Teil des Kapitels behandelt wird.
15 16
Die Stichwörter dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■
Die Basis-Konfiguration des .NET Framework Die Konfiguration von Anwendungen Binäre und Text-Ressourcen in allgemeinen .NET-Anwendungen Grundlagen zur Lokalisierung einer Anwendung Die Lokalisierung von Windows.Forms-Anwendungen Die Lokalisierung von WPF-Anwendungen Ansätze zur Lokalisierung in der Praxis
17
18
19
Konfiguration einer Anwendung
In der Praxis müssen viele Werte, mit denen eine Anwendung arbeitet, so verwaltet werden, dass der Anwender die Möglichkeit besitzt, diese zu ändern. Ein Beispiel dafür sind die einzelnen Argumente eines Verbindungsstrings beim Datenzugriff. Früher – vor .NET – wurden solche Daten häufig in der Windows-Registrierdatenbank (englisch: Registry) oder in Ini-Dateien verwaltet. Beide Varianten sind mittlerweile veraltet. Die Registry verursacht in der Praxis viele Probleme, u. a. weil Sie die Konfigurationsdaten nicht einfach mit Ihrer Anwendung ausliefern können. IniDateien ermöglichen zwar die Auslieferung der Konfigurationsdaten mit der Anwendung, besitzen aber ein spezielles Format, das programmtechnisch nur mit speziellen (Windows-API-)Hilfsmitteln bearbeitet werden kann. Viel besser wäre, die Initialisierungsdaten in einer XML-Datei zu verwalten. Und genau das stellt Ihnen .NET in Form von Anwendungskonfigurationsdateien zur Verfügung.
20
21
22
23
935
Index
15.1
Konfiguration, Ressourcen und Lokalisierung
15.1.1 machine.config enthält die BasisKonfiguration
Die Basis: machine.config
Das .NET Framework verwaltet eine Basis-Konfigurationsdatei mit dem Namen machine.config im Ordner Microsoft.NET\Framework\v2.0.50727\CONFIG im Windows-Ordner. Die Version v2.0.50727 war die letzte .NET-2.0-Version vor .NET 3.5. Da .NET 3.5 auf .NET 2.0 basiert, wird auch die 2.0-Konfiguration verwendet. Auf Ihrem System kann die Versionsnummer auch höher sein, falls zwischenzeitlich Verbesserungen an den Bibliotheken oder Tools vorgenommen wurden. Die Datei machine.config enthält allerdings nur Konfigurationssektionen und Einstellungen, die von den in .NET voreingestellten Werten abweichen. Eine komplette Übersicht über die Einstellungen erhalten Sie –inklusive der Dokumentation der möglichen Werte – in der Datei machine.config.comments (die nur der Dokumentation dient). Die Konfigurationen, die in der machine.config-Datei eingestellt sind, gelten zunächst für alle Anwendungen, die unter dem .NET Framework ausgeführt werden. So können Sie im Element configuration/system.net/mailSettings/mailSettings z. B. Einstellungen vornehmen, die zum Versenden von E-Mails über einen SMTP-Server notwendig sind. Alle .NET-Anwendungen, die (über die Klasse System.Net.SmtpClient) E-Mails versenden, verwenden dann per Voreinstellung genau diese Einstellungen. Die Datei machine.config liegt im XML-Format vor und ist in UTF-8 codiert. Die Bearbeitung ist also nur mit einem Editor möglich, der diese Codierung beherrscht. Visual Studio ist dazu natürlich in der Lage. Listing 15.1 zeigt einen Auszug aus einer machine.config mit einigen wichtigen Defaulteinstellungen. Listing 15.1:
Auszug aus der machine.config mit einigen wichtigen Einstellungen
... ... ... ... ... ... ... ... ...
12
13
14
Ich gehe hier nur auf die Grundlagen dieser Datei ein, weil die Einstellungen sehr vielfältig und teilweise komplex sind. Sie als Entwickler werden mit der .NET-Basiskonfiguration wahrscheinlich eher selten arbeiten müssen. machine.config ist eher für Administratoren interessant, die auf einem System grundlegende Einstellungen vornehmen müssen. Einige Einstellungen, wie z. B. die SMPT-Einstellungen, sind aber auch für die Konfiguration einer Anwendung interessant. Die Konfiguration wird in der .NET-Dokumentation unter .NET-ENTWICKLUNG / DOKUMENTATION ZU .NET FRAMEWORK SDK / .NET FRAMEWORK / ALLGEMEINE REFERENZINFORMATIONEN / KONFIGURATIONSDATEISCHEMA beschrieben. Die einzelnen Einstellungen werden in Sektionen verwaltet, die teilweise noch in Sektionsgruppen zusammengefasst sind. Im Element werden diese zunächst beschrieben. Die Sektionsgruppe beinhaltet beispielsweise Sektionen mit Einstellungen, die das Netzwerk betreffen. Die meisten Einstellungen innerhalb der machine.config betreffen Webanwendungen. Wenn Sie Windows-Anwendungen entwickeln, können Sie die gesamte Sektionsgruppe ignorieren. Eine für Window.Forms-Anwendungen wichtige Einstellung ist die Einstellung jitDebugging im Element system.windows.forms, über die Sie das Debuggen bei unbehandelten Ausnahmen ein- oder ausschalten können.
15 16
17
REF
Die Konfiguration verwendet Sektionen und Sektionsgruppen
18
19
20
Achten Sie bei der Bearbeitung der Datei darauf, dass Sie die korrekte Groß-/Kleinschreibung verwenden. XML unterscheidet zwischen Groß- und Kleinschreibung. Alle Elemente und Attribute innerhalb der Konfiguration sind über camelCasing benannt.
21 Einige wichtige Einstellungen der Basis-Konfiguration können Sie (theoretisch) auch mit dem Microsoft-Tool mscorcfg.msc bearbeiten, das ein Plugin der ManagementKonsole ist. Leider wird dieses wichtige Werkzeug nicht mehr mit dem .NET Framework 3.5 ausgeliefert (was viele Entwickler und Administratoren sehr ärgert) und ist nur im SDK des .NET Framework 2.0 enthalten. Bei einer Neuinstallation des .NET Framework 3.5 ist mscorcfg.msc (auch bei der Installation von Visual Studio) leider nicht enthalten und funktioniert auch nicht mehr, wenn Sie das .NET Framework 2.0 SDK installiert haben und danach .NET 3.5 installieren. Für die Einstellung der Codezugriffssicherheit (siehe Kapitel 23) ist mscorcfg.msc aber eigentlich sehr hilfreich (für mich eher: unverzichtbar).
TIPP
22
23
937
Konfiguration, Ressourcen und Lokalisierung
15.1.2 Zur Konfiguration einer Anwendung können Sie eine eigene Konfigurationsdatei verwalten
Die Basis-Konfiguration einer Anwendung
Wenn Sie eine .NET-Anwendung konfigurieren wollen, können Sie dazu im Ordner der Anwendung eine separate Konfigurationsdatei ablegen. In dieser Datei können Einstellungen der machine.config für die Anwendung überschrieben werden. Außerdem können neue, anwendungsspezifische Einstellungen enthalten sein. Diese Datei muss den kompletten Namen der Anwendung (inklusive der .exe-Endung!) mit der Endung .config besitzen. Die Konfigurationsdatei der Anwendung xyz.exe heißt z. B. xyz.exe.config. In einem Visual-Studio-Projekt fügen Sie eine Anwendungskonfigurationsdatei über den Befehl HINZUFÜGEN / NEUES ELEMENT /ANWENDUNGSKONFIGURATIONSDATEI im Kontextmenü des Projekt-Eintrags oder im PROJEKT-Menü hinzu. Nennen Sie diese Datei App.config, auch wenn Visual Studio in einem WPF-Projekt den Namen App1.config vorschlägt. Beim Kompilieren wird die Konfigurationsdatei automatisch umbenannt. Wollen Sie z. B. die SMTP-Einstellungen (zum Senden von E-Mails) für eine Anwendung speziell einstellen, tragen Sie das Element system.net/mailSettings/smtp in die Anwendungskonfigurationsdatei ein: Listing 15.2:
Einstellung in der Anwendungskonfigurationsdatei für das Senden von E-Mails über einen SMTP-Server
Diese Einstellung erlaubt es nun, über eine Instanz der Klasse System.Net.Mail.SmtpClient ohne weitere Angaben über den konfigurierten SMTP-Server eine E-Mail zu versenden: Listing 15.3:
Einfaches Senden einer E-Mail über einen SMTP-Server
// Neuen SmtpClient zum Senden der E-Mail erzeugen, der seine // Voreinstellungen aus der Anwendungskonfiguration ausliest SmtpClient smtpClient = new SmtpClient(); // Versenden der E-Mail try { MailMessage mailMessage = new MailMessage();
938
Konfiguration einer Anwendung
mailMessage.To.Add(new MailAddress("
[email protected]")); mailMessage.Subject = "Treffen"; mailMessage.Body = "Hallo Ford, wir treffen uns " + "heute abend im Restaurant am Ende der Galaxis." smtpClient.Send(mailMessage); } catch (Exception ex) { MessageBox.Show(ex.Message, "E-Mail", MessageBoxButton.OK, MessageBoxImage.Error); }
12
Falls Sie das Senden von E-Mails ausprobieren wollen: Unter Windows XP können Sie einen lokalen SMTP-Server als Teil der Windows-Komponenten nachinstallieren. Diesen verwalten Sie über den Internetinformationsdienste-Manager. Läuft der lokale SMTP-Server, können Sie in der Konfiguration als host localhost eintragen. userName und password bleiben leer.
13
In Vista ist kein SMTP-Server mehr enthalten. Einen freien SMTP-Server finden Sie an der Adresse softstack.com/freesmtp.html.
14
Natürlich können Sie auch einen SMTP-Server verwenden, der im Intranet oder Internet läuft. Diese erfordern aber normalerweise zumindest einen Login.
15
Alternativ können Sie zum direkten Senden von E-Mails auch das IIS-Abholverfahren einsetzen. Bei diesem wird die E-Mail in einen speziellen Ordner kopiert, vom IIS »abgeholt« und versendet. Unter Vista ist mir dies allerdings nicht gelungen. Um die gesendeten E-Mails in das Abholverzeichnis des IIS zu kopieren, müssen Sie die Konfiguration ändern:
16
17
Außerdem müssen Sie in den IIS-Einstellungen das Abholverzeichnis angeben. Unter Vista finden Sie diese Einstellung im Internetinformationsdienste-Manager in den Einstellungen des Webservers unter SMTP-E-MAIL.
18
Beachten Sie auch, dass die Ausnahmen, die beim Senden von E-Mails möglich sind, die eigentliche Fehlermeldung in den inneren Ausnahmen verstecken.
19
Sie können in der SmtpClient-Instanz auch alle notwendigen Angaben wie die des SMTP-Servers in entsprechenden Eigenschaften direkt angeben, genau wie die Absenderadresse beim Senden der E-Mail. Die Konfiguration erleichtert aber die Einstellung dieser Grunddaten, ohne dass die Anwendung neu kompiliert werden muss. Wenn Sie die Einstellungen allerdings über einen Einstellungsdialog in der Anwendung verfügbar machen wollen, müssen Sie diese selbst verwalten, da Sie prinzipiell nicht schreibend auf die Anwendungskonfigurationsdatei zugreifen sollten.
20 INFO
21
22
Daneben sind noch eine Menge anderer vordefinierter Einstellungen möglich, die größtenteils nicht in der machine.config zu finden sind. So können Sie beispielsweise für einzelne Assemblys, die Ihr Programm verwendet, in der Konfigurationsdatei definieren, welche Version und welche Kultur verwendet werden soll und wo die Assembly gespeichert ist. Informationen dazu finden Sie in der Dokumentation der Konfiguration.
23
939
Konfiguration, Ressourcen und Lokalisierung
15.1.3
Grundlagen zu anwendungsspezifischen Konfigurationsdaten
Anwendungen benötigen häufig eigene Konfigurationsdaten, wie z. B. die Angabe von Verbindungs-Informationen zu einer Datenbank. Zur Verwaltung solcher Informationen stehen Ihnen gleich mehrere Möglichkeiten zur Verfügung: ■
■
■
■
Sie können die Einstellungen in Element appSettings der Anwendungskonfigurationsdatei verwalten. Diese Art der Verwaltung ist die klassische, die schon unter .NET 1.0 zur Verfügung stand. Sie hat den Nachteil, dass sie prinzipiell nur das Lesen der Konfigurationsdaten ermöglicht. Das Schreiben ist nur mit einem erhöhten Aufwand möglich und widerspricht den Microsoft-Richtlinien, nach denen eine Anwendung nicht in den Anwendungsordner schreiben soll. Alternativ (und wesentlich einfacher) können Sie mit Hilfe von Visual Studio einen eigenen Konfigurationsabschnitt in der Anwendungskonfiguration verwalten und über eine von Visual Studio generierte Klasse typsicher auf die Einstellungen zugreifen. Diese Variante erlaubt auch das Schreiben der Daten, hat aber ein paar Nachteile, die ich im Abschnitt »Konfiguration einer Anwendung mit den Standard-Features von Visual Studio« (Seite 941) erläutere. Eine weitere Möglichkeit ist die Verwaltung eigener Konfigurationsabschnitte über eine spezielle Handler-Klasse. Diese Art der Konfiguration wird gerne für Klassenbibliotheken genutzt. Das in Kapitel 9 angesprochene log4net setzt z. B. einen eigenen Konfigurationsabschnitt ein. Nicht zuletzt können Sie Ihre Einstellungen natürlich auch in einer eigenen XML-Datei (oder Datenbank) verwalten. Dies erfordert zwar mehr Arbeit als die Verwendung der von Visual Studio erstellten Einstellungs-Klasse, hat aber in der Praxis den enormen Vorteil, dass Sie entscheiden können, wo die Datei verwaltet wird.
Ich bespreche hier lediglich die ersten zwei Punkte. Eigene Konfigurationsabschnitte sind für dieses Kapitel zu speziell. Im Beispiel-Ordner zu diesem Abschnitt finden Sie auf der Buch-DVD ein Beispielprojekt. Die vierte Möglichkeit behandle ich ebenfalls nicht, weil Sie diese mit dem restlichen Wissen aus diesem Buch (inkl. des XMLKapitels 18) wohl problemlos selbst implementieren können. Beachten Sie lediglich, dass Sie Ihre Konfigurationsdateien grundsätzlich im Anwendungsdaten-Ordner für alle, wandernde oder für lokale Benutzer verwalten sollten (siehe Kapitel 10).
15.1.4 appSettings ist der klassische Weg, Konfigurationsdaten zu verwalten
Konfigurationsdaten im appSettings-Element
Das Element appSettings ist seit Urbeginn von .NET für eigene Konfigurationsdaten vorgesehen. Dazu fügen Sie diesem Element add-Elemente hinzu, deren Attribut key einen Schlüssel definiert. Das Attribut value bestimmt den Wert der Einstellung. In der Anwendung erzeugen Sie damit eine Auflistung vom Typ NameValueCollection. Das folgende Beispiel definiert Einstellungen, die den Namen des Anwenders verwalten:
940
Konfiguration einer Anwendung
Im Programm können Sie diese Einträge über die AppSettings-Eigenschaft der Klasse ConfigurationManager aus dem Namensraum System.Configuration auslesen. Das Projekt benötigt dazu eine Referenz auf die Assembly System.Configuration.dll (die per Voreinstellung nicht vorhanden ist). Listing 15.4:
Auslesen von Konfigurationsdaten aus dem appSettings-Element der .config-Datei
string firstName = ConfigurationManager.AppSettings["firstName"]; string lastName = ConfigurationManager.AppSettings["lastName"];
Sie müssen beim Lesen der Einstellungen natürlich darauf reagieren, dass ungültige oder keine Daten gespeichert sein können. Wenn eine abgefragte Konfigurationseigenschaft nicht existiert, gibt AppSettings null zurück, erzeugt also keine Ausnahme.
12
13
HALT
14
Das reicht zu der in einigen Anwendungen immer noch verwendeten klassischen Konfiguration aus. Das Zurückschreiben in die Konfiguration ist zwar (über ConfigurationManager.OpenExeConfiguration und die Arbeit mit dem zurückgegebenen Configuration-Objekt) prinzipiell möglich, aber nicht zu empfehlen, denn:
15
Die Microsoft-Richtlinien zur Erstellung von Windows-Anwendungen sagen u. a. aus, dass eine Anwendung keine Dateien im Anwendungsordner schreiben darf. Der Grund dafür ist, dass Anwendungen häufig unter eingeschränkten Rechten ausgeführt werden (besonders unter Vista mit eingeschaltetem UAC). Der aktuelle Benutzer besitzt dann häufig nicht das Recht, auf den Programmordner schreibend zuzugreifen.
16
Deswegen sollten Sie besser die unter Visual Studio verfügbare Standard-Konfiguration verwenden.
15.1.5
17
Konfiguration einer Anwendung mit den Standard-Features von Visual Studio
Visual Studio ermöglicht eine bequeme Konfiguration von Anwendungen mit der Möglichkeit, die Konfigurationsdaten auch zurückzuschreiben. Die Daten werden dazu in einer eigenen Sektion in der .config-Datei der Anwendung verwaltet. Eine von Visual Studio generierte Klasse übernimmt das Handling dieser Sektion und ermöglicht den typsicheren und einfachen Zugriff auf die verwalteten Daten. Konfigurationsdaten gehören einem von zwei Bereichen an. Dem Anwendungsoder dem Benutzerbereich. Daten, die dem Anwendungsbereich angehören, können nur gelesen werden. Daten, die dem Benutzerbereich angehören, können auch zurückgeschrieben werden. Diese Daten werden dann automatisch in den Ordner der Anwendungsdaten für lokale oder wandernde Benutzer geschrieben (und natürlich aus diesem Ordner auch wieder ausgelesen). Damit richtet sich Ihre Anwendung automatisch nach den Anwendungsrichtlinien von Microsoft zur Verwaltung von Programmdaten.
18 Visual Studio ist in der Lage, eine Klasse zur bequemen Verwaltung von Konfigurationsdaten zu generieren
19
20
21
22
Die Erzeugung einer Konfigurations-Klasse Die Erzeugung einer Konfigurations-Klasse ist unter Visual Studio sehr einfach. Öffnen Sie dazu die Eigenschaften des Projekts und dort das Register EINSTELLUNGEN. Hier können Sie beliebige Einstellungen eintragen, diese mit einem Typ versehen und deren Bereich einstellen. Denken Sie daran, dass nur Einstellungen vom Typ BENUTZER
Die Konfiguration verwalten Sie in den EinstellungsEigenschaften
941
23
Konfiguration, Ressourcen und Lokalisierung
auch geschrieben werden können. Für Anwendungs-Einstellungen sollten Sie einen Wert angeben, für Benutzereinstellungen können Sie einen (Default-)Wert angeben. Die hier angegebenen Werte werden in der Anwendungskonfigurationsdatei im Anwendungsordner (bzw. in der app.config im Projektordner) gespeichert. Abbildung 15.1: Die EinstellungsEigenschaften eines Projekts
INFO
Für die einzelnen Einstellungen können Sie als Typ die vordefinierten .NET-Typen verwenden. Eigene Typen wie spezielle Aufzählungen können Sie nur dann verwenden, wenn diese in einer separaten Assembly verwaltet werden (die natürlich referenziert werden muss). Diese Typen können Sie einstellen, indem Sie im Typ-Feld den Eintrag DURCHSUCHEN wählen. Sobald Sie die Einstellungen speichern, erzeugt Visual Studio bei Bedarf eine app.config-Datei und kopiert die Einstellungen dort hinein. Außerdem wird im Ordner Properties die Klasse Settings generiert. Diese Klasse erlaubt den einfachen Zugriff auf die Einstellungen. Für das obige Beispiel sieht die erzeugte app.config-Datei prinzipiell folgendermaßen aus: Listing 15.5:
Die (gekürzte) Anwendungskonfigurationsdatei des Beispiels
- -
942
Konfiguration einer Anwendung
http://update.galaxy.com False
12
13
Die Einstellungen werden in den Sektionen userSettings und applicationSettings verwaltet, die im oberen Bereich bekannt gemacht werden. Die Klasse Settings enthält einen Handler für diese Sektionen. Diese Klasse finden Sie in der Datei Settings. Designer.cs unterhalb von Properties\Settings.settings. Interessant ist, dass in dieser Klasse normale Eigenschaften implementiert sind, die den Namen und den Typ der Einstellungen tragen. Ein Beispiel ist die FirstName-Eigenschaft unseres Beispiels:
14
public string FirstName { get { return ((string)(this["FirstName"])); } set { this["FirstName"] = value; } }
15 16
Ich denke nicht, dass es notwendig ist, die grundsätzliche Arbeitsweise der Settings-Klasse zu verstehen. Ich denke aber, dass es interessant ist, zu wissen, dass nichts Magisches hinter der Verwaltung der Einstellungen steckt.
17
Das Lesen von Einstellungen Im Programm müssen Sie nun eine Instanz der Settings-Klasse erzeugen. Dazu können Sie die statische Default-Eigenschaft verwenden, die eine implizit erstellte (und in einem privaten Feld zwischengespeicherte) neue, threadsichere Instanz zurückgibt: Listing 15.6:
Eine SettingsInstanz gibt Zugriff auf die Eigenschaften
19
Auslesen der Einstellungen
string firstName = Settings.Default.FirstName; string lastName = Settings.Default.LastName; string updateUrl = Settings.Default.UpdateURL; bool checkForUpdates = Settings.Default.CheckForUpdates;
Ehrlich gesagt ist mir unklar, warum Sie neben der Verwendung der Default-Eigenschaft auch über den Konstruktor weitere Instanzen erzeugen können. Wahrscheinlich hat dies keine besondere Bedeutung. Verwenden Sie grundsätzlich die DefaultEigenschaft, dann sind Sie auch beim Multithreading auf der sicheren Seite. Falls die Einstellung in der Konfigurationsdatei nicht (mehr) vorhanden ist oder einen ungültigen Wert aufweist (z. B. weil der Anwender diesen direkt in der Datei geändert hat), erzeugt das .NET-Konfigurationssystem keine Ausnahme. Stattdessen wird der Defaultwert zurückgegeben, der in den Einstellungen definiert wurde. Es ist in der Praxis also eigentlich unmöglich zu erkennen, ob die Konfigurationsdatei gültig ist oder nicht.
18
20
21
INFO
22 HALT
23
943
Konfiguration, Ressourcen und Lokalisierung
Das Schreiben der Einstellungen Benutzereinstellungen können auch gespeichert werden
In Eigenschaften des Bereichs »Benutzer« können Sie auch Werte zurückschreiben. Damit können Sie dem Benutzer ermöglichen, die Konfiguration innerhalb der Anwendung (z. B. in einem Einstellungsdialog) zu ändern. Nach dem Ändern der Werte rufen Sie die Save-Methode der Settings-Instanz auf, um die Einstellungen zu speichern. Ich zeige dies hier an einem einfachen Beispiel: Listing 15.7:
Schreiben und Speichern von Eigenschaften
// Die Einstellungen schreiben Settings.Default.FirstName = this.firstNameTextBox.Text; Settings.Default.LastName = this.lastNameTextBox.Text; // Die Einstellungen speichern Settings.Default.Save();
So weit ist alles einfach und in Ordnung. Nun kommt aber der etwas undurchsichtige und leider auch problematische Teil:
Einstellungs-Provider und der Speicherort der Daten mit dem Bereich »Benutzer«. Provider übernehmen die Verwaltung der Daten
Das .NET-Konfigurationssystem verwendet so genannte Provider, die die Verwaltung der Einstellungsdaten übernehmen. Sie erreichen die Provider über die Eigenschaft Providers der Settings-Instanz. Per Voreinstellung ist in dieser Auflistung nur ein Provider enthalten. Der Sinn ist, dass Sie auch andere Provider einhängen können, um die Daten in einer anderen Form zu verwalten (z. B. in einer XML-Datei). Der Standard-Provider ist vom Typ System.Configuration.LocalFileSettingsProvider. Dieser Provider verwaltet die Benutzereinstellungen in einer StandardKonfigurationsdatei für normale Anwender in einem Unterordner im Ordner für lokale Anwendungsdaten des aktuellen Benutzers (unter Vista ist das der Ordner C:\Users\\AppData\Local). Ist für den Benutzer das Roaming aktiviert, werden die Daten im Ordner für wandernde Benutzer (C:\Users\\AppData\Roaming) verwaltet.
Der StandardProvider verwendet eine sehr spezielle Ordnerstruktur
Das wirkliche Problem ist aber die (leider nicht weiter dokumentierte) Ordnerstruktur, die die LocalFileSettingsProvider-Klasse je nach den Einstellungen der Anwendung aufbaut. Dabei wird zunächst ein Ordner angelegt, der dem Schema Firmenname\Dateiname der Assembly entspricht. Ist das Assembly-Attribut AssemblyCompany nicht (in der Datei Properties\AssemblyInfo.cs) angegeben, entfällt der Ordner Firmenname. In dem Basisordner wird ein Ordner angelegt, dessen Name etwas eigenartig erscheint. Er beginnt mit den Dateinamen der Assembly (wobei die Endung nur mit zwei Stellen ausgegeben wird). Dann folgt »_Url_« und danach ein verschlüsselt angegebener URL. Für die Beispiel-Anwendung dieses Abschnitts, deren Dateiname Standard-Konfiguration.exe ist, sieht der Ordner folgendermaßen aus: C:\Users\Jürgen\AppData\Local\Standard_Konfiguration\StandardKonfiguration.ex_Url_0fhjmyk5glu4z0gu4ago0iyxn0wj0bk1
Die verschlüsselte URL enthält (wahrscheinlich) den Hashcode eines URLs für den Ort, von dem aus die Anwendung gestartet wurde. Innerhalb dieses Ordners wird noch ein Ordner angelegt, der die Versionsnummer der Anwendung als Namen trägt. Und in diesem Ordner schließlich finden Sie die Datei user.config, in der die Benutzereinstellungen gespeichert werden.
944
Konfiguration einer Anwendung
Die Einstellungen werden also zum einen Assembly-spezifisch und zum anderen ortsspezifisch gespeichert. Für eine normale Anwendung (keine ClickOnce-Anwendung) bedeutet dies: ■ ■ ■
dass die Einstellungen verloren gehen, wenn die Anwendung an einen anderen Ort kopiert wird, dass die Einstellungen verloren gehen, wenn die Anwendungsdatei umbenannt oder in den Assembly-Attributen der Firmenname geändert wird, und dass die Einstellungen bei jeder Aktualisierung der Anwendung (mit einer neuen Versionsnummer) verloren gehen.
12
Besonders der letzte Punkt ist sehr ärgerlich (aber es gibt eine Lösung dafür). Sie können von einem Anwender nicht erwarten, dass er bei jeder Aktualisierung der Anwendung alle persönlichen Einstellungen wiederholt eingibt.
13
Für diese seltsam erscheinende Verwaltung der Einstellungen gibt es aber einen Grund: Damit wird erreicht, dass auf einem System mehrere Kopien und/oder Versionen einer Anwendung parallel verwendet werden können, ohne dass die Einstellungen durcheinanderkommen.
14
Das Aktualisierungs-Problem gilt aber nur für normale Anwendungen. Für eine Anwendung, die per ClickOnce (siehe Kapitel 16) installiert wird, gehen die Einstellungen bei einem automatischen Update nicht verloren. Der Grund dafür ist, dass bei der (automatischen) Aktualisierung einer ClickOnce-Anwendung implizit die Methode Upgrade der Default-Settings-Instanz aufgerufen wird. Diese Methode liest die Einstellungen der vorherigen Version der Anwendung, die am besten zu der aktuellen Version passt, und schreibt diese in den Ordner der aktuellen Version.
Die Methode Upgrade löst das Problem
16
17
Das ist auch der (relativ unbekannte und schlecht dokumentierte) Trick, um zu erreichen, dass nach einer Aktualisierung einer Anwendung die Einstellungen nicht verloren gehen: Rufen Sie die Upgrade-Methode der Settings-Instanz auf. Die Frage ist nur, wann Sie das machen sollten. Der Trick dazu ist, in den Einstellungen eine spezielle boolesche Einstellung mit dem Bereich »Benutzer«, vielleicht mit dem Namen CallUpgrade, zu verwalten. Die Voreinstellung setzen Sie auf true. Beim Start der Anwendung überprüfen Sie, ob CallUpgrade true ist, und rufen in diesem Fall Upgrade auf. CallUpgrade setzen Sie dann auf false und speichern die Einstellungen: Listing 15.8:
15
18
19
TIPP
20
Der ultimative Trick zum Lesen der Benutzereinstellungen nach dem Aktualisieren einer (normalen) Anwendung
if (Settings.Default.CallUpgrade) { Settings.Default.Upgrade(); Settings.Default.CallUpgrade = false; Settings.Default.Save(); }
21
Dass Upgrade dann auch nach der ersten Installation einer Anwendung aufgerufen wird, ist übrigens kein Problem.
22
Nach dieser Lösung habe ich im Internet sehr lange gesucht. Und sie endlich gefunden ☺. In diesem Zusammenhang: Einen ironischen Dank an Microsoft, dass dieses wichtige Thema in der Dokumentation noch nicht einmal erwähnt wird.
23
945
Konfiguration, Ressourcen und Lokalisierung
INFO
Beim Test mit Visual Studio sollten Sie zudem beachten, dass Anwendungen normalerweise im Visual-Studio-Host-Prozess ausgeführt werden. Einstellungen, die von einer auf diese Art gestarteten Anwendung geschrieben werden, gelten nicht für die direkt gestartete Anwendung, da der Name der .exe-Datei ein anderer ist. Beim Entwickeln einer eigenen Anwendung ist das recht nervig, wenn Sie viele Einstellungen verwalten, diese beim Testen eingeben und dann noch einmal definieren müssen, wenn die Anwendung normal gestartet wird.
15.1.6 Das connectionStrings-Element speichert Verbindungszeichenfolgen
Verbindungszeichenfolgen
Bei der Arbeit mit Datenbanken (Kapitel 19) arbeiten Sie immer wieder mit Strings, die die zur Verbindung mit der Datenbank notwendigen Einstellungen verwalten. Das .NET-Konfigurationssystem ermöglicht, diese Strings in dem connectionStrings-Element innerhalb der Anwendungskonfiguration zu speichern: Listing 15.9:
Konfiguration einer typischen Verbindungszeichenfolge
Im Programm können Sie die Verbindungszeichenfolge über die ConnectionStringsEigenschaft der ConfigurationManager-Klasse auslesen. Den Schlüssel übergeben Sie am Indexer. Dabei sollten Sie in der Praxis berücksichtigen, dass die Verbindungszeichenfolge auch nicht angegeben sein kann: Listing 15.10: Einlesen einer Verbindungszeichenfolge im Programm // Die Verbindungszeichenfolge einlesen ConnectionStringSettings connectionStringSettings = ConfigurationManager.ConnectionStrings["AdventureWorks"]; if (connectionStringSettings != null) { string connectionString = ConfigurationManager.ConnectionStrings[ "AdventureWorks"].ConnectionString; // Verarbeiten der Verbindungszeichenfolge // (z. B., indem eine Verbindung zur Datenbank geöffnet wird) ... } else { MessageBox.Show("Die Verbindungszeichenfolge für die " + "AdventureWorks-Datenbank fehlt in der Anwendungskonfiguration", this.Title, MessageBoxButton.OK, MessageBoxImage.Error); }
INFO
946
Wenn Sie mit LINQ to SQL arbeiten (siehe Kapitel 19), verwaltet der LINQ-to-SQLDesigner die im Verlauf der Erstellung des Objekt-Modells für eine Datenbank definierte Verbindungszeichenfolge ebenfalls im connectionStrings-Element.
Ressourcen
15.1.7
Nicht näher besprochene Konfigurations-Features
Für einige interessante Konfigurations-Features bleibt in diesem Kapitel kein Platz. Dazu gehören: ■
■
■ ■
Ein benutzerdefiniertes Upgrade von Einstellungen bei einem Update der Anwendung über ein Überschreiben der Upgrade-Methode. Über die Methode GetPreviousVersion erhalten Sie Zugriff auf Einstellungen der vorherigen Version. Die Implementierung eigener Provider zur Verwaltung der Einstellung in speziellen (XML-)Dateien oder Datenbanken. Dazu implementieren Sie eine Klasse, die von SettingsProvider abgeleitet wird und die die Schnittstelle IApplicationSettingsProvider implementiert. Trivial ist das aber erstaunlicherweise nicht (auch, weil dieses Feature kaum dokumentiert ist). Der Provider wird dann über das SettingsProvider-Attribut der Settings-Klasse bekannt gemacht. Die Möglichkeit, mehrere .settings-Dateien (im Wurzelordner des Projekts) zu verwalten, um Einstellungen in Gruppen aufzuteilen. Die Möglichkeit, verschiedene Konfigurationsdateien (u. a. die machine.config und die Anwendungskonfiguration anderer Anwendungen) über die Methoden OpenMachineConfiguration, OpenExeConfiguration und GetSection (u. a.) der Klasse ConfigurationManager direkt zu bearbeiten. Diese Möglichkeit nutze ich allerdings in Kapitel 16 bei der Besprechung eines Setup mit einer benutzerdefinierten Aktion.
15.2
12
13
14
15 16
Ressourcen 17
Das Thema »Ressourcen« habe ich in diesem Buch für WPF speziell bereits in Kapitel 14 behandelt. In diesem Abschnitt geht es nun um Ressourcen für allgemeine .NETAnwendungen. Diese sind im Besonderen – auch für WPF – sehr wichtig für die Lokalisierung einer Anwendung, die ab Seite 953 besprochen wird.
18
.NET unterscheidet verschiedene Arten von Ressourcen (wobei ich die speziellen WPF-Wörterbuch-Ressourcen hier nicht aufführe): ■ ■
■
■ ■
19
Binäre Ressourcen, die in einer Datei verwaltet werden, die in der Laufzeit der Anwendung eingelesen werden, binäre Ressourcen, die in das Anwendungsmanifest eingebunden werden (in das Projekt eingebundene Dateien mit der Einstellung EINGEBETTETE RESSOURCE in der Option BUILDVORGANG), binäre Ressourcen, die in die Anwendung eingebunden werden (in das Projekt eingebundene Dateien mit der Einstellung RESSOURCE in der Option BUILDVORGANG), Beliebige Ressourcen, die in einer speziellen XML-Datei mit der Endung .resx verwaltet werden und in die Anwendung eingebettet sind, und .resx-Ressourcen, die in der Laufzeit der Anwendung aus der .resx-Datei eingelesen werden.
20
21
22
Diese Vielfalt stammt teilweise daher, dass in neueren .NET-Versionen neuere (und bessere) Möglichkeiten hinzugekommen sind.
23
Der erste Punkt ist relativ klar und wurde prinzipiell bereits in Kapitel 14 behandelt: Ressourcen können natürlich in separaten Dateien verwaltet werden, die in der Laufzeit eingelesen werden. Ein Beispiel dafür sind Bilder, die in der Anwendung ange-
947
Konfiguration, Ressourcen und Lokalisierung
zeigt werden sollen, bei denen aber die Möglichkeit bestehen soll, dass diese ohne ein Neu-Kompilieren der Anwendung ausgetauscht werden können. In Nicht-WPFAnwendungen lesen Sie solche Ressourcen prinzipiell genauso ein wie in WPFAnwendungen, nur dass Sie hier in der Regel nicht mit einem URL arbeiten, sondern mit einem normalen Dateipfad. Wenn Sie die Ressourcen im Ordner der Anwendung erwarten, können Sie den Anwendungsordner mit einem kleinen Trick ermitteln: Listing 15.11: Kleiner Trick zum Ermitteln des Anwendungsordners string appPath = System.IO.Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location);
Ich gehe auf diese Art Ressourcen in diesem Kapitel nicht weiter ein. Die wesentlichen Ressourcen in einer allgemeinen .NET-Anwendung sind binäre Ressourcen, die direkt in die Anwendung eingebettet sind und solche, die über eine .resx-Datei entweder eingebettet sind oder in der Laufzeit eingelesen werden. Direkt eingebettete Ressourcen werden für einfache Dateien wie Bilder verwendet, die nicht lokalisiert werden müssen. .resx-Dateien können zum einen mehrere Ressourcen verwalten und werden zum anderen für die Lokalisierung einer Anwendung verwendet.
15.2.1 Dateien können auf zwei Arten in eine Anwendung eingebettet werden
Binäre, eingebettete Ressourcen
Wenn Sie einem Visual-Studio-Projekt eine Datei hinzufügen, können Sie die Eigenschaft BUILDVORGANG dieser Datei auf EINGEBETTETE RESSOURCE oder auf RESSOURCE stellen. In beiden Fällen wird die Datei in die Anwendung eingebettet und kann in der Laufzeit ausgelesen werden. Die Frage ist nur, was diese Einstellungen voneinander unterscheidet. EINGEBETTETE RESSOURCE ist die ältere Variante. Ressourcen, die auf diese Art eingebettet sind, müssen über die GetManifestResourceStream-Methode einer AssemblyInstanz eingelesen werden, die die Assembly repräsentiert, die die Ressource enthält. Das ist heute für eigene Ressourcen nicht mehr zeitgemäß, wird aber intern von der von Visual Studio automatisch erzeugten Klasse verwendet, die die Arbeit mit .resx-Dateien (die auf diese Weise eingebettet sind) erleichtert. Der BUILDVORGANG »RESSOURCE« steht für die modernere Art, Ressourcen direkt in die Anwendung einzubetten. Solche Ressourcen lesen Sie in WPF ein, indem Sie einen URI angeben, der die Ressource kennzeichnet. Die für das Einlesen verwendeten Klassen (wie z. B. BitmapImage) arbeiten deshalb mit einer Uri-Instanz als Quellangabe. In Kapitel 14 bin ich bereits darauf eingegangen. Mir ist allerdings unklar, ob und wie Sie solche Ressourcen auch in einer Nicht-WPF-Anwendung einlesen können. Zur Referenzierung der Ressource können Sie natürlich auch eine Uri-Instanz verwenden. Aber wie erhalten Sie dann Zugriff auf die Daten (das Bild, die XML-Datei etc.)? Wahrscheinlich ist diese Frage aber eher rhetorisch, denn in modernen Nicht-WPF-Anwendungen werden Ressourcen normalerweise in .resx-Dateien verwaltet … Deswegen gehe ich hier auch nicht näher auf dieses Thema ein.
15.2.2 .resx-Dateien verwalten beliebige Ressourcen
948
.resx-Ressourcen
In modernen .NET-Anwendungen werden Ressourcen üblicherweise in speziellen Dateien mit der Endung .resx verwaltet. WPF ist da so etwas wie eine Ausnahme, denn wie Sie in Kapitel 14 gesehen haben, verwendet eine WPF-Anwendung für
Ressourcen
»normale« Ressourcen eingebettete Dateien und die speziellen Wörterbuch-Ressourcen. Aber auch in einer WPF-Anwendung werden .resx-Ressourcen verwendet, nämlich für eine Variante der Lokalisierung (wie Sie ab Seite 953 noch sehen werden). Eine .resx-Ressourcen-Datei ist eine XML-Datei in einem festgelegten Format. Eine solche können Sie einem Projekt über den Befehl HINZUFÜGEN / NEUES ELEMENT / RESSOURCEN-DATEI im Kontextmenü des Projekteintrags im Projektmappenexplorer hinzufügen. Sie können beliebig viele Ressourcen-Dateien verwalten und diese zur besseren Übersicht in Unterordnern speichern. Zur besseren Übersicht sollten Sie zumindest einen Unterordner für Ihre Ressourcen erstellen. Ich nenne diesen (wie Visual Studio für die Ressourcen, die über die Projekteigenschaften bearbeitet werden können) Resources. Außerdem sollten Sie Ihre Ressourcen zumindest nach dem Typ trennen. So erhalten Sie z. B. die RessourcenDateien Strings.resx und Images.resx. Wenn Sie auf diese Weise vorgehen, verbessern Sie die Übersicht und erleichtern nebenbei die Lokalisierung. Die Eigenschaft BUILDVORGANG von .resx-Ressourcen-Dateien muss auf EINGEBETTETE RESSOURCE stehen, damit diese korrekt (und auswertbar) in die Assembly integriert werden. Per Voreinstellung ist dies jedoch bereits der Fall. Für den Fall, dass die Anwendung eine Ausnahme mit der Meldung wirft, dass eine Ressource nicht gefunden werden kann, sollten Sie die BUILDVORGANG-Einstellung überprüfen.
Einem Projekt können Sie beliebig viele .resx-Dateien hinzufügen
12
13 TIPP
14
15 INFO
16
Ich verwende als Beispiel den Windows.Forms-Nettorechner aus Kapitel 2. Wenn Sie dies nachvollziehen wollen, kopieren Sie das Projekt in einen anderen Ordner und öffnen Sie dieses. Legen Sie dann einen Unterordner Resources und in diesem die .resx-Dateien Strings.resx und Images.resx an. Platzieren Sie ein PictureBox-Steuerelement auf dem Formular, dessen Eigenschaft SizeMode Sie auf Zoom einstellen. Benennen Sie dieses mit dem Namen pbxFlag.
17
18 Abbildung 15.2: Das NettorechnerProjekt mit einer PictureBox auf dem Formular und zwei Ressourcen-Dateien
19
20
21
22
23 Wenn Sie eine Ressourcen-Datei in Visual Studio öffnen, zeigt Visual Studio nicht die XML-Basisdaten an, sondern einen praktischen Editor.
949
Konfiguration, Ressourcen und Lokalisierung
Abbildung 15.3: Der RessourcenEditor von Visual Studio
In diesem Editor können Sie Texte, Bilder, Symbole (Icons), Audio-Dateien und beliebige Dateien hinzufügen. Dazu stellen Sie die linke Liste im oberen Bereich (die per Voreinstellung auf ZEICHENFOLGEN steht) auf die entsprechende Ressourcen-Art um. Ressourcen fügen Sie hinzu, indem Sie einen der Befehle in der Liste RESSOURCE HINZUFÜGEN wählen. Alternativ können Sie eine Datei auch aus dem Windows Explorer (oder einem alternativen Dateimanager) in den Editierbereich ziehen. Text-Ressourcen bearbeiten Sie natürlich direkt. Ich füge der Images.resx-Datei ein Bild mit Namen Flag hinzu (Abbildung 15.4). Abbildung 15.4: Der RessourcenEditor mit einem hinzugefügten Bild
Binäre Ressourcen können Sie dann noch umbenennen, was durchaus Sinn macht, wenn Sie Dateien hinzufügen und diese einen anderen Namen aufweisen (in meinem Fall war das die Datei GreatBritainFlag.png).
INFO
Die Basisdateien binärer Ressourcen, die Sie aus einer Datei hinzugefügt oder neu erstellt haben, werden von Visual Studio in das Projekt kopiert. Die Eigenschaft BUILDVORGANG dieser Dateien steht auf KEINE. Die Eigenschaft IN AUSGABEVERZEICHNIS kopieren auf NICHT KOPIEREN. Diese Dateien werden beim Kompilieren also nicht weiter berücksichtigt. Visual Studio benötigt die Dateien aber, weil die Ressourcendaten in der .resx-Datei nicht eingebettet sind, sondern über die Datei lediglich referenziert werden. Die Texte des Formulars des Nettorechners sollen in der Datei Strings.resx verwaltet werden. Damit bereite ich die eine Basis-Variante der Lokalisierung vor. Die Texte gebe ich gleich in der englischen Sprache an, weil diese die Basissprache einer Lokalisierung ist. Dazu erfahren Sie aber im Abschnitt »Lokalisierung« ab Seite 953 mehr.
950
Ressourcen
Abbildung 15.5 zeigt die von mir angelegten Ressourcen in der Strings.resx-Datei. Abbildung 15.5: Die in der Datei Strings.resx angelegten Ressourcen
12
13
14 Die .resx-Datei können Sie übrigens auch über den Symbolleisten- oder Kontextmenübefehl CODE ANZEIGEN in der originalen XML-Form öffnen um sich den Inhalt anzuschauen (oder diesen zu bearbeiten, was in der Praxis für Text-Ressourcen durchaus Sinn macht – vor allen Dingen, wenn diese längere Texte beinhalten). Das Einlesen der Ressourcen ist dann ein Leichtes: Visual Studio generiert nämlich bei jeder Speicherung einer geänderten .resx-Datei eine Klasse, die den Zugriff ermöglicht. Diese finden Sie in der .designer.cs-Datei, die der Ressource untergeordnet ist. Über statische Eigenschaften können Sie auf die Ressourcen zugreifen. Dazu müssen Sie den Namensraum der Klasse angeben, der sich aus dem Stammnamensraum der Assembly und dem Namen des Unterordners zusammensetzt. Für unseren Fall sieht das Einlesen der Texte und des Bildes (im Load-Ereignis des Formulars) folgendermaßen aus:
15 Für das Einlesen der Ressourcen generiert Visual Studio eine Klasse
16
17
18
Listing 15.12: Einlesen von Ressourcen für die Oberfläche im Load-Ereignis eines Formulars private void StartForm_Load(object sender, EventArgs e) { // Die String-Ressourcen einlesen this.Text = Resources.Strings.Title; this.lblGross.Text = Resources.Strings.GrossLabel; this.lblTax.Text = Resources.Strings.TaxLabel; this.lblNet.Text = Resources.Strings.NetLabel; this.btnCalculate.Text = Resources.Strings.CalculateButton; this.btnClose.Text = Resources.Strings.CloseButton;
19
20
// Das Bild einlesen this.pbxFlag.Image = Resources.Images.Flag;
21
}
Um das Beispiel zu vervollständigen, muss auch der Ereignishandler des ClickEreignisses des Rechnen-Schalters die Ressourcen für die Meldungen verwenden:
22 Listing 15.13: Verwenden von Ressourcen für die Ausgabe von Meldungen im Programm private void btnCalculate_Click(object sender, EventArgs e) { // Deklaration von Variablen für die Berechnung double gross, tax, net;
23
951
Konfiguration, Ressourcen und Lokalisierung
// Die Eingaben überprüfen und gleichzeitig in double-Werte // konvertieren und in die dafür vorgesehenen Variablen schreiben if (double.TryParse(this.txtGross.Text, out gross)) { if (double.TryParse(this.txtTax.Text, out tax)) { // Beide Eingaben sind in Ordnung, // also kann gerechnet werden // Den Nettowert berechnen net = gross * 100 / (100 + tax); // Den berechneten Wert in die Netto-TextBox schreiben this.txtNet.Text = net.ToString(); } else { // Die Steuer-Eingabe ist nicht OK this.txtGross.Text = null; MessageBox.Show(Resources.Strings.Message_InvalidTax); } } else { // Die Brutto-Eingabe ist nicht OK this.txtGross.Text = null; MessageBox.Show(Resources.Strings.Message_InvalidGross); } }
Und schon wird der (ausgeführte) Nettorechner (zunächst ausschließlich) in der englischen Sprache angezeigt (Abbildung 15.6). Abbildung 15.6: Der Nettorechner in der Version, die mit Ressourcen arbeitet
Das ist schon alles zu den Grundlagen eingebetteter .resx-Dateien. In früheren Versionen von .NET war es zudem noch notwendig, .resx-Ressourcen direkt einzulesen. Dieses Feature bespreche ich hier nicht. Wenn Sie wissen wollen, wie dies geht, schauen Sie sich die generierte Klasse in der .designer.cs-Datei an.
Die Standard-Ressourcen Ein spezielles Visual-Studio-Feature ist, dass Sie in den Eigenschaften des Projekts ein Register für allgemeine Ressourcen finden (Abbildung 15.7). Die hier angegebenen Ressourcen werden in der Datei Resources.resx im Ordner Resources verwaltet. Das ist aber auch schon alles, was diese Ressourcen von den selbst angelegten unterscheidet. Ich würde Ihnen empfehlen, die Standard-Ressourcen nicht zu verwenden und stattdessen mehrere eigene Ressourcen-Dateien zu pflegen.
952
Lokalisierung
Abbildung 15.7: Die Standard-Ressourcen, die Visual Studio in den Projekteigenschaften zur Verfügung stellt
12
13
14 Nun, da Sie die Grundlagen eingebetteter Ressourcen kennen, geht es direkt weiter zur Lokalisierung, die darauf aufbaut. Dort erwähne ich auch die Möglichkeiten, die Sie mit losen .resx-Dateien haben, die nicht in die Anwendung eingebettet werden.
15.3
15
Lokalisierung
16
Die Lokalisierung einer Anwendung ist zumindest für Anwendungen, die in mehrere Länder ausgeliefert werden, ein sehr wichtiger Bestandteil der Entwicklung. Glücklicherweise ist die Lokalisierung unter .NET prinzipiell recht einfach.
15.3.1
17
Grundlagen der Lokalisierung
Lokalisierung bedeutet, in einer Anwendung zu ermöglichen, dass diese in verschiedenen Sprachen ausgegeben wird. .NET unterstützt die Lokalisierung über einfache Techniken, wie .resx-Dateien. Das Ganze basiert auf einem System, das mit so genannten Satelliten-Assemblys arbeitet, die für verschiedene Kulturen angelegt sind.
Lokalisierung = Mehrsprachigkeit
19
Kulturen Eine .NET-Anwendung (genauer: jeder Thread in einer solchen) wird immer unter einer Kultur ausgeführt. Die aktuell eingestellte erreichen Sie über Thread.CurrentThread.CurrentCulture und Thread.CurrentThread.CurrentUICulture. Beide Eigenschaften referenzieren ein CultureInfo-Objekt mit Informationen zu der Kultur. Die Unterscheidung in zwei Kulturen ist etwas verwirrend, aber eben vorhanden. CurrentUICulture wird (zumindest unter Windows.Forms und ASP.NET) von allen Steuerelementen verwendet, die kulturspezifische Informationen ausgeben1. Ein Beispiel dafür ist das ASP.NET-Calendar-Steuerelement (nicht sein Windows.Forms-Pendant MonthCalendar!), das seine Texte in allen möglichen Kulturen darstellen kann. Außerdem wird CurrentUICulture von der .NET-Lokalisierung verwendet. CurrentCulture wird von allen anderen .NET-Typen verwendet, die kulturspezifisch arbeiten. Dazu gehört z. B. die ToString-Methode der DateTimeOffset-Klasse, die ein Datum passend zur Kultur formatiert ausgibt.
18
Die aktuelle Kultur ist in zwei Eigenschaften des aktuellen Thread gespeichert
20
21
22
23 1
Wenn Sie das einmal ausprobieren, sollten Sie beachten, dass ausgerechnet die sehr kulturabhängigen Windows.Forms-Steuerelemente MonthCalendar und DateTimePicker die .NET-Kultur nicht berücksichtigen (aber stattdessen die unter Windows eingestellte Kultur).
953
Konfiguration, Ressourcen und Lokalisierung
Warum Microsoft zwei Eigenschaften zur Einstellung der Kultur angelegt hat, ist mir allerdings vollkommen unklar. Einen solchen Unsinn, wie die Oberfläche in Deutsch, aber alle Formatierungen in Englisch auszugeben, wird wohl kein Mensch auf dieser Welt benötigen. Na ja … Ein CultureInfoObjekt verwaltet die KulturInformationen
Ein CultureInfo-Objekt liefert neben den allgemeinen Kultur-Informationen wie dem Namen eine Menge Informationen darüber, wie Daten in der jeweiligen Kultur formatiert oder ausgegeben werden müssen. Alle .NET-Methoden zum Formatieren von Daten setzen diese Informationen ein. Die Eigenschaft DateTimeFormat der CultureInfo-Klasse liefert z. B. Informationen über das Datumsformat. Auf diese Dinge will ich hier aber nicht eingehen, weil Sie sich darauf verlassen können, dass alle .NET-Methoden, die Daten auf irgendeiner Weise formatiert ausgegeben, die aktuelle Kultur berücksichtigen. Schauen Sie sich CultureInfo einmal an, um die Vielfalt der intern verwalteten Informationen zu erkennen. Die aktuell eingestellte Kultur wird beim Start einer .NET-Anwendung aus der Einstellung des Windows-Systems ausgelesen. Wenn eine lokalisierte Anwendung die Kultur nicht explizit einstellt und auf einem englischen System gestartet wird, wird diese automatisch in Englisch angezeigt (sofern für die englische Kultur Ressourcen vorhanden sind). Auf einem deutschen System werden alle Texte in Deutsch ausgegeben. Die Kultur können Sie auch explizit einstellen, wozu Sie eine CultureInfo-Instanz verwenden, die Sie über CultureInfo.CreateSpecificCulture erzeugen: Listing 15.14: Explizites Einstellen der aktuellen Kultur Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en"); Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
Idealerweise erfolgt diese Einstellung beim Start der Anwendung, da Sie damit erreichen, dass die Anwendung die eingestellte Kultur auch wirklich verwendet. Ein nachträgliches Einstellen ist relativ schwierig, da bereits geöffnete Formulare/Fenster nicht automatisch mit den Ressourcen der anderen Kultur ausgestattet werden (ein Beispiel dafür finden Sie aber auf Seite 958). Prinzipiell können Sie dem Anwender damit aber ermöglichen, die Kultur unabhängig von der Systemeinstellung einzustellen. Was bei der Arbeit mit Kulturen wesentlich wichtiger ist, ist die Einteilung der Kulturen, der Name der Kulturen, die Bedeutung von Satelliten-Assemblys und der Ressourcen-Fallback-Mechanismus.
Die Einteilung von Kulturen Kulturen werden unter .NET in drei Bereiche eingeteilt: ■
■
954
Die so genannte invariante Kultur ist die Kultur, die verwendet wird, wenn für die aktuelle Kultur keine Ressource gefunden wird. Die Texte und Formatierungen dieser Kultur entsprechen laut der Dokumentation der englischen Kultur. Höchstwahrscheinlich entspricht .NET-intern die invariante Kultur aber der USamerikanischen (was aber nicht dokumentiert ist). Neutrale Kulturen stehen für Kulturen, die dieselbe Grund-Sprache verwenden. Die neutrale Deutsche Kultur findet sich z. B. in Deutschland, Österreich, der Schweiz und in Teilen von Italien. Auch wenn die einzelnen Kulturen die deutsche Sprache in abgewandelter Form verwenden, handelt es sich dennoch um
Lokalisierung
■
Deutsch (OK, einen echten Schweizer zu verstehen, ist fast unmöglich, aber das gilt auch, wenn ein Hoch-Norddeutscher auf einen Tief-Bayern trifft ☺). Neutrale Kulturen werden mit einem zweistelligen, kleingeschriebenen Kürzel gekennzeichnet. »de« steht z. B. für die deutsche, »en« für die englische Kultur. Spezifische Kulturen stehen für Kulturen, die eine spezifische Sprache verwenden. Das ist z. B. die Deutsch-Deutsche, die Deutsch-Östereichische, die Deutsch-Schweizerische oder die Englisch-Australische. Spezifische Kulturen werden mit dem Namen der entsprechenden neutralen Kultur gekennzeichnet, gefolgt von einem Bindestrich und einem groß geschriebenen Kürzel für das jeweilige Land. Beispiele sind »de-DE«, »de-AT«, »de-CH« und »en-AU«.
12
Diese Unterscheidung ist wichtig für die Implementierung der Ressourcen zu Lokalisierung einer Anwendung. So können Sie entscheiden, dass Sie vorwiegend Ressourcen für neutrale Kulturen implementieren. Für die deutsche Sprache reicht es z. B. in der Regel aus, wenn Sie eine neutrale deutsche Ressource erzeugen. In einigen Kulturen gibt es aber trotz einer gemeinsamen Sprache teilweise erhebliche Unterschiede. In Irland werden z. B. teilweise andere Begriffe verwendet als in Großbritannien oder anderen englischsprachigen Ländern. Ein Beispiel dafür ist die Automarke Opel, die in Irland »Opel« heißt, in Großbritannien »Vauxhall« und in Australien »Holden«2. Solche Probleme können Sie recht einfach lösen, indem Sie für die einzelnen spezifischen Kulturen separate Ressourcen erzeugen.
13
Die Namen der Kulturen
16
14
15
Die Namen der unter .NET verfügbaren Kulturen werden in der .NET-Dokumentation der CultureInfo-Klasse sehr ausführlich erläutert. Tabelle 15.1 zeigt wegen der Vielfalt nur eine Auswahl. Kultur-/Sprachbezeichner
Kultur bzw. Sprache
Kultur-/Sprachbezeichner
Kultur bzw. Sprache
Leerer String ("")
Die invariante Kultur
fr-FR
Französisch in Frankreich
nl
Neutrales Niederländisch
fr-CH
Französisch in der Schweiz
nl-BE
Niederländisch in Belgien
de
Neutrales Deutsch
nl-NL
Niederländisch in den Nieder- de-AT landen
Deutsch in Österreich
en
Neutrales Englisch
de-DE
Deutsch in Deutschland
en-AU
Englisch in Australien
de-LI
Deutsch in Liechtenstein
en-IE
Englisch in Irland
de-LU
Deutsch in Luxemburg
en-GB
Englisch in Großbritannien
de-CH
Deutsch in der Schweiz
en-US
Englisch in den USA
es
Neutrales Spanisch
fr
Neutrales Französisch
es-MX
Spanisch in Mexiko
fr-BE
Französisch in Belgien
es-VE
Spanisch in Venezuela
2
17 Tabelle 15.1: Einige der vielen in .NET verfügbaren Kulturen
18
19
20
21
22
23
Was wahrscheinlich daran liegt, dass »Opel« sich in der englischen Aussprache etwas eigenartig anhört. Die (Republik-)Iren haben aber eine andere Betonung, die Vokale viel stärker betont (etwa so wie bei uns) und deshalb kein Problem mit »Opel«
955
Konfiguration, Ressourcen und Lokalisierung
Satelliten-Assemblys und der Fallback-Mechanismus Satelliten-Assemblys sind die Basis der Lokalisierung
Satelliten-Assemblys sind das, was hinter der ganzen Lokalisierung steckt. Eine Satelliten-Assembly ist eine Assembly, die kulturspezifische Ressourcen speichert. Satelliten-Assemblys können alle Arten von Ressourcen verwalten, also .resx-Ressourcen, eingebettete binäre und WPF-Wörterbuch-Ressourcen (in BAML-Form). Satelliten-Assemblys werden in Unterordnern relativ zu der Anwendung (oder Assembly) verwaltet, die mit den Ressourcen arbeitet. Die Unterordner tragen den Namen der Kultur, für die die Satelliten-Assembly erzeugt wurde. Der wesentliche Punkt ist nun, dass die Ressourcen der invarianten Kultur normalerweise3 in der eigentlichen Assembly gespeichert sind. Alle Ressourcen, die ich dem Nettorechner im Abschnitt ».resx-Ressourcen« hinzugefügt habe, gehören z. B. dazu. Diese Ressourcen sind im Projekt also grundsätzlich bekannt. Wird nun eine bestimmte Ressource angefordert, sucht die Ressourcen-Verwaltung von .NET nach einer Satelliten-Assembly in einem Ordner, der den Namen der aktuellen spezifischen Kultur entspricht. Auf einem deutschen System wäre das z. B. der Ordner de-DE. Wird diese gefunden, wird sie eingelesen und auf die angeforderte Ressource überprüft.
.NET verwendet implizit einen Fallback-Mechanismus
Ist keine Ressource für die spezifische Kultur vorhanden, sucht .NET nach einer Ressource für die neutrale Kultur. Auf einem deutschen System würde also im Ordner de nach einer Satelliten-Assembly gesucht werden und in dieser nach der Ressource. Wird die Ressource auch dort nicht gefunden, wird schließlich die verwendet, die in der eigentlichen Assembly gespeichert ist (also die invariante Ressource). Das Ganze wird als »Fallback-Mechanismus« bezeichnet und ist sehr hilfreich bei der Entwicklung von lokalisierten Anwendungen. So werden für eine mehrsprachige Anwendung in der Praxis häufig neben Ressourcen für die invariante Kultur primär Satelliten-Assemblys für die neutralen Kulturen implementiert. Die Texte (oder Bilder), die in spezifischen Kulturen unterschiedlich sind, werden in Satelliten-Assemblys für die spezifische Kultur verwaltet. Dabei werden in diesen Assemblys nur die Ressourcen verwaltet, die sich von den neutralen unterscheiden. Das Opel-Beispiel ist eine typische Anwendung für diese Technik. In einer Anwendung, die in der englischen Sprache veröffentlicht werden soll, würde die Assembly selbst alle allgemeinen Begriffe speichern (da Englisch ja für die invariante Kultur verwendet wird). Eine Satelliten-Assembly im Ordner en-GB würde (u. U. lediglich) die Ressource für den britischen Namen von Opel (Vauxhall) verwalten, eine SatellitenAssembly im Ordner en-AU würde den australischen Namen (Holden) verwalten. Der Fallback-Mechanismus erleichtert somit den aufwändigen Prozess der Lokalisierung erheblich. Satelliten-Assemblys haben übrigens deshalb das »Satellit« im Namen, weil diese in der Anwendung nicht fest referenziert werden (wie ein Satellit, der die Erde ohne Verbindung zu dieser umrundet). Sie können Satelliten-Assemblys problemlos entfernen, ohne dass die Anwendung deswegen nicht mehr ausgeführt werden kann (damit nehmen Sie aber natürlich die entsprechenden Ressourcen weg). Anders herum können Sie auch nachträglich Satelliten-Assemblys für andere Kulturen hinzufügen, was für komplexe Anwendungen u. U. Sinn macht. So können Sie eine Anwendung auch nachträglich mit einer neuen Sprache ausstatten.
3
956
Es kann auch konfiguriert sein, dass die Ressourcen der invarianten Kultur in einer speziellen Satelliten-Assembly verwaltet werden
Lokalisierung
Die Frage ist nun, wie Ihre Anwendung zu den Satelliten-Assemblys für die einzelnen unterstützten Kulturen kommt. Dies kann auf zwei Arten geschehen: Die einfachste ist, dass Sie neben der invarianten .resx-Dateien in einem Projekt weitere verwalten, deren Name dem Basisnamen der Ressource entspricht, vor deren Endung aber der Name der Kultur angefügt wird. Eine String-Ressource für die neutrale deutsche Kultur würde z. B. Strings.de.resx heißen. Sie können natürlich auch .resx-Ressourcen für spezifische Kulturen verwalten, wie z. B. für die deutsche in Deutschland (Strings.de-DE.resx). Wenn Sie ein solches Projekt kompilieren, erzeugt der Compiler automatisch passende Unterordner und in diesem die entsprechenden Satelliten-Assemblys. Im Fall des Nettorechners würden Sie im Ausgabeordner deshalb die Unterordner de und de-DE finden, die jeweils eine Satelliten-Assembly mit dem Namen NetCalculator.resources.dll verwalten.
Satelliten-Assemblys werden beim Kompilieren erzeugt, wenn kulturspezifische .resx-Dateien vorhanden sind
12
13
Eine andere Möglichkeit ist, die Satelliten-Assemblys (z. B. als Klassenbibliothek) nachträglich zu kompilieren. Diese müssen dann lediglich den korrekten Namensraum aufweisen und die Ressourcen müssen in demselben Unterordner gespeichert sein. So können Sie eine bereits laufende Anwendung ohne ein Neu-Kompilieren auch nachträglich mit neuen Kulturen ausstatten, indem Sie die erzeugten neuen Satelliten-Assemblys einfach in der Kultur entsprechende Unterordner kopieren (bzw. bei einem Update der Anwendung installieren).
14
15
Lokalisierung am Beispiel 16
Am Nettorechner zeige ich nun die Lokalisierung am Beispiel. Um diesen in die deutsche Sprache zu lokalisieren, kopieren Sie die Strings.resx-Datei in die Datei Strings.de.resx und ändern in dieser Datei die String-Ressourcen in die deutsche Sprache. Zudem sollten Sie auch die Datei Images.resx in die Datei Images.de.resx kopieren und in dieser das Bild (auf die deutsche Flagge) ändern. Achten Sie aber darauf, dass der Name der Ressource derselbe ist. Sie müssen die Bild-Ressource aber nicht unbedingt ändern, da der Fallback-Mechanismus dafür sorgt, dass zumindest das in der Anwendungs-Assembly gespeicherte Bild für die invariante Kultur gefunden wird.
17
18
Kompilieren Sie dann das Projekt und führen Sie dieses aus. Der Nettorechner sollte nun (auf einem deutschen System) in deutscher Sprache angezeigt werden.
19 Abbildung 15.8: Der Nettorechner in Deutsch
20
21
22 Wenn Sie Ressourcen-Dateien verwalten, erzeugt Visual Studio per Voreinstellung für jeden dieser Ressourcen-Dateien eine separate Klassendatei. Für die spezifischen Ressourcen ist diese Datei allerdings leer. Das Erzeugen der leeren Datei können Sie verhindern, indem Sie den Wert der Eigenschaft BENUTZERDEFINIERTES TOOL löschen (was natürlich nicht für die invariante Ressource gilt).
INFO
23
957
Konfiguration, Ressourcen und Lokalisierung
Anders herum kann es sein, dass Sie eine Ressourcen-Datei von einem anderen Projekt in ein Projekt kopieren, aber die Klasse nicht generiert wird. Tragen Sie in diesem Fall in der Eigenschaft BENUTZERDEFINIERTES TOOL den Wert »ResXFileCodeGenerator« ein. Die Klasse sollte daraufhin automatisch erzeugt werden.
Nachträgliches Hinzufügen von Satelliten-Assemblys Das nachträgliche Hinzufügen von Satelliten-Assemblys macht in speziellen Situationen Sinn. Eine denkbare Anwendung wäre, in dem eigentlichen Projekt nur die invariante Kultur zu unterstützen und die Satelliten-Assemblys erst später, in einem Build-Lauf aus .resx-Dateien zu erzeugen, die von Übersetzern in die entsprechende Sprache überführt wurden. Somit sparen Sie sich bei der Entwicklung eines Projekts die Pflege der Ressourcen für die verschiedenen unterstützten Kulturen. Damit verhindern Sie aber auch, dass Sie während der Entwicklung testen können, ob die Anwendung in den verschiedenen Sprachen noch korrekt dargestellt wird. Eine andere Anwendung wäre, bereits installierte Versionen der Anwendung nachträglich mit weiteren Kulturen auszustatten. Dies können Sie prinzipiell relativ einfach erreichen, indem Sie dem entsprechenden Projekt .resx-Dateien für neue Kulturen hinzufügen, das Projekt kompilieren und die neuen Satelliten-Assemblys an die Anwender ausliefern (idealerweise per Online-Update, was bei ClickOnceAnwendungen problemlos möglich ist, wie Sie in Kapitel 16 sehen werden).
Eine Umschaltung der Sprache In vielen Anwendungen ist es möglich, die Sprache umzuschalten, während die Anwendung ausgeführt wird. Mit der bisher implementierten Lösung (bei der Sie die Ressourcen selbst einlesen und den Steuerelementen zuweisen) ist dieses Problem schnell gelöst (bei der bequemeren Lokalisierung, die ich im Abschnitt »Lokalisierung einer Windows.Forms-Anwendung« und bei der WPF-Lokalisierung ist dies allerdings nicht mehr ganz so einfach). Der Trick ist, die Zuweisung der Ressourcen an die Steuerelemente in einer separaten Methode auszuführen, die den Namen der Kultur übergeben bekommt. Diese Methode stellt dazu auch die aktuelle Kultur um: Listing 15.15: Methode zum Einstellen der Kultur in der Laufzeit private void SetCulture(string cultureName) { // Die Kultur für den aktuellen Thread einstellen Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureName); Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture; // Die String-Ressourcen einlesen this.Text = Resources.Strings.Title; this.lblGross.Text = Resources.Strings.GrossLabel; this.lblTax.Text = Resources.Strings.TaxLabel; this.lblNet.Text = Resources.Strings.NetLabel; this.btnCalculate.Text = Resources.Strings.CalculateButton; this.btnClose.Text = Resources.Strings.CloseButton; // Das Bild einlesen this.pbxFlag.Image = Resources.Images.Flag; }
958
Lokalisierung
Diese Methode rufen Sie beim Laden des Formulars auf. Die aktuell einzustellende Kultur lesen Sie aus der Anwendungskonfiguration aus. Dann benötigen Sie nur noch Steuerelemente zum Einstellen der Kultur (wie ein Menü oder RadioButton-Elemente). Die entsprechenden Ereignishandler rufen die SetCulture-Methode auf und speichern die aktuell eingestellte Kultur Anwendungskonfiguration. Ich verzichte aus Platzgründen hier auf ein Beispiel. Dieses finden Sie aber auf der Buch-DVD.
12 DISC
15.3.2
Lokalisierung einer Windows.Forms-Anwendung
Die Prinzipien der Lokalisierung gelten für jede Art von Anwendung. In einer Windows.Forms-Anwendung können Sie jedoch ein spezielles Feature benutzen: Sie können ein Windows.Forms-Formular recht einfach lokalisieren, indem Sie die Eigenschaft Language auf eine andere Sprache als die invariante setzen, die Beschriftungen der Steuerelemente und – was ein besonders hilfreiches Feature ist – auch die Position und Größe der Steuerelemente anpassen. Dabei sollten Sie aber die folgenden Punkte beachten: ■
■
Formulare ermöglichen ein spezielles Lokalisations-Feature
14
15
Vergeben Sie allen Steuerelementen, die zu lokalisierende Texte enthalten, vor der Lokalisierung einen vernünftigen Namen. Der Name taucht in der automatisch erzeugten .resx-Datei wieder auf. Legen Sie idealerweise bereits bei der Entwicklung des Formulars alle Beschriftungen in der englischen Sprache an. Die Grundbeschriftung gilt für die invariante Kultur.
16
Danach stellen Sie die Eigenschaft Language um und passen die Beschriftungen und ggf. auch die Größe und Position der Steuerelemente an. Visual Studio erzeugt für die invariante und alle eingestellten neutralen oder spezifischen Kulturen eine .resxDatei, die dem Formular zugeordnet wird. Diese Ressourcen werden automatisch (in InitializeComponent) eingelesen.
17
18
Der einzige Nachteil dieser Vorgehensweise ist, dass die erzeugten .resx-Dateien nicht sehr gut lesbar sind, weil zum einen die Namen der Steuerelemente als Ressourcenname auftauchen, zum anderen aber auch Positions-, Größen- und andere Informationen in der Ressource verwaltet werden. Wenn Sie diese Ressourcen einem Übersetzer zukommen lassen, ist dieser damit u. U. überlastet.
19
Ansonsten ist dieses Feature – besonders wegen der Möglichkeit, die Größe und Position der Steuerelemente anzupassen – sehr hilfreich. Bei der Lokalisierung von Anwendungen müssen Sie nämlich sehr darauf achten, dass Texte in anderen Sprachen auch länger sein können, als der verfügbare Platz zulässt. Alle Strings, die Sie im Programm verwenden, müssen Sie natürlich auch lokalisieren, wozu Sie die Basis-Lokalisierung verwenden, die ich zu Anfang des LokalisierungsAbschnitts erläutert habe. Leider gibt es keine direkte Möglichkeit, die Strings, die in Meldungen etc. verwendet werden, über ein Visual-Studio-Tool zu extrahieren. Es gibt allerdings kommerzielle Tools, die die Lokalisierung erleichtern (siehe »Ansätze zu einem Lokalisieren in der Praxis«).
13
20
21 INFO
22
23
959
Konfiguration, Ressourcen und Lokalisierung
TIPP
Um weitere Sprachen zu ermöglichen, müssen Sie nicht den eher umständlichen Weg über das Editieren der Beschriftungen auf dem Formular gehen. Sie können auch eine der bereits vorhandenen .resx-Dateien kopieren, entsprechend der neuen Kultur umbenennen und deren Texte anpassen. Das Kopieren ist eigenartigerweise aber nicht über Visual Studio möglich. Kopieren Sie die Datei unter Windows, schalten Sie den Projektmappen-Explorer in Visual Studio über den zweiten Schalter in der Symbolleiste so um, dass er alle Dateien des Projektordners anzeigt, und fügen Sie die neue Ressourcen-Datei über deren Kontextmenü hinzu. Danach stellen Sie im Formular die entsprechende Sprache ein und passen ggf. die Größe und Position der Steuerelemente an.
15.3.3 Lokalisierung einer WPF-Anwendung Die Lokalisierung einer WPF-Anwendung ist leider ein nicht allzu einfacher Prozess. Prinzipiell können Sie dazu natürlich die Basis-Technik verwenden, die ich bereits beschrieben habe: Erstellen Sie .resx-Ressourcen und lesen Sie diese beim Laden eines Fensters in die Steuerelemente. Wünschenswert ist aber, dass die Lokalisierung relativ (oder idealerweise voll-)automatisch in XAML direkt erfolgt und dass im Lokalisierungsprozess Dateien herauskommen, die von externen Übersetzern leicht bearbeitet werden können. Leider hat Microsoft dieses für die Praxis sehr wichtige Thema sehr vernachlässigt (was auch erkannt wurde, siehe blogs.windowsclient.net/ rob_relyea/archive/2008/04/10/wpf-localization.aspx). Die von Microsoft vorgeschlagene Technik ist etwas obskur. Es gibt aber eine Menge an anderen Lösungen (z. B. die Lokalisierung über XML-Dateien, separate Ressourcen-Dictionaries oder eine Abwandlung der Microsoft-Technik). Ich kann hier nicht auf alle eingehen. Deswegen behandle ich nur eine einfache, gute und leichtgewichtige Technik über .resx-Dateien und die eigenartige Microsoft-Version. Hinweise zu anderen Möglichkeiten finden Sie im Internet, u. a. an den folgenden Adressen (denken Sie wie immer daran, dass Sie diese Adressen auch in der Datei Links.html auf der Buch-DVD finden): ■ ■ ■
blogs.microsoft.co.il/blogs/tomershamam/archive/2007/10/30/wpf-localization-onthe-fly-language-selection.aspx www.codeproject.com/KB/WPF/WPFUsingLocbaml.aspx www.codeproject.com/KB/WPF/WPF-Mulit-Lingual.aspx
Vorbereitungen Als Vorbereitung der Lokalisierung sollten Sie die Fenster Ihrer Anwendung so gestalten, dass die unterschiedlich großen Texte in den verschiedenen Sprachen kein Problem werden können. Dazu sollten Sie die folgenden Punkte beachten: ■ ■ ■
960
Verwenden Sie für Beschriftungs-Steuerelemente möglichst keine feste Breite und (bei Steuerelementen, die den Text umbrechen) auch keine feste Höhe. Verwenden Sie die Eigenschaft SizeToContent, die dafür sorgt, dass die Größe an den Inhalt angepasst wird. Das gilt besonders für Fenster. Setzen Sie als Layout-Steuerelemente vorwiegend solche ein, die eine dynamische Breite der enthaltenen (Beschriftungs-)Steuerelemente erlauben. Ein Grid mit mehreren Spalten ist sehr gut dafür geeignet. Nutzen Sie die Möglichkeit, dass sich Spalten des Grid die gleiche Breite teilen.
Lokalisierung
■ ■ ■
Vermeiden Sie die Verwendung des Canvas-Containers. Falls Sie mit der Margin-Eigenschaft arbeiten, erlauben Sie extra Platz für den Fall, dass Texte in anderen Sprachen länger ausfallen. Schalten Sie die TextWrapping-Eigenschaft von TextBlock-Elementen (und ggf. auch von TextBox-Elementen) auf Wrap oder WrapWithOverflow.
Weitere Hinweise zur Vorbereitung der Lokalisierung einer WPF-Anwendung finden Sie in der Visual-Studio-Dokumentation in dem Thema »Übersicht über WPF-Globalisierung und -Lokalisierung«, das Sie finden, indem Sie nach »WPF-Globalisierung« suchen.
12 REF
13 Lokalisierung auf die einfache Art Zur Lokalisierung einer WPF-Anwendung können Sie dieselbe Grund-Technik verwenden wie schon am Anfang dieses Abschnitts (am Beispiel des Windows.FormsNettorechners ab Seite 953). Dazu verwalten Sie die Ressourcen in .resx-Dateien und lesen diese wie im Windows.Forms-Beispiel beim Laden des Fensters ein. In der Praxis ist dieser Aufwand jedoch zu hoch. Der nächste Ansatz wäre also, die Ressourcen direkt in XAML zu referenzieren. Dazu verwenden Sie die x:StaticMarkuperweiterung, um auf die (statischen) Eigenschaften der Lokalisierungs-Ressourcen-Klassen zuzugreifen. Als Beispiel verwende ich den Nettorechner aus Kapitel 12, den ich um dieselben Ressourcen (außer dem Bild) erweitert habe wie in dem Windows.Forms- Beispiel:
14 Eine Lokalisierung direkt in XAML ist ideal
15 16
Listing 15.16: Auszug aus der XAML-Datei des versuchsweise lokalisierten WPF-Nettorechners
17
Leider funktioniert dieser Ansatz (noch) nicht. Das Problem ist, dass WPF Markuperweiterungen erst in der Laufzeit auswertet. Die Ressourcen-Eigenschaften sind aber als internal gekennzeichnet, weswegen die WPF-Laufzeitumgebung diese nicht auslesen kann. Bereits der Visual-Studio-WPF-Designer meldet z. B. am ersten Label den Fehler, dass »der statische Member "GrossLabel"« nicht im »NetCalculator.Resources.Strings-Typ« enthalten ist.
18
19 WPF kann die internen Ressourcen-Eigenschaften per Voreinstellung nicht auslesen
20
21
Eine erste Lösung des Problems wäre, den internal-Modifikator der Eigenschaften in der .designer-Datei der Ressource in public zu ändern. Danach ist erst einmal alles in Ordnung. Sogar der Visual-Studio-WPF-Designer kommt mit dem Einlesen der Ressourcen zurecht.
22
Das Problem ist aber, dass die .designer-Datei bei jeder Änderung der RessourcenDatei von Visual Studio (bzw. von dem benutzerdefinierten Tool ResXFileCodeGenerator) neu erzeugt wird. Ihre Änderungen gehen dann verloren.
23
961
Konfiguration, Ressourcen und Lokalisierung
TIPP
Die Lösung des Problems ist die Verwendung eines anderen Generators. Stellen Sie in der Eigenschaft BENUTZERDEFINIERTES TOOL der .resx-Datei für die invariante Kultur dazu den Generator PublicResXFileCodeGenerator ein. Und schon ist das Problem beseitigt und Sie können Ressourcen direkt in XAML einbauen. Das einzige Problem dieser Technik ist, dass eine dynamische Sprachumschaltung prinzipiell nicht möglich ist. Dafür gibt es vielleicht Lösungen, ich habe aber noch keine gefunden.
Lokalisierung nach Microsoft-Art Die einfache Art der Lokalisierung wird von Microsoft in der Dokumentation nur am Rande erwähnt, obwohl diese meiner Meinung nach eine sehr interessante ist. Damit können Sie die Lokalisierung gut steuern und haben strukturierte RessourcenDateien. Die Ressourcen-Dateien sind zudem sehr einfach aufgebaut und können deswegen auch problemlos von externen Übersetzern in andere Sprachen überführt werden. Microsoft schlägt vor, die Lokalisierung über LocBaml.exe erst nach der Fertigstellung vorzunehmen
Microsoft denkt aber bei der Lokalisierung etwas komplexer. Die Lokalisierung erfolgt auf eine etwas andere, sehr obskure Art (deren Hintergründe zudem sehr schlecht bis gar nicht dokumentiert sind, aber dafür haben Sie ja dieses Buch ☺). Die Basis der Lokalisierung nach Microsoft-Art ist, dass der komplette XAML-Code von Fenstern (oder Seiten) als Ressource in Satelliten-Assemblys gespeichert wird. Zudem ist die invariante Kultur in diesem Fall nicht in der Anwendungs-Assembly gespeichert, sondern wird explizit auf eine bestimmte Kultur wie z. B. die US-amerikanische gesetzt. Wenn Sie ein Hauptfenster mit Namen MainWindow haben und neben der invarianten auch die deutsche Kultur unterstützen, haben Sie also: ■
■
■
die eigentliche Definition des Hauptfensters in der MainWindow.xaml-Datei, die in die Anwendungs-Assembly integriert wird (die aber in der Laufzeit des Programms gar nicht verwendet wird), eine in die Satelliten-Assembly für die invariante (hier US-amerikanische) Kultur integrierte Ressource, die den kompletten XAML-Code von MainWindow.xaml enthält. Die lokalisierbaren Texte der WPF-Elemente sind hier in der englischen Sprache angegeben. eine in die Satelliten-Assembly für die deutsche Kultur (hier US-amerikanische) integrierte Ressource, die noch einmal den kompletten XAML-Code von MainWindow.xaml enthält. Die lokalisierbaren Texte der WPF-Elemente sind hier in Deutsch angegeben.
Je nach eingestellter Kultur verwendet WPF dann das Fenster in der entsprechenden Satelliten-Assembly. Die eigentliche Lokalisierung erfolgt erst nach Fertigstellung der Anwendung über das Tool LocBaml.exe, das ein Bestandteil der WPF-Beispiele ist. Der Weg dahin ist aber etwas kompliziert. Ich halte nicht viel von dieser verwirrenden Art, eine WPF-Anwendung zu lokalisieren. Ein riesiges Problem ist, dass Sie den ganzen Lokalisierungsprozess wiederholen müssen, wenn das Hauptfenster nachträglich geändert wird (was ja in der Praxis schon einmal vorgekommen sein soll ☺ – oh, das sollte vielleicht einmal jemand Microsoft sagen?).
962
Lokalisierung
Folgen Sie den folgenden Schritten, wenn Sie die verwirrende Microsoft-Technik einmal ausprobieren wollen: 1. 2.
Als erste wichtige Voraussetzung sollten Sie die Oberfläche der Anwendung in der invarianten Kultur (also in der englischen Sprache) entwickeln. Zur Lokalisierung über LocBaml.exe müssen Sie dann alle lokalisierbaren Elemente über das Attribut x:Uid mit einer eindeutigen ID versehen. Microsoft empfiehlt dazu den Build-Prozessor MSBuild in der folgenden Form zu verwenden: msbuild /t:updateuid Projektname.csproj Damit erzeugen Sie automatisch x:Uid-Einträge, die den Namen des WPF-Elements tragen, sofern einer angegeben ist. Ist kein Name angegeben, wird der x:Uid-Eintrag entsprechend des Typnamens des WPF-Element mit einem hochgezählten Index erzeugt
STEPS
12
13
14
MSBuild ist ein Bestandteil des Windows-SDK. Sie finden dieses Programm normalerweise im Ordner C:\Windows\Microsoft.NET\Framework\v3.5. Sie können die Visual Studio-Eingabeaufforderung verwenden, die Sie im Startmenüeintrag von Visual Studio im Ordner VISUAL STUDIO TOOLS finden, um dieses Programm zu starten.
15
Die erzeugten IDs können Sie natürlich auch nachbearbeiten:
16
Fool on the Hill Matt Ruff
12
Der DOM-Baum dieses Dokuments entspricht Abbildung 18.1. Abbildung 18.1: Ein typischer DOM-Baum
13
14
15
16 In Klammern habe ich die dem jeweiligen Objekt entsprechende Klasse im X-DOM angegeben. Wie Sie sehen, ist der Aufbau recht logisch und entspricht dem Aufbau des XML-Dokuments. Beachtenswert ist, dass auch XML-Kommentare und Attribute in entsprechende Objekte überführt werden.
17
In Wirklichkeit ist das DOM noch komplexer, da z. B. auch Textinhalt von XMLElementen und -Attributen als Objekt dargestellt wird.
18
INFO
Das DOM basiert also auf einem Baum von Objekten, die je nach Art des XMLBestandteils, den sie repräsentieren, von einem unterschiedlichen Typ sind. Diese Typen werden von einer gemeinsamen Basisklasse abgeleitet. Im X-DOM ist das die Klasse XObject. XObject enthält eine Referenz auf das Parent-Element. Von XObject sind alle Klassen des X-DOM abgeleitet. Abbildung 18.2 zeigt eine Übersicht der wichtigsten X-DOM-Klassen.
XObject ist die Basisklasse
19
20 Abbildung 18.2: Einfaches Klassendiagramm der wichtigsten X-DOMKlassen
21
22
23
1033
XML
EXKURS
XNode definiert einen Knoten im XML-Dokument
Dieses Klassendiagramm verwendet UML-Verbindungslinien zur Darstellung der Beziehungen zwischen den Klassen. Eine Linie mit einem Pfeil bedeutet, dass eine Klasse von einer anderen abgeleitet ist. Der Pfeil zeigt zur Superklasse. Eine Linie mit einer Raute steht dafür, dass eine Klasse eine andere in einem Feld oder einer Eigenschaft referenziert. Die Klasse, an der die Raute angegeben ist, referenziert die andere. Ein Stern steht dafür, dass mehrere Instanzen der anderen Klasse referenziert werden können. Der Name an der Linie ist der Name des Feldes oder der Eigenschaft. Die für die Arbeit mit XML-Dokumenten wesentliche Klasse ist XNode. Eine Instanz dieser Klasse stellt ein »Knoten« im XML-Dokument dar. Ein Knoten ist im Allgemeinen ein Objekt, das von einem anderen Element als Kind-Element referenziert wird. Nicht zu den Knoten gehören XML-Attribute, die durch die Klasse XAttribute gekennzeichnet werden. XNode selbst ist abstrakt, bietet aber wichtige Eigenschaften und Methoden zur Navigation und zum Einfügen neuer Knoten innerhalb des Dokuments (allerdings nur nach oben und zur Seite). Über die Eigenschaft NextNode können Sie z. B. den nächsten nebengeordneten Knoten ermitteln. Die Methode Ancestors gibt eine Auflistung der »Vorfahren« des Knoten (also der übergeordneten Knoten) zurück, AddAfterSelf fügt einen neuen nebengeordneten Knoten in das XML-Dokument ein.
XContainer definiert einen Container-Knoten
XContainer ist ein spezialisierter Knoten, der als Zusatz Kind-Elemente aufnehmen kann (in seiner Nodes-Auflistung). XContainer ist ebenfalls abstrakt, liefert aber einige weitere Navigations- und Editiereigenschaften und -methoden (die sich auf die Kind-Elemente beziehen). Die Eigenschaft FirstNode referenziert z. B. den ersten Kind-Knoten, die Methode Descendants liefert eine Auflistung aller untergeordneten Knoten (auch die in weiter untergeordneten Container-Elementen).
XElement repräsentiert ein XML-Element
Die von XContainer abgeleitete Klasse XElement repräsentiert ein XML-Element (also das, was durch einen kompletten XML-Tag inklusive Inhalt dargestellt wird). Als wichtigen Zusatz zu XContainer verwaltet XElement den Namen des Elements (in der Eigenschaft Name), erlaubt einen gespeicherten Wert (in der Eigenschaft Value) und ermöglicht die Definition von Attributen.
XDocument repräsentiert ein XML-Dokument
XDocument schließlich repräsentiert ein XML-Dokument. In seiner Root-Eigenschaft referenziert es ein XElement-Objekt, das das Wurzel-Element des XML-Dokuments ist. Daneben werden die XML-Deklaration, XML-Prozessinstruktionen und andere XML-Dokument-Basisinformationen verwaltet. XDocument weicht vom W3C-DOM ab. In diesem ist die Dokument-Klasse von der Element-Klasse abgeleitet (was im alten .NET-DOM auch so implementiert ist). Im W3C-DOM können Dokumente nur über die Dokument-Klasse gelesen und geschrieben werden. Das X-DOM erlaubt aber auch das Lesen und Schreiben von XML-Daten über ein XElement. Das X-DOM ermöglicht damit, dass Sie einzelne Knoten eines XML-Dokuments verwalten können, ohne gleich ein Dokument erzeugen zu müssen. Sie können ein XElement sogar als XML-Dokument (allerdings dann natürlich ohne XML-Prozessinstruktionen und weitere spezielle Dokument-Features) serialisieren oder ein XML-Dokument in ein XElement einlesen.
XText verwaltet textuelle Daten, XAttribute die Daten eines Attributs
1034
Bleiben nur noch die ebenfalls von XNode abgeleiteten Klassen XText und XAttribute. XText verwaltet den Textinhalt von XML-Elementen, die einen solchen besitzen. In der Praxis haben Sie mit dieser Klasse aber nur selten zu tun, da die Eigenschaft Value der XElement-Klasse auch Strings verarbeiten kann (die implizit in ein XTextObjekt konvertiert werden). XAttribute verwaltet die Daten eines XML-Attributs.
Noch nicht behandelte XML-Grundlagen
18.1.2
XML-Schema (XSD), XPath und XSLT
Die XML Schema Definition (XSD), XPath und XSLT sind Begriffe, die im Zusammenhang mit XML immer wieder auftauchen. Sie sollten zumindest die Bedeutung dieser Begriffe kennen, wenn Sie mit XML-Dokumenten arbeiten. Besonders XSD wird in der Praxis sehr häufig eingesetzt, weil es den fehlerfreien Umgang mit XML-Dokumenten ermöglicht.
12 XSD Die XML Schema Definition (XSD), die auch als »XML Schema« bezeichnet wird, ist ein vom W3C entworfener Standard zur Beschreibung der Struktur eines XML-Dokuments. Das originale W3C-Dokument zur Beschreibung von XSD finden Sie an der Adresse www.w3.org/XML/Schema. XSD ist aber sehr komplex und der originale Standard nicht einfach zu lesen.
13
14 An der Adresse www.w3schools.com/schema/default.asp finden Sie eine sehr gute Erläuterung von XSD. REF
15
Ein XSD-Dokument ist selbst ein XML-Dokument. Es enthält spezifische XML-Elemente, die: ■ ■ ■ ■ ■ ■
Elemente beschreiben, die in einem XML-Dokument vorkommen können oder vorkommen müssen, Attribute beschreiben, die an einem XML-Elemente enthalten sein können oder müssen, beschreiben, welche Elemente Kinder eines Elements sein können oder müssen, den Datentyp von Elementen und Attributen festlegen, festlegen, ob ein Element leer sein darf oder nicht, und die den Defaultwert eines leeren Elements festlegen können.
16
17
18
XSD ist der Nachfolger der älteren DTD (Document Type Definition), mit der die Struktur eines XML-Dokuments ebenfalls festgelegt werden konnte. XSD besitzt aber wesentlich mehr Möglichkeiten als DTD.
19
Listing 18.2 zeigt ein Beispiel-XSD-Dokument, das die Struktur einer XML-Datei beschreibt, die Artikeldaten verwalten soll. Beachten Sie die Zeilennummern (die nicht Bestandteil von XSD sind, die Ihnen aber dabei helfen, die Erläuterungen in Tabelle 18.1 zuzuordnen). Listing 18.2:
20
Beispiel-XSD-Dokument
01: 02: 06: 07: 08: 09: 10: 11: 12: 13:
21
22
23
1035
XML
14: 15: 16: 17:
18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:
Das folgende XML-Dokument entspricht diesem Schema: Die wilde Geschichte vom Wassertrinker John Irving Fool on the Hill 2004 Matt Ruff
Beachten Sie die Angabe des XMLSchema-instance1-Namensraums (http://www. w3.org/2001/XMLSchema-instance), der es ermöglicht, Nullwerte anzugeben (über xsi:null="true"). XSD ist nicht einfach zu lesen, weil XSD-Dokumente sehr komplex sind. Eigentlich ist das Ganze aber recht einfach und logisch strukturiert: ■
■
1
1036
xs:Element beschreibt ein XML-Element. Über die Attribute minOccurs und maxOccurs können Sie festlegen, wie häufig das Element vorkommen muss bzw. kann. Per Voreinstellung stehen beide auf 1. Das Attribut nillable legt fest, ob der Wert null erlaubt ist (über das Attribut xsi:nil="true" im Element). xs:Attribute beschreibt ein Attribut. Das Attribut use bestimmt mit required (Voreinstellung), dass das Attribut vorhanden sein muss und mit optional, dass das Attribut vorhanden sein kann.
Der etwas eigenartige Name dieses Namensraums stammt wohl daher, dass er für Instanzen eines XML-Schemas (also XML-Dokumente) gilt.
Noch nicht behandelte XML-Grundlagen
■ ■
xs:complexType sagt aus, dass das übergeordnet beschriebene Element ein komplexer Typ ist, also Kind-Elemente beinhalten kann. xs:all und xs:sequence beschreiben eine Liste von Elementen, die als Inhalt des übergeordneten Elements enthalten sein können oder müssen. Der Unterschied ist, dass xs:sequence die Reihenfolge vorschreibt und xs:all nicht. Ich verwende in der Praxis lieber xs:all, weil ich es als unsinnig empfinde, die Reihenfolge der Elemente vorzuschreiben. Über die Attribute minOccurs und maxOccurs können Sie festlegen, wie häufig die Elementliste vorkommen muss bzw. kann. Per Voreinstellung stehen beide auf 1.
12
Daneben existieren noch weitere XSD-Elemente wie xs:Any, das beliebige andere Elemente zulässt, die nicht im Schema definiert sind, xs:choice, das bestimmt, dass eines der unterhalb definierten Elemente vorkommen kann, und die in der Praxis wichtigen Möglichkeiten, Einschränkungen und Aufzählungen in XSD zu definieren, die die zulässigen Werte eines Elements bestimmen. Für eine Beschreibung des kompletten XSD-Standards fehlt mir aber hier der Platz. Deswegen folgt nur noch ein kleines Beispiel für die wichtigen Einschränkungen und Aufzählungen.
13
14
Eine Einschränkung definieren Sie, indem Sie den Typ eines Elements über einen xs:simpleType angeben: Listing 18.3:
15
Definition einer Einschränkung in XSD
16
17
Eine Aufzählung können Sie ebenfalls in einem xs:simpleType direkt definieren (über xs:enumeration-Elemente). In der Praxis werden diese aber häufig über ein separates xs:simpleType-Element definiert, das als Typ in einem xs:Element oder xs:Attribute referenziert wird:
18 19
Listing 18.4: Definition einer (separaten) Aufzählung in XSD
20
21
22
...
Interessant sind auch die Typangaben, die in Form von XSD-Typen erfolgen. xs:string steht z. B. für einen String. Wenn Sie ein XSD-Dokument mit Visual Studio erzeugen, hilft IntelliSense bei der Auswahl.
23
1037
XML
TIPP
REF
Wenn Sie für ein XML-Dokument ein XML-Schema erzeugen wollen, können Sie so vorgehen, dass Sie in Visual Studio ein Beispiel-Dokument erzeugen, das alle vorgesehenen Möglichkeiten beinhaltet (inklusive Daten in den erwarteten Datentypen, optionalen Werten, die in einem Element angegeben sind und in einem anderen nicht etc.). Über den Befehl SCHEMA ERSTELLEN im XML-Menü können Sie aus diesem Dokument ein Schema erstellen. Dieses müssen Sie in der Praxis nur noch wenig nachbearbeiten, wenn Sie eine XML-Datei mit allen Möglichkeiten verwendet haben. Visual Studio nutzt dazu übrigens das .NET-Tool xsd.exe. In der Praxis werden XSD-Dokumente häufig von (meist kommerziellen) Tools erzeugt, die die Definition der XML-Struktur über einfache Editoren ermöglichen. XMLSpy von Altova (www.altova.com/products/xmlspy/xml_editor.html) und der in der Free-Edition kostenlose XMLFox (www.xmlfox.com) sind Beispiele dafür. Falls Sie mit Listing 18.2 (verständlicherweise) noch Probleme haben, hilft Tabelle 18.1 vielleicht, die das XSD-Schema erläutert.
Tabelle 18.1: Beschreibung der im Beispiel verwendeten XSD-Elemente
1038
Element
Bedeutung
06:
beschreibt, dass das Wurzel-Element ein Element mit dem Namen books sein muss.
07:
beschreibt ein XML-Element, das dem Wurzel-Element untergeordnet sein kann. Das Element ist ein komplexer Typ (mit Unterelementen, im Gegensatz zu einem simpleType, der nur einen einfachen Wert verwalten kann). Der mögliche Inhalt des Elements wird als Inhalt von complexType beschrieben.
08:
beschreibt, dass hier eine Sequenz von Elementen folgen kann, die sequence untergeordnet sind. Im Beispiel besteht die Sequenz nur aus einem Element, dem book-Element. Da die optionalen Attribute minOccurs und maxOccurs nicht angegeben sind, können beliebig viele book-Elemente untergeordnet werden.
09:
beschreibt, dass hier ein Element mit dem Namen book folgt (weil davor keine Mindestanzahl angibt, muss allerdings kein book-Element enthalten sein, weil davor keine Maximalanzahl angibt, können beliebig viele book-Elemente enthalten sein).
10:
beschreibt, dass das book-Element ein komplexer Typ ist (mit Unterelementen).
11:
ist ähnlich. Beide legen fest, dass die unterhalb beschriebenen Elemente (je nach deren Anzahl-Einstellung!) enthalten sein können oder müssen. Der Unterschied zwischen und ist, dass die angegebene Reihenfolge erzwingt, aber nicht.
12:
beschreibt, dass der komplexe Typ des book-Elements ein Element mit Namen title und mit dem XSD-Typ string besitzen muss.
Noch nicht behandelte XML-Grundlagen
Element
Bedeutung
13:
beschreibt, dass der komplexe Typ des book-Elements ein Element mit Namen year und mit dem XSD-Typ unsignedShort besitzen kann (weil minOccurs auf 0 und maxOccurs auf 1 gesetzt sind). Außerdem ist über xs:nillable="true" festgelegt, dass das Element – falls es vorhanden ist – auch einen Nullwert speichern kann.
14: 15: 16:
gibt an, dass der komplexe Typ des book-Elements ein Element mit Namen author enthalten muss. Dieses ist wieder ein komplexer Typ, der alle der im folgenden beschriebenen Elemente in einer beliebigen Reihenfolge beinhalten muss.
17 und 18
Diese Angaben beschreiben auf ähnliche Weise den Inhalt des author-Elements.
23:
beschreibt, dass der komplexe Typ des book-Elements ein Attribut mit dem Namen isbn und dem XSD-Typ string enthalten muss.
24:
beschreibt, dass der komplexe Typ des book-Elements ein Attribut mit dem Namen id und dem XSD-Typ integer enthalten kann.
Das erzeugte Schema ist natürlich nicht sinnlos. Zum einen können Sie das Schema in Visual Studio nutzen: Wenn Sie eine gleichnamige XML-Datei im Projekt integriert haben, können Sie entsprechend des Schemas IntelliSense nutzen. Außerdem zeigt Visual Studio Fehler im XML-Dokument an.
Tabelle 18.1: Beschreibung der im Beispiel verwendeten XSD-Elemente (Forts.)
12
13
14
15
Schemas können in Visual Studio und im Programm genutzt werden
16
17
Sie können ein XML-Schema aber auch direkt in einem XML-Dokument angeben: Listing 18.5:
Angabe eines XML-Schema in einem XML-Dokument
18
19
Beachten Sie das Leerzeichen zwischen der URL des Speicherorts und dem Namen des Schemas. INFO
20
Wenn Sie ein solches XML-Dokument einlesen, können Sie dafür sorgen, dass dieses automatisch gegen das Schema geprüft wird. Entspricht es nicht dem Schema, wird eine XmlSchemaValidationException geworfen. In der Praxis werden XML-Dokumente aber eher explizit in der Laufzeit gegen ein Schema geprüft. Damit haben Sie die Möglichkeit, sicherzustellen, dass bei der Verarbeitung des XML-Dokuments alle erwarteten Daten im erwarteten Typ vorhanden sind. Auf dieses Feature gehe ich im Abschnitt »XML-Dokumente über XSD validieren« ab Seite 1075 ein.
21
22
XPath XPath (XML Path Language) ist wie XSD ein Standard des W3C. XPath definiert eine Sprache zur Adressierung von Teilen eines XML-Dokuments. XPath wird häufig dazu eingesetzt, in XML-Dokumenten nach Teilinformationen zu suchen. In .NET arbeiten Sie mit XPath, wenn Sie die alten XML-DOM-Klassen (XmlDocument,
23
1039
XML
XmlElement etc.) einsetzen. In den neuen (XDocument, XElement etc.) setzen Sie zur Abfrage stattdessen LINQ to XML ein.
Deswegen gehe ich hier auch nicht näher auf XPath ein. Ein Beispiel-Pfad zur Abfrage aller Bücher in der XML-Datei des Abschnitts »XSD« sieht z. B. so aus: books/book
Wollen Sie alle Bücher mit einem Rating von 5 abfragen, sieht der XPath-Pfad folgendermaßen aus: books/book[rating=5]
Die Abfrage eines Buchs mit einer bestimmten ISBN-Nummer sieht so aus: books/book[@isbn='978-3423117371']
Und nur der Vollständigkeit halber folgt eine Abfrage über ein XmlDocument: Listing 18.6: XPath-Abfrage mit einem XmlDocument XmlDocument xmlDocument = new XmlDocument(); xmlDocument.Load("Books.xml"); // Abfrage aller Bücher mit einem Rating von 5 Console.WriteLine("Bücher mit einem Rating von 5:"); xPath = "books/book[rating=5]"; foreach (XmlNode node in xmlDocument.SelectNodes(xPath)) { Console.WriteLine(node.SelectSingleNode("title").InnerText); }
XPath hat bei der Abfrage von Daten einige Einschränkungen und erfordert das Lernen der XPath-Sprache. Wenn Sie stattdessen LINQ to XML verwenden, müssen Sie keine separate Abfragesprache lernen. Deswegen gehe ich in diesem Kapitel auch nicht weiter auf XPath ein. An der Adresse www.w3schools.com/xpath/default.asp finden Sie eine hervorragende Einführung in XPath. REF
XSL/XSLT Die Extensible Stylesheet Language (XSL) ist in der Praxis ein ebenso wichtiger Teil des XML-Standards wie XSD und (wenn Sie nicht mit LINQ to XML arbeiten) XPath. XSL ist ein Standard des W3C, der eine XML-Sprache zur Transformation eines XMLDokuments in ein anderes Format beschreibt. Das andere Format kann jedes beliebige (Text-)Format sein. So können Sie z. B. ein XML-Dokument in ein HTML-Dokument transformieren oder in eine kommaseparierte Textdatei. Das Kürzel XSLT steht übrigens für »XSL Transformation« (also die Transformation eines XML-Dokuments über XSL). Beide Begriffe werden in der Praxis analog verwendet.
REF
Ich gehe hier nicht auf den sehr komplexen XSLT-Standard ein. Bei W3 Schools finden Sie (wie für alle XML-Standards) eine sehr gute Beschreibung: www.w3schools.com/ xsl/default.asp. In .NET können Sie die Klasse XslCompiledTransform verwenden, um XML-Dokumente über XSL zu transformieren.
1040
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Im Abschnitt »Transformation von XML-Dokumenten mit LINQ to XML« (Seite 1067) beschreibe ich. wie Sie XML-Dokumente auch mit LINQ to XML transformieren können.
18.1.3
Die XML-APIs
Das .NET Framework liefert einige APIs zur Arbeit mit XML-Dokumenten. Die (mehr oder weniger) aktuellen beschreibt die folgende Auflistung. ■
■
■
■
■
■
12
LINQ to XML und die dazu verwendeten Klassen wie XDocument und XElement bieten sehr durchdachte und performante Möglichkeiten. Über diese Klassen können Sie XML-Dokumente (über eine Microsoft-Abwandlung des W3C-DOM) erzeugen und auch lesen. Die Abfrage von XML-Daten erfolgt hierbei über LINQ to XML. Ältere Klassen wie XmlDocument und XmlElement bieten einen ähnlichen Zugriff wie XDocument und XElement (über das W3C-DOM Level 1 und W3C-DOM Level 2 Core), sind aber weniger performant und nicht für LINQ to XML optimiert. Die Abfrage von Daten in einem XmlDocument erfolgt über XPath. Die älteren DOMKlassen behandle ich in diesem Kapitel nicht. Die Klassen XmlReader und XmlWriter (die intern von XDocument und XElement verwendet werden) bieten einen schnellen sequenziellen Zugriff auf XMLDokumente (der allerdings eine komplexere Programmierung erfordert). XmlReader erlaubt zudem die Prüfung gegen ein XML-Schema, was diesen auch zum Einlesen der Daten in ein XElement oder XDocument interessant macht. Beide Klassen behandle ich grundlegend ab Seite 1069. Die Klasse XPathNavigator erlaubt die performante Navigation in einem XmlDocument über XPath (die weniger performante XPath-Navigation ist auch direkt in einem XmlDocument möglich). Diese Klasse bespreche ich in diesem Kapitel nicht. Die Klasse XslCompiledTransform erlaubt das Transformieren eines XML-Dokuments über ein XSL-Dokument in ein anderes Format. XslCompiledTransform behandle ich in diesem Kapitel ebenfalls nicht. Schließlich haben Sie mit der in Kapitel 17 behandelten XML-, SOAP- und Datenvertrag-Serialisierung zusätzliche Möglichkeiten, XML-Dokumente zu bearbeiten.
13
14
15
16
17
18 19
Ich gehe in diesem Kapitel nicht auf alle Möglichkeiten ein, weil ich denke, dass LINQ to SQL und die damit verbundenen Klassen die besten Möglichkeiten bieten. Die älteren DOM-Klassen (XmlDocument, XmlElement etc.) behandle ich nicht mehr, weil der Zugriff über XDocument, XElement etc. ähnlich ist.
20
XmlReader und XmlWriter sind in manchen Situationen als leichtgewichtige Möglichkeit, XML-Dokumente zu bearbeiten, interessant (und wurden in Kapitel 17 auch schon eingesetzt). Deswegen gehe ich auch grundlegend auf diese Klassen ein.
21
Die Serialisierung habe ich bereits in Kapitel 17 behandelt.
18.2
22
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
23
Die Klassen des X-DOM (XElement, XDocument etc.) ermöglichen eine sehr flexible Arbeit mit XML-Dokumenten. Obwohl der Namensraum dieser Klassen (System. Xml.Linq) suggeriert, sie würden ausschließlich zu LINQ gehören, ist dies nicht der
1041
XML
Fall. Diese Klassen sind eine Microsoft-Abwandlung des W3C-DOM. Sie sind lediglich für LINQ optimiert, da sie zum einen in vielen Fällen IEnumerable-Auflistungen zurückgeben (die mit LINQ bearbeitet werden können). Zum anderen bieten sie eine hilfreiche Technik, XML-Dokumente über Konstruktoren im Programmcode zu konstruieren, die sehr gut in LINQ-Selektoren eingesetzt werden kann. Sie können ein XML-Dokument jedoch auch ohne LINQ lesen (allerdings nicht gezielt abfragen). Das Erzeugen und Ändern von XML-Dokumenten ist mit LINQ prinzipiell erst gar nicht möglich (LINQ ist nur für das Lesen von Daten vorgesehen). Ich zeige in diesem Abschnitt also zuerst, wie Sie XML-Dokumente direkt über die X-DOM-Klassen lesen, erzeugen und bearbeiten. Danach erläutere ich, wie Sie XML-Dokumente mit LINQ to XML abfragen.
18.2.1 Load und Parse erlauben das Laden aus einer Datei oder einem XML-String
INFO
Laden und Parsen von einfachen XMLDokumenten
Die Klassen XElement und XDocument bieten beide eine Load- und eine Parse-Methode zum Einlesen eines XML-Dokuments (oder im Fall von XElement auch nur eines TeilDokuments ab einem Knoten). Load lädt ein XML-Dokument über einen URI, einen TextReader oder einen XmlReader. Die Parse-Methode erzeugt das X-DOM über einen übergebenen XML-String. Bei der Bearbeitung von XML-Dokumenten unterscheiden sich beide Klassen nicht wesentlich. Mit XElement können Sie XML-Dokumente prinzipiell genauso bearbeiten, wie mit XDocument. XDocument bietet aber zusätzliche Features wie die Möglichkeit, die XML-Deklaration zu beeinflussen oder spezielle XML-Prozessinstruktionen einzufügen.
Laden eines XML-Dokuments So können Sie ein XML-Dokument z. B. aus einer lokalen Datei oder aus dem Internet laden (die verwendeten Klassen XDocument und XElement im Beispiel können Sie auch vertauschen, dies macht keinen Unterschied): Listing 18.7:
Laden von XML-Dokumenten aus einer Datei
// XML-Dokument aus dem Internet laden XDocument xDocument = XDocument.Load("http://juergen-bayer.net/" + "buecher/csharpkompendium/XML-Beispiele/Books.xml"); // XML-Dokument aus einer lokalen Datei laden XElement xElement = XElement.Load("Books.xml");
INFO
1042
Der Load-Methode der zweiten Anweisung übergebe ich einen relativen Dateinamen. Load erwartet einen URI, wenn Sie einen String übergeben. Ich bin mir bei Microsoft nie so sicher, ob relative Dateinamen wirklich immer funktionieren, aber scheinbar können Sie hier problemlos einen relativen Dateinamen angeben, der dann relativ zum Anwendungsordner gilt. Möglich sind aber auch absolute Dateinamen und Internet-URLs wie in der ersten Anweisung im Beispiel. Eine einzulesende XMLDatei muss dem Programm beim Kompilieren nicht bekannt sein (sie muss also nicht mit dem Buildvorgang INHALT in das Projekt integriert werden).
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Das Auslesen einer Ressource (mit Buildvorgang RESSOURCE) ist leider (noch) nicht möglich (was etwas eigenartig ist, aber wahrscheinlich hat das LINQ-to-XML-Team sich nicht mit dem WPF-Team ausgetauscht). Besonders das Laden aus dem Internet kann natürlich in der Praxis Probleme verursachen, falls das XML-Dokument nicht verfügbar ist oder das Laden zu viel Zeit in Anspruch nimmt. Außerdem kann es auch sein, dass das XML-Dokument ungültig ist. Sie sollten deswegen in der Praxis eine Ausnahmebehandlung vorsehen (für eine lokale Datei fangen Sie die üblichen IO-Ausnahmen wie FileNotFoundException ab, für ein Laden aus dem Internet die WebException).
12
13
Parsen eines XML-Strings Parse ermöglicht das Parsen eines XML-Strings in ein X-DOM: Listing 18.8: Parsen eines XML-String
14
string xmlString = "" + "Basilikum-Tofu" + "Haselnuss-Tofu" + ""; XElement xElement2 = XElement.Parse(xmlString);
15
Der String muss natürlich ein gültiges XML-Dokument enthalten. Die XML-Deklaration muss allerdings nicht enthalten sein (die Angabe der Codierung macht bei einem String auch nicht wirklich Sinn).
16
18.2.2 Erzeugen eines XML-Dokuments ohne Namensraum
17
Das Erzeugen eines XML-Dokuments in der Laufzeit ist schon etwas aufwändiger als das Lesen und Parsen. Das Prinzip ist aber einfach: Sie erzeugen dazu zunächst ein XElement-Objekt, das das Wurzel-Element darstellt. Am Konstruktor übergeben Sie den Namen des Elements und – bei XML-Elementen mit Text-Inhalt – optional auch den Inhalt.
Das X-DOMObjektmodell erlaubt ein flexibles Erzeugen
Zum Hinzufügen von Unter-Knoten können Sie nun zwei Wege gehen: den DOMähnlichen Weg über den Aufruf der Add-Methode oder den .NET-spezifischen, als »Funktionale Konstruktion« bezeichneten Weg.
19
Die DOM-ähnliche Methode XContainer erlaubt es, über die Add-Methode einem XML-Container beliebige Objekte unterzuordnen. Der Typ des Arguments dieser Methode ist Object. Sie können also prinzipiell jedes Objekt übergeben. Objekte, die nicht von XObject abgeleitet sind, werden allerdings (über ToString) in einen String umgewandelt. Ansonsten können Sie auch Instanzen der X-DOM-Klassen übergeben, um den Baum des XML-Dokuments aufzubauen. Das Ganze ist – verglichen mit dem alten DOM über XmlDocument, XmlElement etc. – sehr einfach.
18
20 XContainer.Add erlaubt das Hinzufügen von Kind-Knoten
21
22
Ich baue auf diese Weise das XML-Dokument von Seite 1032 nach. Sehr hilfreich ist, dass prinzipiell alle konkreten Klassen des X-DOM einen Konstruktor besitzen, über den Sie den Namen und optional auch den Wert des Objekts definieren können:
23
1043
XML
Listing 18.9: Dynamisches Erzeugen eines XML-Dokuments // Das Wurzel-Element erzeugen XElement rootElement = new XElement("books"); // Das erste Buch rootElement.Add(new XComment("Ein super geniales Buch")); XElement bookElement = new XElement("book"); bookElement.Add(new XAttribute("isbn", "978-3257224450")); bookElement.Add(new XElement("title", "Die wilde Geschichte vom Wassertrinker")); XElement authorElement = new XElement("author"); authorElement.Add(new XElement("firstName", "John")); authorElement.Add(new XElement("lastName", "Irving")); bookElement.Add(authorElement); rootElement.Add(bookElement); // Das zweite Buch rootElement.Add(new XComment("Wundervoll verträumtes Buch")); bookElement = new XElement("book"); bookElement.Add(new XAttribute("isbn", "978-3423117371")); bookElement.Add(new XElement("title", "Fool on the Hill")); authorElement = new XElement("author"); authorElement.Add(new XElement("firstName", "Matt")); authorElement.Add(new XElement("lastName", "Ruff")); bookElement.Add(authorElement); rootElement.Add(bookElement);
INFO
Falls Ihnen das Erzeugen hier sehr komplex vorkommt: In der Praxis werden XMLDokumente in der Regel dynamisch erzeugt, z. B. indem die Daten aus einer Datenbank eingelesen werden. Dieses Erzeugen sieht dann schon nicht mehr ganz so schlimm aus. Die funktionale Konstruktion des X-DOM erleichtert das Erzeugen zudem erheblich.
Funktionale Konstruktion Die von Microsoft etwas hochtrabend benannte »Funktionale Konstruktion2« ermöglicht die Erstellung eines XML-Elements in einer einzigen Anweisung: Listing 18.10: Erzeugen eines XML-Dokuments mit der so genannten »Funktionalen Konstruktion« XElement booksElement = new XElement("books", new XComment("Ein super geniales Buch"), new XElement("book", new XAttribute("isbn", "978-3257224450"), new XElement("title", "Die wilde Geschichte vom Wassertrinker"), new XElement("author", new XElement("firstName", "John"), new XElement("lastName", "Irving"))), new XComment("Wundervoll verträumtes Buch"), new XElement("book", new XAttribute("isbn", "978-3423117371"), new XElement("title", "Fool on the Hill"), new XElement("author", new XElement("firstName", "Matt"), new XElement("lastName", "Ruff"))));
2
1044
Der Begriff »Funktionale Konstruktion« soll an die funktionale Programmierung anknüpfen. In meinen Augen hat die klassische Anwendung von Konstruktoren und params-Argumenten mit der funktionalen Programmierung aber rein gar nichts zu tun.
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Hinter dieser Technik steckt allerdings nichts Magisches: Die konkreten X-DOMKlassen besitzen Konstruktoren, denen am zweiten Argument ein Objekt, oder, bei von XContainer abgeleiteten Klassen, auch beliebig viele Objekte als Inhalt übergeben werden können. Damit können Sie die Erzeugung eines ganzen XML-Dokuments in einer Anweisung programmieren. Sehr einfach zu verstehen ist eine Anweisung mit dieser Variante allerdings nicht. Achten Sie darauf, dass Sie die einzelnen Ebenen des XML-Dokuments wie im Beispiel einrücken, damit das Ganze einigermaßen verständlich bleibt.
12
Die funktionale Konstruktion ist sehr hilfreich bei der Abfrage von Daten mit LINQ, für den Fall, dass Sie diese dynamisch in ein XML-Dokument transformieren müssen. Dazu setzen Sie die funktionale Konstruktion im Selektor der Abfrage ein. Dazu erfahren Sie unter »Erzeugen von XML-Dokumenten über LINQ« ab Seite 1065 mehr.
13
18.2.3 Speichern von Nullwerten
14
Das Handling von Nullwerten in einem XML-Dokument ist nicht so einfach, wie Sie vielleicht denken. Wenn Sie einfach nur den Wert null in ein Element schreiben, bleibt dieses leer:
15
XElement rootElement = new XElement("demo", new XElement("value", null));
Dieses Beispiel ergibt ein leeres Element value:
16
Ein leeres Element ist aber dummerweise nicht mit null kompatibel (was etwas verwirrend sein kann). Ein leeres Element wird, wenn es in einen String eingelesen wird, als Leerstring repräsentiert. Wenn Sie das Element über die Value-Eigenschaft lesen (die vom Typ String ist), erhalten Sie einen leeren String zurück. Wenn Sie (wie unter »Lesen von Textinhalten« ab Seite 1055 besprochen wird) das Element in einen Nullable-Wert umwandeln, erhalten Sie eine FormatException:
Ein leeres Element ist etwas anderes als ein Element, das null speichert
17
18
int? number = (int?)rootElement.Element("value"); // FormatException
Das Problem ist, dass Elemente in XML nach XSD nur dann Nullwerte speichern können, wenn das Attribut nil, das zum XSD-Namensraum gehört, auf true festgelegt ist. Beim Anfügen eines Elements, das (evtl. auch erst später) Nullwerte erlauben soll, müssen Sie dieses Attribut also angeben. Hilfreich ist dabei die Eigenschaft InstanceNamespace der Klasse XmlSchema, die den XML-Schema-Instanz-Namensraum als String verwaltet.
19
Listing 18.11: Beispiel für das Anfügen eines XML-Elements, das Nullwerte zulässt
21
20
XNamespace xsi = XmlSchema.InstanceNamespace; XElement element = new XElement("demo", new XAttribute(XNamespace.Xmlns + "xsi", xsi), new XElement("value", new XAttribute(xsi + "nil", true), // Nullwerte zulassen null)); // Der Wert
22
Das erzeugte XML-Element sieht folgendermaßen aus:
23
1045
XML
INFO
Nun sollte das Konvertieren in einen Nullable-Wert funktionieren. Leider ist das aber eigenartigerweise nicht der Fall. Sie erhalten immer noch eine FormatException: int? number = (int?)rootElement.Element("value"); // FormatException
Laut einer Aussage von Vitek Karas, einem Member des LINQ-to-XML-Teams in einem Forum (auf meine entsprechende Frage), wird xs:nil von XElement nicht berücksichtigt. Das LINQ-to-XML-Team wird dieses Problem laut Vitek aber in der Zukunft lösen.
Der Workaround um dieses Problem ist eine kleine LINQ to XML-Abfrage: TIPP
XNamespace xsi = XmlSchema.InstanceNamespace; int? number = (int?)demoElement.Elements("intValue").Where( x => ((string)x.Attribute(xsi + "nil")) != "true").FirstOrDefault();
Diese Abfrage funktioniert für den Fall, dass das Element einen zum erwarteten Typ passenden Wert speichert oder dass das Element leer ist und xs:nil angegeben ist. Sie funktioniert nicht für den Fall, dass das Element leer ist, ohne dass xs:nil angegeben ist. Achten Sie also darauf, dass Sie beim Lesen von XML-Dokumenten, die leere Elemente beinhalten können, über ein XSD-Schema erzwingen, dass für leere Elemente, die in einen Nullable-Wert konvertiert werden sollen, xs:nil angegeben ist.
18.2.4 Gemischte Inhalte Ein XML-Element kann einen gemischten Inhalt besitzen: LINQ to XML ist cool.
Solche Inhalte erzeugen Sie über eine Mischung aus mehreren XText- und XElementObjekten: Listing 18.12: Erzeugen eines XML-Elements mit gemischtem Inhalt XElement summaryElement = new XElement("summary", new XElement("italic", "LINQ to XML"), new XText(" ist "), new XElement("bold", "cool"), new XText("."));
Beim Lesen der Value-Eigenschaft werden interessanterweise die untergeordneten XML-Elemente als reiner Text ausgegeben: Console.WriteLine(summaryElement.Value); // LINQ to XML ist cool.
Die Eigenschaft Elements liefert bei gemischtem Inhalt nur die untergeordneten XML-Elemente: foreach (XElement subElemement in summaryElement.Elements()) { Console.WriteLine(subElemement.ToString()); }
Das Ergebnis ist: LINQ to XML cool
Das Problem ist, dass ein XText-Objekt kein Element ist. XText ist direkt von XNode abgeleitet.
1046
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Wenn Sie alle Knoten eines gemischten Inhalts auswerten wollen, müssen Sie also die Nodes-Methode verwenden: Listing 18.13: Lesen von gemischtem Inhalt foreach (XNode subNode in summaryElement.Nodes()) { if (subNode is XText) { XText subText = (XText)subNode; Console.WriteLine(subText.Value); } else if (subNode is XElement) { XElement subElement = (XElement)subNode; Console.WriteLine(subElement.Value); }
12
13
14
}
18.2.5 Serialisieren und Speichern eines X-DOM Ein dynamisch erzeugtes (oder eingelesenes und geändertes) XML-Dokument können Sie über die Save-Methode speichern. Save erwartet einen Dateinamen, einen TextWriter oder einen XmlWriter. Damit ist das Speichern in eine Datei oder auch das Serialisieren in einen Stream möglich:
15
16
// Speichern in eine Datei rootElement.Save("Books.xml"); // Serialisieren in einen Stream using (MemoryStream stream = new MemoryStream()) { using (XmlWriter xmlWriter = XmlWriter.Create(stream)) { rootElement.Save(xmlWriter); } }
17
18
Beim direkten Speichern in eine Datei erlaubt Save wie Load relative Dateipfade. Wenn Sie das XML-Dokument als String lesen wollen, rufen Sie die ToStringMethode auf:
19
string xml = rootElement.ToString();
18.2.6 Erzeugen von XML-Dokumenten mit Namensraum
20
In der Praxis werden XML-Dokumente häufig mit zumindest einem Namensraum ausgestattet. Das Laden eines solchen Dokuments unterscheidet sich nicht vom Laden eines normalen Dokuments (das Lesen allerdings schon).
21
Beim Erzeugen müssen Sie den Namensraum aber natürlich definieren. Aber auch das ist – verglichen mit dem alten DOM – sehr einfach (nur nicht allzu intuitiv).
22
Ein Weg, den Namensraum anzugeben, ist, diesen in geschweiften Klammern vor den Elementnamen zu setzen: XElement demoElement = new XElement("{http://www.mut.de/" + "C#-Kompendium/samples}demo", "Testwert");
23
Dieser spezielle String wird von einem Konvertierungsoperator der Klasse XName verarbeitet (die den Namen eines XElement oder XAttribute verwaltet). Dieser Operator
1047
XML
XNamespace verwaltet einen Namensraum
speichert den in geschweiften Klammern angegebenen Teilstring in der Eigenschaft Namespace und den Rest in der Eigenschaft LocalName. Einfacher ist aber, den Namensraum zuvor in einem XNamespace-Objekt zu erzeugen. XNamespace definiert einen Zuweisungsoperator für String, weswegen Sie direkt einen String zuweisen können. Da XNamespace auch den Additionsoperator erzeugt (wobei ein XName-Objekt resultiert), können Sie das XNamespace-Objekt einfach mit dem Namen des Elements addieren: Listing 18.14: Erzeugen eines X-DOM mit Namensraum XNamespace ns = "http://www.mut.de/C#-Kompendium/samples"; XElement productsElement = new XElement(ns + "products", new XElement(ns + "product", new XAttribute("id", "1001"), new XElement(ns + "name", "Basilikum-Tofu"), new XElement(ns + "price", "2.50")), new XElement(ns + "product", new XAttribute("id", "1002"), new XElement(ns + "name", "Haselnuss-Tofu"), new XElement(ns + "price", "2.45"))); // Das Ergebnis ausgeben Console.WriteLine(productsElement.ToString());
TIPP
HALT
Wenn Sie sich jetzt fragen, warum ns nicht einfach nur ein String ist: Das würde nicht unbedingt funktionieren. Ein XNamespace-Objekt addiert mit einem String ergibt ein XName-Objekt, dessen Eigenschaft Namespace auf den XNamespace-Wert gesetzt und dessen Eigenschaft LocalName auf den String gesetzt ist. Die Addition zweiter Strings würde aber nur ein einfaches XName-Objekt ergeben (ohne Namensraum), sofern der Namensraum-String nicht geschweifte Klammern enthält. (XName)("x" + "y") ergibt ein XName-Objekt mit LocalName == "xy", (XName)("{x}" + "y") ein XName-Objekt mit LocalName == "x" und NamespaceName == "y". Wie Sie im Beispiel sehen, müssen Sie jedes Element, das einem Namensraum zugeordnet werden soll, mit dem Namensraum kennzeichnen. Es reicht nicht aus, nur ein übergeordnetes Element zu kennzeichnen. Lassen Sie den Namensraum eines Elements weg, wird dieses mit dem leeren Namensraum (xmlns="") gekennzeichnet. Wie Sie im Beispiel ebenfalls sehen, müssen Attribute aber nicht explizit mit dem Namensraum gekennzeichnet werden. Diese erben den Namensraum vom übergeordneten XML-Element. Abbildung 18.3 zeigt das Ergebnis des Beispielprogramms.
Abbildung 18.3: Das erzeugte XML-Dokument an der Konsole
1048
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Wenn Sie mehrere Namensräume angeben wollen, können Sie mehrere XNamespaceObjekte erzeugen und diese den entsprechenden Elementen zuweisen. Das hat dann in der erzeugten XML-Datei aber den kleinen Nachteil, dass die Namensraum-Angaben für Elemente mit unterschiedlichen Namensräumen ggf. mehrfach erscheinen. Der Haupt-Namensraum (der dem Wurzel-Element zugeordnet ist) wird dann aber nicht mehrfach ausgegeben (weil alle XML-Elemente, die dem Wurzel-Element untergeordnet sind und denen nicht explizit ein eigener Namensraum zugeordnet ist, den Haupt-Namensraum erben).
Mehrere Namensräume sind problemlos möglich
12
Das folgende Beispiel: Listing 18.15: Erstellen eines XML-Dokuments mit mehreren Namensräumen
13
XNamespace ns1 = "http://www.mut.de/C#-Kompendium/samples"; XNamespace ns2 = "http://www.juergen-bayer.net"; XElement productsElement = new XElement(ns1 + "products", new XElement(ns1 + "product", new XAttribute("id", "1001"), new XElement(ns2 + "rating", "4"), new XElement(ns1 + "name", "Basilikum-Tofu"), new XElement(ns1 + "price", "2.50")), new XElement(ns + "product", new XAttribute("id", "1002"), new XElement(ns2 + "rating", "5"), new XElement(ns1 + "name", "Haselnuss-Tofu"), new XElement(ns1 + "price", "2.45")));
14
15
16
führt zu dem folgenden XML-Dokument: 4 Basilikum-Tofu 2.50 5 Haselnuss-Tofu 2.45
17
18 19
Der zweite Namensraum ist hier mehrfach enthalten. Für die Auswertung des Dokuments ist das absolut kein Problem. Wenn Sie das Dokument aber klein halten wollen, weil Sie dieses z. B. über ein Netzwerk übertragen, oder wenn Sie die Übersichtlichkeit fördern wollen, sollten Sie für die Namensräume, die nicht der HauptNamensraum sind, Aliase verwenden.
20
Diese teilen Sie dem Wurzel-Element als Attribut mit. Die Eigenschaft XNamespace.Xmlns liefert dazu den XML-Basis-Namensraum:
21
Listing 18.16: Erstellen eines XML-Dokuments mit mehreren Namensräumen und Alias für einen Unter-Namensraum
22
XNamespace ns1 = "http://www.mut.de/C#-Kompendium/samples"; XNamespace ns2 = "http://www.juergen-bayer.net"; XElement productsElement = new XElement(ns1 + "products", new XAttribute(XNamespace.Xmlns + "jb", ns2), new XElement(ns1 + "product", new XAttribute("id", "1001"), new XElement(ns2 + "rating", "4"),
23
1049
XML
new XElement(ns1 + "name", "Basilikum-Tofu"), new XElement(ns1 + "price", "2.50")), new XElement(ns + "product", new XAttribute("id", "1002"), new XElement(ns2 + "rating", "5"), new XElement(ns1 + "name", "Haselnuss-Tofu"), new XElement(ns1 + "price", "2.45")));
Das Ergebnis sieht dann aus wie ein typisches XML-Dokument mit mehreren Namensräumen: 4 Basilikum-Tofu 2.50 5 Haselnuss-Tofu 2.45
18.2.7
Navigieren in einem XML-Dokument (ohne Namensräume)
Die Klassen XNode und XContainer bieten einige Eigenschaften und Methoden zum Navigieren in einem XML-Dokument.
INFO
Für die Praxis sollten Sie die Navigationsmöglichkeiten kennen. Einige davon werden Sie auch häufiger einsetzen. Die eigentliche Abfrage von XML-Daten erfolgt aber in der Regel über LINQ to XML (wobei aber Navigations-Eigenschaften und -Methoden in den LINQ-Abfragen eingesetzt werden). LINQ to XML beschreibe ich ab Seite 1061. Tabelle 18.2 beschreibt zunächst die Eigenschaften und Methoden der Klasse XNode, die die Navigation zur Seite und nach oben erlauben. Beachten Sie die Unterscheidung, dass einige Methoden Knoten zurückgeben und andere Elemente.
Tabelle 18.2: Die NavigationsEigenschaften und -Methoden der Klasse XNode
1050
Eigenschaft / Methode
Beschreibung
XElement Parent
referenziert das übergeordnete Element, falls eines existiert.
XNode NextNode
referenziert den nächsten nebengeordneten Knoten (Knoten auf derselben Ebene bzw. »Geschwister-Knoten«), falls einer existiert.
XNode PreviousNode
referenziert den vorherigen nebengeordneten Knoten, falls einer existiert.
IEnumerable NodesAfterSelf()
liefert eine Auflistung der nebengeordneten Knoten, die dem Knoten in der Dokument-Reihenfolge folgen.
IEnumerable NodesBeforeSelf()
liefert eine Auflistung der nebengeordneten Knoten, die dem Knoten in der Dokument-Reihenfolge voranstehen.
IEnumerable ElementsAfterSelf( [XName name ])
liefert eine Auflistung der nebengeordneten Elemente, die dem Knoten in der Dokument-Reihenfolge folgen. Sie können einen Namen übergeben, dem die zurückgegebenen Elemente entsprechen müssen.
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Eigenschaft / Methode
Beschreibung
IEnumerable ElementsBeforeSelf( [XName name ])
liefert eine Auflistung der nebengeordneten Elemente, die dem Knoten in der Dokument-Reihenfolge voranstehen, alternativ unter Spezifizierung eines Elementnamens.
IEnumerable Ancestors( [XName name ])
Diese Methode liefert eine Auflistung mit allen übergeordneten Elementen (auch die in höheren Ebenen als das direkte Parent-Element). Optional können Sie einen Namen übergeben, den die Elemente dann tragen müssen, um im Ergebnis zu erscheinen.
Tabelle 18.2: Die NavigationsEigenschaften und -Methoden der Klasse XNode (Forts.)
12
13
Die Klasse XContainer fügt Eigenschaften und Methoden zur Navigation nach unten hinzu (Tabelle 18.3). Eigenschaft / Methode
Beschreibung
XNode FirstNode
referenziert den ersten Kind-Knoten (falls einer existiert).
XNode LastNode
referenziert den letzten untergeordneten Knoten (falls einer existiert).
IEnumerable Nodes()
Diese Methode (!) gibt eine Auflistung der Kind-Knoten zurück.
XElement Element( XName name)
gibt eine Referenz auf das erste Kind-Element zurück, das den übergebenen Namen trägt.
IEnumerable Elements([XName name ])
gibt eine Auflistung aller Kind-Elemente bzw. nur der Elemente zurück, die den übergebenen Namen tragen.
IEnumerable Descendants( [XName name ])
Diese Methode liefert eine Auflistung mit allen untergeordneten Elementen (auch die in tieferen Ebenen als die direkten Kind-Elemente). Optional können Sie einen Namen übergeben, den die Elemente dann tragen müssen, um im Ergebnis zu erscheinen.
IEnumerable DescendantNodes()
liefert eine Auflistung mit allen untergeordneten Knoten (auch die in tieferen Ebenen als die direkten Kind-Knoten).
Tabelle 18.3: Die zusätzlichen Navigations-Eigenschaften und -Methoden der Klasse XContainer
14
15
16
17
18 19
XElement schließlich erlaubt zusätzlich den Zugriff auf die Attribute eines XML-Elements (Tabelle 18.4), auf den Wert und den Namen und bietet einige weitere Methoden zur Navigation nach oben und nach unten. Eigenschaft/Methode
Beschreibung
XAttribute FirstAttribute
liefert eine Referenz auf das erste Attribut, sofern Attribute vorhanden sind.
bool HasAttributes
gibt an, ob Attribute vorhanden sind.
bool HasElements
gibt an, ob das Element Kind-Elemente besitzt.
bool IsEmpty
gibt an, ob das Element leer ist.
XAttribute LastAttribute
liefert eine Referenz auf das letzte Attribut, sofern Attribute vorhanden sind.
20 Tabelle 18.4: Die zusätzlichen Navigations- und Zugriffs-Eigenschaften und -Methoden der Klasse XElement
21
22
23
1051
XML
Tabelle 18.4: Die zusätzlichen Navigations- und Zugriffs-Eigenschaften und -Methoden der Klasse XElement (Forts.)
INFO
EXKURS
Eigenschaft/Methode
Beschreibung
XName Name
verwaltet den Namen des Elements.
string Value
verwaltet den Textinhalt des Elements. Falls das Element Kind-Elemente besitzt, verwaltet Value deren XML-Darstellung.
XAttribute Attribute( XName name)
liefert eine Referenz auf das Attribut, das den übergebenen Namen trägt.
IEnumerable Attributes( [XName name ])
gibt eine Auflistung der Attribute des Elements zurück. Wenn Sie einen Namen übergeben, wird eine Auflistung zurückgegeben, die entweder nur ein oder kein Attribut referenziert (da Attributnamen in XML eindeutig sein müssen).
IEnumerable AncestorsAndSelf( [XName name ])
liefert eine Auflistung mit dem Element selbst und allen übergeordneten Elementen, wobei Sie diese auf Elemente mit einem bestimmten Namen einschränken können.
IEnumerable DescendantsAndSelf( [XName name ])
Diese Methode ergibt eine Auflistung, die das Element selbst und alle untergeordneten Elemente (auch die in tieferen Ebenen als die direkten Kind-Elemente) enthält. Wenn Sie einen Namen übergeben, werden nur die untergeordneten Knoten in die Auflistung geschrieben, die diesen Namen tragen.
IEnumerable DescendantNodesAndSelf()
Diese Methode ergibt eine Auflistung, die das Element selbst und alle untergeordneten Knoten (auch die in tieferen Ebenen als die direkten Kind-Knoten) enthält.
Die Methoden, die eine Auflistung zurückgeben, können auch auf einer Auflistung des Typs aufgerufen werden, in dem sie definiert wird. Diese Fähigkeit wird über Erweiterungsmethoden im Namensraum System.Xml.Linq zur Verfügung gestellt.
Beim Navigieren müssen Sie sich über die Struktur des XML-Dokuments weitgehend im Klaren sein. Das ist aber ein Problem, das in der Praxis immer wieder auftritt: XML definiert zwar einen Standard zur Verwaltung von Daten. Wie dieser verwendet wird, ist aber nicht festgelegt. So können die Daten eines Elements z. B. als Attribut oder als Kind-Element angegeben sein. Beim Datenaustausch zwischen verschiedenen Systemen einigen sich die Partner natürlich auf ein Format. Hilfreich dabei ist die XML Schema Definition (XSD), die die Struktur eines Dokuments definiert. Ich schreibe dies hier nur, damit Sie nicht in Verwirrung geraten bei der Frage, wie Sie unbekannte XML-Dokumente durchgehen. Dass die Struktur eines Dokuments unbekannt ist, ist in der Praxis eher selten anzutreffen. Normalerweise wissen Sie sehr gut, wie ein Dokument aussieht (und können dieses ggf. gegen ein Schema prüfen um die Struktur sicherzustellen, bevor Sie dieses auswerten). Bei XML hat sich bewährt, dass ein Dokument einem Schema entsprechen muss. Entspricht das Dokument nicht dem Schema, wird es verworfen. Das Prüfen gegen ein Schema behandle ich im Abschnitt »XML-Dokumente über XSD validieren« ab Seite 1075.
1052
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
In der Praxis kommen aber auch viele XML-Dokumente vor, die optionale Elemente besitzen können (aber nicht müssen). Auf das Nichtvorhandensein dieser Elemente sollten Sie beim Lesen natürlich vorbereitet sein. Zur Demonstration der Navigation in einem XML-Dokument lese ich die Books.xmlDatei von Seite 1032 ein und navigiere ein wenig in dem Dokument. Beachten Sie die Kommentare im Beispiel:
12
Listing 18.17: Navigieren in einem eingelesenen, bekannten XML-Dokument // Die Datei 'Books.xml' im Ordner der Anwendung einlesen XElement booksElement = XElement.Load("Books.xml");
13
// Alle dem Wurzel-Element untergeordneten 'book'-Elemente // einlesen. Die Elements-Methode liefert alle direkt // untergeordneten Kind-Elemente. Da ein Name übergeben wird, // werden hier nur alle Kind-Elemente mit diesem Namen zurückgegeben. foreach (var bookElement in booksElement.Elements("book")) { // Das Element 'author' einlesen. Hier wird davon ausgegangen, // dass genau ein Kind-Element mit dem Namen 'author' existiert. // Deswegen kann die Element-Methode verwendet werden, die // eine Referenz auf das erste Kind-Elemente mit den übergebenen // Namen zurückgibt. XElement authorElement = bookElement.Element("author");
14
15
16
// Die Inhalte der Elemente 'firstName' und 'lastName' ausgeben. // Hier greife ich über die Element-Methode wieder auf das erste // Kind-Element mit den übergebenen Namen zu (weil ich sicher bin, // dass es nur ein Element mit diesem Namen geben kann). Über die // Eigenschaft Value lese ich den Wert des Elements aus, der in // diesem Fall ein XText-Objekt ist (das aber mit String // kompatibel ist). Console.WriteLine(authorElement.Element("firstName").Value + " " + authorElement.Element("lastName").Value);
17
18
// Das Element 'title' auslesen XElement titleElement = bookElement.Element("title"); Console.WriteLine(titleElement.Value);
19
// Das Attribut 'isbn' auslesen. Die Attribute-Methode gibt // Zugriff auf das Attribut mit den übergebenen Namen. XAttribute isbnAttribute = bookElement.Attribute("isbn"); Console.WriteLine("ISBN: " + isbnAttribute.Value);
20
Console.WriteLine(); }
Dieses Beispiel überprüft nicht, ob erwartete Elemente ggf. nicht vorhanden sind. Ich gehe bei diesem Beispiel davon aus, dass das XML-Dokument über ein entsprechendes XML-Schema vor dem Einlesen überprüft wurde. Wenn das Schema korrekt definiert ist, können Sie sicher sein, dass beim Einlesen keine Fehler auftreten (es sei denn, das Schema lässt optionale Elemente oder Attribute zu, darauf müssen Sie natürlich reagieren). Falls Sie ein XML-Dokument einlesen, ohne dieses vorher gegen ein Schema geprüft zu haben, müssen Sie vor der Weiterverarbeitung eines ausgelesenen Knotens oder Elements die entsprechende Referenz auf null überprüfen. Die Methoden des X-DOM sind (wie im DOM definiert) so fehlerfreundlich, dass Sie beim Nichtvorhandensein eines angeforderten Knotens keine Ausnahme werfen, sondern einfach nur null oder eine leere Auflistung zurückgeben.
21 HALT
22
23
1053
XML
Auf eine weitere Demonstration der anderen Möglichkeiten zur Navigation verzichte ich, da ich denke, dass diese in der Praxis nur in sehr speziellen Fällen benötigt werden. Die Beschreibungen in Tabelle 18.2, Tabelle 18.3, Tabelle 18.4 müssten ausreichen, Ihnen eine Vorstellung von den Navigationsmöglichkeiten zu geben. Beachten Sie, dass Sie mit den Methoden Ancestors und Descendants auch ein komplettes XML-Dokument nach Knoten bzw. Elementen mit einem bestimmten Namen durchsuchen können, unabhängig von deren Position im Dokument. Ein Beispiel dafür finden Sie im Abschnitt »Abfragen von Elementen unabhängig von deren Position im XML-Dokument« auf Seite 1064.
TIPP
Ein wichtiger Trick ist noch das Ermitteln des Wurzelknotens von einem beliebigen Element aus: XElement rootElement = element.AncestorsAndSelf.Last();
18.2.8 Navigieren in einem XML-Dokument mit Namensräumen XML-Namensräume müssen beim Navigieren und Lesen berücksichtigt werden
Enthält ein XML-Dokument Namensräume, müssen Sie diese beim Navigieren dann berücksichtigen, wenn Sie einer Methode einen Namen übergeben. Der Name, den Sie übergeben, besteht in Wirklichkeit nicht nur aus dem eigentlichen ElementNamen, sondern auch aus dem Namensraum. Bei XML-Dokumenten oder Namensraum ist der Namensraum leer. Wollen Sie aber Knoten oder Elemente einlesen, die einem Namensraum zugeordnet sind, müssen Sie wie schon beim Erzeugen eines XML-Dokuments mit Namensräumen den Namensaum mit angeben. Als Beispiel verwende ich das auf Seite 1049 erzeugte Artikel-XML-Dokument mit zwei XML-Namensräumen: Listing 18.18: Navigieren in einem XML-Dokument mit Namensräumen // Einlesen der XML-Datei 'Products.xml' im Programmordner XElement rootElement = XElement.Load("Products.xml"); // Die Namensräume festlegen XNamespace ns1 = "http://www.mut.de/C#-Kompendium/samples"; XNamespace ns2 = "http://www.juergen-bayer.net"; // Alle 'Product'-Elemente durchgehen. Diese Elemente gehören zum // Wurzel-Namensraum, weswegen dieser angegeben werden muss. foreach (var productElement in rootElement.Elements(ns1 + "product")) { // Das Attribut 'id' auslesen. // Hier muss kein Namensraum angegeben werden. Console.WriteLine(productElement.Attribute("id").Value); // Den Wert des Elements 'name' auslesen, das zum // Wurzel-Namensraum gehört Console.WriteLine("Name: " + productElement.Element(ns1 + "name").Value); // Den Wert des Elements 'rating' auslesen, das zum // zweiten Namensraum gehört Console.WriteLine("Wertung (1 - 5): " + productElement.Element(ns2 + "rating").Value); Console.WriteLine(); }
1054
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Interessant ist der Fall, dass Sie in einem XML-Dokument mit Namensraum oder Namensräumen über Ancestors oder Descendants nach Knoten bzw. Elementen suchen, die irgendwo im Dokument vorkommen können. In diesem Fall müssen Sie natürlich auch die Namensräume mit berücksichtigen. Enthält das XML-Dokument mehrere Namensräume, müssen Sie ggf. mehrfach suchen. Aber der Sinn von Namensräumen ist ja u. a., dass diese Elemente in einem XML-Dokument logisch voneinander trennen.
12
Falls Sie eine seitliche Navigation verwenden, gilt das natürlich genauso. Das folgende Beispiel demonstriert dies: Listing 18.19: Demonstration einer seitlichen Navigation in einem XML-Dokument mit Namensräumen
13
// Das erste Buch referenzieren. Die Umwandlung nach // XElement kann hier vorgenommen werden, weil es sich // um ein Element handelt XElement firstBook = (XElement)rootElement.FirstNode;
14
// Das erste Kind des Buchs ermitteln XElement firstBookChild = (XElement)firstBook.FirstNode; Console.WriteLine(firstBook.Name);
15
// Die weiteren Kinder hinter dem ersten Kind-Element ermitteln foreach (XElement nextBookChild in firstBookChild.ElementsAfterSelf()) { Console.WriteLine(nextBookChild.Name); }
16
Die Ausgabe des Beispiels ist: "{http://www.mut.de/C#-Kompendium/samples}product" "{http://www.mut.de/C#-Kompendium/samples}name" "{http://www.mut.de/C#-Kompendium/samples}price"
17
Das dem book-Element ebenfalls untergeordnete Element rating fehlt hier, weil es nicht demselbem Namensraum zugeordnet ist, wie dem des Elements, von dem die Navigation ausging.
18
18.2.9 Lesen von Textinhalten Das Lesen von Textinhalten von Elementen oder Attributen ist über die Eigenschaft Value der Klasse XElement bzw. XAttribute möglich. Value ist aber in der Praxis sehr gefährlich: Da diese Eigenschaft den Wert des Elements als String zurückgibt, erhalten Sie für den Fall, dass Datums- oder Dezimalzahlwerte gespeichert sind, diese Werte in dem Format, das im XML-Dokument gespeichert ist.
Value ermöglicht das Lesen des rohen Inhalts eines Elements
Zahl- und Datumswerte sind in (dem Standard entsprechenden) XML-Dokumenten im XSD-Format gespeichert. Datumswerte werden im UTC-Format gespeichert, Zahlen im englischen Format.
Zahl- und Datumswerte sind normalerweise im XSD-Format gespeichert
Der Konstruktor der XElement-Klasse und die SetValue-Methode halten sich an das XSD-Format:
19
20
21
22
Listing 18.20: Demo für das Verwalten von Daten, die in verschiedenen Kulturen unterschiedlich formatiert werden // Ein Demo-XML-Dokument erzeugen XElement demoElement = new XElement("demo", new XElement("doubleValue", 1.5), new XElement("dateValue", DateTimeOffset.Now));
23
1055
XML
// Das Dokument ausgeben Console.WriteLine(demoElement.ToString());
Das Ergebnis dieses Beispiels ist: 1.5 2008-04-27T00:30:07.335+01:00
Beim Einlesen können Sie nicht einfach Value auslesen, da diese Eigenschaft den einfachen String-Wert des Elements zurückgibt. Das Datum könnten Sie noch nach DateTimeOffset oder DateTime parsen, die Zahl aber auf einem deutschen System so ohne weiteres aber schon nicht mehr (in diesem Fall würde 15 herauskommen). Eine Typumwandlung bringt die Lösung
Die Lösung des Problems ist, das XAttribut- oder das XElement-Objekt in den erwarteten Typ umzuwandeln. Beide Klassen implementieren Operatoren für explizite Konvertierungen in die Standardtypen, die das XSD-Format verwenden: Listing 18.21: Korrektes Lesen von Elementen, die in verschiedenen Kulturen unterschiedlich formatiert werden DateTimeOffset date = (DateTimeOffset)demoElement.Element("dateValue"); Console.WriteLine(date); // 27.04.2008 00:30:07 +1:00 double number = (double)demoElement.Element("doubleValue"); Console.WriteLine(number); // 1,5
XAttribut und das XElement besitzen Konvertierungsoperatoren für: ■ ■ ■
INFO
Alle numerischen Standardtypen, für die Typen string, bool, DateTime, DateTimeOffset, TimeSpan und Guid, für Nullable-Werte der angegebenen Typen.
Eine Konvertierung in einen Nullable-Typ ist beim Lesen von Elementen interessant, bei denen es vorkommen kann, dass diese nicht existieren. So können Sie z. B. das ggf. nicht existierende Element intValue auslesen, ohne eine Ausnahme zu erzeugen: int? intNumber = (int?)demoElement.Element("intValue");
HALT
Wie ich bereits im Abschnitt »Speichern von Nullwerten« (Seite 1045) angemerkt habe, funktioniert die Umwandlung in einen Nullable-Wert nur für Elemente, die entweder nicht vorhanden sind oder die einen Wert verwalten. Bei der Konvertierung eines Elements, das vorhanden, aber leer ist, wird (auch wenn xsi:nil="true" angegeben ist) in .NET 3.5.0.0 (ohne Service Pack) eine FormatException geworfen.
Der Workaround um dieses Problem ist eine kleine LINQ to XML-Abfrage: TIPP
XNamespace xsi = XmlSchema.InstanceNamespace; int? number = (int?)demoElement.Elements("intValue").Where( x => ((string)x.Attribute(xsi + "nil")) != "true").FirstOrDefault();
Diese Abfrage funktioniert für den Fall, dass das Element einen zum erwarteten Typ passenden Wert speichert oder dass das Element leer ist und xs:nil angegeben ist. Sie funktioniert nicht für den Fall, dass das Element leer ist, ohne dass xs:nil angegeben ist.
1056
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
In der Praxis müssen Sie natürlich überprüfen, ob das einzulesende XML-Dokument die Daten im XSD-Format verwaltet. In einigen – eher unprofessionell erzeugten – XML-Dokumenten war das in meiner Praxis leider nicht der Fall. Dann müssen Sie natürlich Value auslesen und den Wert im Programm entsprechend konvertieren. Nutzen Sie dabei die Tatsache, dass Sie den zur Konvertierung verwendeten Parseoder To-Methoden in der Regel ein CultureInfo-Objekt (am IFormatProvider-Argument) übergeben können, das die Kultur bestimmt.
INFO
12
18.2.10 Ändern eines XML-Dokuments Das X-DOM erlaubt (wie auch das DOM) das vielfältige Ändern eines XML-Dokuments. So können Sie z. B. den Wert von Elementen über deren Value-Eigenschaft überschreiben. Sie können aber auch Knoten und Attribute an beliebigen Stellen im Dokument einfügen oder vorhandene Knoten und Attribute löschen. Die Klassen XNode, XContainer und XElement stellen dazu einige Methoden zur Verfügung. Eine der wichtigsten, die Add-Methode, habe ich bereits beim Erstellen eines XML-Dokuments verwendet. Tabelle 18.5 fasst die Methoden zusammen. Methode
Beschreibung
void Add( Object content )
fügt einem Container-Knoten ein oder mehrere beliebige XContainer Kind-Objekte oder Attribute an. Handelt es sich um ein Objekt des X-DOM, wird dieses entsprechend seiner Bedeutung behandelt. Wird Add z. B. auf einem XElement aufgerufen und Sie übergeben ein XAttribute, wird dieses in die Attribute des Elements geschrieben. Objekte, die nicht zum X-DOM gehören, werden entweder über einen vorhandenen Standard-Formatierer oder über die ToString-Methode in einen String konvertiert.
void Add( Object[] content )
void AddAfterSelf( Object content ) void AddAfterSelf( Object[] content ) void AddBeforeSelf( Object content )
Diese Methoden fügen einen oder mehrere Knoten direkt hinter dem Element an, auf dem sie aufgerufen werden. Sie fügen keinen Kind-Knoten an, sondern einen Knoten auf derselben Ebene.
In Klasse
X-DOM erlaubt alle denkbaren Manipulationen
14
Tabelle 18.5: Die Methoden zur Manipulation des X-DOM
15
16
17
18
XElement
19 fügen einen oder mehrere Knoten direkt vor dem Element an, auf dem sie aufgerufen werden.
XElement
20
void AddBeforeSelf( Object[] content ) void AddFirst( Object content )
13
21 Diese Methoden fügen einen oder mehrere Knoten als erstes Kind eines Elements an.
XElement
22
void AddFirst( Object[] content ) void Remove()
löscht einen Knoten.
XNode
void RemoveAll()
löscht alle Kind-Knoten und Attribute aus einem Element.
XElement
void RemoveAttributes()
löscht alle Attribute aus einem Element.
XElement
23
1057
XML
Tabelle 18.5: Die Methoden zur Manipulation des X-DOM (Forts.)
Methode
Beschreibung
In Klasse
void RemoveNodes()
löscht alle Kind-Knoten aus einem Container.
XContainer
void ReplaceAll( Object content )
ersetzt den kompletten Inhalt eines Elements (inklusive aller XElement untergeordneten Knoten) durch einen neuen.
void ReplaceAll( Object[] content ) void ReplaceAttributes( ersetzt alle Attribute eines Elements durch neue. Object content )
XElement
void ReplaceAttributes( Object[] content ) void ReplaceNodes( Object content )
ersetzt den kompletten Inhalt eines Containers durch einen neuen.
XContainer
ersetzt den Knoten, auf dem die Methode aufgerufen wird, durch einen oder mehrere neue.
XNode
void ReplaceNodes( Object[] content ) void ReplaceWith( Object content ) void ReplaceWith( Object[] content ) void SetAttributeValue( Über diese Methode können Sie ein Attribut hinzufügen und XName name, dabei ein evtl. vorhandenes automatisch überschreiben. Object value)
XElement
void SetElementValue( XName name, Object value)
fügt ein Element hinzu oder überschreibt ein ggf. vorhande- XElement nes Element. Unter der Tabelle finden Sie eine kurze Beschreibung dieser Methode
SetValue( Object value)
setzt den Wert eines Elements. Im Vergleich mit der Value- XElement Eigenschaft, die nur mit Strings arbeitet, können Sie über SetValue auch beliebige Objekte schreiben. Die Standardtypen werden dabei automatisch in das XSD-Format konvertiert.
Schreiben von Werten Den Wert eines XElements können Sie über dessen Value-Eigenschaft schreiben. Value ist aber vom Typ String. Wenn Sie konvertierte Datums- oder Dezimalzahlwerte über Value schreiben, halten Sie sich nicht an das XSD-Format und bekommen mit dem eigenen XML-Dokument Probleme beim Lesen. Deswegen mein dringender Rat:
HALT
Verwenden Sie zum Schreiben von Werten nur dann Value, wenn es sich tatsächlich um Strings handelt. Alle anderen Werte schreiben Sie über SetValue. Diese Methode berücksichtigt das korrekte XSD-Format von Datumswerten und Dezimalzahlen (und ggf. anderen Daten).
Ändern des Dokuments Die interessantesten Methoden für das Ändern eines XML-Dokuments sind SetAttributeValue und SetElementValue. Über diese Methoden können Sie ein Attribut oder ein Kind-Element hinzufügen und ein ggf. vorhandenes dabei gleich überschreiben:
1058
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Listing 18.22: Bequemes Erstellen oder Überschreiben von Kind-Elementen XElement logElement = new XElement("log"); logElement.SetElementValue("failedConnections", 1); ... logElement.SetElementValue("failedConnections", 2);
Die Methoden zum Ersetzen von Elementen kopieren den oder die übergebenen Knoten zwischen. Damit kann beim Ersetzen von Kind-Elementen durch Teile des Element-Baums selbst kein Fehler entstehen. Deswegen funktioniert z. B. die folgende, unsinnige Anweisung:
12
13
logElement.ReplaceAll(logElement.Nodes());
Interessant ist noch, dass Sie Elemente eines XML-Dokuments inklusive aller KindKnoten problemlos einem anderen XML-Dokument einfügen können. Normalerweise dürfen Knoten, die in einem DOM verwaltet werden, nicht gleichzeitig auch an einem anderen enthalten sein. Das X-Dom erzeugt beim Anfügen aber eine tiefe Kopie der übergebenen Knoten:
Knoten können kopiert werden
14
Listing 18.23: Anfügen eines Teilbaums eines X-DOM in ein anderes X-DOM
15
XElement carsElement = new XElement("cars", new XElement("car", new XElement("name", "Ford Puma"), new XElement("color", "Red")), new XElement("car", new XElement("name", "Citroen C1"), new XElement("color", "Silver")));
16
17
XElement personElement = new XElement("person", new XElement("name", "Jürgen Bayer")); // Das erste car-Element des ersten X-DOM in das // zweite X-DOM kopieren personElement.Add(carsElement.FirstNode);
18
// Das Ergebnis ausgeben Console.WriteLine(personElement.ToString());
19
Das Ergebnis des Beispiels ist: Jürgen Bayer Ford Puma Red
20
21
Dieses Feature ist in einigen Situationen sehr hilfreich. Im alten .NET-DOM (XmlDocument, XmlElement etc.) ist das Kopieren eines Teils eines XML-Dokuments in ein anderes nicht so einfach möglich.
22
Ich denke, die anderen Methoden sind in Tabelle 18.5 ausreichend gut beschrieben, sodass weitere Beispiele nicht notwendig sind.
23
1059
XML
18.2.11 Die Klasse XDocument XDocument bietet Zugriff auf DokumentEigenschaften
Die Klasse XDocument verwaltet wie XElement ein XML-Dokument im Speicher. Anders als im W3C-DOM müssen Sie im X-DOM aber kein XDocument verwenden, um mit XML-Dokumenten zu arbeiten (wie Sie ja bereits gesehen haben). XDocument bietet lediglich den Zugriff auf spezielle XML-Dokument-Eigenschaften wie die XML-Deklaration oder XML-Prozessinformationen (die dazu vorgesehen sind, XML-Parser über spezielle Gegebenheiten zu informieren, die in der Praxis aber für XML in der Regel nicht verwendet werden). XDocument bietet ähnliche Konstruktoren wie XElement und ebenfalls die Methoden Load und Parse. Im Unterschied zu XElement wird das XML-Dokument aber nicht direkt verwaltet, sondern über ein internes XElement, das Sie über die Eigenschaft Root erreichen.
Tabelle 18.6 beschreibt die wesentlichen Eigenschaften. Tabelle 18.6: Die wichtigen Eigenschaften von XDocument
Eigenschaft
Beschreibung
XDeclaration Declaration
verwaltet die XML-Deklaration. Über die Eigenschaften Encoding, Standalone und Version des referenzierten XDeclaration-Objekts können Sie die Deklaration beeinflussen (was Sie aber nicht müssen, XDocument verwendet wie XElement die aktuelle Standard-Deklaration).
XDocumentType DocumentType
verwaltet die (für XML veraltete, aber bei HTML noch verwendete) DokumenttypDefinition (DTD), die optional die Struktur des Dokuments beschreibt. Sie sollten die Struktur eines XML-Dokuments stattdessen besser über ein Schema (XSD) definieren.
XElement Root
verwaltet das Wurzel-Element.
Eine Eigenschaft für Prozessinformationen fehlt, aber diese können Sie über einen der Konstruktoren hinzufügen. Dieser erlaubt in einem Object-params-Array das Hinzufügen verschiedener Elemente: ■ ■ ■ ■ ■
Ein XElement, das das Wurzel-Element darstellt, ein XDeclaration-Objekt, das die XML-Deklaration definiert, ein XDocumentType-Objekt, das den Dokumenttyp definiert, beliebig viele XProcessingInstruction-Objekte, die Prozessinstruktionen für Parser beinhalten, beliebig viele Kommentare in XComment-Objekten, die auf der Ebene des Dokuments (vor dem Wurzel-Element) ausgegeben werden.
Damit können Sie z. B. ein XHTML-Dokument erzeugen: Listing 18.24: Erzeugen eines XHTML-Dokuments XNamespace htmlNamespace = "http://www.w3.org/1999/xhtml"; XElement rootElement = new XElement(htmlNamespace + "html", new XElement(htmlNamespace + "body", new XElement(htmlNamespace + "h1", "Test-Dokument"))); XDocument xhtmlDocument = new XDocument( // Die XML-Deklaration new XDeclaration("1.0", "utf-8", "no"),
1060
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
// Kommentar new XComment("Beispiel-XHTML-Dokument"), // Eine Prozess-Info, die aussagt, dass das Dokument // mit einem CSS-Stylesheet verknüpft werden soll new XProcessingInstruction("xml-stylesheet", "href='styles.css' type='text/css'"), // Der für XHTML notwendige Dokumenttyp new XDocumentType("html", "-//W3C//DTD HTML 4.0 Transitional//EN", "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd", null), // Das Wurzel-Element rootElement);
12
Ich gehe aus Platzgründen nicht auf die einzelnen Bestandteile ein. Ich denke, in den speziellen Fällen, in denen Sie eine DTD-Definition oder Prozessinformationen benötigen, werden Sie wissen, was Sie machen.
13
Das Ergebnis dieses Beispiels ist:
14
Listing 18.25: Das erzeugte XHTML-Dokument Test-Dokument
15
16
18.2.12 LINQ to XML 17
LINQ to XML erlaubt die Abfrage von in einem XDocument oder XElement verwalteten XML-Dokumenten über LINQ. LINQ to XML besteht im Wesentlichen aus einigen LINQ-Erweiterungsmethoden in der Klasse System.Xml.Linq.Extensions, die sich speziell auf XNode und XElement beziehen. Dass diese Klassen sehr LINQ-freundlich gestaltet wurden (z. B. weil sie hilfreiche Konstruktoren und IEnumerable-Auflistungen bieten) lässt sie auch als Teil von LINQ to XML gelten. Wie Sie aber gesehen haben, müssen Sie LINQ to XML nicht einsetzen, um mit XDocument oder XElement zu arbeiten.
18 19
LINQ to XML ist aber hervorragend geeignet, um XML-Dokumente gezielt abzufragen, um XML-Dokumente aus einer anderen Datenquelle dynamisch zu erzeugen oder um XML-Dokumente (ohne XSLT) zu transformieren.
20
Für diese Möglichkeiten benötigen Sie neben dem Import des Namensraums System.Xml.Linq auch den Import von System.Linq.
21
18.2.13 Abfragen mit LINQ to XML Das Abfragen von Daten mit LINQ to XML ist an sich einfach. Für die Praxis sind aber ein paar Tricks wichtig.
22
Das Beispiel-Dokument Für die folgenden Abfrage-Beispiele verwende ich ein XML-Dokument, das Buchdaten speichert (wie in den vorhergehenden Beispielen prinzipiell auch):
23
1061
XML
Listing 18.26: Das Beispiel-XML-Dokument Die wilde Geschichte vom Wassertrinker 5 John Irving Fool on the Hill 5 2004 Matt Ruff A Long Way Down 4 2006 Nick Hornby State of Fear 4 2005 Michael Crichton
Einfache Abfragen Die grundsätzliche Abfrage ist einfach. Das folgende Beispiel projiziert den Titel und den Autor aller book-Elemente in einen anonymen Typ: Listing 18.27: Einfaches Auslesen eines XML-Dokuments // Die XML-Datei 'Books.xml' im Anwendungsordner einlesen XElement booksElement = XElement.Load("Books.xml"); // Alle Bücher auslesen und in einen anonymen Typ projizieren var books1 = from book in booksElement.Elements("book")
1062
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
select new { Title = book.Element("title").Value, Author = new { FirstName = book.Element("author").Element("firstName").Value, LastName = book.Element("author").Element("lastName").Value, } };
12
// Die Bücher durchgehen foreach (var book in books1) { Console.WriteLine(book.Title + ": " + book.Author.FirstName + " " + book.Author.LastName); }
13
Abfragen mit Einschränkung 14
Das Einschränken des Ergebnisses natürlich genauso möglich, wie unter LINQ mit einfachen Auflistungen. Das folgende Beispiel fragt alle Bücher ab, deren ISBNNummer mit »978-3« beginnt:
15
Listing 18.28: Abfrage eines XML-Dokuments mit Einschränkung var books2 = from book in booksElement.Elements("book") where book.Attribute("isbn").Value.StartsWith("978-3") select new { Title = book.Element("title").Value, Author = new { FirstName = book.Element("author").Element("firstName").Value, LastName = book.Element("author").Element("lastName").Value, } };
16
17
18
Abfragen auf XML-Dokumente, die Nullwerte verwalten können Die Abfrage wird schwierig, wenn Elemente nicht vorhanden oder leer sein können. Im Beispiel ist dies für das Element year der Fall. Beim Auslesen oder Abfragen solcher Elemente gilt hier dasselbe Problem wie beim normalen Auslesen (siehe »Speichern von Nullwerten«, Seite 1045 und »Lesen von Textinhalten«, Seite 1055). Sie müssen das Problem, dass eine Typumwandlung in eine Nullable zurzeit nur möglich ist, wenn das Element entweder gar nicht vorhanden ist oder einen Wert speichert, umgehen. Das folgende Beispiel versucht, den Titel und das Jahr aller Bücher abzufragen:
Für die Abfrage von Elementen mit möglichen Nullwerten benötigen Sie den Workaround
19
20
Listing 18.29: Versuch der Abfrage eines Elements, das Nullwerte zulässt
21
var books3 = from book in booksElement.Elements("book") select new { Title = book.Element("title").Value, Year = (int?)book.Element("year") };
22
Dieser Versuch resultiert in .NET 3.5.0.0 in einer FormatException.
23
1063
XML
Mit dem bereits genannten Workaround funktioniert die Abfrage: Listing 18.30: Funktionierende Abfrage von Elementen, die auch Nullwerte zulassen XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance"; var books3 = from book in booksElement.Elements("book") select new { Title = book.Element("title").Value, Year = (int?)book.Elements("year").Where( b => ((string)b.Attribute(xsi + "nil")) != "true") .FirstOrDefault() }; foreach (var book in books3) { Console.WriteLine(book.Title + ": " + (book.Year != null ? book.Year.Value.ToString() : "Das Jahr fehlt")); }
Diese Abfrage funktioniert mit Elementen, die einen passenden Wert verwalten, mit leeren Element mit xs:nil="true" und mit nicht vorhandenen Elementen. Sie führt bei einem leeren Feld, bei dem xs:nil="true" nicht angegeben ist, allerdings zu einer FormatException. Den Workaround müssen Sie natürlich auch einsetzen, wenn Sie nach einem Element einschränken, das auch leer sein kann: Listing 18.31: Einschränken nach einem Element, das Nullwerte zulässt var books4 = from book in booksElement.Elements("book") where (int?)book.Elements("year").Where( b => ((string)b.Attribute(xsi + "nil")) != "true") .FirstOrDefault() >= 2005 select new { Title = book.Element("title").Value, Year = (int)book.Element("year") };
Beachten Sie, dass ich in der Projektion nun das year-Element direkt nach int umwandle. An dieser Stelle ist sichergestellt, dass das year-Element einen Wert speichert.
INFO
Sie müssen aber natürlich immer davon ausgehen, dass in einem Element auch ungültige Werte gespeichert sein können. Deswegen sollten Sie zumindest die FormatException abfangen, die in diesem Fall geworfen wird. Für die Praxis rate ich aber dringend dazu, zumindest die Struktur eines XML-Dokuments, das zwischen verschiedenen Systemen oder Partnern ausgetauscht werden soll, über ein XSD-Dokument zu definieren und das XML-Dokument vor dem Auswerten gegen dieses Schema zu prüfen. Der Abschnitt »XML-Dokumente über XSD validieren« zeigt ab Seite 1067, wie Sie dies programmieren.
Abfragen von Elementen unabhängig von deren Position im XML-Dokument Über die Methoden Ancestors, AncestorsAndSelf, Descendants und DescendantsAndSelf können Sie nach XML-Elementen suchen, die irgendwo im XML-Dokument
1064
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
gespeichert sein können. Ancestors ergibt alle übergeordneten Knoten (auch höherer Ebenen als das direkte Parent-Element), AncestorsAndSelf liefert den Knoten, auf dem diese Methode aufgerufen wurde, gleich mit. Descendants liefert alle untergeordneten Elemente (auch niedrigerer Ebenen als die direkten Kind-Elemente), DescendantsAndSelf liefert das Element mit, auf dem diese Methode aufgerufen wurde. So können Sie z. B. ausgehend vom Wurzel-Element alle author-Elemente auslesen:
12
Listing 18.32: Abfragen von Elementen, die irgendwo im XML-Dokuments vorkommen var authors = from author in booksElement.Descendants("author") select new { FirstName = author.Element("firstName").Value, LastName = author.Element("lastName").Value };
13
14
Abfrage in XML-Dokumenten mit Namensräumen Schließlich ist natürlich auch die Abfrage von XML-Dokumenten wichtig, die Namensräume beinhalten. Im Beispiel wäre das der Namensraum http://www. juergen-bayer.net, dem das rating-Element zugeordnet ist. Das Prinzip ist aber genau dasselbe wie beim Hinzufügen oder Lesen von Elementen:
15
Listing 18.33: Abfrage eines Elements, das einem Namensraum zugeordnet ist
16
XNamespace jb = "http://www.juergen-bayer.net"; var books5 = from book in booksElement.Elements("book") where (int)book.Element(jb + "rating") > 4 select new { Title = book.Element("title").Value, Rating = (int)book.Element(jb + "rating") };
17
18
18.2.14 Erzeugen von XML-Dokumenten über LINQ Das Erzeugen eines XML-Dokuments mit LINQ ist sehr einfach, weil die speziellen Konstruktoren der Klasse XElement das Erzeugen in einer Anweisung erlauben. So können Sie beliebige Daten abfragen (alle Auflistungen, Datenbanken und sonstige Daten, die einer LINQ-Erweiterung zu Verfügung steht) und aus diesen ein XMLDokument erzeugen. Das Prinzip ist einfach und basiert auf den LINQ-Grundlagen und der funktionalen Konstruktion (Seite 1044).
LINQ erlaubt die flexible Erzeugung von XElement-Objekten
19
20
Die Besonderheit ist allerdings, dass Sie die LINQ-Abfrage als Inhalt eines XElementObjekts angeben können. Damit können Sie z. B. ein Wurzel-Element mit Kind-Elementen füllen, die Sie aus einer beliebigen Datenquelle auslesen.
21
Das folgende Beispiel fragt auf diese Weise die Artikeldaten aus dem Kapitel 11 in ein XML-Dokument ab und speichert dieses:
22
Listing 18.34: Projizieren von Daten in ein XML-Element // Beispiel-Artikel einlesen List products = GetProducts();
23
1065
XML
// Die Artikel mit LINQ in ein XML-Dokument projizieren var productsElement = new XElement("products", from product in products select new XElement("product", new XElement("name", product.Name), new XElement("price", product.Price))); productsElement.Save("Products.xml");
Dieses Feature ist deswegen möglich, weil die Konstruktoren von XElement an Object-Referenzen auch IEnumerable-Auflistungen verarbeiten und die einzelnen Objekte in dieser Auflistung als Kind-Elemente anlegen. Dieses Feature ist in der Praxis sehr hilfreich. Es bedeutet auch, dass Sie LINQ-Abfragen bei der funktionalen Konstruktion von XML-Dokumenten ebenfalls in tieferen Ebenen des Dokuments einsetzen können. Das macht das Erzeugen eines XML-Dokuments nicht gerade übersichtlicher, aber in der Praxis wesentlich einfacher als mit anderen Mitteln.
Leere Elemente berücksichtigen und bedingungsabhängiges Erzeugen von XML-Elementen Leere Daten müssen besonders behandelt werden
Bei der Erzeugung von XML-Dokumenten über LINQ und LINQ to XML müssen Sie darauf achten, dass Felder der Datenquelle auch leer sein können. In den Beispieldaten ist das für das Feld CategoryId der Fall, das in einem Artikel nicht angegeben ist. Bei der Abfrage erhalten Sie keinen Fehler: var productsElement2 = new XElement("products", from product in products select new XElement("product", new XElement("name", product.Name), new XElement("category", product.CategoryID)));
Im Ergebnis sind nun aber ggf. leere XML-Elemente enthalten: Per Anhalter durch die Galaxis 2 ... Programmieren lernen (E-Book)
Da das Handling solcher Elemente sehr schwierig ist, sollten Sie diese speziell behandeln. Sie können das Element bedingungsabhängig erzeugen: Listing 18.35: Projektion in ein XML-Dokument mit einer bedingungsabhängigen Erzeugung eines Elements var productsElement2 = new XElement("products", from product in products select new XElement("product", new XElement("name", product.Name), product.CategoryID != null ? new XElement("category", product.CategoryID) : null));
Der »Trick« dabei ist, dass Sie in dem Fall, dass das abgefragte Feld nicht in ein Element projiziert werden soll, dem XElement-Konstruktor an dieser Stelle einfach null übergeben. Netterweise ignoriert XElement null-Werte einfach.
1066
Lesen und Schreiben mit XElement, XDocument und LINQ to XML
Das Ergebnis dieses Beispiels ist: Per Anhalter durch die Galaxis 2 ... Programmieren lernen (E-Book)
12
Eine andere Möglichkeit ist, das XML-Element mit xs:nil auszustatten:
13
Listing 18.36: Projektion in ein XML-Dokument mit einem bedingungsabhängigen Hinzufügen des xsi:nil-Attributs zu einem Element XNamespace xsi = XmlSchema.InstanceNamespace; var productsElement3 = new XElement("products", new XAttribute(XNamespace.Xmlns + "xsi", xsi), from product in products select new XElement("product", new XElement("name", product.Name), new XElement("category", product.CategoryID, product.CategoryID == null ? new XAttribute(xsi + "nil", true) : null)));
14
15
16
Das Ergebnis ist nun: Per Anhalter durch die Galaxis 2 ... Programmieren lernen (E-Book)
17
18 19
Das sieht ganz schön komplex aus, ist in der Praxis mit Sicherheit auch nicht einfach zu handhaben und wird bei komplexeren Abfragen zu so mancher Ausnahme führen. Aber leider müssen Sie Nullwerte immer so berücksichtigen, dass diese bei der Auswertung des XML-Dokuments keine Probleme verursachen können.
20
18.2.15 Transformation von XML-Dokumenten mit LINQ to XML Über die im vorigen Abschnitt besprochene Technik können Sie XML-Dokumente auch sehr gut in ein anderes XML-Format transformieren (z. B. in XHTML). Das Vorgehen entspricht dem Transformieren anderer Daten in ein XML-Dokument, ist aber für die Praxis sehr interessant. In dieser kommt es nämlich immer wieder vor, dass die Struktur eines XML-Dokuments, das in einer Anwendung verwendet wird, geändert wird, wenn die Anwendung weiterentwickelt wird. Wenn eine neue Version der Anwendung dann beim Kunden ggf. XML-Dokumente einlesen muss, die in der alten Struktur vorliegen, müssen diese transformiert werden. Das gilt z. B. auch dann, wenn Sie mit einer Serialisierung arbeiten und die serialisierten Typen in neuen Versionen der Anwendung ändern.
LINQ to XML erlaubt ein sehr flexibles und einfaches Transformieren
21
22
23
1067
XML
Zur Transformation können Sie das sehr komplexe XSLT verwenden. Sie können die Transformation aber auch einfach über LINQ to XML vornehmen. Der Programmcode, der dabei herauskommt, sieht für externe Programmierer zwar auch nicht besonders übersichtlich aus. Für den Programmierer, der den Programmcode entwickelt hat, ist der Aufbau aber relativ logisch. Das folgende Beispiel soll das Prinzip verdeutlichen. Angenommen, eine erste Version der Anwendung speichert Artikeldaten in einem einfachen XML-Dokument: 1001 Neill Pryde Excess 5.4 1002 Neill Pryde Excess 6.4
Die neue Version der Anwendung erwartet die Artikel-ID nicht in einem Element, sondern einem Attribut. Außerdem erwartet diese das Element price, das allerdings Nullwerte erlaubt: Neill Pryde Excess 5.4 Neill Pryde Excess 6.4
Die Transformation über LINQ to XML ist sehr gradlinig: Listing 18.37: Transformieren eines XML-Dokuments in ein anderes Format über LINQ to XML // Einlesen der alten Version XElement productsElement = XElement.Load("Products.xml"); // Konvertieren dieser Version in eine neue XNamespace xsi = XmlSchema.InstanceNamespace; XElement newProductsElement = new XElement("products", new XAttribute(XNamespace.Xmlns + "xsi", xsi), from productElement in productsElement.Elements("product") select new XElement("product", new XAttribute("id", productElement.Element("id").Value), new XElement("name", productElement.Element("name").Value), new XElement("price", new XAttribute(xsi + "nil", "true"), null)));
TIPP
1068
In der Praxis müssten Sie zudem noch eine Versionsverwaltung implementieren, z. B., indem Sie in einem Element mit Namen version einfach eine hochgezählte Versionsnummer verwalten. So können Sie das XML-Dokument auch mehrfach ändern und sicher sein, dass auch die dritte Version Ihrer Anwendung mit einem XML-Dokument der ersten Version zurechtkommt.
Schnelles Lesen und Schreiben mit einem XmlReader bzw. XmlWriter
18.2.16 Nicht besprochene Features Für einige LINQ-to-XML-Features blieb in diesem Kapitel kein Platz. Dazu gehören: ■ ■
Annotationen, die die Verwaltung von benutzerdefinierten Daten in einem XObject erlauben. Streaming von Projektionen in ein X-DOM über die XStreamingElement-Klasse, um die Speicher-Effizienz zu verbessern.
18.3
12
Schnelles Lesen und Schreiben mit einem XmlReader bzw. XmlWriter
13
Neben dem X-DOM sind die älteren Klassen XmlReader und XmlWriter in Sonderfällen zum Lesen und Schreiben von XML-Dokumenten ebenfalls interessant. Der Vorteil dieser Klassen ist eine sehr hohe Performance und ein sehr geringer Speicherverbrauch. Die Nachteile sind: ■ ■ ■
14
Dass nur ein sequenzieller Zugriff (in eine Richtung) möglich ist, dass das Lesen und Schreiben sehr aufwändig und fehlerträchtig ist und dass eine Suche im XML-Dokument wegen des sequenziellen Zugriffs sehr schwierig zu implementieren ist.
15
Für die Erzeugung oder das Lesen sehr großer XML-Dokumente haben XmlReader und XmlWriter den Vorteil der hohen Geschwindigkeit und der geringen Speicherbelastung auf ihrer Seite. Das X-DOM hebt diesen Vorteil aber auch wieder teilweise auf, weil XmlReader und XmlWriter intern verwendet werden. Das einzige Problem des X-DOM könnte bei großen XML-Dokumenten der Speicherverbrauch sein.
16
17
Aus Platzgründen zeige ich in diesem Abschnitt nur die Grundlagen der Klasse XmlReader und XmlWriter, nicht deren vollständigen Möglichkeiten.
18.3.1
Lesen über einen XmlReader
Zum Lesen eines XML-Dokuments über einen XmlReader erzeugen Sie eine Instanz über die statische Create-Methode. Dieser Methode können Sie einen URI, einen Stream oder einen TextReader übergeben.
18
INFO Create erzeugt einen XmlReader
19
Danach rufen Sie die Read-Methode in einer Schleife auf und verarbeiten die XMLDaten im Schleifenkörper:
20
while (xmlReader.Read()) { // Die XML-Daten verarbeiten }
21
Read setzt das XmlReader-Objekt auf den jeweils nächsten Knoten. Die Daten dieses Knotens können Sie dann über Methoden und Eigenschaften des XmlReader-Objekts lesen.
Bei der Verarbeitung müssen Sie die verschiedenen XML-Knoten unterscheiden. Der XmlReader ist dabei noch wesentlich genauer als ein XElement: Für den XmlReader ist jeder logische Bestandteil eines XML-Dokuments ein Knoten. Das kann z. B. ein Kommentar sein, aber auch ein Whitespace-Zeichen, das zwischen anderen Knoten angegeben ist (z. B. die CRLF-Zeichen, mit denen die Zeilen getrennt werden). Sogar XMLElemente werden in ihre Bestandteile aufgeteilt (Start-Tag, Inhalt und Ende-Tag).
22 Der XmlReader arbeitet mit einzelnen Knoten
23
1069
XML
Um die Arbeit zu erleichtern, können Sie dem XmlReader aber auch XmlReaderSettings übergeben, die mit einigen Eigenschaften das Parsen beeinflussen: ■ ■ ■ NodeType gibt den Typ des aktuellen Knotens zurück
IgnoreComments: Sorgt dafür, dass Kommentare ignoriert werden. IgnoreProcessingInstructions: Sorgt dafür, dass Verarbeitungs-Instruktionen ignoriert werden. IgnoreWhitespace: Sorgt dafür, dass Whitespace-Zeichen ignoriert werden.
Wenn Sie eine XML-Datei gezielt durchgehen wollen, müssen Sie die Struktur der XML-Daten kennen und beim Durchgehen der einzelnen Knoten deren Typ und Namen abfragen. Den Typ eines Knotens erhalten Sie über die Eigenschaft NodeType als Wert der Aufzählung XmlNodeType. Die wichtigsten XmlNodeType-Konstanten sind Element (Start-Tag), EndElement (Ende-Tag), Text (der Text eines Elements) und Attribute (ein Attribut). Den Namen des aktuellen Knotens können Sie aus der Eigenschaft Name des XmlReader-Objekts auslesen, den Wert erhalten Sie über die Eigenschaft Value. Beim Lesen müssen Sie berücksichtigen, dass der XmlReader alle Knoten des XMLDokuments durchgeht. Er kann also z. B. auf einem Kommentar-Knoten stehen, auf einem Element-Start-Tag, auf einem Element-Textinhalt, einem Element-End-Tag, auf einem Whitespace-Zeichen etc. Zur Demonstration gehe ich einmal die BuchXML-Datei von Seite 1062 durch und gebe den Typ und Namen (falls vorhanden) der einzelnen Knoten aus (wobei ich die sinnlosen Whitespace-Zeichen aber ignoriere): Listing 18.38: Ausgabe aller Knoten eines XML-Dokuments (außer Whitespace-Knoten) // Die Datei 'Books.xml' im Anwendungsordner einlesen XmlReaderSettings settings = new XmlReaderSettings(); settings.IgnoreWhitespace = true; XmlReader xmlReader = XmlReader.Create("Books.xml", settings); // Das XML-Dokument durchgehen und die einzelnen // Knoten ausgeben while (xmlReader.Read()) { Console.WriteLine(xmlReader.NodeType + (xmlReader.Name != String.Empty ? ": " + xmlReader.Name : null) + (xmlReader.Value != String.Empty ? ": " + xmlReader.Value : null)); } // XmlReader schließen xmlReader.Close();
Das Ergebnis dieses Programms ist das folgende (allerdings gekürzte): XmlDeclaration: xml: version="1.0" encoding="utf-8" Element: books Comment: Ein super geniales Buch Element: book Element: title Text: Die wilde Geschichte vom Wassertrinker EndElement: title Element: jb:rating Text: 5 EndElement: jb:rating Element: year Element: author Element: firstName Text: John EndElement: firstName Element: lastName
1070
Schnelles Lesen und Schreiben mit einem XmlReader bzw. XmlWriter
Text: Irving EndElement: lastName EndElement: author EndElement: book ... EndElement: books
Beim Lesen von Elementen sollten Sie beachten, dass der XmlReader einen Zeiger auf den nächsten Knoten verwaltet. Jede Read- oder Move-Methode verschiebt diesen Zeiger. Dabei müssen Sie auch für die Auswertung eigentlich sinnlose Knoten einlesen wie z. B. Element-Start-Tags und Whitespace-Zeichen, oder diese überspringen. Dabei helfen die folgenden Methoden: ■
■ ■
■
■
12
13
MoveToContent: Diese Methode bewegt den XmlReader auf einen Inhaltsknoten (Textknoten ohne Leerraum, CDATA-, Element-Start-Tag-, Element-End-Tag-, EntityReference- oder EndEntity-Knoten). Ist der aktuelle Knoten bereits ein Inhaltsknoten, passiert nichts. Im anderen Fall wird der XmlReader auf den nächsten Knoten bewegt, der ein Inhaltsknoten ist, oder auf das Ende des XMLDokuments. MoveToContent ist sehr hilfreich zum Überspringen von Knoten, die keine Inhaltsknoten sind, wie z. B. Kommentare, Prozessinstruktionen, Whitespace-Zeichen etc. Da ein Inhaltsknoten ein Knoten verschiedenen Typs ist, gibt MoveToContent den Typ des Knotens zurück, auf dem der XmlReader danach steht. Read: Liest den nächsten Knoten ein. Gibt true zurück, wenn der Zeiger danach nicht auf dem Ende des Dokuments steht. ReadStartElement: überprüft, ob der aktuelle Knoten ein Element-Start-Tag ist, und verschiebt den Zeiger auf den nächsten Knoten. Ist der aktuelle Knoten kein Element-Start-Tag, wird eine XmlException geworfen. Sie können den Namen des erwarteten Elements übergeben, in diesem Fall überprüft ReadStartElement, ob der aktuelle Knoten ein Element-Start-Tag mit dem angegebenen Namen ist. ReadEndElement: überprüft, ob der aktuelle Knoten ein Element-End-Tag ist, und verschiebt den Zeiger auf den nächsten Knoten. Ist der aktuelle Knoten kein Element-Start-Tag, wird eine XmlException geworfen. ReadElementContentAs: Diese Methode und eine Vielzahl weiterer auf Standardtypen spezialisierte Methode (wie ReadElementContentAsString) lesen den Inhalt des XML-Elements, auf dessen Start-Tag der XmlReader gerade steht. Der XmlReader wird nach dem Lesen auf den Knoten hinter dem Element-End-Tag verschoben. Ist der aktuelle Knoten kein Element-Start-Tag, wird eine XmlException geworfen. An den Argumenten localName und namespaceURI können Sie den Namen und den Namensraum des Elements übergeben. Die Methode überprüft dann, ob das aktuelle Element diesen Namen trägt, und wirft im negativen Fall eine XmlException. Für Elemente, die keinem Namensraum zugewiesen sind, geben Sie einen leeren String an (nicht null!).
Beachten Sie, dass es sich bei den beschriebenen Methoden nur um eine Auswahl der in meinen Augen wichtigsten handelt. Die XmlReader-Klasse bietet darüber hinaus noch weitere, ggf. hilfreiche Methoden wie z. B. ReadContentAs und ReadSubtree.
14
15
16
17
18 19
20
21
22
INFO
Attribute eines Elements können Sie lesen, wenn der XmlReader auf dem Start-Tag steht. Dazu können Sie die GetAttribute-Methode verwenden oder den Indexer des XmlReaders, dem Sie den Namen des Attributs übergeben. Beachten Sie, dass Sie nach dem Aufruf von ReadStartElement nicht mehr auf die Attribute zugreifen können.
23
1071
XML
HALT
Beim Lesen von Elementen, die leer sein können, müssen Sie aufpassen. XmlReader behandelt die zwei Formen eines leeren Elements ( und ) unterschiedlich: In der ersten Form (mit End-Tag) stehen zwei Knoten zur Verfügung, in der zweiten aber nur der Element-Start-Knoten! Diese dumme Eigenart müssen Sie beim Lesen immer berücksichtigen. Dabei hilft die Eigenschaft IsEmptyElement, die für einen Element-Start-Knoten angibt, ob dieser über einen End-Knoten verfügt oder nicht. Das folgende Beispiel zeigt für das Element year, wie Sie dies in der Praxis einsetzen. Beachten Sie die Kommentare. Als Beispiel lese ich die Bücher-XML-Datei ein und werte alle Bücher aus. Dabei behandle ich die Sonderfälle, dass das Element rating zum Namensraum http:// www.juergen-bayer.net gehört und dass das Element year auch nicht angegeben sein kann: Listing 18.39: Einlesen eines XML-Dokuments mit Elementen, die einem Namensraum zugeordnet sind, und anderen, die leer sein können XmlReaderSettings settings = new XmlReaderSettings(); settings.IgnoreWhitespace = true; XmlReader xmlReader = XmlReader.Create("Books.xml", settings); while (xmlReader.Read()) { if (xmlReader.NodeType == XmlNodeType.Element) { // Element-Start-Knoten: Den Namen ermitteln if (xmlReader.Name == "book") { // Neues Buch: Das Attribut 'isbn' einlesen string isbn = xmlReader.GetAttribute("isbn"); // Das Startelement einlesen und damit den Zeiger // auf den nächsten Knoten setzen xmlReader.ReadStartElement("book"); // Den Inhalt der Elemente title, rating und year auslesen string title = xmlReader.ReadElementContentAsString("title", String.Empty); int rating = xmlReader.ReadElementContentAsInt("rating", "http://www.juergen-bayer.net"); // Beim Jahr haben wir das Problem, dass dieses: // 1. nicht angegeben sein kann und // 2. in der Form und nicht angegeben // sein kann, die dummerweise unterschiedlich behandelt // werden müssen. int? year = null; if (xmlReader.IsEmptyElement) { // Das Element 'year' ist in der Kurzform angegeben // (): Nur den Start-Tag lesen xmlReader.ReadStartElement(); } else { // Das Element 'year' ist nicht in der Kurzform angegeben, // kann aber trotzdem leer sein: Start-Tag einlesen xmlReader.ReadStartElement(); // Den Inhalt überprüfen if (xmlReader.Value != String.Empty) { // Den vorhandenen Inhalt als int einlesen
1072
Schnelles Lesen und Schreiben mit einem XmlReader bzw. XmlWriter
year = xmlReader.ReadContentAsInt(); } // Den End-Tag einlesen xmlReader.ReadEndElement(); } Console.WriteLine("Titel: " + title); Console.WriteLine("Rating: " + rating); Console.WriteLine("Jahr: " + (year != null ? year.Value.ToString() : "Nicht angegeben"));
12
// Den Start-Tag des author-Elements einlesen xmlReader.ReadStartElement("author");
13
// Vorname und Nachname einlesen string firstName = xmlReader.ReadElementContentAsString( "firstName", String.Empty); string lastName = xmlReader.ReadElementContentAsString( "lastName", String.Empty); Console.WriteLine("Autor: " + firstName + " " + lastName);
14
// Den author-End-Tag einlesen xmlReader.ReadEndElement();
15
// Den book-End-Tag einlesen xmlReader.ReadEndElement();
16
Console.WriteLine(); } } }
17
// XmlReader schließen xmlReader.Close();
Beachten Sie bitte, dass dies meine Lösung des Einlesens der Beispiel-XML-Datei ist. Andere Programmierer würden wahrscheinlich auch andere Lösungen entwickeln. Getreu der Regel: 100 Programmierer, 100 Lösungen ☺ (nach meinem Fachlektor: »100 Programmierer, 200 Lösungen«).
18 INFO
19
18.3.2 Schreiben über einen XmlWriter Das Erzeugen eines XML-Dokuments über einen XmlWriter kommt in der Praxis wahrscheinlich häufiger vor als das Lesen über einen XmlReader. Der Grund dafür ist, dass das Schreiben deutlich einfacher ist, weil Sie beim Schreiben die Struktur des zu erzeugenden XML-Dokuments genau kennen.
20
Der XmlWriter schreibt aber, wie der XmlReader liest: Sequenziell und immer einen Knoten nach dem anderen. Deshalb ist das Schreiben auch (verglichen mit XElement) recht aufwändig.
21
Vor dem Schreiben müssen Sie natürlich einen XmlWriter erzeugen, was über die Create-Methode erfolgt. Create erwartet am ersten Argument einen Dateinamen als String, einen Stream, einen StringBuilder, einen TextWriter oder einen anderen XmlWriter. Am zweiten Argument können Sie XmlWriterSettings übergeben, über die Sie z. B. bestimmen können, dass das XML-Dokument eingerückt erzeugt wird (was ansonsten nicht der Fall ist):
22
23
1073
XML
Listing 18.40: Erzeugen eines XmlWriter, der mit Einrückung in einen StringBuilder schreibt XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = " "; StringBuilder sb = new StringBuilder(); XmlWriter xmlWriter = XmlWriter.Create(sb, settings);
Über verschiedene Write-Methoden können Sie dann einzelne Knoten schreiben. Die wichtigsten sind: ■
WriteStartDocument: Schreibt die XML-Deklaration mit der Version 1.0. Das optionale Argument standalone bestimmt den Wert des entsprechenden Attributs.
■
WriteStartElement: Schreibt den Start-Tag eines Elements. Den Namen übergeben Sie am Argument localName. Am Argument ns können Sie optional auch den Namensraum übergeben.
■
WriteValue: Schreibt einen Element-Wert. Diese Methode erlaubt die Übergabe verschiedener Typen, die beim Schreiben in das XML-Dokument korrekt (nach XSD) formatiert werden. Datums- und Dezimalzahlwerte sollten Sie immer über WriteValue schreiben (und nicht über WriteElementString).
■
WriteAttributeString: Schreibt ein Attribut.
■
WriteElementString: Schreibt ein komplettes Element, optional inklusive StringWert. Am Argument localName übergeben Sie den Namen des Elements, am Argument ns den Namensraum. Soll das Element keinem Namensraum zugeordnet sein, übergeben Sie hier einen leeren String (nicht null, null führt zu einer NullReferenceException).
■
WriteEndElement: Schreibt den End-Tag des Elements, dessen End-Tag an dieser Stelle erwartet wird.
Damit können Sie relativ einfach das Bücher-XML-Dokument erzeugen. Das folgende Beispiel schreibt allerdings nur die beiden ersten Bücher. Etwas umständlich ist einmal wieder das Handling des year-Elements, das Nullwerte zulässt: Listing 18.41: Schreiben eines XML-Dokuments mit Elementen mit Namensraum und optional leeren Elementen // Den Dokumenten-Start schreiben xmlWriter.WriteStartDocument(true); // Standalone // Das Wurzel-Element schreiben xmlWriter.WriteStartElement("books"); // Die Namensräume definieren xmlWriter.WriteAttributeString("xmlns", "xsi", null, XmlSchema.InstanceNamespace); string jbNamespace = "http://www.juergen-bayer.net"; xmlWriter.WriteAttributeString("xmlns", "jb", null, jbNamespace); // Erstes Buch xmlWriter.WriteComment("Ein super geniales Buch"); xmlWriter.WriteStartElement("book"); xmlWriter.WriteAttributeString("isbn", "978-3257224450"); xmlWriter.WriteElementString("title", String.Empty, "Die wilde Geschichte vom Wassertrinker"); xmlWriter.WriteElementString("rating", jbNamespace, "5");
1074
XML-Dokumente über XSD validieren
xmlWriter.WriteStartElement("year"); xmlWriter.WriteAttributeString("nil", XmlSchema.InstanceNamespace, "true"); xmlWriter.WriteEndElement(); xmlWriter.WriteStartElement("author"); xmlWriter.WriteElementString("firstName", String.Empty, "John"); xmlWriter.WriteElementString("lastName", String.Empty, "Irving"); xmlWriter.WriteEndElement(); // author xmlWriter.WriteEndElement(); // book
12
// Zweites Buch xmlWriter.WriteStartElement("book"); xmlWriter.WriteAttributeString("isbn", "978-3423117371"); xmlWriter.WriteElementString("title", String.Empty, "Fool on the Hill"); xmlWriter.WriteElementString("rating", jbNamespace, "5"); xmlWriter.WriteStartElement("year"); xmlWriter.WriteValue(2004); xmlWriter.WriteEndElement(); xmlWriter.WriteStartElement("author"); xmlWriter.WriteElementString("firstName", String.Empty, "Matt"); xmlWriter.WriteElementString("lastName", String.Empty, "Ruff"); xmlWriter.WriteEndElement(); // author xmlWriter.WriteEndElement(); // book
13
14
15
xmlWriter.WriteEndElement(); // books
16
// Den XmlWriter schließen xmlWriter.Close();
Das Ergebnis dieses Beispiels entspricht ziemlich genau dem XML-Dokument auf 1062, allerdings natürlich nur mit den ersten beiden Büchern.
17
Neben den im Beispiel verwendeten Methoden können Sie noch andere Methoden zum Schreiben verwenden, wie z. B. WriteBase64 (binäre Daten Base64-codiert schreiben) und WriteCData (CDATA-Sektion schreiben).
18.4
18
XML-Dokumente über XSD validieren
Wie ich bereits mehrfach angemerkt habe, spielt XSD zur Validierung von XMLDokumenten in der Praxis eine große Rolle. Glücklicherweise ist das Validieren eines XML-Dokuments gegen ein vorhandenes XML-Schema sehr einfach. Dazu verwenden Sie zum Lesen des Dokuments einen XmlReader (den Sie aber auch XElement, XDocument oder anderen Klassen wie XmlDocument übergeben können). Den XmlReader erzeugen Sie mit XmlReaderSettings, in deren Eigenschaft ValidationType Sie den Wert ValidationType.Schema schreiben. Das Schema geben Sie über die Schemas-Auflistung an, deren Add-Methode am ersten Argument den Ziel-Namensraum verlangt. Wenn Sie hier null angeben, wird der im Schema ggf. angegebene Ziel-Namensraum verwendet. Am zweiten Argument übergeben Sie den URL des Schemas.
19
Ein XmlReader erlaubt das validierende Lesen
20
21
22
Wird das XML-Dokument dann eingelesen, resultiert eine XmlSchemaValidationException, wenn es nicht dem Schema entspricht.
23
1075
XML
Ich setze als Beispiel ein einfaches Schema für eine Artikel-XML-Datei ein: Listing 18.42: Einfaches Beispiel-Schema
Das folgende Dokument entspricht diesem Schema nicht: Listing 18.43: Nach dem Beispiel-Schema ungültiges XML-Dokument Cabrinha Switchblade 3 - 10m² 1402 Cabrinha Switchblade 3 - 12m² Cabrinha Switchblade 3 - 12m² Nicht bekannt
Listing 18.44 zeigt das Einlesen mit der Prüfung gegen das Schema: Listing 18.44: Einlesen eines XML-Dokuments mit Prüfung gegen ein Schema // XmlReader zum validierenden Lesen erzeugen und initialisieren string xsdFileName = "Products.xsd"; string xmlFileName = "Products.xml"; XmlReaderSettings settings = new XmlReaderSettings(); settings.ValidationType = ValidationType.Schema; settings.Schemas.Add(null, xsdFileName); XmlReader xmlReader = XmlReader.Create( new XmlTextReader(xmlFileName), settings); try {
1076
XML-Dokumente über XSD validieren
// Den Reader zum Lesen in ein XDocument verwenden XDocument xdoc = XDocument.Load(xmlReader); // Das XML-Dokument verarbeiten. Hier ist sicher, dass das // XML-Dokument dem Schema entspricht Console.WriteLine(xdoc.ToString()); } catch (XmlSchemaValidationException ex) { // Fehler bei der Validierung Console.WriteLine(ex.Message); }
12
Das Ergebnis dieses Beispiels ist eine XmlSchemaValidationException mit der Meldung »Das Element 'price' ist ungültig – Der Wert 'Nicht bekannt' ist gemäß seinem Datentyp 'http://www.w3.org/2001/XMLSchema:decimal' ungültig -- Die Zeichenfolge 'Nicht bekannt' ist kein gültiger Decimal-Wert.«.
13
14
Diese Fehlermeldung ist sehr sprechend und kann gut zur Fehlersuche verwendet werden. Das einzig Dumme ist, dass nur der erste Fehler gemeldet wird. Um alle Fehler auszuwerten, müssen Sie etwas mehr programmieren. Dafür ist hier aber kein Platz mehr. Auf der Buch-DVD finden Sie dazu ein Beispiel aus meinem Codebook im Ordner »Alle Fehler ermitteln«. Das Beispiel setzt eine eigene hilfreiche Klasse ein, über die Sie alle Fehler in einem XML-Dokument ermitteln können. Und noch ein Hinweis zum Schluss: Ich habe im Beispiel bewusst auf das Validieren gegen ein Dokument mit mehreren Namensräumen verzichtet, weil dies nicht so einfach ist (mir ist es bisher auf jeden Fall noch nicht gelungen, aber ich bin auch kein XML-Schema-Experte ☺).
15
DISC
16
17
INFO
18
Damit beende ich das XML-Kapitel und hoffe, dass Sie einen guten Einblick in die Möglichkeiten erhalten haben. Und falls Sie wissen, wie ein XML-Dokument mit mehreren Namensräumen gegen ein oder mehrere Schemas geprüft werden kann, mailen Sie mir.
19
20
21
22
23
1077
Inhalt
19
Datenbanken mit LINQ to SQL bearbeiten 12
Zur Arbeit mit Datenbanken stehen Ihnen mehrere Möglichkeiten zur Verfügung. Dieses Kapitel behandelt das sehr einfach anzuwendende LINQ to SQL, beschreibt aber in der Einführung auch die anderen Möglichkeiten.
13
LINQ to SQL ist deswegen sehr einfach, weil es zum einen auf einem Objektmodell basiert und zum anderen LINQ als Abfragesprache einsetzt. Das ebenfalls interessante ADO.NET Entity Framework verfolgt einen ähnlichen Ansatz, bietet aber mehr Möglichkeiten. Leider befand sich dieses zu der Zeit, als ich diese Zeilen schrieb, noch in der Beta-Phase. Da aber auch das ADO.NET Entity Framework den Zugriff über LINQ bietet, sind die Ähnlichkeiten mit LINQ to SQL groß.
14
15
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
16
Übersicht über die Möglichkeiten, in .NET 3.5 Datenbanken zu bearbeiten Die Einschränkungen von LINQ to SQL Erstellen eines Datenbankmodells in Visual Studio Nachbearbeitung des Datenbankmodells Der Datenkontext Abfragen mit LINQ to SQL Bearbeiten von Daten über das Objektmodell Business-Objekt-Modelle oder: Die partiellen Klassen und eine Reaktion auf Datenereignisse Transaktionen Behandlung von Konflikten beim Aktualisieren von Daten
17
18
19
Was ich in diesem Kapitel nicht behandle, sind Datenbankgrundlagen. Diese – und hier im Besonderen Grundlagen zu relationalen Datenbanken und zum SQL Server – sollten Sie kennen. Wichtig sind die Konzepte des Primärschlüssels und von Beziehungen zwischen Tabellen. Im Abschnitt »Die Beispiel-Datenbank« stelle ich allerdings die in den Beispielen dieses Kapitels verwendete Datenbank kurz vor und zeige, wie Sie diese mit dem SQL Server Management Studio (Express) grundsätzlich bearbeiten können.
20
21
Bei Wikipedia finden Sie eine gute Erläuterung des relationalen Datenbankmodells an der Adresse de.wikipedia.org/wiki/Relationale_Datenbank.
22 REF
Aufgrund der enormen Komplexität von LINQ to SQL kann ich leider nicht auf alle Möglichkeiten eingehen. Ich beschreibe LINQ to SQL hier aus der Praxis-Sicht, mit allen mir bekannten Fallstricken. Um tiefer in LINQ to SQL einzusteigen, müssen Sie aber ggf. spezielle Literatur oder (die nicht sehr gute) Dokumentation lesen.
1079
Index
23
Datenbanken mit LINQ to SQL bearbeiten
Ich hatte ursprünglich auch geplant, ein Beispiel zur Zwei-Wege-Datenbindung mit LINQ to SQL in einer WPF-Anwendung in dieses Kapitel zu integrieren. Leider ist mir dieses sehr praxisorientierte Beispiel aber nicht gelungen. Ich hatte dabei so massive Probleme, dass ich (zunächst) aufgeben musste. Ein sehr großes Problem ist, dass die Zwei-Wege-Datenbindung mit LINQ to SQL eigentlich nicht dokumentiert ist. Zudem liefert Microsoft keine Beispiele, die die Daten auch in die Datenbank zurückschreiben (ausschließlich lesende Beispiel gibt es allerdings schon). Im Internet ist dazu auch nichts zu finden und Nachfragen im Microsoft-LINQ-to-SQL-Forum (forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=2035) haben ebenfalls nichts ergeben. Dieses Thema scheint zurzeit noch nicht allzu bekannt zu sein. In den Buch-Beispielen finden Sie mein Testprojekt »Editieren mit Datenbindung (Inkompletter Versuch)« im Ordner Ändern, Anfügen und Löschen der Beispiele zu diesem Kapitel. Beachten Sie aber, dass dieses Projekt nur ein Versuch ist, der mir nicht wirklich gelungen ist. Die vielen TODO-Kommentare in diesem Projekt sprechen Bände … Falls ich eine Lösung für die Probleme finde, schreibe ich im Buch-Blog dazu einen Beitrag.
19.1
Grundlagen
Bevor ich die (einerseits einfache, aber andererseits auch mit Fallstricken gespickte) Arbeit mit LINQ to SQL beschreibe, folgen einige wichtige Grundlagen. Dazu gehört eine Einführung in die Datenbank, die in diesem Kapitel für die Beispiele verwendet wird, eine Übersicht über die modernen Möglichkeiten, Datenbanken zu bearbeiten, und die Einschränkungen von LINQ to SQL.
19.1.1
Der Begriff »Entität«
Der Begriff »Entität« kommt in diesem Kapitel und in der LINQ-to-SQL-Dokumentation sehr häufig vor. Deswegen sollten Sie diesen können. Eine Entität (englisch: Entity) ist in der Informatik im Allgemeinen ein eindeutig identifizierbares Objekt, dem Informationen zugeordnet sind. In einer objektorientierten Anwendung ist jedes Objekt eine Entität. In einer relationalen Datenbank ist ein einzelner Datensatz einer Tabelle eine Entität. An diesem Beispiel wird doch deutlich, warum dieser Begriff überhaupt verwendet wird. Damit können Sie ein logisches Objekt bezeichnen, dessen Speicherform damit aber nicht festgelegt ist. Ob das Objekt als Objekt in einem Programm existiert, als Objekt serialisiert ist oder als Datensatz in einer Tabelle gespeichert ist, ist vollkommen unerheblich. Die Entität »Kunde« steht immer für einen Kunden, egal in welcher Form dieser verwaltet wird. Eine Menge von Entitäten wird übrigens als Entitätsmenge bezeichnet (das hätten Sie jetzt nicht gedacht, oder? ☺). In einer Datenbank ist eine Tabelle eine Entitätsmenge, in einem objektorientierten Programm ist dies eine Auflistung.
19.1.2
Die Beispiel-Datenbank
Für die Beispiele dieses Kapitels verwende ich die SQL-Server-Beispiel-Datenbank AdventureWorksLT. Diese von Microsoft zur Verfügung gestellte Datenbank verwaltet die Daten der fiktiven Firma Adventure Works Cycles, die Fahrräder und Fahrrad-
1080
Grundlagen
Zubehör herstellt und verkauft. Microsoft stellt mehrere Versionen dieser Datenbank zur Verfügung. Die LT-Version ist die »Light«-Version, die die wenigsten Tabellen enthält und die deswegen sehr übersichtlich ist.
Die Installation der Datenbank Die im Buch verwendete Version der AdventureWorksLT-Datenbank ist auf der Buch-DVD im Ordner Beispiele\Datenbanken gespeichert. Alternativ können Sie die Datenbank natürlich auch bei Microsoft herunterladen. Sie finden diese an der Adresse www.codeplex.com/MSFTDBProdSamples/Release/ProjectReleases.aspx. Achten Sie darauf, dass Sie die richtige Version (in unserem Fall die Light-Version) für den SQL-Server herunterladen, der auf Ihrem System installiert ist. Wenn Sie lediglich Visual Studio installiert haben, ist dies der SQL Server 2005 (Express).
12
13
SQL-Server-Datenbanken können unter .NET 3.5 auf zwei Arten in einer Anwendung eingebunden werden. Die eine Möglichkeit ist die klassische, bei der die Datenbank fest in den SQL Server eingebunden ist. Diese Variante ist eher für große Datenbanken in Unternehmens-Umgebungen wichtig, bei denen mehrere Anwendungen auf dieselbe Datenbank zugreifen.
14
15
Die andere Möglichkeit unter .NET 3.5 ist, dass Sie die Datenbank lose in einem beliebigen Ordner (z. B. dem Anwendungsordner) verwalten und in der Laufzeit über den SQL Server Express (nicht den »richtigen« SQL Server!) dynamisch einbinden. Ich zeige natürlich, wie Sie beide Varianten einsetzen.
16
In den Beispielen setze ich die erste Variante ein. Eine Installation im SQL Server hat den Vorteil, dass Sie die Datenbank wesentlich einfacher bearbeiten können (und dass ich diese in den Beispielen nicht immer mitliefern muss).
17 Abbildung 19.1: Das SQL Management Studio Express mit der integrierten AdventureWorksLTDatenbank
18
19 20
21
22
Zur Installation der Datenbank im SQL Server (Express) benötigen Sie zunächst idealerweise das SQL Server Management Studio. Über dieses sehr hilfreiche Werkzeug können Sie SQL Server administrieren, Datenbanken hinzufügen und diese bearbeiten. Falls dieses auf Ihrem System noch nicht installiert ist, können Sie die Express-Edi-
Das SQL Server Management Studio erleichtert die Administration
1081
23
Datenbanken mit LINQ to SQL bearbeiten
tion des SQL Server Management Studio kostenfrei bei Microsoft herunterladen. Der Link zu der Download-Seite ist aber sehr komplex und wird sich in Zukunft mit Sicherheit ändern. Suchen Sie an der Adresse www.microsoft.com/downloads nach »Microsoft SQL Server Management Studio Express«, um den Download zu finden. Auf der Download-Seite können Sie die Sprache umstellen, um das SQL Server Management Studio Express in Deutsch herunterzuladen. Im SQL Server Management Studio Express müssen Sie nun zunächst die AdventureWorksLT-Datenbank anfügen. Dazu wählen Sie den Befehl ANFÜGEN des DATENBANKEN-Ordners im OBJEKT-EXPLORER. Im erscheinenden Dialog betätigen Sie den HINZUFÜGEN-Schalter, suchen nach der Datei AdventureWorksLT_Data.mdf (die die eigentliche Datenbank enthält), wählen diese aus und bestätigen den DatenbankAnfügen-Dialog dann mit OK.
Die grundlegende Struktur der Datenbank Die einzelnen Tabellen dieser Datenbank können Sie über den TABELLEN-Ordner bearbeiten. Die wesentlichen Befehle dazu finden Sie im Kontextmenü der Tabelleneinträge. Hier können Sie die Tabelle zur Bearbeitung der Daten öffnen, aber auch die Struktur der Tabelle verändern. Ein Datenbankdiagramm liefert eine Übersicht
Eine wichtige Übersicht ist ein Datenbankdiagramm, das Sie über den Befehl NEUES DATENBANKDIAGRAMM im Kontextmenü des DATENBANKDIAGRAMME-Ordners erzeugen können. Wenn Sie diesen Ordner zum ersten Mal auswählen, meldet der SQL Server, dass Unterstützungsobjekte für Datenbankdiagramme fehlen, die Sie aber hinzufügen können, indem Sie die entsprechende Meldung mit JA bestätigen. Nachdem Sie den Befehl NEUES DATENBANKDIAGRAMM ausgeführt haben, erscheint ein Dialog zur Auswahl der Tabellen. Wählen Sie hier alle Tabellen, betätigen Sie den HINZUFÜGEN-Schalter und schließen Sie den Dialog. Speichern Sie das erzeugte Diagramm unter einem Namen ab, der aussagt, dass es alle Tabellen beinhaltet (z. B. AllTables). Da ich in diesem Kapitel nicht auf alle Tabellen zugreife, erzeugen Sie ein zweites Diagramm, dem Sie nur die Tabellen hinzufügen, deren Name mit »Product« beginnt. Abbildung 19.2 zeigt das von mir erstellte und so angepasste Diagramm, dass die Beziehungen zwischen den Tabellen besser erkennbar sind. Da hier nicht der Platz bleibt, die Tabellen ausführlich zu beschreiben (was ich im Übrigen auch für sinnlos halte) stelle ich kurz die Bedeutung der einzelnen Tabellen vor: ■
■
■
1082
Product: Verwaltet die Daten von Produkten der fiktiven Firma Adventure Works Cycles. Dabei handelt es sich um Daten zu Fahrrädern und Fahrradteilen, die diese Firma verkauft. ProductCategory: Verwaltet die Daten der Produkt-Kategorien (wie Bikes, Forks, Chains). Diese sind hierarchisch organisiert: Eine Produkt-Kategorie kann eine übergeordnete Kategorie haben, die über ihr Feld ParentProductCategoryID referenziert wird. Die Kategorie Mountain Bikes hat als übergeordnete Kategorie z. B. Bikes. Bei den Kategorien der höchsten Ebene verwaltet das Feld ParentProductCategoryID den Wert null (allerdings natürlich in Form des entsprechenden Datenbank-Typs). ProductModel: Jedem Produkt ist über das Feld ProductModelID ein ProduktModell zugeordnet. Das Produkt-Modell ist dem Produkt übergeordnet und enthält allgemeine Informationen zum Produkt. Ein Produkt ist ein spezialisiertes
Grundlagen
■
■
Produkt-Modell. Das Produkt-Modell »Mountain 100 Silver« kommt als Produkt z. B. in den Varianten »Mountain 100 Silver, 38«, »Mountain 100 Silver, 42«, »Mountain 100 Silver, 44« etc. vor. ProductDescription: Diese Tabelle verwaltet Beschreibungen zu Produkt-Modellen. Das Besondere dieser Tabelle ist, dass die Beschreibungen in mehreren Sprachen verwaltet werden. ProductModelProductDescription: Diese Tabelle hat die besondere Bedeutung, die Beziehung zwischen den Tabellen ProductModel und ProductDescription herzustellen. Bei dieser Beziehung handelt es sich um eine N:M-Beziehung: Einem ProductModel-Datensatz können beliebig viele ProductDescription-Datensätze zugeordnet sein. Der Grund dafür ist, dass die Produkt-Beschreibungen mehrsprachig verwaltet werden. Für diesen Zweck gibt das Feld Culture der ProductModelProductDescription-Tabelle die Kultur an. Gleichzeitig können einem ProductDescription-Datensatz theoretisch aber auch beliebig viele ProductModel-Datensätze zugeordnet sein. Der Sinn dieser Beziehung ist, dass es vorkommen kann, dass eine Beschreibung für mehrere Produkt-Modelle gilt. In der AdventureWorksLT-Datenbank ist dies aber nicht der Fall.
12
13
14
Abbildung 19.2: SQL-ServerManagementExpress-Datenbankdiagramm für die Artikeltabellen der AdventureWorksLT-Datenbank
15
16
17
18
19 20
21
22 Das hinter dem Tabellennamen in Klammern angegebene SalesLT bedeutet übrigens, dass die Tabellen zu dem Datenbankschema »SalesLT« gehören. Ein Datenbankschema trennt Tabellen innerhalb einer Datenbank auf der logischen Ebene. Die Tabellen der Light-Version der AdventureWorks-Datenbank gehören alle zum dem Schema, das der Verkaufsabteilung zugeordnet ist. In der großen Version der Datenbank sind weitere Tabellen enthalten, die anderen Schemen, wie z. B. dem für
23
1083
Datenbanken mit LINQ to SQL bearbeiten
die Produktion, zugeordnet sind. Wenn Sie die Daten mit SQL abfragen, müssen Sie das Schema immer vor dem Tabellennamen schreiben. In LINQ to SQL haben Sie damit aber nicht viel zu tun. Das Erforschen der restlichen Möglichkeiten des SQL Server Management Studio Express muss ich nun Ihnen überlassen, da dafür in diesem Kapitel kein Platz ist.
19.1.3
Übersicht über die Möglichkeiten, in .NET 3.5 Datenbanken zu bearbeiten
In .NET 3.5 haben Sie einige Möglichkeiten, Datenbanken zu bearbeiten. Sie können: ■ ■ ■ ■ ■
Das klassische ADO.NET, LINQ to SQL, das ADO.NET Entity Framework, externe O/R-Mapper und spezielle Komponenten des jeweiligen Datenbanksystems verwenden.
ADO.NET ADO.NET ist die klassische .NET-Art
Das Bearbeiten von Datenbanken mit ADO.NET ist die ursprüngliche Variante, Datenbanken unter .NET zu bearbeiten. ADO.NET bietet dazu zwei grundsätzliche Möglichkeiten: Zum einen können Sie (über DataTable- und DataSet-Objekte) eine Datenbank oder Teile davon in relationalen Tabellen im Speicher abbilden und diese relativ einfach bearbeiten. Zum anderen können Sie Datensätze (über einen DataReader) sequenziell abfragen und über den direkten Aufruf von SQL-Anweisungen (über ein Command-Objekt) auch manipulieren. Obwohl die Arbeit mit DataTableObjekten im Speicher relativ einfach ist, ist zur Arbeit mit ADO.NET aber ein gutes SQL-Wissen notwendig. Außerdem arbeiten Sie unter ADO.NET auch im Programm noch mit relationalen Daten, was in einer modernen, objektorientierten Programmierung nicht mehr zeitgemäß ist. Insgesamt ist der Aufwand, eine Datenbank mit ADO.NET zu bearbeiten, wesentlich höher als bei der Verwendung von LINQ to SQL, dem ADO.NET Entity Framework oder einem der externen O/R-Mapper.
ADO.NET ist performant
Der wesentliche Vorteil von ADO.NET ist die Performance. Nach einem sehr ausführlichen Performancetest, den ich gegen verschiedene Datenbanksysteme mit den unterschiedlichen Möglichkeiten ausgeführt habe, liegt ADO.NET in den meisten Punkten vorne. Der Unterschied zu dem ebenfalls (wenigstens mit dem SQL Server) sehr schnellen LINQ to SQL ist aber nur minimal.
REF
ADO.NET wird in diesem Buch nicht behandelt. Sie finden das ausgelagerte Kapitel aber in Form eines Artikels an der Adresse www.juergen-bayer.net/artikel/csharp/ ado.net/ado.net.aspx.
LINQ to SQL LINQ to SQL implementiert ein Objektmodell
1084
LINQ to SQL ermöglicht das sehr bequeme Bearbeiten von Datenbanken über ein Objektmodell, das alle integrierten Tabellen in Form einer Klasse abbildet und automatisch daraus Instanzen erzeugt. Schon alleine das macht die Arbeit mit einer Datenbank wesentlich einfacher. Hinzu kommt, dass LINQ to SQL die Abfrage der Daten über LINQ ermöglicht. Die Performance von LINQ to SQL ist (für den SQL Server, den ich zurzeit ausschließlich getestet habe) verglichen mit den anderen Möglichkeiten (außer dem ADO.NET Entity Framework) sehr gut. Der wesentliche Nachteil von LINQ to SQL ist, dass zurzeit noch keine offiziellen Provider für andere
Grundlagen
Datenbanksysteme als den SQL Server und den SQL Server Compact Edition zur Verfügung stehen. Andere Nachteile sind ein eingeschränkter Support für bestimmte Datenbank-Features. So werden N:M-Beziehungen nicht direkt unterstützt, sondern lediglich über eine Klasse, die die Zwischentabelle repräsentiert.
Das ADO.NET Entity Framework Das ADO.NET Entity Framework wird die Nachteile von LINQ to SQL aufheben. So werden N:M-Beziehungen direkt unterstützt. Wahrscheinlich wird es auch gleich mit dem Erscheinen Provider für wichtige andere Datenbanksysteme wie z. B. Oracle geben. Leider befand sich das ADO.NET Entity Framework, das einen ähnlichen Ansatz hat wie LINQ to SQL, zur der Zeit, als ich diese Zeilen schrieb, noch in der Betaphase. Es soll Mitte 2008 im Rahmen des .NET 3.5 Service Pack 1 erscheinen. Wenn ich dann Zeit habe (was u. a. davon abhängt, wie gut der Wind dann hier in Irland ist ☺), schreibe ich im Blog zu diesem Buch einen Artikel dazu.
Das ADO.NET Entity Framework ist LINQ to SQL ähnlich
12
13
14 Dass .NET dann bald zwei Frameworks zum objektorientierten Zugriff auf Datenbanksysteme enthält, ist etwas verwirrend. Tatsächlich liegt es angeblich daran, dass diese beiden Frameworks von separaten Teams entwickelt wurden, die sich nicht abgesprochen haben. Microsoft empfiehlt, LINQ to SQL für einfache Szenarien einzusetzen und das ADO.NET Entity Framework für komplexere.
INFO
15
16
Ein Vergleich von LINQ to SQL und dem ADO.NET Entity Framework finden Sie an der Adresse dotnetaddict.dotnetdevelopersjournal.com/adoef_vs_linqsql.htm. REF
Externe O/R-Mapper Zum objektorientierten Zugriff auf ein relationales Datenbanksystem können Sie zudem auch externe O/R-Mapper (Object/Relational-Mapper) verwenden. Im Internet finden Sie eine Vielzahl verschiedener O/R-Mapper-Systeme. Eines der bekanntesten ist NHibernate (www.hibernate.org/343.html). Eine Übersicht der verfügbaren O/RMapper für .NET finden Sie an der Adresse sharptoolbox.com/categories/objectrelational-mappers.
17 Externe O/RMapper bieten u. U. Alternativen
18
19
Alle diese O/R-Mapper haben zwar gemeinsam, dass Sie die Tabellen einer relationalen Datenbank auf Objekte mappen. Ansonsten sind sie aber sehr verschieden und erfordern eine lange Einarbeitungszeit. Der Vorteil einiger dieser O/R-Mapper ist allerdings, dass sie häufig auch für kleinere Datenbanksysteme wie z. B. MySQL zur Verfügung stehen und für diese auch wirklich funktionieren. Die Open-Source-Community entwickelt zwar auch Provider für LINQ to SQL (und wahrscheinlich auch für das ADO.NET Entity Framework), aber die Äquivalente für »alte« O/R-Mapper wie NHibernate sind wesentlich ausgereifter. In meinen Augen haben diese externen O/R-Mapper also auf jeden Fall noch Bedeutung für den Zugriff auf nicht-kommerzielle Datenbanksysteme wie MySQL.
20
21
Für den Zugriff auf Datenbanksysteme, für die ein offizieller Provider für LINQ to SQL oder das ADO.NET Entity Framework vorliegt, würde ich aber auf jeden Fall diese verwenden. Ein Grund dafür ist, dass die Microsoft-Frameworks mit Sicherheit weiterentwickelt werden. Ein anderer ist, dass in meinem Datenbank-Performancetest zumindest die O/R-Mapper NHibernate und das OOP-Datenbanksystem db4objects schlechter abgeschnitten haben als LINQ to SQL.
22
23
1085
Datenbanken mit LINQ to SQL bearbeiten
Das (unverbindliche) Ergebnis meines Datenbank-Performance-Tests finden Sie in Form der Datei Datenbank-Zugriffstechnologie-Vergleichstest.pdf auf der Buch-DVD. DISC
Spezielle Komponenten des jeweiligen Datenbanksystems Zur Bearbeitung einer Datenbank können Sie schließlich noch spezielle Komponenten verwenden, die das jeweilige Datenbanksystem zur Verfügung stellt. Diese werden in der Regel als API zur Verfügung gestellt, häufig als DLL-Dateien oder COM-Komponenten (mit etwas Glück auch als .NET-Klassenbibliotheken). Bei einigen Datenbanksystemen wie Caché (www.intersystems.de/cache) und db4objects (www.db4o.com) – beides OOP-Datenbanksysteme1 – wird sogar vorwiegend deren API verwendet, um auf die Daten zuzugreifen. Für die Arbeit mit »normalen« Datenbanksystemen, für die ein LINQ-to-SQL-, ADO.NET-Entity-Framework- oder ADO.NET-Provider zur Verfügung steht, besteht jedoch meist kein Sinn, direkt auf das API des Datenbanksystems zuzugreifen. Für Microsoft-Access-Datenbanken ist zumindest der schreibende Zugriff über das Access-API (DAO) aber wesentlich schneller als der Zugriff über ADO.NET (was ein echter Tipp ist: Setzen Sie DAO ein, wenn Sie effizient in Access-Datenbanken schreiben wollen).
19.1.4
Einschränkungen von LINQ to SQL
LINQ to SQL hat zurzeit noch einige Einschränkungen im Vergleich zu »echten« O/RMappern: ■ ■ ■
■
N:M-Beziehungen werden nicht direkt unterstützt. Das Ableiten von Klassen wird nicht unterstützt. Eine Klasse repräsentiert in LINQ to SQL immer genau eine Tabelle oder eine Sicht. .NET 3.5.0.0 liefert lediglich Provider für den SQL Server (Express) und den SQL Server Compact Edition mit. Für den SQL Server Compact Edition kann das Objektmodell nicht mit Visual Studio erzeugt werden. Dazu müssen Sie das Tool SqlMetal.exe verwenden. Externe Entwickler arbeiten zwar an LINQ-toSQL-Providern für andere Datenbanksysteme (wie MySQL und Postgress, siehe code2code.net/DB_Linq), aber diese sind noch nicht ausgereift. Für Oracle gibt es allerdings bereits einen (kommerziellen) ADO.NET-Entity-Framework-Provider (crlab.com/oranet), weswegen ich mich sehr ärgere, dass das ADO.NET Entity Framework noch nicht fertig gestellt ist. Das Erstellen einer einzigen Klasse zur Bearbeitung mehrerer Tabellen wird nicht unterstützt. Dieses Problem können Sie allerdings über eine Sicht lösen, die Sie im Datenbanksystem definieren und für die Sie im Datenbankmodell eine Klasse erzeugen (lassen).
Die größte Einschränkung ist zurzeit, dass kein Provider für das in vielen Firmen eingesetzte Datenbanksystem von Oracle besteht. Ich hoffe allerdings, dass Oracle selbst einen Provider zur Verfügung stellt (wie schon bei ADO.NET), aber ich befürchte, dass Oracle nur das ADO.NET Entity Framework unterstützen wird. Wenn Sie auf Oracle-Datenbanken zugreifen wollen, müssen Sie zurzeit noch ADO.NET oder einen der externen O/R-Mapper verwenden.
1
1086
Wobei das hervorragende, aber leider auch teuere Caché auch eine relationale Engine besitzt.
Erstellen eines Datenbankmodells
19.2
Erstellen eines Datenbankmodells
LINQ to SQL arbeitet mit einem Datenbankmodell, das in einer .dbml-Datei gespeichert ist. Das Datenbankmodell beschreibt, welche Tabelle in welche Klasse umgesetzt wird, welche Felder in welche Eigenschaften projiziert werden, welche Beziehungen bestehen etc. Das Datenbankmodell wird automatisch in ein Klassenmodell umgewandelt. Mit diesem arbeiten Sie, um auf die Daten zuzugreifen.
Eine .dbml-Datei speichert das Datenbankmodell
12
Das Klassenmodell können Sie für SQL-Server-Datenbanken mit Visual Studio erzeugen. Der entsprechende Visual-Studio-Designer erlaubt dazu u. a. das Ziehen von Tabellen aus dem Server-Explorer auf das Modell. Ein benutzerdefiniertes Tool übernimmt die Konvertierung in das äquivalente Klassenmodell.
13
Für SQL-Server-Compact-Datenbanken (die Kompakt-Edition des SQL Servers, die komplett in .NET-Assemblys implementiert ist) müssen Sie SqlMetal.exe verwenden. Darauf gehe ich hier nicht näher ein. Die grundsätzliche Verwendung ist aber einfach:
14
SqlMetal.exe Datenbankname.sdf /Code Dateiname.cs
19.2.1
15
Erstellen eines Datenbankmodells mit Visual Studio auf der Basis einer vorhandenen Datenbank
16
Zum Erstellen eines Datenbankmodells für eine vorhandene Datenbank mit Visual Studio müssen Sie zunächst eine Verbindung zur Datenbank im Server-Explorer aufbauen. Danach können Sie Tabellen aus dem Server-Explorer auf ein Datenbank Modell ziehen.
17
Registrieren einer Datenbank im Server-Explorer Zum Registrieren einer Datenbank im Server-Explorer öffnen Sie diesen zunächst ggf. (ANSICHT / SERVER-EXPLORER). Wählen Sie dann den Befehl VERBINDUNG HINZUFÜGEN im Kontextmenü des DATENVERBINDUNGEN-Eintrags. Wählen Sie die Datenquelle MICROSOFT SQL SERVER und geben Sie als Servernamen ».\SQLEXPRESS« ein. Wundern Sie sich nicht, wenn der SQL Server Express nicht in der Liste auftaucht, die Sie dort aufklappen können. Zum einen muss der SQL-Server-Browser-Dienst ausgeführt werden, damit überhaupt SQL Server in der Liste auftauchen. Zum anderen werden Express-Instanzen des SQL Servers aus mir unerklärlichen Gründen (zumindest auf meinen Systemen) nicht in der Liste aufgeführt. Außerdem benötigt das Aufklappen der Liste sehr viel Zeit (wahrscheinlich weil im ganzen Internet nach SQLServer-Instanzen gesucht wird – was jetzt ironisch gemeint war ☺).
Der ServerExplorer gibt Zugriff auf Datenbanken
18
19 20
Wählen Sie dann aus der Liste im Bereich MIT DATENBANK VERBINDEN die AdventureWorksLT-Datenbank aus.
21
Bestätigen Sie den Dialog dann mit OK. Der Server-Explorer enthält nun einen neuen Eintrag für die Datenbank. Klappen Sie diesen auf (was gleich auch eine Verbindung des Server-Explorers zur Datenbank aufbaut) um die Tabellen zu sehen (Abbildung 19.4).
22
23
1087
Datenbanken mit LINQ to SQL bearbeiten
Abbildung 19.3: Der Dialog zum Hinzufügen einer Datenbankverbindung zum Server-Explorer
Abbildung 19.4: Der Server-Explorer mit der AdventureWorksLT-Datenbank
1088
Erstellen eines Datenbankmodells
Erstellen des Datenmodells Fügen Sie dem Projekt nun ein Element vom Typ LINQ TO SQL-KLASSEN hinzu (was eigentlich das Datenbankmodell ist). Nennen Sie dieses vielleicht AdventureWorksLT.dmbl. Dann ziehen Sie alle Tabellen, die Sie bearbeiten wollen (in unserem Fall einfach alle, deren Name mit »Product« beginnt) aus dem Server-Explorer auf das Datenbankmodell. Wenn Sie die Ansicht nun noch auf 100% zoomen, den Methodenbereich rechts ausblenden (alles über das Kontextmenü) und die erstellten Klasseneinträge ein wenig besser positionieren, sieht das Datenbankmodell in etwa aus wie in Abbildung 19.5.
12
Abbildung 19.5: Das erzeugte Datenbankmodell
13
14
15
16
17
18
19 19.2.2 Erstellen eines komplett neuen Datenbankmodells mit Visual Studio Eine andere, für die Praxis durchaus auch interessante Vorgehensweise ist, zuerst das Datenbankmodell zu erzeugen und aus diesem dann die Datenbank. Dazu fügen Sie einem Projekt ein Datenbankmodell hinzu und diesem (über das Kontextmenü) Klassen mit Eigenschaften und Beziehungen zu anderen Klassen. Ist das Modell dann fertig gestellt, erzeugen Sie eine Instanz der DatenkontextKlasse, die Visual Studio generiert hat, wobei Sie am Konstruktor einen Verbindungsstring zum Datenbanksystem oder eine Verbindung zu einer anderen Datenbank auf dem SQL Server übergeben müssen. Dann rufen Sie deren CreateDatabaseMethode auf, um die Datenbank zu erzeugen.
Visual Studio erlaubt, zuerst das Datenbankmodell zu erzeugen und daraus dynamisch die Datenbank
20
21
22
Dieses Feature ist sehr interessant für den Fall, dass eine Datenbank beim Kunden erst noch erzeugt werden muss.
23
1089
Datenbanken mit LINQ to SQL bearbeiten
DISC
Auf der Buch-DVD finden Sie ein Beispiel zum Erstellen einer Datenbank über ein Datenbankmodell in Form des Projekts »Erstellen einer Datenbank über das Datenmodell«.
19.2.3 Das grundlegende Datenbankmodell Das grundlegende Datenbankmodell ist in der .dbml-Datei in XML-Form gespeichert und ziemlich komplex. Sie können dieses nicht über einen Doppelklick in Visual Studio öffnen. Sie können aber den Befehl ÖFFNEN MIT im Kontextmenü des entsprechenden Eintrags verwenden und dann die Option QUELLCODE-EDITOR (TEXT) wählen, um das Datenbankmodell im Visual-Studio-Texteditor zu öffnen. Listing 19.1 zeigt einen kleinen Ausschnitt dieser Datei. Listing 19.1:
Auszug aus dem Datenbankmodell
new Product { Name = product.Name }); int count = products.Count();
13
Das Problem dabei wäre, dass aufgrund der Nachverfolgung der Objekterzeugung Referenzen auf unvollständig gefüllte Objekte verwaltet werden würden. Deswegen lässt LINQ to SQL eine solche Konstruktion auch nicht zu und wirft eine NotSupportedException. Die Nachverfolgung der Objekterzeugung kann übrigens über die Eigenschaft ObjectTrackingEnabled des Datenkontextes auch abgeschaltet werden. Ich denke, dass dies aber in der Praxis zu massiven Problemen mit mehrfach vorkommenden Objekten führt. In Anwendungen, die immer den aktuellen Status der Entitäten in der Datenbank anzeigen müssen, können Sie auch andere Techniken einsetzen, wie z. B. den Aufruf der Refresh-Methode des Datenkontextes zum Aktualisieren einer Entität oder einer Entitätsmenge. Für einzelne Felder können Sie auch die Option AUTO SYNC einstellen.
14
15
INFO
16
19.2.8 Erzeugen des Datenkontextes
17
Das Erzeugen des Datenkontextes ist eigentlich einfach: Sie erzeugen eine Instanz der entsprechenden Klasse:
18
AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext();
Zwei Dinge müssen Sie aber dazu wissen: Woher der Datenkontext seine Verbindungsinformationen erhält und wann dieser (neu) erzeugt werden soll bzw. muss.
19
Die Verbindungsinformationen 20
Per Voreinstellung erhält der Datenkontext seine Verbindungsinformationen aus der Anwendungskonfiguration. In der app.config finden Sie dazu im connectionStringsElement einen entsprechend des Datenmodells benannten Verbindungsstring. Diesen können Sie natürlich (beim Kunden z. B.) anpassen. Der Konstruktor der Datenkontext-Klasse liest den Verbindungsstring aus und stellt diesen ein.
21
Eine andere Möglichkeit ist, dem Konstruktor den Verbindungsstring explizit zu übergeben:
22
string connectionString = @"Data Source=.\SQLEXPRESS;" + "Initial Catalog=AdventureWorksLT;Integrated Security=True"; AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext(connectionString);
23
Diese Möglichkeit können Sie nutzen, wenn Sie den String selbst verwalten wollen.
1097
Datenbanken mit LINQ to SQL bearbeiten
Eine dritte Möglichkeit ist, eine vorhandene Verbindung zu verwenden: AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext(Vorhandene Verbindung);
Diese Variante ist dann interessant, wenn Sie bereits über eine Verbindung verfügen, weil Sie z. B. parallel mit ADO.NET arbeiten. ADO.NET verwaltet Verbindungen in einem Pool
In der Regel werden die Verbindungen aber lediglich über einen Verbindungsstring definiert und das Handling dem Datenkontext überlassen. Dieser öffnet die Verbindung nur bei Bedarf und schließt sie direkt danach wieder. Das hat zum einen den Vorteil, dass das Datenbanksystem nicht mit zu vielen offenen Verbindungen belastet wird. Der andere Vorteil ist, dass Sie sich nicht um das Schließen der Verbindung kümmern müssen. Nachteile sind dadurch nicht zu erwarten, weil ADO.NET (das für die Verbindung zuständig ist) Verbindungen auf dem Rechner, der die Verbindung herstellt, in einem internen Pool (pro Prozess) verwaltet und nach »Schließen« noch für eine Minute (per Voreinstellung) geöffnet hält. Wird die Verbindung innerhalb dieser Zeit noch einmal »geöffnet«, wird einfach eine bereits geöffnete Verbindung aus dem Pool zurückgegeben.
Wann sollte der Datenkontext (neu) erzeugt werden? Der Datenkontext behält gescheiterte Datenbankänderungen bei
Die Frage, wann der Datenkontext neu erzeugt werden sollte, ist nicht so einfach zu beantworten. Ein großes Problem des Datenkontextes ist, dass dieser alle anstehenden Änderungen intern in einer Art Auflistung (einem »Änderungs-Set«) verwaltet und diese bei einem Scheitern der Übertragung der Änderungen an das Datenbanksystem beibehält. Wenn Sie z. B. ein Product-Objekt löschen und später die Änderungen an die Datenbank übertragen (über einen Aufruf der SubmitChangesMethode), kann es sein, dass die Datenbank das Löschen nicht zulässt. Die aktuell anstehende Änderung bleibt dann aber im Änderungsset des Datenkontextes bestehen. Wenn Sie danach andere Änderungen vornehmen und versuchen diese zu speichern, schlägt das Speichern wieder fehl, weil der Datenkontext noch einmal versucht, das gelöschte Product-Objekt in der Datenbank zu löschen. Die aktuell anstehenden Änderungen können Sie zwar über die Methode GetChangeSet abfragen, aber – in der ersten LINQ-to-SQL-Version –leider nicht entfernen.
HALT
Erzeugen Sie den Datenkontext nur nach Bedarf
Als einzige Lösung dieses Problems bleibt, den Datenkontext auf jeden Fall immer dann neu zu erzeugen, nachdem beim Aufruf von SubmitChanges ein Fehler aufgetreten ist. Microsoft hat laut Aussagen in einem Forum (von einem Microsoft-Mitarbeiter) den Datenkontext so gestaltet, dass dieser eine möglichst kurze Lebensdauer besitzen kann. Im Prinzip ist es kein Problem, den Datenkontext immer nur so lange am Leben zu erhalten, wie Sie diesen aktuell benötigen. Wenn Sie z. B. in zwei Fenstern einer WPF-Anwendung mit dem Datenkontext arbeiten, verwalten Sie diesen als privates Feld in den Fenstern und erzeugen ihn beim Laden des jeweiligen Fensters. Was Sie auf jeden Fall nicht machen sollten, ist, den Datenkontext als statische Eigenschaft global zu verwalten und beim Start der Anwendung zu erzeugen. In einer Webanwendung sollten Sie den Datenkontext für jedes Webformular neu erzeugen. Gerade in einer solchen Anwendung sollten Sie nicht auf die Idee kommen, den Datenkontext statisch zu verwalten (was schon deswegen zu massiven
1098
Abfragen mit LINQ to SQL
Fehlern führt, weil in einer Webanwendung statische Eigenschaften für alle Sitzungen gelten). Sie sollten den Datenkontext aber auch nicht in der Sitzung verwalten. Machen Sie dies trotzdem, riskieren Sie massive Probleme mit der Tatsache, dass der Datenkontext nicht übertragbare Änderungen immer wieder versucht, zu speichern.
19.2.9 Das LINQ-to-SQL-Protokoll 12
LINQ to SQL ermöglicht es auf einfache Weise, die aktuell zur Datenbank gesendeten SQL-Anweisungen zu protokollieren. Dazu schreiben Sie in die Eigenschaft Log des Datenkontextes eine Instanz einer von TextWriter abgeleiteten Klasse, z. B. einen StreamWriter: Listing 19.2:
13
Protokollieren der SQL-Anweisungen von LINQ to SQL
// Das Protokoll auf einen StreamWriter setzen string logFileName = "LINQ to SQL.log"; using (StreamWriter logWriter = new StreamWriter(logFileName, false)) { dataContext.Log = logWriter;
14
15
// Daten abfragen foreach (var producCategory in dataContext.ProductCategories) { Console.WriteLine(producCategory.Name); }
16
} // Das Protokoll anzeigen Process.Start(logFileName);
17
Die im Beispiel gezeigte Variante, eine separate Protokolldatei zu verwenden (statt Console.Out, was auch möglich wäre), hilft, die Übersicht zu behalten und trotzdem alle generierten SQL-Anweisungen evaluieren zu können. Das hilft wiederum beim Verständnis der Arbeitsweise von LINQ to SQL.
19.3
18
Abfragen mit LINQ to SQL
19
LINQ to SQL bietet sehr mächtige Werkzeuge zum Abfragen von Daten (über LINQ). Einige Dinge sind aber auch unterschiedlich bzw. müssen Sie beachten.
19.3.1
Einfache Abfragen
20
Nach der ganzen (leider notwendigen) Theorie geht es nun zur Praxis. Diese ist (mit dem notwendigen Grundwissen) Gott sei Dank nicht so schwierig. Die reine Abfrage von Daten ist auf jeden Fall schon einmal über LINQ sehr einfach. Dazu fragen Sie die entsprechenden Eigenschaften des Datenkontextes ab. So können Sie z. B. alle Produkt-Kategorien der ersten Ebene abfragen:
21
Listing 19.3:
22
Einfache LINQ-Abfrage auf dem Datenmodell für die AdventureWorks-Datenbank
// Erzeugen des Datenkontextes mit dem in der // Anwendungskonfiguration verwalteten Verbindungs-String AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext();
23
1099
Datenbanken mit LINQ to SQL bearbeiten
// Abfragen aller Produkt-Kategorien der ersten Ebene var productsCategoriesOfFirstLevel = from productCategory in dataContext.ProductCategories where productCategory.ParentCategory == null select productCategory; foreach (var productCategory in productsCategoriesOfFirstLevel) { Console.WriteLine(productCategory.Name); }
Das Ergebnis dieses Programms ist: Bikes Components Clothing Accessories
INFO
Ich denke, Sie können sich vorstellen, dass Sie auch bei LINQ to SQL für die Abfrage von Daten dieselben Möglichkeiten haben, wie unter LINQ direkt. Ich gehe deshalb hier nicht mehr auf bereits bekannte Dinge wie komplexere Abfragen und Projektionen ein, sondern behandle nur noch die für LINQ to SQL spezifischen Dinge.
19.3.2 Abfragen mit LIKE und dynamische Abfragen SqlMethods.Like ermöglicht LIKE-Abfragen
Obwohl LINQ bereits sehr flexibel ist, können Sie nicht alle Abfrage-Probleme damit lösen. Bei der Arbeit mit Datenbanken ist z. B. der der SQL-Operator LIKE sehr hilfreich, der von LINQ direkt nicht abgebildet wird. LIKE erlaubt den Stringvergleich mit einem Muster, das die Platzhalterzeichen % und _ beinhalten kann. Das Prozentzeichen steht für beliebig viele beliebige Zeichen. Das Prozentzeichen steht für genau ein beliebiges Zeichen.
LIKE-Abfragen In LINQ to SQL können Sie LIKE aber (zumindest für den SQL Server) einsetzen. Dazu verwenden Sie die Like-Methode der SqlMethods-Klasse aus dem Namensraum System.Data.Linq.SqlClient: Listing 19.4: Abfragen mit LIKE // Alle Produkte abfragen, die "Frame" im Namen tragen, // gefolgt von einem Bindestrich, einem beliebigen Zeichen, // einem Leerzeichen und weiteren beliebigen Zeichen. var products1 = from product in dataContext.Products where SqlMethods.Like(product.Name, "%Frame-_ %") select product;
Obwohl LIKE speziell für den SQL Server implementiert ist, sollte die Abfrage auch auf anderen Datenbanksystemen funktionieren, die dieselbe LIKE-Syntax verwenden.
Dynamische Abfragen Dynamische Abfragen sind über Dynamic.cs möglich
1100
Dynamische Abfragen sind genau wie unter LINQ direkt, zum einen über einen progressiven Aufbau der Abfrage möglich. Zum anderen können Sie aber auch die Beispiel-Datei Dynamic.cs in das Projekt einbinden, um Abfragen als String formulieren zu können. Diese Möglichkeit ist gerade bei der Abfrage von Datenbanken sehr interessant. Informationen zu Dynamic.cs finden Sie im LINQ-Kapitel (im Tipps-undTricks-Abschnitt).
Abfragen mit LINQ to SQL
Bei der Verwendung von dynamischen Abfragen können Sie für die Einschränkung nicht die where-Klausel verwenden, sondern müssen die Where-Erweiterungsmethode (und ggf. auch OrderBy und andere der in Dynamic.cs enthaltenen Erweiterungsmethoden) direkt aufrufen: Listing 19.5:
Dynamische Abfrage in LINQ to SQL
using System.Linq.Dynamic;
12
... // Den Abfrage-String zusammenstellen string queryString = "ListPrice > 3000 && Color = \"Black\"";
13
// Die Sortierung definieren string orderString = "ListPrice descending, Name ascending";
14
// Abfragen var products2 = from product in dataContext.Products.Where(queryString) .OrderBy(orderString) select product;
15
19.3.3 Einschränkungen von LINQ to SQL gegenüber LINQ Die Einschränkungen von LINQ to SQL gegenüber LINQ finden Sie in der Visual-Studio-Dokumentation, indem Sie im Index nach »Übersetzen von Standardabfrageoperatoren (LINQ to SQL)« suchen.
16 REF
17
Im Wesentlichen gelten die folgenden Einschränkungen: ■
■
■
■ ■ ■ ■
Prädikate: LINQ to SQL unterstützt zwar erstaunlich viele der Methoden und Eigenschaften der Standardtypen in Prädikaten (z. B. String-Methoden wie StartsWith, ToLower, Replace und DateTime-Member wie AddDays und DayOfYear), aber nicht spezielle Methoden wie z. B. GetHashCode. Concat und Union behalten die Sortierung der verketteten Sequenzen nicht bei. Außerdem können diese Methoden nicht direkt mit Table-Referenzen aufgerufen werden. Sie müssen die zu verkettenden Sequenzen separat abfragen und idealerweise dabei gleich sortieren. Außerdem können Sie scheinbar keine zwei separaten Tabellen (mit gleichen Feldern) miteinander verketten. Intersect und Except werden für »Multisets« nicht unterstützt (was auch immer damit gemeint ist, auf normalen Table-Instanzen oder Abfragen funktionieren beide Methoden einwandfrei). DefaultIfEmpty wird nur in der Überladung ohne Argument unterstützt. FirstOrDefault wird nur in der Überladung ohne Prädikat-Argument unterstützt. Die Überladung mit Prädikat wirft eine NotSupportedException. Last und LastOrDefault werden nicht unterstützt und führen zu einer NotSupportedException. Take und Skip arbeiten nur dann zuverlässig, wenn sie auf sortierte Abfragen angewendet werden. Für »Multisets« (was auch immer das ist) ist für Take und Skip laut der Dokumentation keine »Semantik« definiert. Wenn Sie Take und Skip zusammen verwenden, muss die Sortierung der Sequenzen, auf denen diese Methoden angewendet werden, dieselbe sein. Im anderen Fall ist das Ergebnis nicht korrekt.
18
19 20
21
22
23
1101
Datenbanken mit LINQ to SQL bearbeiten
■ ■ ■
TakeWhile und SkipWhile werden nicht unterstützt. ElementAt und ElementAtOrDefault werden nicht unterstützt. Sum gibt bei einer leeren Sequenz nicht 0, sondern null zurück. Außerdem werden integer-Summierungen nicht als long-Ergebnis zurückgegeben und ergeben folglich ggf. einen Überlauf.
Das Projekt »Einschränkungen in LINQ to SQL« in den Buch-Beispielen demonstriert diese Einschränkungen. DISC
19.3.4 Aufgeschobene Ausführung mit LINQ to SQL und das Erzeugen von SQL LINQ to SQL erzeugt SQL-Anweisungen
LINQ to SQL verwendet genau wie LINQ eine aufgeschobene Ausführung. Eine Abfrage wird erst dann wirklich ausgeführt, wenn die Daten gelesen werden. Anders als bei LINQ wird die Abfrage aber nicht direkt ausgeführt, sondern über einen Ausdrucksbaum in SQL umgewandelt, welches dann dem Datenbanksystem gesendet wird. Das Datenbanksystem wertet die Abfrage aus, erzeugt ein Ergebnis (oder einen Fehler) und sendet dieses zurück. Der Datenkontext nimmt das Ergebnis entgegen und reflektiert dies im aktuellen Objektmodell. Die aufgeschobene Ausführung bedeutet, dass Sie Abfragen problemlos progressiv aufbauen können. Anders als bei LINQ werden die Abfragen beim Lesen der Daten aber nicht der Reihe nach ausgeführt. LINQ to SQL ist so schlau, aus progressiv aufgebauten Abfragen eine einzige SQL-Anweisung aufzubauen. Das folgende Beispiel demonstriert dies. Listing 19.6: Progressiver Aufbau einer Abfrage var products = from product in dataContext.Products where product.ListPrice > 1000 select product; products = from product in products where product.Color == "Red" select product;
TIPP
Die erzeugten SQL-Anweisungen können Sie sich in einem Visualisierer anschauen, dem LINQ to SQL Visualizer. Diesen Visualisierer, der eigentlich zu Visual Studio gehören sollte, finden Sie auf der Buch-DVD im Ordner Visual-Studio-Tools oder an der Adresse weblogs.asp.net/scottgu/archive/2007/07/31/linq-to-sql-debugvisualizer.aspx. Kopieren Sie die Datei SqlServerQueryVisualizer.dll in den Ordner Visual Studio 2008\Visualizers in Ihrem Dokumente-Ordner (unter XP: Eigene Dateien). Danach können Sie den Visualisierer auf einer Variable verwenden, die eine LINQ- Abfrage verwaltet. Abbildung 19.6 zeigt die Abfrage von Listing 19.6. Wie Sie sehen, wurde die progressiv aufgebaute Abfrage in eine einzige SQL-Abfrage mit einer zusammengesetzten Bedingung umgesetzt. Dieses Umsetzen in SQL hat auch noch die Auswirkung, dass Unterabfragen nicht wie bei LINQ aufgeschoben ausgeführt werden, wenn die in der Unterabfrage enthaltenen Daten abgefragt werden. Unterabfragen werden als SQL-Unterabfragen in
1102
Abfragen mit LINQ to SQL
die SQL-Anweisung eingebettet und somit bei der (aufgeschobenen) Ausführung der äußeren Abfrage ausgeführt. Das vermeidet überflüssige Roundtrips. Abbildung 19.6: Die Beispiel-Abfrage im SQL Server Query Visualizer
12
13
14
15
19.3.5 Auflösen von Beziehungen Das Auflösen von Beziehungen zwischen Tabellen ist in LINQ to SQL, verglichen mit dem relationalen ADO.NET, sehr einfach. Beim Erzeugen der Klassen für das Datenbankmodell hat Visual Studio (bzw. SqlMetal) für alle Beziehungen entsprechende Eigenschaften in die Klassen integriert. Bei einer 1:N-Beziehung ist der Typ diese Eigenschaft auf Seiten der Klasse für die Master-Entität vom Typ EntitySet, der Typ in der Klasse für die Detail-Entität entspricht dem der Master-Klasse. So können Sie die einer Master-Entität untergeordneten Detail-Entitäten abfragen oder die einer Detail-Entität zugeordnete Master-Entität: Listing 19.7:
16
17
18
Auflösen einer Beziehung
// Abfragen aller Produkt-Kategorien der ersten Ebene Console.WriteLine("Alle Produktkategorien mit Artikeln:"); foreach (var productCategory in dataContext.ProductCategories) { Console.WriteLine(); Console.WriteLine(productCategory.Name);
19 20
// Die untergeordneten Produkte ermitteln foreach (var product in productCategory.Products) { Console.WriteLine(" " + product.ID + ":" + product.Name); }
21
}
Abbildung 19.7 zeigt den letzten Teil des Ergebnisses. Bei der Auflösung von Beziehungen zwischen Master- und Detailtabellen sortiert LINQ to SQL die Objekte, die die Detaildatensätze darstellen, nicht immer nach deren Primärschlüssel. Im obigen Beispiel ist das zwar der Fall (was ich überprüft habe). In meiner Praxis war dies aber häufig auch nicht der Fall. Das Problem liegt bereits im verwendeten Datenbanksystem, das nicht (immer) implizit nach dem Primärschlüssel sortiert. Wenn Sie Detaildaten also explizit nach dem Primärschlüssel sortiert ausgeben wollen, müssen Sie diese über OrderBy sortieren.
22 HALT
23
1103
Datenbanken mit LINQ to SQL bearbeiten
Abbildung 19.7: Der letzte Teil des Ergebnisses des Beispiels
N:M-Beziehungen Da LINQ to SQL N:M-Beziehungen in deren originaler Form über Zwischentabellen darstellt, müssen Sie diese bei der Abfrage berücksichtigen. Wenn Sie z. B. alle Produkt-Modelle ermitteln wollen, in deren englischer Beschreibung der Begriff »Carbon« vorkommt, müssen Sie die Zwischenentität ProductModelProductDescription berücksichtigen. LINQ hilft dabei enorm über die Erweiterungsmethoden: Listing 19.8: Abfragen einer N:M-Beziehung var carbonProductModels = from productModel in dataContext.ProductModels where productModel.ProductModelProductDescriptions .Where(pmps => pmps.Culture == "en") .Any(pmps => pmps.ProductDescription.Description.Contains("Carbon")) select productModel;
Aufgeschobene Ausführung bei 1:N-Beziehungen EntitySet verwendet ebenfalls aufgeschobene Ausführung
Die Klasse EntitySet, die die Beziehungen auf der Seite der Master-Entität verwaltet, fragt die referenzierten Daten erst dann ab, wenn diese verwendet werden. Dies führt zu einem großen Vorteil bei der Arbeit mit 1:N-Beziehungen, da bei der Abfrage der Master-Daten zunächst nur diese vom Datenbanksystem abgerufen werden. Die Abfrage benötigt also relativ wenig Zeit und Arbeitsspeicher. Erst wenn die Detail-Entitäten benötigt werden (also wenn die entsprechende Eigenschaft gelesen wird), werden die Daten abgefragt. Ein Performance-Nachteil ist dadurch nicht zu erwarten, weil die Daten ja sowieso abgefragt werden müssen. Hat der Datenkontext diese einmal abgefragt, sind die Daten auch im Cache, womit die nächste Abfrage wesentlich schneller ausgeführt wird. Sie können den Datenkontext allerdings über die Eigenschaft LoadOptions auch zwingen, Beziehungen sofort bei der Abfrage aufzulösen, indem Sie dieser Eigenschaft ein entsprechend initialisiertes DataLoadOptions-Objekt übergeben. Das soll an dieser Stelle aber nur ein Tipp sein, der nicht weiter behandelt wird.
1104
Abfragen mit LINQ to SQL
19.3.6 Abfragen an die Oberfläche binden Das Binden einer LINQ-to-SQL-Abfrage an die Oberfläche ist eigentlich nicht Besonderes. Dabei nutzen Sie einfach die entsprechende Datenbindungs-Funktionalität (die sich in Windows.Forms, WPF und ASP.NET leider voneinander unterscheidet) zum Binden von Daten in Listenform und binden diese an die entsprechenden Steuerelemente. Für eine ausführliche Besprechung der recht komplexen Datenbindung unter WPF war in Kapitel 14 leider kein Platz (und ist auch hier keiner). Ich will aber zeigen, wie Sie abgefragte Daten recht einfach in einem WPF-ListView darstellen können (damit Sie einmal sehen, was Sie eigentlich abfragen ☺).
12
Erstellen Sie dazu eine WPF-Anwendung (wozu Sie die Projektvorlage AdventureWorksLT-Anwendung verwenden können, die ich bereits genannt habe). Platzieren Sie ein ListView auf dem Fenster und stellen Sie dieses folgendermaßen ein:
13
14
Listing 19.9: ListView zur Darstellung einiger Produktdaten
15
16
17
Im Fenster benötigen Sie eine Referenz auf den Datenkontext. Im Loaded-Ereignis erzeugen Sie diesen und binden die Products-Auflistung (in diesem Beispiel ohne Abfrage) an das ListView:
18
Listing 19.10: Das Hauptfenster der Anwendung
19
public partial class MainWindow : Window { /* Der Datenkontext */ AdventureWorksLTDataContext dataContext;
20
/* Konstruktor. Initialisiert das Fenster. */ public MainWindow() { InitializeComponent(); }
21
/* Erzeugt den Datenkontext */ private void Window_Loaded(object sender, RoutedEventArgs e) { // Erzeugen des Datenkontextes mit dem in der // Anwendungskonfiguration verwalteten Verbindungs-String this.dataContext = new AdventureWorksLTDataContext();
22
// Anbinden der Artikel an das ListView this.productListView.ItemsSource = this.dataContext.Products;
23
} }
1105
Datenbanken mit LINQ to SQL bearbeiten
Und schon zeigt die Anwendung die Produkt-Daten an (Abbildung 19.8). Abbildung 19.8: Das WPF-Beispiel in Aktion
Basierend auf der WPF-Datenbindung und mit LINQ-Abfragen können Sie nun schon sehr flexible Anwendungen erstellen, in denen der Anwender Daten visualisieren kann (inklusive Suchen, Sortieren und Filtern).
DISC
Das leider relativ aufwändige Sortieren nach einem Klick auf eine Spaltenüberschrift behandle ich in dem Projekt »Abfragen an die Oberfläche binden – Mit Sortierung«, das Sie in den Buch-Beispielen zu diesem Abschnitt finden. Beachten Sie, dass es sich dabei nur um eine Lösung von vielen handelt. WPF ist sehr komplex … Das Beispiel ist ein wenig »tricky«. Lesen Sie den Kommentar an der Methode productListViewHeader_Click in MainWindow.xaml.cs.
19.3.7 In der Praxis können Sie meist auf Verknüpfungen verzichten
(Verzicht auf) Verknüpfungen in LINQ to SQL
Verknüpfungen (Joins) sind in LINQ to SQL genauso möglich wie in LINQ. In den meisten Fällen können Sie aber auf eine Verknüpfung verzichten, weil Sie über die Eigenschaften in den Entitäts-Klassen auf die verknüpften Entitäten zugreifen können. Das ist in der Praxis ein sehr hilfreiches Feature, weil die LINQ-Methoden sehr gradlinig sind. So können Sie z. B. die Namen aller Kategorien und deren Artikel in einen anonymen Typ projizieren, ohne dass Sie eine Verknüpfung benötigen: Listing 19.11: Projizieren der Daten zweier Entitätsmengen in einen anonymen Typ ohne Verknüpfung var productInfos = from category in dataContext.ProductCategories from product in category.Products select new { CategoryName = category.Name, ProductName = product.Name }; foreach (var productInfo in productInfos) { Console.WriteLine(productInfo.CategoryName + " - " + productInfo.ProductName); }
1106
Abfragen mit LINQ to SQL
Die resultierende SQL-Anweisung ist sehr effizient: SELECT [t0].[Name] AS [CategoryName], [t1].[Name] AS [ProductName] FROM [SalesLT].[ProductCategory] AS [t0], [SalesLT].[Product] AS [t1] WHERE [t1].[ProductCategoryID] = [t0].[ProductCategoryID]
Sie können ohne Verknüpfung aber auch komplexere Abfragen gestalten. So können Sie z. B. alle Kategorien abfragen, denen Artikel mit einem Preis größer als 3000 $ untergeordnet sind:
12
Listing 19.12: Abfrage mit einer Einschränkung auf die Detail-Entität var highPriceCategories1 = from category in dataContext.ProductCategories where category.Products.Max(product => product.ListPrice > 3000) select category; foreach (var category in highPriceCategories) { Console.WriteLine(category.Name); }
13
14
Eine noch komplexere Abfrage gibt auch die Produkte aus, die teurer sind als 3000 $:
15
Listing 19.13: Abfrage mit einer Einschränkung auf die Detail-Entität mit Ausgabe der Detail-Entitäten var highPriceCategories2 = from category in dataContext.ProductCategories where category.Products.Max(product => product.ListPrice > 3000) select new { Name = category.Name, HighPriceProducts = category.Products.Where( p => p.ListPrice > 3000) }; foreach (var category in highPriceCategories2) { Console.WriteLine(category.Name); foreach (var product in category.HighPriceProducts) { Console.WriteLine(" " + product.Name + ": " + product.ListPrice); } }
16
17
18
19 Abbildung 19.9: Das Ergebnis der Abfrage auf Kategorien mit Produkten, die einen Preis größer als 3000 $ haben
20
21
22
Beachten Sie aber bitte, dass dies nur eine Lösung des gestellten Problems ist. Dieses Problem können Sie z. B. auch über eine Abfrage mit zwei from-Klauseln lösen (die ja in dem Aufruf der SelectMany-Methode resultiert):
23
1107
Datenbanken mit LINQ to SQL bearbeiten
Listing 19.14: Abfrage mit einer Einschränkung auf die Detail-Entitäten mit einem flachen Ergebnis var highPriceCategories3 = from category in dataContext.ProductCategories from product in category.Products.Where(p => p.ListPrice > 3000) select new { CategoryName = category.Name, ProductName = product.Name, ProductListPrice = product.ListPrice }; foreach (var category in highPriceCategories3) { Console.WriteLine(category.CategoryName + " - " + category.ProductName + ": " + category.ProductListPrice); }
Abbildung 19.10: Das Ergebnis der flachen Abfrage auf Kategorien mit Produkten, die einen Preis größer als 3000 $ haben
Diese Lösung ist in der Praxis wahrscheinlich die beste. Sie erzeugt eine sehr effiziente SQL-Anweisung und ermöglicht das Binden der abgefragten Daten an Oberflächenelemente. Auf weitere Beispiele verzichte ich. Probieren Sie LINQ für die Abfrage verknüpfter Daten einfach aus. Haben Sie dabei aber immer die Effizienz der SQL-Anweisung im Sinn.
19.3.8 Gruppierungen LINQ to SQL unterstützt LINQ-Gruppierungen in vollem Maße und setzt diese in äquivalente SQL-Anweisungen um. In vielen Fällen ist eine Gruppierung aber, wie eine Verknüpfung, mit LINQ nicht mehr notwendig, weil Sie das jeweilige Problem auch gut über die Standard-Erweiterungsmethoden lösen können. Dies gilt besonders dann, wenn Sie eine Gruppierung nur verwenden, um über die Gruppen eine Berechnung auszuführen und nur die Objekte in das Ergebnis zu übernehmen, die einer definierten Bedingung entsprechen. So können Sie z. B. nur alle Produkt-Kategorien anfragen, die die mindestens 30 Produkte besitzen: Listing 19.15: Abfragen ohne Gruppierung var largeCategories = from category in dataContext.ProductCategories where category.Products.Count() >= 30 select category;
1108
Abfragen mit LINQ to SQL
19.3.9 Die Effizienz der Abfrage Wenn Sie Daten abfragen, erzeugt LINQ to SQL aus der Abfrage eine SQL-Anweisung und sendet diese zur Datenbank. Wenn Sie nicht projizieren, werden immer alle Felder der entsprechenden Entität abgefragt. Eine solche Abfrage ist sehr ineffizient, besonders wenn Sie nur einige Eigenschaften des Entitäts-Objekts abfragen wollen. Verwenden Sie für reine Abfragen also – besonders bei Entitäts-Objekten mit vielen Eigenschaften – grundsätzlich eine Projektion.
Projektionen sind sehr effizient
Wenn Sie allerdings Daten ändern wollen, müssen Sie dazu die Entitäts-Objekte verwenden. In diesem Fall sollten Sie darauf verzichten, Abfragen unnötigerweise mehrfach auszuführen oder mehrfach auszuwerten. Der Datenkontext fragt diese bei jeder Auswertung des Ergebnisses erneut aus der Datenbank ab. Und das sogar dann, wenn er alle erhaltenen Ergebnisse verwirft, weil die Entitäten bereits im Cache gespeichert sind. Das ist natürlich hochgradig ineffizient.
Abfragen auf Entitäts-Objekte sind nicht effizient
12
13
14 Den Beweis für diese Aussage finden Sie in Form des Projekts zu diesem Abschnitt in den Buch-Beispielen. In diesem werden das Protokoll des Datenkontextes auf die Konsole gelegt und das Ergebnis einer Abfrage zweimal hintereinander durchlaufen: An der Konsole wird bei jedem Durchgehen des Ergebnisses eine separate SQL-Anweisung ausgegeben. Für den endgültigen Beweis, dass die jeweilige SQL-Anweisung auch zum SQL Server gesendet wird, habe ich diesen vor dem zweiten Durchgehen ausgeschaltet. Das Ergebnis war wie erwartet eine SqlException. Das Problem können Sie lösen, indem Sie das Ergebnis einer Abfrage möglichst nicht mehrfach auswerten. Sollte dies nötig sein, erzeugen Sie über ToArray aus dem Ergebnis ein Array oder über ToList eine Auflistung. Diese können Sie beliebig auswerten, ohne eine erneute Abfrage der Daten zu bewirken.
DISC
15
16
17
INFO
19.3.10 Verzögertes Laden
18
Das Effizienz-Problem können Sie zudem lösen, indem Sie im Datenmodell-Designer für Eigenschaften, die nur selten abgefragt werden, die Einstellung DELAY LOADED auf true setzen. Diese Einstellung bewirkt, dass die entsprechenden Felder bei einer Abfrage zunächst nicht in die SQL-Anweisung übernommen werden. Erst wenn Sie diese lesen, wird deren Wert mit einer separaten SQL-Anweisung abgefragt. Das kann – besonders bei Feldern mit binären Daten (wie Bildern) – die Performance erheblich steigern und die Netzwerkbelastung (bei einem entfernten Datenbankserver) erheblich reduzieren.
19 20
Als Beispiel habe ich in dem Projekt zu diesem Abschnitt in der Product-Entität die Einstellung DELAY LOADED für die Eigenschaften SelStartDate, SelEndDate, DiscontinuedDate, ThumbNailPhoto, ThumbnailPhotoFileName, RowGuid und ModifiedDate auf true eingestellt. Das folgende Listing fragt die Artikel der Kategorie 35 ab. Es setzt das Protokoll des Datenkontextes auf die Konsole:
21
22 Listing 19.16: Demo für das verzögerte Laden // Erzeugen des Datenkontextes mit dem in der // Anwendungskonfiguration verwalteten Verbindungs-String AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext();
23
1109
Datenbanken mit LINQ to SQL bearbeiten
// Log auf die Konsole setzen dataContext.Log = Console.Out; // Alle Produkte der Kategorie 35 abfragen var products = from product in dataContext.Products where product.ProductCategoryID == 35 select product; // Das Ergebnis einmal durchgehen, dabei das letzte // Product referenzieren Product lastProduct = null; foreach (var product in products) { lastProduct = product; // Name ausgeben Console.WriteLine("Name: " + product.Name); Console.WriteLine(); // Das Modifizierdatum ausgeben, das verzögert geladen wird Console.WriteLine("Modifizierdatum: " + product.ModifiedDate); Console.WriteLine(); } // Noch einmal die Daten des letzten Produkts ausgeben Console.WriteLine("Letztes Produkt noch einmal:"); Console.WriteLine("Name: " + lastProduct.Name); Console.WriteLine("Modifizierdatum: " + lastProduct.ModifiedDate);
Das Ergebnis (Abbildung 19.11) zeigt deutlich, dass die erste Abfrage nur die Felder enthält, die nicht verzögert geladen werden. Bei der jeweils ersten Abfrage der ModifiedDate-Eigenschaft wird eine separate SQL-Anweisung abgesetzt. Bei der nochmaligen Abfrage ganz unten (für das letzte Produkt) wird aber keine Abfrage mehr erzeugt. Abbildung 19.11: Das Ergebnis der Demo für das verzögerte Laden
1110
Abfragen mit LINQ to SQL
19.3.11 Der Cache Wie ich ja bereits erwähnt habe, hält der Datenkontext alle abgefragten Entitäten in seinem Cache. Wird die entsprechende Entität wiederholt abgefragt, wird das Objekt aus dem Cache zurückgegeben. Diese gilt sowohl für das einfache Lesen eines Objekts, z. B. über die Eigenschaft eines Master-Objekts, das Detail-Objekte referenziert, als auch für die Abfrage mit LINQ: Wenn Sie z. B. komplette Produkte abfragen, zwischenzeitlich deren Preis in der Datenbank geändert wird und Sie die Produkte noch einmal abfragen, erhalten Sie bei der zweiten Abfrage dasselbe Ergebnis wie bei der ersten. Das können Sie sehr einfach mit dem folgenden Programm nachvollziehen:
Wiederholte Abfragen ergeben prinzipiell dasselbe Ergebnis
12
13
Listing 19.17: Demo für das Caching // Erzeugen des Datenkontextes mit dem in der // Anwendungskonfiguration verwalteten Verbindungs-String AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext();
14
// Abfrage der Produkte var products1 = dataContext.Products .Where(p => p.ProductCategoryID == 35).OrderBy(p => p.ProductID); foreach (var product in products1) { Console.WriteLine(product.Name + ": " + product.ListPrice); }
15
16
Console.WriteLine(); Console.Write("Ändern Sie nun den Preis von einem der Artikel " + "der Kategorie 35 "); Console.ReadLine(); Console.WriteLine();
17
// Erneute Abfrage der gesamten Produkte var products2 = dataContext.Products .Where(p => p.ProductCategoryID == 35).OrderBy(p => p.ProductID); foreach (var product in products2) { Console.WriteLine(product.Name + ": " + product.ListPrice); }
18
19
Wenn Sie, während das Programm anhält, die Products-Tabelle über den ServerExplorer oder das SQL Server Management Studio öffnen, von einem Artikel der Kategorie 35 den Preis ändern und das Programm dann weiter ausführen (womit die zweite Abfrage ausgeführt wird), werden Sie sehen, dass das Programm noch den alten Preis anzeigt.
20
Und das sogar, obwohl der Datenkontext die Abfrage noch einmal zum SQL Server gesendet hat. Er hat aber alle im Ergebnis enthaltenen Produkte ignoriert, weil er diese (bezogen auf den Primärschlüssel) bereits im Cache hält. Ein wenig verwirrend (und gut zu wissen) ist die Tatsache, dass Abfragen mit Projektionen in anonyme (oder andere) Typen davon nicht betroffen sind. Wenn Sie das Programm erweitern, können Sie dies erkennen:
21 Abfragen mit Projektionen sind davon nicht betroffen
22
Listing 19.18: Um Projektionen in einen anonymen Typ erweitertes Programm
23
// Erneute Abfrage mit einer Projektion Console.WriteLine(); Console.WriteLine("Das Ergebnis einer Projektion in einen " + "anonymen Typ:");
1111
Datenbanken mit LINQ to SQL bearbeiten
var products3 = from p in dataContext.Products where p.ProductCategoryID == 35 orderby p.ProductID select new { Name = p.Name, ListPrice = p.ListPrice }; foreach (var product in products3) { Console.WriteLine(product.Name + ": " + product.ListPrice); } Console.WriteLine(); Console.Write("Ändern Sie nun den Preis von demselben Artikel " + "noch einmal "); Console.ReadLine(); Console.WriteLine(); // Erneute Abfrage mit einer Projektion Console.WriteLine("Das Ergebnis einer Projektion in einen " + "anonymen Typ:"); var products4 = from p in dataContext.Products where p.ProductCategoryID == 35 orderby p.ProductID select new { Name = p.Name, ListPrice = p.ListPrice }; foreach (var product in products4) { Console.WriteLine(product.Name + ": " + product.ListPrice); }
Abbildung 19.12: Das Ergebnis des Caching-Beispiels, nachdem der Preis eines Artikels zweimal geändert wurde
Projektionen werden nicht in den Cache geschrieben
INFO
Wie Sie in Abbildung 19.12 sehen, ergab die zweite Abfrage, die nach dem Ändern des Preises eines der Produkte ausgeführt wurde, den alten Preis. Die dritte Abfrage – mit einer Projektion – ergab aber den aktuellen Preis in der Datenbank. Und auch die vierte Abfrage – ebenfalls mit einer Projektion – ergab nach einer erneuten Änderung des Preises den aktuellen Preis. Daraus ergibt sich, dass nur Instanzen von Entitäts-Klassen zwischengespeichert werden, keine Instanzen anderer Typen. Das hat für Sie schon einmal die Bedeutung, dass Sie eine Projektion verwenden können, wenn Sie die aktuellen Datenbankdaten abfragen wollen. Die Entitäts-Objekte verwalten dann aber immer noch die alten Daten!
Vorteile des Caching Caching sorgt für Datenkonsistenz
1112
In der Praxis hat das Caching von Entitäts-Objekten zwei Vorteile: Zum einen werden Daten nach dem ersten Lesen wesentlich schneller ermittelt, wenn Sie über eine Auflistungs-Eigenschaft auf die Daten zugreifen. Zum anderen sind die einmal abge-
Bearbeiten von Daten über das Objektmodell
fragten Objekte konsistent, weil zwischenzeitliche Änderungen in der Datenbank nicht mehr reflektiert werden. Das ist deswegen wichtig, da Entitäts-Objekte auch geändert und die Änderung in die Datenbank zurückgeschrieben werden können (mit anderen Typen ist das nicht möglich, auch wenn diese in einer Projektion abgefragt werden). Auf diese Weise bleiben die Änderungen der jeweiligen Anwendung erhalten, auch wenn die Entität in der Datenbank zwischenzeitlich geändert wird.
12
Mögliche Caching-Probleme Das Caching kann in der Praxis natürlich in einer Mehrbenutzer-Umgebung auch zu Problemen führen. Zum einen kann es sein, dass das Programm immer mit den aktuellen Daten aus der Datenbank arbeiten soll. Zum andern kann es vorkommen, dass zu viele Daten gelesen wurden und der Speicher überzulaufen droht. Bisher habe ich aber noch nicht herausgefunden, was dann passiert. Das Lesen mehrerer 100.000 Datensätze war in meinem Performance-Test auf jeden Fall kein Problem. Wahrscheinlich wird der Datenkontext in diesem Fall die älteren Objekte, die keine gespeicherten Änderungen aufweisen, aus dem Speicher entfernen. Dokumentiert ist das aber scheinbar nicht (zumindest habe ich nichts dazu gefunden …).
Caching kann auch zu Problemen führen
13
14
Lösen von Caching-Problemen
15
Sie können beide Probleme lösen, indem Sie alle Änderungen speichern und einfach einen neuen, frischen Datenkontext erzeugen. In meinen Augen ist dies die beste Lösung, wenn Sie hauptsächlich mit Entitäts-Objekten arbeiten.
16
Das erste Problem können Sie auch noch anders lösen: ■
■
Sie rufen die Refresh-Methode des Datenkontext-Objekts auf, der Sie ein Entitäts-Objekt oder eine Entitätsmenge übergeben. Refresh liest die aktuellen Daten aus der Datenbank in das Objekt bzw. die Objekte. Sie stellen den Datenkontext über false in der Eigenschaft ObjectTrackingEnabled so ein, dass er Objekte nicht nachverfolgt. Damit haben Sie bei der erneuten Abfrage immer die aktuellen Objekte. Sie müssen dabei allerdings beachten, dass:
17
18
Sie dann ggf. (und in größeren Projekten mit sehr großer Wahrscheinlichkeit) für ein und dieselbe Entität aus der Datenbank mit unterschiedlichen Objekten arbeiten. Das kann in der Praxis enorme Probleme verursachen; ■ dann bei abgeschalteter Objekt-Verfolgung der Datenkontext beim Speichern eines Objekts nicht mehr nachvollziehen kann, ob dieses in der Zwischenzeit von einem anderen Benutzer geändert wurde (siehe »Behandlung von Konflikten beim Aktualisieren«, Seite 1128). Mit der Einstellung von ObjectTrackingEnabled auf false würde ich in der Praxis sehr vorsichtig umgehen. Setzen Sie lieber Projektionen ein, wenn Sie die aktuell in der Datenbank gespeicherten Daten explizit abfragen wollen, oder erzeugen Sie einfach einen neuen Datenkontext (natürlich, nachdem Sie die aktuellen Änderungen gespeichert oder überprüft haben, ob aktuell noch Änderungen anstehen, was über die GetChangeSet-Methode möglich ist). ■
19.4
19 20
21
22
Bearbeiten von Daten über das Objektmodell
23
Über das LINQ-to-SQL-Objektmodell können Sie Entitäten ändern, löschen und anfügen. Dies ist zunächst recht einfach, bietet aber in der Praxis auch einige Tücken.
1113
Datenbanken mit LINQ to SQL bearbeiten
19.4.1 Das Anfügen, Ändern und Löschen prinzipiell einfach
Anfügen, Ändern und Löschen von Entitäten
Das Anfügen, Ändern, und Löschen von Entitäten geschieht auf eine sehr gradlinige Art über Methoden des Datenkontextes. Über InsertOnSubmit oder InsertAllOnSubmit fügen Sie neue Entitäts-Objekte an. Zum Ändern einer Entität überschreiben Sie die Daten des jeweiligen Objekts. Zum Löschen rufen Sie die Methode DeleteOnSubmit auf, der Sie ein einzelnes Entitäts-Objekt übergeben, oder die Methode DeleteAllOnSubmit, der Sie eine Auflistung von Entitäts-Objekten übergeben. Ihre Änderungen werden aber zunächst nur lokal im Datenkontext (in einem Änderungs-Set) gespeichert. Erst wenn Sie die SubmitChanges-Methode aufrufen, werden die aktuell anstehenden Änderungen in einer der Datenbank entsprechenden korrekten Reihenfolge übertragen. Dabei spielt es prinzipiell keine Rolle, in welcher Reihenfolge Sie die Änderungen definiert haben. LINQ to SQL ändert die Reihenfolge so, dass diese in der Datenbank logisch ist. So können Sie z. B. im Datenkontext erst ein Produkt-Modell löschen und danach dessen Produkte. Bei SubmitChanges werden aber erst die Produkte gelöscht und dann das Modell.
Anfügen Neue Entitäten fügen Sie über InsertOnSubmit hinzu
Zum Anfügen von Entitäten erzeugen Sie neue Instanzen der entsprechenden Klasse und initialisieren diese. Danach muss das neue Objekt dem Datenkontext bekannt gemacht werden. Das geschieht einmal über die entsprechende EntitySet-Eigenschaft des Datenkontextes und deren InsertOnSubmit-Methode: Listing 19.19: Anfügen einer neuen Entität in den Datenkontext AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext(); // Neues Artikel-Modell erzeugen und anfügen ProductModel newProductModel = new ProductModel { Name = "Radon MCS Carbon 8.0", ModifiedDate = DateTime.Now }; dataContext.ProductModels.InsertOnSubmit(newProductModel);
Dabei müssen Sie drauf achten, dass alle in der Datenbank erwarteten Felder korrekt gefüllt sind (was im Beispiel der Fall ist). Im nächsten Beispiel, bei dem ein Produkt hinzugefügt wird, müssen schon mehrere Eigenschaften gefüllt werden. Dazu gehören auch die Beziehungen zu den Entitäten ProductModel und ProductCategory: Listing 19.20: Anfügen einer neuen Entität, die Beziehungen zu anderen Entitäten hat // Die Kategorie für den neuen Artikel ermitteln ProductCategory mountainBikeCategory = dataContext.ProductCategories.Single( pc => pc.Name == "Mountain Bikes"); // Neuen Artikel erzeugen und anfügen Product newProduct1 = new Product() { Name = "Radon MCS Carbon 8.0, 17", ProductModel = newProductModel, ProductCategory = mountainBikeCategory, StandardCost = 2800,
1114
Bearbeiten von Daten über das Objektmodell
ListPrice = 3106, ProductNumber = "RD-MCS-CARB-8-17", SellStartDate = DateTime.Now, ModifiedDate = DateTime.Now }; Products.InsertOnSubmit(newProduct1);
Wenn Sie Instanzen von Detail-Entitäten erzeugen und eine Referenz auf die MasterEntität besitzen, können Sie die neuen Detail-Entitäten auch der entsprechenden Auflistung in der Master-Entität hinzufügen:
12
Listing 19.21: Anfügen einer neuen Detail-Entität über die entsprechende Eigenschaft der MasterEntität
13
// Einen weiteren Artikel erzeugen und dieses Mal der // Products-Auflistung der Mountainbike-Kategorie anfügen Product newProduct2 = new Product() { Name = "Radon MCS Carbon 8.0, 19", ProductModel = newProductModel, StandardCost = 2800, ListPrice = 3106, ProductNumber = "RD-MCS-CARB-8-19", SellStartDate = DateTime.Now, ModifiedDate = DateTime.Now }; mountainBikeCategory.Products.Add(newProduct2);
14
15
16
Der Datenkontext kümmert sich automatisch um die korrekte Auflösung der Beziehungen. Bisher wurden die neuen Entitäten aber noch nicht in die Datenbank geschrieben. Sie werden lediglich im Datenkontext verwaltet. Wenn Sie diese allerdings auslesen wollen, werden Sie mit einer etwas eigenartigen Gegebenheit konfrontiert: Über die jeweilige EntitySet-Eigenschaft des Datenkontextes können Sie diese nicht auslesen. Wenn Sie diese durchgehen, erhalten Sie keine Referenz auf die neuen Entitäten:
Die neuen Entitäten werden zunächst lediglich im Datenkontext verwaltet
17
18
Listing 19.22: Durchgehen einer EntitySet-Eigenschaft, um eine hinzugefügte Entität zu ermitteln
19
bool found = false; foreach (var product in dataContext.Products) { if (product == newProduct1) { found = true; break; } } Console.WriteLine("Das erste neue Produkt " + "wurde " + (found ? null : "NICHT ") + "in den Produkten des Datenkontextes gefunden");
20
21
Verwaltung im
Das Programm gibt aus, dass das neue Produkt nicht gefunden wurde. Das mag auf irgendeine Weise logisch sein (schließlich gehören diese Entitäten noch nicht zur Datenbank), hat aber die Konsequenz, dass Sie im Programm ein wenig aufpassen müssen, wenn Sie die Entitäten durchgehen, bevor die Änderungen in die Datenbank übertragen wurden.
Die Verwaltung im Datenkontext ist ein wenig inkonsistent
22
23
Besonders eigenartig wird dies allerdings, wenn Sie die Produkte der Kategorie durchsuchen. In diesem Fall finden Sie das neue Produkt. Und das gilt auch für das
1115
Datenbanken mit LINQ to SQL bearbeiten
erste Produkt, das im Programm nicht der Products-Auflistung des ProductCategoryObjekts hinzugefügt wurde: Listing 19.23: Suche nach der neuen Entität über die referenzierende Eigenschaft in der Master-Entität found = false; foreach (var product in mountainBikeCategory.Products) { if (product == newProduct1) { found = true; break; } } Console.WriteLine("Das erste neue Produkt " + "wurde " + (found ? null : "NICHT ") + "in den Produkten der Kategorie gefunden");
Abbildung 19.13 zeigt die Beispielanwendung dieses Abschnitts, bei der auch nach dem zweiten Produkt gesucht und zudem die in der Products-Auflistung des neuen ProductModel-Objekts verwalteten Product- Objekte ausgegeben wurden. Abbildung 19.13: Die Beispielanwendung zeigt die Inkonsistenzen des Datenkontextes
Das mag alles logisch sein, in der Praxis müssen Sie aber auf diese Eigenarten achten. SubmitChanges überträgt die Änderungen in die Datenbank
Um die Änderungen zu speichern, rufen Sie schließlich die SubmitChanges-Methode des Datenkontextes auf: dataContext.SubmitChanges();
In der Praxis müssen Sie hier natürlich eine Fehlerbehandlung vorsehen. Darauf gehe ich im Abschnitt »Fehlerbehandlung beim Anfügen, Ändern und Löschen« (Seite 1119) ein.
INFO
Nach dem Speichern sind die Referenzen im Datenkontext konsistent. Wenn Sie über den Datenkontext direkt die Produkte abfragen, werden auch die hinzugefügten aufgelistet. Ob das in der Praxis allerdings immer so ist, müssen Sie im Einzelfall überprüfen. Ich hatte in dem Bereich mit verschiedenen O/R-Mappern und auch mit LINQ to SQL so einige Probleme. Im Notfall hilft die Aktualisierung einer Entitätsmenge über die Refresh-Methode des DataContext-Objekts.
Anfügen (und Ändern) in der Praxis Das Anfügen (und Ändern) ist in der Praxis leider nicht so einfach. Das Problem ist, dass eine Datenbanktabelle normalerweise mit so einigen Regeln versehen ist. Die Product-Tabelle erzwingt z. B.:
1116
Bearbeiten von Daten über das Objektmodell
■
dass für einen Product-Datensatz dessen ProductCategory und ProductModel angegeben ist, ■ dass die Artikelnummer (im Feld ProductNumber) nicht mehrfach angegeben ist, ■ dass die Datumsfelder gefüllt sind ■ und dass das Feld rowguid (das den Datensatz ein-eindeutig identifiziert) eine eindeutige GUID speichert (dieses Feld habe ich bei der Nachbearbeitung des Datenbankmodells entsprechend angepasst, siehe Seite 1090). Da das Tool zur Erzeugung des Datenkontextes und der Entitäts-Klassen die Regeln der Datenbanktabelle leider nicht in die Klasse reflektiert (was in meinen Augen möglich wäre), können Sie (auch für andere Fälle) ungültige Werte in das EntitätsObjekt schreiben und erhalten dann erst beim Aufruf von SubmitChanges eine Ausnahme, die vom Datenbanksystem stammt. In der Praxis bedeutet dies, dass Sie die Datenbank kennen und wissen müssen, welche Tabellenfelder mit welchen Wertebereichen beschrieben werden müssen, damit ein Speichern der Änderungen möglich ist. Sie können aber auch ganz einfach herausfinden, welche Felder in einer Tabelle mit welchen Werten gefüllt sein müssen, damit ein Datensatz gespeichert werden kann: Öffnen Sie die Tabelle dazu über den Server-Explorer in Visual Studio (über den Befehl TABELLENDATEN ANZEIGEN im Kontextmenü des Tabelleneintrags) und versuchen Sie dort einen neuen Datensatz anzulegen, indem Sie zunächst nur ein Feld mit einem Wert versehen. Beachten Sie, dass in vielen Datenbanktabellen das Primärschlüssel-Feld automatisch gefüllt wird. Wenn Sie dann versuchen, aus dem Datensatz herauszugehen, meldet das Datenbanksystem, welche Fehler beim Speichern aufgetreten sind. So können Sie sukzessive ermitteln, welche Felder zum Speichern notwendig sind. Den damit hinzugefügten Datensatz können Sie über Visual Studio natürlich auch wieder löschen. Dazu müssen Sie die Abfrage allerdings einmal über den Symbolleisten-Schalter mit dem Ausrufezeichen aktualisieren.
12
13
14
15 TIPP
16
17
18
Leider funktioniert diese Technik nicht für Felder, die zwar keine Nullwerte zulassen, aber in der Datenbank beim Anfügen eines Datensatzes mit einem Defaultwert versehen werden, wenn sie leer sind. Um diese Felder herauszufinden, müssen Sie sich wohl die Tabellendefinition anschauen (über den Server Explorer oder über das SQL Server Management Studio).
19 20
Ein großes Problem sind auch Datumsfelder, die im SQL-Server ein Datum kleiner dem 1.1.1753 12:00:00 nicht zulassen. Wenn ein solches Feld keine Nullwerte zulässt und Sie die entsprechenden DateTime-Eigenschaften im Entitäts-Objekt nicht mit einem Datum versehen, verwalten diese das Datum 01.01.0001 00:00:00. Beim Versuch, die Änderungen (über SubmitChanges) zu speichern, erhalten Sie eine SqlTypeException mit der Meldung »SqlDateTime-Überlauf; muss zwischen 1/1/1753 12:00:00 AM und 12/31/9999 11:59:59 PM liegen.«. Leider sagt diese dumme Meldung nicht, welches Feld damit gemeint ist.
HALT
21
22
Dieses Problem betrifft im Besonderen Felder, die zwar keine Nullwerte zulassen, aber von der Datenbank beim Anfügen mit einem Defaultwert versehen werden. Für die AdventureWorksLT-Datenbank ist dies z. B. der Fall für das Feld ModifiedDate der ProductCategory-Tabelle der Fall. In einer SQL-Anweisung kann dieses Feld weggelas-
23
1117
Datenbanken mit LINQ to SQL bearbeiten
sen werden. Die Datenbank stellt automatisch das aktuelle Datum ein. Bei der Arbeit mit LINQ to SQL wird aber versucht, das minimale Datum zu schreiben, wenn Sie die DateTime-Eigenschaft der entsprechenden Eigenschaft nicht mit einem Datum versehen. Auch für solche (Datums-)Felder, die in der Datenbank zwar eigentlich mit einem Defaultwert versehen werden, müssen Sie also einen passenden Wert angeben.
Ändern Das Ändern von Entitäten ist im Prinzip sehr einfach: Sie schreiben neue Werte in die Eigenschaften des entsprechenden Objekts. Der Datenkontext verwaltet einen internen Status für die einzelnen Objekte, der aussagt, dass das Objekt geändert wurde. Wenn Sie dann die SubmitChanges-Methode aufrufen, werden alle Änderungen in die Datenbank geschrieben: Listing 19.24: Ändern von Entitäten newProduct1.ListPrice += 100; newProduct2.ListPrice += 100; // Die Änderungen in die Datenbank übertragen dataContext.SubmitChanges();
Dabei können natürlich dieselben Fehler auftreten wie beim Einfügen von Objekten (weswegen Sie eine Fehlerbehandlung vorsehen sollten).
Löschen DeleteOnSubmit löscht eine Entität aus dem Datenkontext
Das Löschen ist prinzipiell genauso einfach wie das Ändern. Dazu rufen Sie die DeleteOnSubmit- oder die DeleteAllOnSubmit-Methode auf, der Sie eine oder mehrere Entitäten übergeben. Gelöscht werden die Datensätze auch wieder erst, wenn Sie SubmitChanges aufrufen: Listing 19.25: Löschen von Entitäten dataContext.Products.DeleteOnSubmit(newProduct1); dataContext.Products.DeleteOnSubmit(newProduct2); dataContext.ProductModels.DeleteOnSubmit(newProductModel); dataContext.SubmitChanges();
Beim Löschen treten normalerweise dann Fehler auf, wenn Sie eine Entität löschen, die noch in anderen Entitäten des Datenbanksystems referenziert wird. So könnten Sie z. B. nicht die Mountainbike-Kategorie löschen. Es kann aber sein, dass die Datenbank so definiert ist, dass sie ein kaskadierendes Löschen ermöglicht. In diesem Fall werden beim Löschen einer Master-Entität automatisch auch alle DetailEntitäten gelöscht. Da dieses Verhalten in der Praxis aber sehr gefährlich ist, wird eine Datenbank nur sehr selten so definiert. Wenn Sie allerdings in einem Rutsch die Master-Entität und alle Detail-Entitäten löschen (wie in Listing 19.25), tritt natürlich kein Fehler auf. Sie müssen sich dabei noch nicht einmal an die korrekte Reihenfolge halten (solange Sie SubmitChanges nicht aufrufen). So könnten Sie z. B. erst das Artikel-Modell und dann die Artikel löschen. Beim Aufruf von DeleteOnSubmit oder DeleteAllOnSubmit werden die entsprechenden Entitäten im Datenkontext lediglich als zu löschend gekennzeichnet (was dadurch geschieht, dass diese im Änderungsset in der Auflistung der zu löschenden Entitäten referenziert werden). Wenn Sie die Entitätsmengen des Datenkontextes vor
1118
Bearbeiten von Daten über das Objektmodell
Aufruf von SubmitChanges durchgehen, sind alle Objekte noch vorhanden. Erst nach dem Aufruf von SubmitChanges sind diese auch aus dem Datenkontext gelöscht. Dieses Verhalten müssen Sie natürlich immer im Auge behalten.
Wann sollten Sie SubmitChanges aufrufen? Um nicht allzu viele Probleme damit zu haben, dass der Datenkontext vor dem Aufruf von SubmitChanges inkonsistent sein kann, sollten Sie SubmitChanges relativ früh aufrufen. Das muss nicht unbedingt nach jedem Hinzufügen, Ändern oder Löschen sein. Es macht durchaus Sinn, mehrere Entitäten hinzuzufügen, zu ändern oder zu löschen und SubmitChanges für eine Gruppe von Änderungen aufzurufen. Halten Sie diese aber logisch zusammen. Ein gutes Beispiel dafür ist das Löschen des neuen Produktmodells und der zugehörigen Produkte in Listing 19.25.
Rufen Sie SubmitChanges früh auf
12
13
Ich würde vor allen Dingen das Hinzufügen, Ändern und Löschen nicht mischen, sondern für jede dieser Aktionen SubmitChanges separat aufrufen. Die Auswertung von Fehlern wird sehr schwierig, wenn sehr viele Änderungs-Aktionen gleichzeitig ausgeführt werden.
14
Mit SubmitChanges können Sie so etwas wie eine kleine Transaktion implementieren, der Sie SubmitChanges für mehrere zusammengehörige Datenbank-Änderungsaktionen aufrufen. In Wirklichkeit wird ein SubmitChanges-Aufruf auch in einer DatenbankTransaktion gekapselt. Dies bedeutet, dass im Fehlerfall alle anstehenden Änderungen zurückgerollt (bzw. nicht in die Datenbank übernommen) werden. Transaktionen sind für die Praxis sehr hilfreich, für solche Fälle sind aber echte Transaktionen besser geeignet. Diese beschreibe ich ab Seite 1126.
15
16
19.4.2 Fehlerbehandlung beim Anfügen, Ändern und Löschen
17
Beim Anfügen, Aktualisieren und Löschen von Objekten können natürlich dann, wenn LINQ to SQL die Änderungen in die Datenbank schreibt, Fehler auftreten. SubmitChanges wird in diesem Fall eine entsprechende Ausnahme werfen. In den meisten Fällen handelt es sich dabei um Fehler, die dadurch verursacht werden, dass die Änderungen Regeln der Datenbank verletzen. Ein anderer möglicher Fehler wird dadurch verursacht, dass zu aktualisierende Datensätze zwischenzeitlich von einem anderen Benutzer in der Datenbank geändert wurden. Auf diese speziellen Fehler komme ich im Abschnitt »Behandlung von Konflikten beim Aktualisieren« ab Seite 1128 zurück.
18
19
Sie können Ausnahmen, die beim Aufruf von SubmitChanges auftreten, natürlich ganz einfach abfangen. Das ist aber nicht das Problem, das in diesem Abschnitt behandelt werden soll.
20
Das SubmitChanges-Problem
21
SubmitChanges führt dann zu Problemen, wenn eine Änderung nicht in die Datenbank übertragen werden kann. Schlägt SubmitChanges mit einer Ausnahme fehl, wurden alle anstehenden Änderungen nicht ausgeführt. Dazu gehören auch diese, die prinzipiell nicht zu einem Fehler führen würden. Wie bereits gesagt, wird SubmitChanges in einer implizit erzeugten Transaktion ausgeführt, die im Fehlerfall komplett zurückgerollt wird.
SubmitChanges verwirft die anstehenden Änderungen nicht, wenn Fehler auftreten
22
23
Ein DataContext-Objekt verwaltet alle anstehenden Änderungen in einem so genannten Änderungsset (Changeset). Dieses bleibt komplett bestehen, wenn eine Ausnahme auftritt. Sie können die aktuell anstehenden Änderungen über die GetChangeSet-
1119
Datenbanken mit LINQ to SQL bearbeiten
Methode abfragen. Diese liefert eine Referenz auf ein ChangeSet-Objekt zurück, über dessen Auflistungen Inserted, Updated und Deleted Sie die Objekte auslesen können, die eingefügt, aktualisiert bzw. gelöscht werden sollen. Das Problem ist nun, dass, nachdem SubmitChanges einmal mit einer Ausnahme wegen eines Datenbankfehlers gescheitert ist, jeder weitere Aufruf von SubmitChanges in der Regel denselben Fehler verursacht. Die alten, anstehenden Änderungen sind ja noch im Änderungsset enthalten. Dieses Verhalten ist natürlich prinzipiell in Ordnung. Schließlich wollen Sie nicht alle Änderungen verlieren, nur weil vielleicht eine Eigenschaft in einem EntitätsObjekt einen für die Datenbank ungültigen Wert aufweist. Das Problem ist aber, dass Sie zwar das Änderungsset durchgehen können, aber weder abfragen können, welches Objekt welche Fehler verursacht hat, noch anstehende Änderungen zurücknehmen können. Das ist in der Praxis sehr ärgerlich. Angenommen, Sie ermöglichen dem Benutzer die Änderung von Product-Entitäten und haben keine spezielle Eingabevalidierung vorgesehen. Der Benutzer ändert ein Produkt, gibt dabei aber ungültige Daten ein: Listing 19.26: Ändern eines Produkts mit ungültigen Daten (einer bereits existierenden Produkt-Nummer) // Ein Produkt auslesen Product product = dataContext.Products.Single( p => p.ProductNumber == "FR-R92B-58"); // Die Produktnummer ändern product.ProductNumber = "FR-R92R-58"; // Versuchen, zu speichern try { dataContext.SubmitChanges(); } catch (SqlException ex) { Console.WriteLine(ex.Message); }
Beim Aufruf von SubmitChanges wird eine SqlException mit der Fehlermeldung generiert, dass eine UNIQUE-Key-Einschränkung verletzt wurde (weil die Produktnummer bereits einem anderen Produkt vergeben wurde und das ProductNumber-Feld in der Datenbank eindeutig indiziert ist). Sie zeigen dem Anwender die (für diesen eher verwirrende) Fehlermeldung an und überlassen diesem die Entscheidung. Er kann natürlich den Fehler dadurch beseitigen, dass er gültige Daten einträgt und noch einmal versucht, zu speichern (was dann möglich sein sollte). Er kann aber auch entscheiden, zu einem anderen Produkt zu wechseln und dessen Daten zu bearbeiten: Listing 19.27: Ändern eines weiteren Produkts, nachdem bei der ersten Änderung ein Fehler aufgetreten ist // Ein anderes Produkt auslesen und dieses ändern product = dataContext.Products.Single( p => p.ProductNumber == "HL-U509-R"); product.ListPrice += .10M;
1120
Bearbeiten von Daten über das Objektmodell
// Versuchen, zu speichern try { dataContext.SubmitChanges(); } catch (SqlException ex) { Console.WriteLine(ex.Message); }
12
Obwohl diese Änderung gültig ist, schlägt SubmitChanges mit derselben Fehlermeldung fehl wie zuvor. Die erste Änderung ist, neben der zweiten, immer noch im Änderungsset enthalten.
13
Dieses Problem ist leider nicht einfach zu lösen. Schön wäre, wenn Sie nach dem Fehlschlagen von SubmitChanges das Änderungsset durchgehen und ermitteln könnten, für welche Objekte Fehler aufgetreten sind. Durchgehen könnte das Änderungsset, aber Sie erhalten leider keine Informationen darüber, ob für das jeweilige Objekt beim Versuch zu speichern Fehler aufgetreten sind:
14
Listing 19.28: Durchgehen des aktuellen Änderungssets
15
ChangeSet changeSet = dataContext.GetChangeSet(); Console.WriteLine(); Console.WriteLine("Anzufügende Entitäten:"); foreach (var insert in changeSet.Inserts) { if (insert is Product) { Console.WriteLine(((Product)insert).ProductNumber); } } Console.WriteLine(); Console.WriteLine("Zu ändernde Entitäten:"); foreach (var update in changeSet.Updates) { if (update is Product) { Console.WriteLine(((Product)update).ProductNumber); } } Console.WriteLine(); Console.WriteLine("Zu löschende Entitäten:"); foreach (var delete in changeSet.Deletes) { if (delete is Product) { Console.WriteLine(((Product)delete).ProductNumber); } }
16
17
18
19 20
Abbildung 19.14: Das Beispielprogramm zeigt die Fehlermeldungen der zwei SubmitChangesAufrufe und das Änderungsset an
1121
21
22
23
Datenbanken mit LINQ to SQL bearbeiten
Leider verwaltet das Änderungsset lediglich Object-Referenzen auf die jeweiligen Entitäts-Objekte. Sie haben keine Möglichkeit, den Fehler herauszufinden, und leider auch keine, die Objekte aus dem Änderungsset zu löschen. Schade. Da hat wohl ein Entwickler nicht wirklich mitgedacht (vor allen Dingen, weil bei den Batch-Aktualisierungen im alten ADO.NET Informationen über die Fehlerursache geliefert werden).
INFO
Die einzige sinnvolle Lösung dieses massiven Problems ist in meinen Augen, im Fehlerfall – wenn Sie den Fehler nicht über eine Bearbeitung der entsprechenden Entitäts-Objekte beseitigen können – einen neuen Datenkontext zu erzeugen. Dies ist auch der Weg, den Microsoft in der LinqDataSource-Komponente geht, die in ASP.NET die Verbindung zwischen einer LINQ-to-SQL-Datenquelle und Steuerelementen herstellt. Die im alten DataContext-Objekt anstehenden Änderungen werden damit automatisch verworfen. Damit müssen Sie natürlich sehr vorsichtig umgehen und sicher sein, dass nicht noch Änderungen von vorhergehenden Aktionen, die noch nicht gespeichert wurden, im DataContext-Objekt vorhanden sind.
19.5
In zusätzlichen partiellen Teilklassen können Sie die EntitätsKlassen erweitern
Business-Objekt-Modelle oder: Die partiellen Klassen und eine Reaktion auf Datenereignisse
Das Tool zur Erzeugung des Datenkontextes und der Entitäts-Klassen legt alle Entitäts-Klassen als partielle Klasse an. Um den Entitäts-Objekten zusätzliche Funktionalität zu vergeben, können Sie weitere Teile der partiellen Klassen erzeugen, in denen Sie beliebig Felder, Eigenschaften und Methoden hinzufügen können. Dass Sie dazu nicht die automatisch generierten Teilklassen verwenden können, ist logisch, denn diese werden mit großer Wahrscheinlichkeit (z. B. nach einer minimalen Änderung des Datenbankmodells) automatisch neu generiert. Ich denke nicht, dass ich dafür ein Beispiel bringen muss. Denken Sie einfach dran, dass Sie zusätzliche Funktionalität in einer separaten Teilklasse implementieren. Für ein richtiges Business-Objekt-Modell nutzen Sie dabei die Möglichkeit, dass Sie in der Einstellung ENTITY NAMESPACE den Namensraum angeben können, in dem die Entitäts-Klassen verwaltet werden. Was aber bei den Entitäts-Klassen (und der Datenkontext-Klasse) darüber hinaus wichtig ist, sind eine ganze Menge partieller Methoden, deren Name mit »On« beginnt. Diese Methoden werden, sofern sie implementiert werden, ihrer Bedeutung nach automatisch aufgerufen und dienen der Erweiterung des Datenbankmodells. Die Datenkontext-Klasse enthält die in Tabelle 19.4 beschriebenen Methoden.
Tabelle 19.4: Die »Erweiterungs«-Methoden des Datenkontextes
1122
Methode
Beschreibung
OnCreated
wird aufgerufen, wenn der Datenkontext erzeugt wird.
InsertEntitätsname
Diese Methode, die wie die weiteren in dieser Tabelle beschriebenen für jede einzelne Entität definiert ist, wird aufgerufen, bevor eine Entität in den Datenkontext eingefügt wird. In dieser Methode können Sie ein komplettes benutzerdefiniertes Einfügen in die Datenbank implementieren, oder Logik implementieren, die ausgeführt werden soll, wenn Entitäten in die Datenbank eingefügt werden.
Business-Objekt-Modelle oder: Die partiellen Klassen und eine Reaktion auf Datenereignisse
Methode
Beschreibung
UpdateEntitätsname
wird aufgerufen, bevor eine Entität im Datenkontext geändert wird.
DeleteEntitätsname
wird aufgerufen, bevor eine Entität im Datenkontext gelöscht wird.
Die Methoden InsertEntitätsname, UpdateEntitätsname und DeleteEntitätsname sind für Business-Objekt-Modelle eingeschränkt interessant (eingeschränkt deswegen, weil die OnValidate-Methode der Entitäts-Klassen für die Fälle besser geeignet ist, in denen Defaultwerte geschrieben oder Eigenschaften gegen andere validiert werden sollen). Sie bilden aber auch eine riesige Falle: Wenn Sie diese Methoden (für eine spezielle Logik, die beim Einfügen, Ändern und Löschen ausgeführt werden soll) definieren, müssen Sie am Ende der Methode die Methoden ExecuteDynamicInsert, ExecuteDynamicUpdate bzw. ExecuteDynamicDelete aufrufen, damit der Datenkontext die Änderungen in die Datenbank schreibt. Machen Sie das nicht, passiert einfach nichts: Die Änderungen verschwinden im Nirwana.
Tabelle 19.4: Die »Erweiterungs«-Methoden des Datenkontextes (Forts.)
12 HALT
13
14
Dass Sie damit die Möglichkeiten haben, das eigene Einfügen, Ändern und Löschen von Entitäten in die Datenbank zu implementieren, ist vielleicht in Sonderfällen ein nettes Feature. Dass Sie aber versehentlich die Aufrufe der Aktualisierungsmethoden weglassen können, ist ein dummer Designfehler des LINQ-to-SQL-Teams (sorry, aber ich habe Stunden nach der Ursache gesucht, auch dank der miesen Dokumentation …).
15
Die Entitäts-Klassen sind mit den in Tabelle 19.5 beschriebenen Methoden ausgestattet.
17
Methode
Beschreibung
OnLoaded
wird aufgerufen, wenn die Daten des Entitäts-Objekts aus der Datenbank gelesen werden.
OnValidate
Diese Methode, die aufgerufen wird, bevor Änderungen in die Datenbank geschrieben werden, ist sehr gut für die Validierung des Objektstatus in Business-ObjektModellen und für das Schreiben von Defaultwerten geeignet.
OnCreated
wird immer dann aufgerufen, wenn eine Instanz der Entitäts-Klasse erzeugt wird. Sie können diese Methode also an Stelle eines Konstruktors verwenden um z. B. Defaultwerte in das Objekt zu schreiben. OnCreated wird auch dann aufgerufen, wenn LINQ to SQL eine Kopie einer Entität anlegt, um die ursprünglichen Werte zu kopieren. Dieses Verhalten sollten Sie im Auge behalten.
16
Tabelle 19.5: Die »Erweiterungs«-Methoden der Entitäts-Klassen
18
19 20
21
OnEigenschaftsname- Diese Methode, die wie die folgende für alle Eigenschaften der Entitäts-Klasse implementiert ist, wird aufgerufen, bevor der Wert einer Eigenschaft geändert wird. In dieChanging ser Methode können Sie eine Validierung des geschriebenen Werts für eine spezifische Eigenschaft implementieren.
22
OnEigenschaftsname- wird aufgerufen, nachdem der Wert einer Eigenschaft geändert wurde. Changed
23
1123
Datenbanken mit LINQ to SQL bearbeiten
Automatisierungen und Validierungen in Business-Objekt-Modellen Die partiellen Methoden erlauben eine Automatisierung und eine Validierung der Daten
Das automatisierte Schreiben von Daten und das Validieren der Daten von Objekten ist in Business-Objekt-Modellen (Modelle, die die in einer Geschäftsanwendung zur Verwaltung der Daten implementierten Klassen abbilden und die in mehreren Anwendungen eingesetzt werden) sehr wichtig. Die partiellen Methoden der Datenkontext-Klasse und der Entitäts-Klassen erlauben zwar keine wirklich professionelle Validierung (die mit externen, z. B. in XML definierten Regel arbeiten würde, die einfach geändert werden können). Die Möglichkeiten sind für kleine bis mittelgroße Anwendungen aber ausreichend. So können Sie in dem Klassenmodell der AdventureWorksLT-Datenbank z. B. sehr einfach dafür sorgen, dass bei jedem Hinzufügen, Ändern und Löschen von ProductObjekten eine spezielle Logik ausgeführt wird, indem Sie in der dazu bereits vorgesehenen Datei AdventureWorksLT.cs die InsertProduct, die UpdateProduct- und die DeleteProduct-Methode definieren. Diese Datei wird bei einer Neuerzeugung des Datenkontextes von dem benutzerdefinierten Tool in Visual Studio nicht überschrieben.
TIPP
Nutzen Sie dabei IntelliSense: Schreiben Sie in Ihrem Teil der partiellen Klasse »partial« und betätigen Sie dann die Leertaste: IntelliSense zeigt die partiellen Methoden an, die Sie definieren können. Listing 19.29: Automatisiertes Schreiben von Eigenschaftswerten als Teil einer Business-Logik partial class AdventureWorksLTDataContext { partial void InsertProduct(Product instance) { /* Spezielle Logik, die beim Einfügen einer Product* Entität in die Datenbank ausgeführt werden soll */ ... this.ExecuteDynamicInsert(instance); } partial void UpdateProduct(Product instance) { /* Spezielle Logik, die beim Ändern einer Product* Entität in der Datenbank ausgeführt werden soll */ ... this.ExecuteDynamicUpdate(instance); } partial void DeleteProduct(Product instance) { /* Spezielle Logik, die beim Löschen einer Product* Entität in der Datenbank ausgeführt werden soll */ ... this.ExecuteDynamicDelete(instance); } }
HALT
1124
Beachten Sie den expliziten Aufruf der ExecuteDynamicInsert- und der ExecuteDynamicUpdate-Methode, die dafür sorgen, dass die Änderungen in die Datenbank geschrieben werden. Achten Sie auch darauf, dass Sie die richtige Methode zum dynamischen Einfügen, Ändern oder Löschen aufrufen!
Business-Objekt-Modelle oder: Die partiellen Klassen und eine Reaktion auf Datenereignisse
Ich denke, das Definieren der Datenkontext-Methoden wird in der Praxis nur selten notwendig sein. Wesentlich häufiger werden die partiellen Methoden der EntitätsKlassen definiert. So können Sie z. B. in den Methoden OnStandardCostChanging und OnListPriceChanging der Product-Klasse die geschriebenen Preise überprüfen: Listing 19.30: Überprüfen von Eigenschaften beim Schreiben
12
partial void OnStandardCostChanging(decimal value) { if (value < 0) { throw new Exception("Die Standardkosten dürfen " + "nicht kleiner als 0 sein"); } }
13
14
partial void OnListPriceChanging(decimal value) { if (value < 0) { throw new Exception("Der Listenpreis darf nicht " + "kleiner als 0 sein"); } }
15
16
Diese Methoden werden aufgerufen, wenn die Eigenschaften beschrieben werden. Die Methode OnValidate erlaubt das Definieren von Defaultwerten und die Überprüfung mehrerer Eigenschaften gegeneinander. Hier können Sie z. B. sicherstellen, dass die Standardkosten kleiner sind als der Listenpreis, und die Eigenschaft ModifiedDate bei jedem Hinzufügen und jeder Änderung auf das aktuelle Datum setzen. Dazu wäre es sinnvoll, die Eigenschaft im Datenmodelldesigner als schreibgeschützt zu kennzeichnen. Den Wert können Sie über das private Feld schreiben, das so benannt ist wie die Eigenschaft, mit einem Unterstrich am Anfang.
17
18
Das Argument action der OnValidate-Methode gibt Auskunft über die ausgeführte Aktion:
19
Listing 19.31: Validierung mehrerer Eigenschaften gegeneinander und automatisches Schreiben von Eigenschaftswerten partial void OnValidate(System.Data.Linq.ChangeAction action) { switch (action) { case System.Data.Linq.ChangeAction.Insert: case System.Data.Linq.ChangeAction.Update: // Validierung ausführen if (this.StandardCost >= this.ListPrice) { throw new Exception("Die Standardkosten müssen " + "kleiner sein als der Listenpreis"); }
20
21
22
// Das Modifzierdatum setzen this._ModifiedDate = DateTime.Now; break;
23
case System.Data.Linq.ChangeAction.Delete: break;
1125
Datenbanken mit LINQ to SQL bearbeiten
case System.Data.Linq.ChangeAction.None: break; } }
Validate wird aufgerufen, bevor der Datenkontext die Änderungen in die Datenbank schreibt (also beim Aufruf von SubmitChanges).
Beim Hinzufügen eines Produkts (und beim Ändern) müssen (bzw. können) Sie nun das Modifizierdatum nicht mehr angeben. Außerdem werden Ihre Validierungen berücksichtigt. Damit können Sie (mit ein wenig Vorsicht) schon ein recht gutes Business-ObjektModell mit definierten Regeln aufbauen. Die geworfenen Ausnahmen müssen Sie natürlich im Programm auch abfangen ☺.
19.6
Transaktionen
Transaktionen sichern die Datenintegrität
Transaktionen sind in modernen Datenbankanwendung kaum wegzudenken. Eine Transaktion ist eine Gruppe von Datenbank-Änderungen, die entweder nur komplett oder gar nicht ausgeführt werden darf. Ein Beispiel für eine Transaktion ist eine Überweisung von einem Bankkonto auf ein anderes. Bei einer solchen muss der überwiesene Betrag von dem einen Konto abgezogen und dem andern Konto hinzugezählt werden. Werden die dazu notwendigen Aktualisierungs-Anweisungen nicht in einer Transaktion ausgeführt und tritt nach der ersten Anweisung ein Fehler auf, verwaltet die Datenbank ungültige Daten. Wird der ganze Vorgang aber in eine Transaktion verpackt, wird diese beim Eintritt eines Fehlers komplett zurückgerollt. Die Datenbankintegrität bleibt damit gewahrt.
SubmitChanges wird in einer Transaktion ausgeführt
Transaktionen sind prinzipiell einfach. Eine Art Transaktion haben Sie bereits, wenn Sie im Datenkontext mehrere Aktualisierungen vornehmen und erst danach SubmitChanges aufrufen. Da SubmitChanges intern in einer Transaktion ausgeführt wird, werden im Fehlerfall alle Änderungen automatisch zurückgerollt.
Explizite Transaktionen geben mehr Möglichkeiten
Sie können aber auch explizite Transaktionen ausführen, was Ihnen mehr Möglichkeiten gibt. So können Sie innerhalb einer Transaktion z. B. mehrfach SubmitChanges aufrufen, um sicherzustellen, dass die zwischenzeitlichen Änderungen prinzipiell in Ordnung sind. Tritt bei einem Aufruf ein Fehler auf, wird die Transaktion nicht bestätigt und damit automatisch zurückgerollt. Explizite Transaktionen können zudem geschachtelt werden. Außerdem haben Sie die Möglichkeit, eine bereits vorhandene Transaktion zu nutzen, die Sie z. B. von einer Klassenbibliothek erhalten, die über ADO.NET auf die Datenbank zugreift. Innerhalb einer Transaktion gehören alle Änderungen der Transaktion. Werden innerhalb der Transaktion Entitäten angefügt, geändert oder gelöscht, sind diese Änderungen per Voreinstellung nach außen (für andere Transaktionen) nicht sichtbar (was Sie aber über den Isolationsgrad der Transaktion auch beeinflussen können). Interessant ist, dass Abfragen innerhalb der Transaktion die geänderten Daten reflektieren, obwohl diese noch gar nicht in die Datenbank geschrieben wurden. Mit einer expliziten Transaktion haben Sie zudem die Möglichkeit, den Isolationsgrad zu steuern, um z. B. Ihre Änderungen nach außen zum Lesen freizugeben. Darauf gehe ich aber hier nicht ein (weil die 40 Seiten, die ich für dieses Kapitel geplant habe, schon wieder überschritten sind … OK, im Planen war ich noch nie gut, aber sagen Sie das nicht der Frau meiner potenziellen Kinder, die genau dies behauptet – was ich natürlich abstreite).
1126
Transaktionen
Eine neue explizite Transaktion zu implementieren ist unter LINQ to SQL sehr einfach. Dazu packen Sie alle zugehörigen Anweisungen in einen using-Block, der ein TransactionScope-Objekt erzeugt. Am Ende des Blocks rufen Sie die CompleteMethode auf, um die Transaktion abzuschließen. Wenn im Block eine Ausnahme auftritt, sorgt die automatisch am Ende aufgerufene Dispose-Methode für ein Zurückrollen der Transaktion (aber natürlich nur, wenn nicht zuvor Complete aufgerufen wurde).
Ein TransactionScope-Objekt steuert die Transaktion
12
Die TransactionScope-Klasse ist im Namensraum System.Transactions definiert, für den Sie die Assembly System.Transactions.dll referenzieren müssen. So können Sie z. B. das Hinzufügen mehrerer Produkt-Modelle in eine Transaktion verpacken:
13
Listing 19.32: Verwendung einer neuen, expliziten Transaktion
14
// Neuen TransactionScope erzeugen using (TransactionScope scope = new TransactionScope()) { // Neues Artikel-Modell erzeugen und anfügen ProductModel newProductModel1 = new ProductModel { Name = "Radon MCS Carbon 8.0", ModifiedDate = DateTime.Now }; dataContext.ProductModels.InsertOnSubmit(newProductModel1); dataContext.SubmitChanges();
15
16
// Neues Artikel-Modell erzeugen und anfügen ProductModel newProductModel2 = new ProductModel { Name = "Radon ZR LTD", ModifiedDate = DateTime.Now }; dataContext.ProductModels.InsertOnSubmit(newProductModel2); dataContext.SubmitChanges();
17
18
// Abfragen der Daten, die nur innerhalb der Transaktion gelten var radons = from pm in dataContext.ProductModels where pm.Name.StartsWith("Radon") select pm; foreach (var pm in radons) { Console.WriteLine(pm.Name); }
19 20
// Die Transaktion abschließen scope.Complete(); }
21
Das Beispiel zeigt, dass die neuen Produkt-Modelle innerhalb der Transaktion sichtbar sind. Die Ausgabe des Programms ist: Radon MCS Carbon 8.0 Radon ZR LTD
22
Mit einer expliziten Transaktion können Sie also auch die Konsistenzprobleme lösen, die Sie ansonsten hätten, wenn Sie lediglich den einmaligen Aufruf von SubmitChanges als Transaktion verwenden.
23
1127
Datenbanken mit LINQ to SQL bearbeiten
Eine anderer, nicht uninteressanter Einsatz ist auch, dass Sie testweise Daten ändern (z. B. um LINQ to SQL besser kennenzulernen). Wenn Sie am Ende der Transaktion Complete nicht aufrufen, werden alle Änderungen zurückgerollt und die Datenbank bleibt unberührt.
TIPP
In der Praxis treten bei komplexen Transaktionen häufig Timeouts auf, weil die Datenbank nicht schnell genug reagiert. Eine Transaktion verwendet per Voreinstellung einen Timeout von einer Minute (was allerdings nicht dokumentiert ist). Wird der Timeout überschritten, resultiert eine TransactionAbortedException mit der wenig aussagenden Meldung »Die Transaktion wurde abgebrochen«. Sie können den Timeout aber auch über das timeout-Argument des Konstruktors bestimmen: using (TransactionScope scope = new TransactionScope( TransactionScopeOption.Required, TimeSpan.FromMinutes(3))) { ... }
Wenn Sie den Timeout angeben, müssen Sie ebenfalls das Argument scopeOption angeben, das verwendet wird, wenn Transaktionen geschachtelt werden (z. B. weil diese in mehreren Methoden verwendet werden). TransactionScopeOption.Required sagt aus, dass eine Transaktion erforderlich ist, aber ggf. eine bereits vorhandene verwendet wird.
19.7 LINQ verwendet optimistische Konkurrenz
Behandlung von Konflikten beim Aktualisieren
Beim Aktualisieren von Daten kann es in einer Mehrbenutzerumgebung vorkommen, dass eine geänderte Entität seit dem letzten Lesen von einem anderen Benutzer verändert wurde. LINQ to SQL berücksichtigt diesen Umstand beim Übertragen der Änderungen in die Datenbank über optimistische Konkurrenz. Optimistische Konkurrenz bedeutet, dass das System optimistisch davon ausgeht, dass gelesene Entitäten in der Zwischenzeit nicht von anderen Benutzern (oder Anwendungen) geändert werden. Die (in LINQ to SQL nicht direkt mögliche) pessimistische Konkurrenz geht im Gegensatz dazu pessimistisch vor (was auch sonst …): Sie nimmt an, dass versucht wird, Entitäten zwischenzeitlich zu ändern, und sperrt diese deswegen während der Bearbeitung für Änderungen in der Datenbank.
19.7.1 LINQ to SQL überprüft beim Aktualisieren, ob Entitäten zwischenzeitlich geändert wurden
LINQ to SQL überprüft beim Aktualisieren oder Löschen von Entitäten, ob diese zwischenzeitlich in der Datenbank von einem anderen Benutzer geändert wurden. Sind diese geändert worden, resultiert beim Aktualisieren oder Löschen eine ChangeConflictException. Die Überprüfung erfolgt auf eine von zwei Arten: ■
1128
Die Aktualisierungs-Überprüfung
Ist in der Entität ein Zeitstempel- oder Versions-Feld enthalten und die Eigenschaft entsprechend gekennzeichnet, wird dieses daraufhin überprüft, ob es in der Datenbank einen anderen Wert aufweist als den, der aktuell in dem Entitätsobjekt gespeichert ist.
Behandlung von Konflikten beim Aktualisieren
■
Ist kein Zeitstempel- oder Versions-Feld enthalten, werden alle Felder, deren Einstellung UPDATE CHECK nicht auf Never steht, mit dem Wert der entsprechenden Eigenschaft verglichen. Dabei können Sie noch unterscheiden, dass nur verglichen wird, wenn die Eigenschaft geändert wurde (WhenChanged), oder grundsätzlich (Always).
Das Aktualisieren geschieht auf die Weise, dass alle zu überprüfenden Felder mit in die WHERE-Bedingung der SQL-UPDATE-Abfrage aufgenommen werden. Dabei wird der im Entitätsobjekt gespeicherte Wert mit dem Datenbank-Feld verglichen. Ist dieses geändert worden, wird der Datensatz nicht gefunden und die SQL-Anweisung ergibt, dass kein Datensatz geändert bzw. gelöscht wurde. Daran erkennt der Datenkontext, dass er eine ChangeConflictException werfen muss.
12
13
Die erste Möglichkeit ist die effizienteste und in Business-Objekt-Modellen wahrscheinlich die beste. LINQ to SQL muss dann lediglich das Zeitstempel- oder VersionsFeld mit in die WHERE-Bedingung aufnehmen (natürlich neben dem Primärschlüssel). Da Zeitstempel- oder Versions-Felder vom Datenbanksystem immer automatisch aktualisiert werden, wenn die Entität geändert wird, ist diese Art der Abfrage auch sehr sicher.
14
15
Die zweite Möglichkeit ist weniger effizient, da alle Felder, deren Einstellung UPDATE CHECK nicht auf Never steht, mit in die Überprüfung übernommen werden. Dafür können Sie mit dieser Variante Felder von der Überprüfung komplett ausschließen oder – was in meinen Augen am interessantesten ist – nur in die Überprüfung einschließen, wenn diese seit dem letzten Lesen bzw. Aktualisieren im Entitäts-Objekt geändert wurden. Wenn Sie UPDATE CHECK auf WhenChanged setzen und gleichzeitig AUTO SYNC auf OnUpdate, damit die Änderungen anderer Benutzer bei der Aktualisierung automatisch in die entsprechende Eigenschaft geschrieben werden.
19.7.2
16
17
Behandeln der ChangeConflictException
Die ChangeConflictException, die von SubmitChanges geworfen wird, wenn die zu aktualisierende Entität zwischenzeitlich geändert wurde, können Sie speziell behandeln. Und die ist sogar erstaunlich einfach (wenn man es verstanden hat ☺).
18
Zur Demonstration der ChangeConflictException dient das folgende kleine Programm:
19
Listing 19.33: Basis-Demo für die ChangeConflictException
20
// Erzeugen des Datenkontextes AdventureWorksLTDataContext dataContext = new AdventureWorksLTDataContext(); // Lesen der Artikel der Kategorie 35 var products = from product in dataContext.Products where product.ProductCategoryID == 35 select product;
21
// Die Preise ändern foreach (var product in products) { product.ListPrice += 0.01M; }
22
23
Console.WriteLine("Die Preise der Artikel der Kategorie 35 wurden " + "geändert. Ändern Sie nun irgendein Feld eines oder mehrerer " + "dieser Produkte im SQL Server Management Studio oder im Server " +
1129
Datenbanken mit LINQ to SQL bearbeiten
"Explorer von Visual Studio und betätigen Sie dann die Return-" + "Taste, um die Änderungen in diesem Programm zu speichern."); Console.WriteLine(); Console.WriteLine("Speichern der Änderungen mit Return"); Console.ReadLine(); // Speichern der Änderungen try { dataContext.SubmitChanges(); Console.WriteLine("Die Änderungen wurden erfolgreich " + "gespeichert"); } catch (ChangeConflictException ex) { Console.WriteLine("Die Daten wurden von einem " + "anderen Benutzer geändert oder gelöscht"); }
Wenn Sie das Programm ausführen und es hält nach dem Ändern der Preise der Produkte der Kategorie 35 an, ändern Sie irgendwelche Felder der entsprechenden Datensätze in der Tabelle (bevorzugt die Felder ProductNumber und ListPrice, denn diese werden in der erweiterten Version des Beispiels ausgegeben). Dazu können Sie das SQL Server Management Studio verwenden. Nachdem Sie die Products-Tabelle geöffnet haben, sollten Sie zur besseren Übersicht die SQL-Anweisung anpassen, die für den Abruf der Daten verwendet wird. Klicken Sie dazu auf den SymbolleistenSchalter SQL und erweitern Sie die SQL-Anweisung um eine Einschränkung auf die Produkte der Kategorie 35: SELECT * FROM SalesLT.Product WHERE ProductCategoryID = 35
Führen Sie die geänderte SQL-Anweisung dann über den Schalter mit dem Ausrufezeichen in der Symbolleiste aus. Über diesen Schalter können Sie die Ansicht auch jederzeit aktualisieren, z. B. um zu überprüfen, ob die Demoanwendung Daten geändert hat. Wenn Sie das Programm nach der Änderung der Daten ausführen, zeigt dieses an, dass die ChangeConflictException geworfen wurde.
Dafür sorgen, dass SubmitChanges alle Konflikte erkennt ConflictMode.ContinueOnConflict sorgt dafür, dass alle Konflikte erkannt werden
Damit Sie alle Konflikte korrekt auswerten können, müssen Sie zunächst dafür sorgen, dass SubmitChanges alle Konflikte erkennt. Per Voreinstellung ist dies nicht der Fall, da SubmitChanges nach dem ersten Konflikt abbricht. Sie können am Argument failureMode aber den Wert ConflictMode.ContinueOnConflict angeben, womit SubmitChanges nach dem Eintreten eines Konflikts versucht, die Änderungen zu speichern. Wichtig dabei ist, dass Sie damit lediglich dafür sorgen, dass alle Konflikte erkannt werden. Die implizite Transaktion wird beim Auftreten eines Konflikts am Ende genauso zurückgerollt, als wenn Sie SubmitChanges mit der Default-Einstellung (ConflictMode.FailOnFirstConflict) aufrufen.
Ermitteln der Objekte, die Konflikte verursacht haben ChangeConflicts gibt Zugriff auf die Konflikte
1130
Wenn Sie dafür gesorgt haben, dass alle Konflikte erkannt werden, können Sie über die ChangeConflicts-Eigenschaft des Datenkontextes ermitteln, welche Konflikte aufgetreten sind. Diese Auflistung verwaltet einzelne ObjectChangeConflict-
Behandlung von Konflikten beim Aktualisieren
Objekte, die in der Eigenschaft Object das Entitäts-Objekt referenzieren, das den Konflikt verursacht hat. Die Eigenschaft IsDeleted gibt an, ob der Datensatz in der Zwischenzeit in der Datenbank gelöscht wurde. Ist IsDeleted false, wurde der Datensatz geändert. Die Eigenschaft MemberConflicts verwaltet dann eine Auflistung von MemberChangeConflict-Objekten mit Informationen zu den Konflikten für die einzelnen Felder. Darüber können Sie z. B. den Namen des Felds, den aktuellen Wert in der Datenbank und den aktuellen Wert im Programm ermitteln:
12
Listing 19.34: Ermitteln der Änderungskonflikte try {
13
dataContext.SubmitChanges(ConflictMode.ContinueOnConflict); Console.WriteLine("Die Änderungen wurden erfolgreich " + "gespeichert");
14
} catch (ChangeConflictException) { Console.WriteLine("Die Daten wurden von einem " + "anderen Benutzer geändert oder gelöscht");
15 // Die Problem-Objekte ermitteln foreach (var changeConflict in dataContext.ChangeConflicts) { // Das Objekt referenzieren Product p = (Product)changeConflict.Object;
16
Console.WriteLine("Konflikte beim Speichern des Produkts " + p.ProductNumber + ":"); if (changeConflict.IsDeleted) { // Der Datensatz wurde in der Datenbank gelöscht Console.WriteLine("Der Datensatz wurde zwischenzeitlich " + " aus der Datenbank gelöscht"); } else { // Der Datensatz wurde nicht gelöscht, aber verändert: // Alle Aktualisierungskonflikte durchgehen foreach (var memberConflict in changeConflict.MemberConflicts) { Console.WriteLine("Feld: " + memberConflict.Member.Name); Console.WriteLine("Wert in der Datenbank: " + memberConflict.DatabaseValue); Console.WriteLine("Wert im Programm: " + memberConflict.CurrentValue); Console.WriteLine(); } }
17
18
19 20
21
} }
MemberChangeConflict verwaltet in der Eigenschaft IsModified eine Information darüber, ob die entsprechende Eigenschaft im Entitäts-Objekt verändert wurde. Diese Eigenschaft ist wichtig für das automatische Auflösen von Konflikten.
22
23
1131
Datenbanken mit LINQ to SQL bearbeiten
Auflösen von Konflikten Resolve und ResolveAll lösen Konflikte
Zum Auflösen von Konflikten verwenden Sie die Methode Resolve der einzelnen MemberChangeConflict- oder ObjectChangeConflict-Objekte oder die Methode ResolveAll der ChangeConflicts-Auflistung. Damit können Sie Konflikte auf der Ebene der einzelnen Eigenschaften, der einzelnen Datensätze und aller Datensätze auflösen. Der Resolve- und der ResolveAll-Methode übergeben Sie am ersten Argument einen Wert der RefreshMode-Aufzählung, der festlegt, wie die Auflösung eines Aktualisierungs-Konflikts erfolgen soll. Dazu stehen die folgenden Werte zur Verfügung: ■
■
■
EXKURS
KeepCurrentValues: Sorgt dafür, dass beim nächsten Speichern die in der Datenbank gespeicherten Werte mit den aktuellen Werten des Objekts überschrieben werden. Damit kann es vorkommen, dass Felder, die in der Datenbank zwischenzeitlich geändert wurden, aber nicht im Programm, auf ihren alten Wert zurückgesetzt werden (der im Entitäts-Objekt gespeichert ist). KeepChanges: Überschreibt die im Objekt gespeicherten Werte von Eigenschaften, die im Objekt seit dem Lesen nicht geändert wurden, mit denen, die aktuell in der Datenbank gespeichert sind. Die seit dem letzten Lesen geänderten Eigenschaften bleiben erhalten und überschreiben beim Speichern die Werte der entsprechenden Felder in der Datenbank. Diese Option ist für die Praxis wohl die beste, da sie einerseits dafür sorgt, dass Eigenschaften, die im Programm nicht geändert wurden, in der Datenbank ihren Wert behalten (auch wenn dieser zwischenzeitlich von anderen Benutzern geändert wurde). Andererseits werden Änderungen, die im Programm vorgenommen wurden, in die Datenbank geschrieben. OverwriteCurrentValues: Überschreibt die im Objekt gespeicherten Werte mit denen, die aktuell in der Datenbank gespeichert sind. Damit gehen alle Änderungen, die im Programm vorgenommen wurden, verloren.
Leider ist nicht (auffindbar) dokumentiert, wie das Ganze intern funktioniert. Wahrscheinlich verwaltet der Datenkontext für ein Entitäts-Objekt mehrere Sätze von Daten: Ein Satz verwaltet die Daten, die beim letzten Lesen aus der Datenbank gelesen wurden, ein anderer verwaltet die im Programm aktuell geänderten Eigenschaftswerte. Damit kann der Datenkontext schon einmal erkennen, welche Eigenschaften im Programm geändert wurden. Zusätzlich dazu wird wahrscheinlich ein Satz an Eigenschaftswerten verwaltet, der für die Aktualisierungs-Prüfung (die ja über die WHERE-Klausel der SQL-Anweisung vorgenommen wird) verwendet wird. Damit hat der Datenkontext die Möglichkeit, diese Prüfwerte je nach Auflösungsstrategie auszutauschen, z. B. gegen die beim Versuch zu aktualisieren aktuell gelesenen Datenbank-Werte (was er z. B. bei der Strategie KeepCurrentValues oder KeepChanges für ungeänderte Eigenschaften auch so macht: Der Vergleich in der WHERE-Klausel verwendet dann den aktuellen Datenbank-Wert). Aber das sind nur Vermutungen … Für den Fall, dass Datensätze gelöscht wurden, können Sie der Resolve-Methode eines ObjectChangeConflict-Objekts und der ResolveAll-Methode der ChangeConflicts-Auflistung am zweiten Argument mit true übergeben, dass Konflikte mit gelöschten Datensätzen automatisch gelöst werden. Die entsprechenden Objekte werden dann aus dem Datenkontext entfernt.
1132
Behandlung von Konflikten beim Aktualisieren
In einer normalen Anwendung übernimmt diese Entscheidung natürlich der Benutzer. So können Sie z. B. alle Konflikte durchgehen, die einzelnen Konflikte für die betroffenen Eigenschaften anzeigen und den Benutzer entscheiden lassen, wie er die Konflikte auflösen will. Nach der Auflösung der Konflikte versuchen Sie, SubmitChanges noch einmal aufzurufen. Wurden die betreffenden Datensätze in der Datenbank zwischenzeitlich nicht erneut geändert, sind alle Konflikte gelöst. Da es aber vorkommen kann, dass die entsprechenden Datensätze in der Datenbank zwischenzeitlich von anderen Benutzern erneut geändert wurden, sollten Sie den Aufruf von SubmitChanges in einer (Endlos-)Schleife (in der Praxis mit Notausgang) ausführen. Das folgende erweiterte Beispiel-Programm zeigt eine prinzipielle Lösung dieses Problems (allerdings ohne Schleifen-Notausgang):
12
13
Listing 19.35: Konsolenanwendungsbeispiel für das Speichern von Änderungen mit Konfliktlösung auf Datensatz-Basis
14
// Speichern der Änderungen while (true) { try { // Versuch, die aktuellen Änderungen zu speichern dataContext.SubmitChanges(ConflictMode.ContinueOnConflict);
15
16
Console.WriteLine("Die Änderungen wurden erfolgreich " + "gespeichert"); // Die aktuellen Daten der Entitäts-Objekte ausgeben foreach (var product in products) { Console.WriteLine(product.ProductNumber + ": " + product.ListPrice); }
17
18
break; } catch (ChangeConflictException) { Console.WriteLine("Die Daten wurden von einem " + "anderen Benutzer geändert oder gelöscht");
19
// Die Problem-Objekte ermitteln foreach (var changeConflict in dataContext.ChangeConflicts) { Product p = (Product)changeConflict.Object;
20
// Die Konflikte anzeigen (siehe in Listing 19.34) ...
21
if (changeConflict.IsDeleted) { // Wenn der Datensatz in der Datenbank zwischenzeitlich // gelöscht wurde, wird der Konflikt automatisch gelöst changeConflict.Resolve(RefreshMode.OverwriteCurrentValues, true); } else { // Den Anwender entscheiden lassen, wie der Konflikt für den // Datensatz aufgelöst werden soll
22
23
1133
Datenbanken mit LINQ to SQL bearbeiten
Console.WriteLine("Wie wollen Sie den Konflikt lösen?"); Console.WriteLine("1: Die Datenbankwerte mit den im " + "Objekt gespeicherten Werten überschreiben?"); Console.WriteLine("2: Nur Ihre Änderungen speichern und " + "die anderen Daten mit denen aus der Datenbank " + "überschreiben?"); Console.WriteLine("3: Die Daten im Objekt mit den " + "Datenbankwerten überschreiben?"); string input = Console.ReadLine(); switch (input) { case "1": changeConflict.Resolve(RefreshMode.KeepCurrentValues); break; case "2": changeConflict.Resolve(RefreshMode.KeepChanges); break; case "3": changeConflict.Resolve( RefreshMode.OverwriteCurrentValues); break; } } } }
Was mich echt erstaunt hat, ist, dass das Ganze einfach und sicher funktioniert. Spielen Sie ein wenig mit der Beispiel-Anwendung herum, um ein Gefühl für die Auflösung von Konflikten zu bekommen.
19.8
Wichtige, nicht behandelte Features
Die folgenden LINQ-to-SQL-Features konnte ich im Rahmen dieses Buchs nicht behandeln. Beachten Sie, dass ich hier nicht unbedingt alle noch fehlenden Möglichkeiten aufliste: ■ ■
■ ■ ■ ■ ■ ■ ■
1134
Komplexe LINQ-Abfragen, Daten-Lade-Optionen (über eine DataLoadOptions-Instanz), über die Sie für die Auswertung von Detail-Entitäten globale Filter definieren und dafür sorgen können, dass Detail-Entitäten gleich bei der Abfrage der Master-Entitäten geladen werden (»Eifriges Laden« oder »Eager Loading«), das Anpassen der Änderungsverfolgung, der Isolationsgrad von Transaktionen, geschachtelte Transaktionen, Transaktionen, die von anderen Systemen übernommen werden, Kompilierte Abfragen, die Arbeit mit gespeicherten Prozeduren (Stored Procedures) und Funktionen des SQL-Servers, die Verwendung von SqlMetal zur Generierung eines Objektmodells für den SQL Server Compact Edition, das Anpassen von Insert-, Update- und Delete-Operationen, die Arbeit mit Sichten, die in der Datenbank erzeugt werden, um z. B. die Informationen aus mehreren Tabellen in einer Entitätsmenge zusammenzufassen,
Wichtige, nicht behandelte Features
■ ■
das direkte Ausführen von SQL-Anweisungen über die ExecuteCommand-Methode des Datenkontext-Objekts und die Zwei-Wege-Datenbindung (über BindingList).
Viel Spaß beim Erforschen der weiteren Möglichkeiten. Wenn Sie LINQ to SQL einmal verstanden haben (und die Fallen kennen), geht die Arbeit damit richtig schnell von der Hand.
12
13
14
15
16
17
18
19 20
21
22
23
1135
Teil 4 Fortgeschrittene Programmierung 1139
Multithreading
20
1205
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
21
1229
Assemblys, Reflektion und Anwendungsdomänen
22
1257
Sicherheitsgrundlagen
23
Inhalt
20
Multithreading 12
Multithreading ist eine wichtige Technik in fast allen Arten von Anwendungen. Über Multithreading können Sie eine Anwendung für den Benutzer dadurch verbessern, dass diese bei der Abarbeitung aufwändiger Aufgaben besser (oder überhaupt) auf Eingaben reagiert. Sie können ermöglichen, dass eine Anwendung Aufgaben im Hintergrund ausführt, während der Benutzer weiter damit arbeitet. Oder Sie können einfach mehrere Aufgaben parallel ausführen lassen, um die Ausführungsgeschwindigkeit zu optimieren.
13
Wegen der hohen Bedeutung in der Praxis beleuchtet dieses Kapitel die wichtigsten Aspekte des Multithreading in .NET-Anwendungen. Dazu gehören Themen wie asynchroner Methodenaufruf genauso wie die Verwendung der BackgroundWorkerKlasse, »echte« Threads und die Implementierung eigener Klassen, die nach dem BackgroundWorker-Prinzip arbeiten. Viel Stoff also, besonders, weil auch komplexe, aber wichtige Themen wie die Synchronisierung von Threads und der Zugriff auf die Programmoberfläche behandelt werden.
15
Ich lege in diesem Kapitel allerdings mehr Wert auf die Nähe zur Praxis als auf alle Multithreading-Features. Multithreading ist nicht trivial und bietet einige sehr spezielle Möglichkeiten, auf die ich in diesem Kapitel nicht eingehen kann.
17
Die Stichworte dieses Kapitels sind:
18
■ ■ ■ ■ ■ ■ ■ ■ ■ ■
16
Was ist Multithreading? Einsatz von Multithreading und mögliche Probleme Multithreading in Windows-Anwendungen Multithreading versus asynchronem Methodenaufruf Asynchroner Methodenaufruf Easy-Multithreading über einen BackgroundWorker Einfache Threads Arbeiten mit der ThreadPool-Klasse Synchronisierung von Threads Deadlocks Timer
Falls Sie tiefer in Multithreading einsteigen wollen, empfehle ich die Bücher »Windows Multithreading mit C++ und C Sharp« von Olaf Neuendorf und »Net Multithreading« von Alan Dennis. Beide sind zwar schon etwas älter (2003), aber sehr detailliert (und mit unterschiedlichen Ansätzen). Aktuelle Bücher zu Multithreading unter .NET scheint es leider nicht zu geben.
19
20 21
22 REF
23
1139
Index
■
14
Multithreading
20.1
Einführung in Multithreading
Bevor dieses Kapitel näher auf die einzelnen Möglichkeiten des Multithreading eingeht, behandelt es erst einmal (wie so oft …) die Grundlagen. Dazu gehört, dass Sie wissen, was Multithreading überhaupt ist, welche Probleme auftreten können, wann Sie Multithreading einsetzen sollten bzw. können und was der Unterschied zum asynchronen Ausführen von Methoden ist. Lesen Sie also weiter ☺.
20.1.1 Ein Thread ist ein paralleles Teilprogramm
Was ist ein Thread?
Ein Thread (englisch für »Faden«) ist so etwas wie ein »Programm-Faden«, der mehr oder weniger parallel neben anderen Threads des Prozesses ausgeführt wird. Ein gutes Beispiel für Multithreading ist eine Textverarbeitung wie Microsoft Word (auch wenn Word in den Augen einiger Leute kein gutes Beispiel für eine Textverarbeitung ist …): Wenn Sie das Drucken eines Dokuments starten, starten Sie (wenn Sie die Optionen nicht geändert haben) einen Arbeitsthread. Dieser speichert den aktuellen Stand des Dokuments temporär zwischen und druckt diese Kopie aus. Während des Ausdrucks können Sie im Hauptthread (in der Benutzeroberfläche) problemlos weiterarbeiten. Während des Ausdrucks aktualisiert der Druck-Thread die Benutzeroberfläche, indem er in der Statuszeile meldet, welche Seite gerade gedruckt wird. Mehr bekommen Sie von diesem Arbeitsthread nicht mit (außer, dass das Dokument gedruckt wird). Wenn Sie einmal ausprobieren wollen, wie das Ausdrucken in Word ohne Multithreading aussieht, schalten Sie die Option »Drucken im Hintergrund« in Word ab. In diesem Fall wird der Ausdruck im Hauptthread ausgeführt, und Sie können währenddessen nicht weiterarbeiten.
20.1.2 Multithreading ist ein zentraler Bestandteil von Windows
Was ist Multithreading?
Multithreading (oder auch kurz: Threading) ist ein zentraler Bestandteil von Windows (ab Windows NT). Jeder Windows-Prozess besitzt mindestens einen Thread. In Anwendungen mit einer Oberfläche wird dieser als UI-Thread oder Hauptthread bezeichnet. Der UI-Thread (User-Interface-Thread) kümmert sich u. a. um die Nachrichten, die Windows den Fenstern einer Anwendung sendet, z. B. wenn diese neu gezeichnet werden müssen (was dann zum Aufruf des Paint-Ereignisses führt). Die eigentliche Anwendung wird in der Regel im UI-Thread ausgeführt. Dazu gehören z. B. alle Programme, die Sie in den Methoden programmieren, die den Ereignissen der Steuerelemente zugewiesen sind. In .NET-Anwendungen laufen neben dem UI-Thread automatisch noch zwei weitere Threads. Einer führt die CLR aus, der andere den Garbage Collector. Falls der Prozess an einen Debugger gehängt ist, kann zusätzlich ein Debugger-Thread ausgeführt werden.
INFO
1140
Wenn Sie eine .NET-Anwendung über Visual Studio starten und die Anzahl der laufenden Threads überprüfen, werden mehr als drei Threads angezeigt. Das liegt daran, dass die Anwendung per Voreinstellung in dem Visual-Studio-Prozess (als Host) ausgeführt wird.
Einführung in Multithreading
Eine Anwendung kann aber neben den vordefinierten Threads auch weitere Threads starten, in denen Teilprogramme ausgeführt werden. Die werden häufig als Arbeitsthreads bezeichnet. Im Word-Beispiel oben wird die Oberfläche von Word im UIThread ausgeführt, beim Drucken geht ein Arbeitsthread ans Werk.
Anwendungen können Arbeitsthreads ausführen
20.1.3 Threads aus technischer Sicht Aus der technischen Sicht führt ein Thread immer eine Methode aus. Genauer gesagt führt ein Thread die Maschinencode-Anweisungen im Speicher der Anwendung aus, die diese Methode ausmachen.
12 Ein Thread führt eine Methode aus
13
Auf einem Einprozessor-Rechner vergibt Windows jedem Thread eine festgelegte Zeitscheibe. Innerhalb dieser Zeitscheibe werden die Anweisungen der Methode (weiter) ausgeführt. Ist die Zeitscheibe abgelaufen, wird die Ausführung unterbrochen, wobei sich Windows die Wiedereintrittsadresse merkt. Dann wird der nächste Thread in der Thread-Liste an dessen Wiedereintrittsadresse weiter ausgeführt. Damit entsteht eine (quasi-)gleichzeitige Abarbeitung mehrerer Methoden. Auf einem Mehrprozessor-Rechner werden Threads zusätzlich noch automatisch an die verfügbaren Prozessoren verteilt, sodass Threads auch wirklich parallel laufen können.
14
15
Der Stack Um Probleme mit Argumenten und Variablen zu vermeiden, die auf dem Stack verwaltet werden, teilt Windows jedem Thread einen eigenen Stack zu. Da alle an eine Methode übergebenen Argumente und deren lokale Variablen auf dem Stack angelegt werden, besitzt jeder Thread eine eigene Kopie seiner Argumente und der lokalen Variablen. Deshalb können auch mehrere Threads dieselbe Methode ausführen, ohne sich gegenseitig zu beeinflussen.
16 Jeder Thread besitzt einen eigenen Stack
17
18
Die Priorität Ein Thread besitzt eine Priorität, die in .NET in fünf Stufen einstellbar ist. Threads mit einer höheren Priorität erhalten eine größere Zeitscheibe als Threads mit einer niedrigen. Diese Priorität bezieht sich aber auf die Priorität, die dem Prozess zugeordnet ist. Damit können Sie die verfügbaren Prozess-Ressourcen für Ihre Anwendung optimieren (was aber in der Praxis sicherlich nicht allzu einfach ist).
Die Priorität steuert die Größe der Zeitscheibe
19
20
Vorder- und Hintergrundthreads Threads können Vorder- oder Hintergrundthread sein. Ein Vordergrundthread läuft immer bis zum Ende der Thread-Methode (es sei denn, er wird explizit abgebrochen), auch wenn der startende Thread (normalerweise der UI-Thread) zwischenzeitlich beendet wird. Ist der startende Thread der UI-Thread und wurde beendet, ist die Anwendung übrigens nicht mehr sichtbar (es sei denn, es handelt sich um eine Konsolenanwendung), weil das Anwendungs-Hauptfenster bereits geschlossen wurde. Der Prozess der Anwendung läuft aber noch so lange weiter, bis alle Vordergrundthreads abgearbeitet wurden.
Ein Vordergrundthread läuft auch weiter, wenn der startende Thread beendet wird
21
22
Vordergrundthreads können Sie immer dann einsetzen, wenn die komplette Ausführung der jeweiligen Thread-Methode garantiert sein muss (z. B. beim Übertragen von Daten, beim Drucken oder beim Schreiben in eine Datei).
23
1141
Multithreading
Hintergrundthreads werden mit dem startenden Thread beendet
Ein Hintergrundthread wird immer automatisch beendet, wenn der Thread beendet wird, der ihn gestartet hat. Hintergrundthreads können Sie z. B. für interne Aufräumarbeiten oder langwierige Berechnungen einsetzen, bei denen es dem Anwender bewusst ist, dass die Arbeiten nicht abgeschlossen werden, wenn er die Anwendung beendet. Wenn Sie echte Threads (im Gegensatz zum asynchronen Aufruf von Methoden, zum BackgroundWorker, zu Timer-Objekten, zu Threads aus dem .NET-ThreadPool) verwenden, können Sie über die Eigenschaft IsBackground der Thread-Klasse bestimmen, ob es sich um einen Vorder- oder Hintergrundthread handelt. Per Voreinstellung ist ein solcher Thread ein Vordergrundthread (IsBackground ist false), läuft also auf jeden Fall zu Ende ab, falls er nicht explizit abgebrochen wird. Alle anderen Threads (beim asynchronen Aufruf von Methoden, bei der Verwendung der BackgroundWorker-Klasse etc.) sind grundsätzlich Hintergrundthreads.
INFO
Die von Microsoft verwendete Semantik ist etwas eigenartig. Genau wie ich dachten alle Entwickler, die ich bisher gefragt hatte, dass der Begriff »Hintergrund« dafür steht, dass der Thread im Hintergrund und damit auch ohne die Anwendung ausgeführt wird. Tatsächlich ist es aber genau umgekehrt. »Hintergrund« bedeutet hier, dass der Thread nicht so wichtig ist, dass er auf jeden Fall zu Ende ausgeführt werden muss. Ein Vordergrundthread ist hingegen in Bezug auf seine komplette Ausführung wichtig.
20.1.4 Threadsicherheit Ein threadsicherer Typ kann problemlos in mehreren Threads verwendet werden
Der Begriff Threadsicherheit wird in Zusammenhang mit Typen verwendet. Ein Typ ist immer dann threadsicher, wenn er in mehreren Threads gleichzeitig verwendet werden kann, ohne das Programm (bzw. System) in einen instabilen, fehlerhaften oder ungültigen Zustand zu versetzen. Threadsicherheit bedeutet im Wesentlichen, dass ein Objekt keine für andere Objekte zugreifbare Daten oder Ressourcen verwendet, oder dass es beim Zugriff auf solche Daten diese für den Zugriff durch andere Threads sperrt. Sperrmechanismen werden in Abschnitt »Threads synchronisieren« Seite 1183 behandelt. Viele .NET-Klassen sind nicht threadsicher. Dazu gehören leider auch die Steuerelemente von Windows.Forms und WPF. Wenn Sie auf diese Steuerelement von anderen Threads aus zugreifen wollen, müssen Sie eine spezielle Technik verwenden, die den Aufruf in den jeweils anderen Thread umbiegt. Wie das geht, zeige ich im Abschnitt »Zugriff auf die Benutzeroberfläche« (Seite 1147).
20.1.5 Mögliche Probleme beim Multithreading Multithreading erscheint zunächst recht einfach. Sie können den einfachsten Weg über eine BackgroundWorker-Komponente gehen, wie ich es im Abschnitt »Einfaches Multithreading mit der BackgroundWorker-Klasse« ab Seite 1152 beschreibe. Oder Sie verwenden »echtes Multithreading« über eine Thread-Instanz und eine selbst geschriebene Methode. Sie können Methoden auch asynchron starten. Oder das ereignisbasierte asynchrone Entwurfsmuster implementieren (OK, das ist schon etwas komplexer) … Multithreading wird aber dann problematisch, wenn mehrere Threads dieselben globalen Daten (z. B. statische Eigenschaften einer Klasse) oder Ressourcen (z. B. eine geöffnete Datei) verwenden, synchronisiert werden müssen oder auf die Benutzerschnittstelle zugreifen.
1142
Einführung in Multithreading
Zugriff auf globale Daten oder Ressourcen Wenn Threads globale Daten oder gemeinsame Ressourcen bearbeiten müssen, muss beim Zugriff auf diese der Zugriff für andere Threads gesperrt werden. Damit wird verhindert, dass ein Thread Daten verwendet, die ein anderer gerade vielleicht noch gar nicht fertig bearbeitet hat, was ungültigen Daten vorbeugt. Das gilt sogar für den Zugriff auf einfache Typen wie long- oder double-Werte, da es beim Lesen oder Schreiben dieser Werte vorkommen kann, dass der Vorgang (auf einem 32-BitBetriebssystem) in zwei Einzelvorgänge aufgeteilt wird und deshalb mittendrin unterbrochen werden kann (was einem anderen Thread die Möglichkeit gibt, auf dieselben Daten zuzugreifen und diese damit ungültig werden zu lassen).
Threads müssen beim Zugriff auf globale Daten diese sperren
12
13
C# bzw. .NET bietet zum Sperren globaler Daten oder Ressourcen ein einfach anzuwendendes Hilfsmittel, das lock-Schlüsselwort (das die Monitor-Klasse kapselt). Diese Technik beschreibe ich im Abschnitt »Den Zugriff auf globale Daten und Ressourcen sperren« ab Seite 1186.
14
Für den Zugriff auf 64-Bit-Zahlwerte bietet die Klasse Interlocked effizientere Möglichkeiten. Diese beschreibe ich ab Seite 1202.
15
Synchronisierung In anderen Fällen werden unterschiedliche Aufgaben in separaten Threads gleichzeitig ausgeführt. Wenn aber ein Thread den Abschluss eines anderen voraussetzt oder die Ergebnisse mehrerer Arbeitsthreads nur gemeinsam verarbeitet werden können, muss eine Synchronisation erfolgen. Neben dem bereits genannten Sperren von Daten und Ressourcen (das auch als Hilfsmittel für die Synchronisation eingesetzt werden kann) können Sie dazu in .NET auch spezielle Methoden der Thread-Klasse und erweiterte Techniken wie WaitHandle-Objekte verwenden. Ab Seite 1195 erfahren Sie mehr darüber.
Threads müssen auch häufig synchronisiert werden
16
17
Die Probleme, die aus dem Sperren und Synchronisieren entstehen können
18
Das Sperren von Objekten und das Synchronisieren sind keine trivialen Techniken, auch wenn die dazu verwendeten Typen recht einfach scheinen. Ein massives Problem dabei sind so genannte Deadlocks, bei denen zwei Threads gegenseitig darauf warten, dass der andere Thread eine gesperrte Ressource freigibt oder ein Synchronisierungs-Objekt signalisiert. Deadlocks und den Umgang damit behandle ich im Abschnitt »Race Conditions und Deadlocks« ab Seite 1191.
19
20
Zugriff auf die Programmoberfläche Ein Thread, der auf die Programmoberfläche (z. B. auf Steuerelemente eines Formulars) zugreifen soll, muss zudem eine wichtige Grundregel der Windows-Programmierung beachten: Nur der Thread, der ein Fenster erzeugt hat, darf auf dieses Fenster zugreifen.
Nur der Thread, der ein Fenster erzeugt hat, darf auf dieses zugreifen.
21
22 Dies bedeutet, dass in einer normalen Anwendung (in der alle Formulare vom UIThread erzeugt werden) nur der UI-Thread auf ein Formular und dessen Steuerelemente zugreifen darf. Ein Arbeitsthread sollte niemals direkt auf Formulare oder Steuerelemente zugreifen.
HALT
23
1143
Multithreading
Wenn Sie diese Warnung nicht beachten, riskieren Sie üble Fehler, deren Ursache Sie nur sehr schwer finden. Diese Fehler resultieren nicht in allen Fällen auch in Ausnahmen, und wenn, dann ist die Ausnahme nur ein Nebenprodukt, das nur wenig oder keine Rückschlüsse auf die wahre Ursache zulässt. Besonders schwierig wird dies, wenn die Fehler nur in besonderen Kontexten und nur beim Kunden auftreten. Der Debugger des .NET Framework (ab Version 2.0) überprüft deswegen beim Ausführen einer Projektmappe in der Debug-Konfiguration, ob ein Arbeitsthread direkt auf Fenster zugreift, die in einem anderen Thread erzeugt wurden, und wirft in diesem Fall eine aussagekräftige InvalidOperationException. In der Release-Konfiguration wird diese Ausnahme aber nicht geworfen, und Ihr Programm ist Cross-ThreadFehlern relativ hilflos ausgesetzt. Einer der Gründe für diese Fehler, die nur sehr schwer nachzuvollziehen sind, liegt an der nicht garantierten Threadsicherheit der Windows-Steuerelemente. Ein anderer Grund liegt in der Architektur der Windows-Nachrichtenverarbeitung. Greift ein Arbeitsthread auf ein Steuerelement oder Fenster zu, das in einem anderen Thread erzeugt wurde, kommt das Windows-Nachrichtensystem für dieses Fenster durcheinander und sendet Nachrichten u. U. an den falschen Thread (den Arbeitsthread), der damit gar nichts anfangen kann. Die fehlende Interaktion mit Windows führt dann zu meist sehr eigenartigen Problemen, wie z. B., dass Teile der Anwendungsoberfläche nicht mehr gezeichnet werden oder bei der Verwendung von SystemObjekten (wie dem Datei-öffnen-Dialog) Fehler auftreten. .NET bietet zur Lösung des UI-Zugriffs-Problems natürlich auch Lösungen. Eine ist die Verwendung der Methoden Invoke oder BeginInvoke, eine andere die Verwendung der BackgroundWorker-Klasse, wie ich im Verlauf dieses Kapitels noch zeige. Näheres dazu erfahren Sie im Abschnitt »Zugriff auf die Benutzeroberfläche« (Seite 1147) und »Einfaches Multithreading mit der BackgroundWorker-Klasse« (Seite 1152).
20.1.6 Wann sollten Sie Multithreading einsetzen? Multithreading macht Sinn, wenn prozessorlastige Aktionen ausgeführt werden
Multithreading macht zunächst in einer Windows-Anwendung immer dann Sinn, wenn eine Aktion viel Zeit in Anspruch nimmt. Wenn Sie länger dauernde Methoden in einer Windows-Anwendung aufrufen, wird der UI-Thread geblockt und kann für diese Zeit keine Windows-Nachrichten verarbeiten. Der Anwender kann mit dem Programm nicht weiterarbeiten. In vielen Fällen wird auch das Fenster nach einem Überdecken durch ein anderes Fenster oder bei Veränderungen durch das Programm nicht neu gezeichnet. Das Problem können Sie teilweise lösen, indem Sie in einer Windows.Forms-Anwendung Application.DoEvents aufrufen. In einer WPF-Anwendung verwenden Sie den Trick, den ich bereits in Kapitel 14 gezeigt habe, um ein Fenster zu aktualisieren: Listing 20.1:
Der Trick zur Aktualisierung eines WPF-Fensters während einer prozessorlastigen Aktion
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { }));
Dabei müssen Sie aber sehr aufpassen, dass der Code, der diesen Trick ausführt, dann u. U. vom Anwender noch einmal ausgeführt werden kann (was beim Multithreading aber auch so ist und was Sie über die Deaktivierung der entsprechenden Steuerelemente effizient verhindern können). Richtiges Multithreading ist das aber nicht, da alle Aufrufe im UI-Thread ausgeführt werden. Wenn die Anwendung im Hintergrund einen Job erledigen soll (z. B. eine
1144
Einführung in Multithreading
Datei herunterladen oder ein Dokument ausdrucken), müssen Sie mit Multithreading arbeiten. Sie müssen dann lediglich Vorkehrungen treffen, dass der Anwender die Thread-Methode nicht mehrfach aufruft (falls dies nicht vorgesehen ist) und dass Sie das Ergebnis beim Ende des Threads sicher verarbeiten können. Auf diese Weise können Sie z. B. ■ ■ ■ ■ ■ ■
komplexe mathematische Berechnungen, komplexe (3D-)Grafikausgaben, das Drucken großer Dokumente, das langwierige Herunterladen von Dateien von einem Webserver, das Versenden und Empfangen von Daten (z. B. über Aufrufe von WebdienstMethoden), und das Aufräumen im Hintergrund (ähnlich dem Garbage Collector)
12
13
14
in Threads ausführen. Ein anderer Einsatz von Threads ist die Performance-Steigerung bei aufwändigen Berechnungen oder beim Herunterladen von mehreren Dateien aus dem Internet. Wenn Sie die Ergebnisse zweier oder mehrere separater Berechnungen benötigen, kann es vorteilhafter sein, diese Berechnungen in separaten Threads auszuführen, was besonders für Mehrprozessor-Maschinen gilt. Dabei können Sie über die Priorität der Threads die wichtigeren (oder potenziell länger dauernden) Berechnungen bevorzugen. Im Einzelfall müssen Sie dies jedoch immer ausprobieren.
Threads können die Performance steigern
15
16
Wenn Sie gleichzeitig mehrere Dateien aus dem Internet herunterladen müssen (oder von einem langsamen Netzwerk), machen mehrere Threads auf jeden Fall Sinn, da Sie die hohe Performance des Rechners bei der langsamen Performance der einzelnen Downloads damit optimal ausnutzen können.
17
Sie sollten sich beim Einsatz des Multithreading folgende Fragen stellen: ■ ■
20.1.7
■
■
19
Nachteile des Multithreading
Multithreading besitzt natürlich auch Nachteile: ■
18
Ist eine saubere Abtrennung der Thread-Methoden von der Benutzerschnittstelle möglich? Kann die Anwendung wirklich neben der Ausführung der Thread-Methode andere Aufgaben erfüllen?
Threads benötigen bei ihrer Erzeugung einige Millisekunden Zeit und belegen ca. einen MB Speicherplatz. Verwendet Ihre Anwendung sehr viele Threads, kann das zum Problem werden. Dieses Problem können Sie allerdings über die ThreadPool-Klasse (Seite 1173) lösen. Die Umschaltung von einem Thread zu einem anderen kostet in Windows ebenfalls Zeit, weil dazu die CPU-Register zunächst für den aktuellen Thread gesichert und für den nächsten wiederhergestellt werden müssen. Wenn Ihre Threads nicht sehr effizient sind und eine große Prozessorlast verursachen, kann es sein, dass die Anwendung mit Threads langsamer läuft als ohne. Ein wesentlich größeres Problem ist das schwierigere und aufwändigere Programmieren. Neben dem komplexeren Zugriff auf die Benutzerschnittstelle müssen Sie sehr sorgsam bei der Verwendung globaler Daten und der Synchronisation von Threads sein. Zu viel Synchronisation kann die Performance negativ beeinflussen.
Multithreading kann die Performance auch vermindern
20 21
22
23
1145
Multithreading
■
■
Außerdem können beim Sperren von globalen Daten oder Ressourcen und bei der Synchronisation Deadlocks entstehen, bei denen ein Thread auf eine Ressource wartet, die ein anderer Thread geblockt hat, und der andere Thread auf eine andere Ressource wartet, die der erste Thread geblockt hat Daneben müssen Sie darauf achten, dass das Ergebnis eines Threads, der ein solches berechnet, korrekt weiterverarbeitet werden kann. Erwartet die Anwendung ein Ergebnis nur, wenn sie sich in einem bestimmten Status befindet, und der Anwender hat diesen zwischenzeitlich geändert, sind Probleme vorprogrammiert. Diese Probleme können Sie über eine Synchronisation lösen. Das Verstehen, Testen und Debuggen einer Anwendung mit mehreren Threads ist (trotz der recht guten Debugging-Unterstützung) wesentlich schwieriger als in einer einfachen Anwendung. In einer Anwendung mit vielen Threads ist es sehr schwierig sich vorzustellen, was alles quasi gleichzeitig ausgeführt wird. Außerdem wird die Fehlerbehandlung erschwert, weil Ausnahmen nur innerhalb der Thread-Methode abgefangen werden können.
Multithreading ist also, trotz der sehr guten Unterstützung in .NET, nicht trivial. Multithreading-Programme sind immer komplexer als Anwendungen, die keine Threads verwenden. Damit bietet sich Raum für potenzielle Fehlerquellen und das Testen, Debuggen und das spätere Erweitern der Anwendung wird nicht unwesentlich erschwert. Lassen Sie sich davon aber nicht entmutigen. Wenn Sie sorgsam planen, möglichst wenig globale Ressourcen verwenden und Synchronisationsprobleme über eine durchdachte Architektur vermeiden (oder wenn Sie nur einen Thread ausführen müssen), ist Multithreading ein einfaches Hilfsmittel, um Anwendungen benutzbarer zu machen.
20.1.8 Die Möglichkeiten Unter .NET haben Sie einige Möglichkeiten, Multithreading zu implementieren. In der folgenden Auflistung stelle ich diese kurz (mit ihren Vor- und Nachteilen) vor: ■
■
■
1146
Die Verwendung der Thread-Klasse gibt Ihnen Zugriff auf die Basis-Technologie. Diese Variante des Multithreading erlaubt es Ihnen, Threads besser zu kontrollieren als mit den anderen Varianten. Sie können z. B. die Priorität einstellen, bestimmen, ob es sich um einen Vorder- oder Hintergrundthread handelt (die von den anderen Möglichkeiten verwendeten Threads sind immer Hintergrundthreads) und den Thread direkt (hart) abbrechen. Außerdem können Sie dem Thread einen Namen geben, was beim Debugging ein wichtiges Feature ist. Wenn Ihr Programm darauf reagieren soll, dass ein echter Thread beendet ist, oder wenn Sie Ergebnisse auswerten wollen, müssen Sie allerdings speziell programmieren. Das asynchrone Ausführen einer Methode vereinfacht die Auswertung des Endes des Threads (der intern verwendet wird) und die Rückgabe von Ergebnissen. Außerdem skalieren asynchron aufgerufene Methoden häufig besser als echte Threads, da die dazu verwendeten Threads aus dem globalen Thread-Pool der CLR entnommen werden. Steuern können Sie das asynchrone Ausführen dafür nicht bzw. nur über eigene Implementierungen. Asynchrone Methoden eignen sich für kurze Operationen und vor allen Dingen für Berechnungen. Die Verwendung der BackgroundWorker-Klasse macht Multithreading noch einfacher. Hier nutzen Sie die Vorteile, dass der interne Thread (in einem Ereignis)
Zugriff auf die Benutzeroberfläche
einen Fortschritt melden kann, dessen Ergebnis direkt in die Oberfläche geschrieben werden kann (was bei anderen Threads nicht möglich ist, siehe im folgenden Abschnitt). Über ein anderes Ereignis werten Sie das Ende der asynchronen Ausführung. Der Nachteil des BackgroundWorker ist ein wesentlich größerer Overhead. Threads aus einem eigenen Thread-Pool (in Form der ThreadPool-Klasse) verwenden Sie immer dann, wenn Sie wenig Kontrolle über die Threads benötigen, diese nur kurz laufen und mehr warten als arbeiten. Der Overhead eines Thread-Pool ist im Vergleich zu echten Threads wesentlich geringer, weswegen dieser auch für Anwendungen geeignet ist, die sehr viele Threads ausführen (wie Webserver).
■
20.2
12
13
Zugriff auf die Benutzeroberfläche
Wenn Sie Threads entwickeln oder Methoden asynchron ausführen, müssen Sie die goldene Windows-Regel einhalten, dass Sie niemals aus einem Thread heraus auf Fenster, Formulare oder Steuerelemente zugreifen dürfen, die in einem anderen Thread erzeugt wurden. Die Gründe dafür habe ich bereits auf Seite 1143 genannt.
Arbeitsthreads dürfen nicht direkt auf die Oberfläche zugreifen
15
Sie können den Zugriff in einer WPF-Anwendung aber über die Invoke-Methode des Dispatcher-Objekts ausführen, das die Dispatcher-Eigenschaft eines Fensters oder Steuerelements zurückgibt. In einer Windows.Forms-Anwendung verwenden Sie dazu die Invoke-Methode, die alle Formulare und Steuerelemente direkt zur Verfügung stellen. Invoke übergeben Sie einen Delegat für eine beliebige Methode, die auf die Steuerelemente und/oder das Fenster zugreift. Invoke ruft diese Methode auf, leitet den Aufruf aber in den Thread um, der das Fenster, Formular oder Steuerelement besitzt. Als Delegat können Sie hier wie üblich eine anonyme Methode, einen Lambda-Ausdruck oder eine normale Methode übergeben.
14
16 Invoke löst das Problem
17
18
In eine WPF-Anwendung müssen Sie der Invoke-Methode am ersten Argument eine Priorität in Form der DispatcherPriority-Aufzählung übergeben. Am zweiten Argument übergeben Sie den Delegat mit der auszuführenden Aktualisierungsmethode.
19
Das folgende Beispiel greift ein wenig vor und erzeugt einen Thread, dessen Methode eine Schleife ausführt. In der Schleife wird ein Label aktualisiert, das auf einem WPF-Fenster liegt:
20
Listing 20.2: Aktualisieren der Benutzeroberfläche in einem Arbeitsthread /* Wird im Demo-Thread gestartet */ private void Demo() { for (int i = 0; i < 100; i++) { // Das Label aktualisieren this.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() => { this.lblInfo.Content = "Durchlauf " + i; }));
21
22
23
// Kleine Pause Thread.Sleep(100); } }
1147
Multithreading
/* Startet den Thread */ private void btnStartThread_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(this.Demo); thread.IsBackground = true; thread.Start(); }
Obwohl Lambda-Ausdrücke in diesem Buch bereits behandelt wurden, sieht der Programmcode vielleicht etwas gewöhnungsbedürftig aus. Invoke erwartet am zweiten Argument (in einer Windows.Forms-Anwendung am ersten) einen beliebigen Delegat. Ich übergeben hier eine neue Instanz des vordefinierten Delegaten Action, dessen Signatur keine Argumente und keine Rückgabe definiert. Der Konstruktor eines Delegaten erwartet die Adresse der Methode, die er kapseln soll. Ich übergebe hier einen Lambda-Ausdruck, der mit dem Action-Delegaten kompatibel ist. Dieser Ausdruck ist verkürzt (ohne Code) der folgende: () => { }. Mir ist bewusst, dass der Lambda-Ausdruck das Lesen des Codes nicht gerade einfacher macht. Lambda-Ausdrücke sind aber gerade in Verbindung mit Invoke eine echte Hilfe (OK, eine anonyme Methode wäre genauso hilfreich), weil Sie nicht extra eine separate Methode zur Aktualisierung der Oberfläche schreiben müssen. Die DispatcherPriority gibt in einer WPF-Anwendung übrigens an, wann die definierte Methode ausgeführt wird. Diese Priorität bezieht sich auf andere Vorgänge, die zurzeit auch in der Warteschlange des Thread auf ihre Ausführung warten, wie z. B. das Rendering oder die Datenbindung. Die im Beispiel verwendete normale Priorität ist die häufigste Priorität für eine Anwendung.
EXKURS
Ein Dispatcher verwaltet pro Thread eine Warteschlange für auszuführende Aufgaben. Diese Warteschlange enthält Einträge, denen eine Priorität zugeordnet ist. Einträge mit einer höheren Priorität werden eher ausgeführt, als Einträge mit einer niedrigeren. Deswegen besteht auch die Möglichkeit, dass synchrone Vorgänge nicht sofort ausgeführt werden, wenn diese eine niedrige Priorität besitzen, weil andere Vorgänge mit einer höheren Priorität ebenfalls in der Warteschlange auf die Ausführung warten. Welche Dispatcher-Eigenschaft Sie in einer WPF-Anwendung oder welches UIObjekt Sie in einer Windows.Forms-Anwendung für den Aufruf der Invoke-Methode verwenden, ist relativ unerheblich. Wichtig ist nur, dass diese zu einem Steuerelement oder Fenster/Formular gehört, das in demselben Thread angelegt wurde, wie die anderen zu aktualisierenden Objekte. Ich verwende im Beispiel deswegen einfach die Dispatcher-Eigenschaft des Fensters (die dasselbe Dispatcher-Objekt referenziert wie die gleichnamige Eigenschaft der Steuerelemente).
20.3 Asynchrones Ausführen ist einfaches Multithreading
1148
Asynchrones Ausführen von Methoden
Asynchrones Ausführen von Methoden ist eine relativ einfach zu programmierende Variante des Multithreading. Wie ich in der Einführung bereits erwähnt habe, ist ein asynchroner Methodenaufruf einem direkt aufgerufen Thread prinzipiell ähnlich, weil ein asynchroner Methodenaufruf ebenfalls in einem Thread ausgeführt wird. Ein Unterschied zum Multithreading ist, dass der verwendete Thread aus dem globalen Thread-Pool der CLR entnommen wird und aus diesem Grunde u. U. besser skaliert. Außerdem können Sie einer asynchron aufgerufenen Methode problemlos Rückgabewerte auswerten und auf das Beenden der Methode reagieren. Dafür können Sie die Ausführung einer solchen Methode nicht steuern, wie dies bei echten
Asynchrones Ausführen von Methoden
Threads möglich ist. Sie können eine asynchron aufgerufene Methode z. B. nicht ohne eine eigene Implementierung abbrechen. Die Priorität des verwendeten Threads ist ebenfalls nicht einstellbar. Außerdem ist der Thread einer asynchron aufgerufenen Methode immer ein Hintergrundthread. Der Thread wird also mit der Anwendung beendet, was natürlich in der Praxis auch Probleme verursachen kann. Alles andere, was für echte Threads gilt (dass diese nicht direkt auf die Benutzerschnittstelle zugreifen dürfen, beim Zugriff auf gemeinsame Daten diese blockieren und eventuell mit anderen Threads synchronisiert werden müssen) gilt aber auch für den asynchronen Aufruf von Methoden.
12 INFO
13
20.3.1 Die asynchron auszuführende Methode Zum asynchronen Ausführen einer Methode benötigen Sie natürlich zunächst die Methode selbst. Die asynchrone Ausführung erfolgt später über einen Delegaten. Diesen können Sie natürlich auch in Form einer anonymen Methode oder eines LambdaAusdrucks erzeugen. Ich verwende hier aber zur Demonstration eine klassische Methode. Diese berechnet die nächste Primzahl, die größer ist als die übergebene Zahl (bewusst nicht in einer optimierten Version):
14
15
Listing 20.3: Methode zur Berechnung einer Primzahl
16
private long NextPrimeNumber(long start) { try { for (long number = start; number { });
13
Beachten Sie, dass ar => { } ein Lambda-Ausdruck ist, der dem AsyncCallback-Delegaten entspricht. Die Methode muss natürlich programmiert werden. In der Methode werten Sie die Rückgabe der asynchron aufgerufenen Methode aus (natürlich nur, falls diese etwas zurückgibt). Diese erhalten Sie über einen Aufruf der EndInvoke-Methode des Delegaten, der die Methode asynchron ausführt. Der Compiler hat diese Methode dynamisch erzeugt, weswegen sie den Typ zurückgibt, den die asynchron aufgerufene Methode zurückgibt:
14
15
Listing 20.5: Auslesen des Ergebnisses der asynchron aufgerufenen Methode AsyncCallback callback = new AsyncCallback(ar => { // EndInvoke aufrufen, um das Ergebnis zu erhalten long primeNumber = primeNumberDelegate.EndInvoke(ar);
16
EndInvoke liest nicht nur das Ergebnis aus. Diese Methode wartet zudem darauf, dass der Thread, der die asynchrone Methode ausführt, komplett beendet ist.
17
Das Auslesen ist im Beispiel sehr einfach, weil dieses eine Referenz auf den Delegaten besitzt, der die Methode asynchron ausführt. Wenn Sie statt eines Lambda-Ausdrucks oder einer anonymen Methode für den Callback eine normale Methode verwenden, erhalten Sie folgendermaßen eine Referenz auf den Delegaten:
18
Listing 20.6: Optionales Auslesen des Delegaten, der die asynchrone Methode ausführt
19
AsyncResult result = (AsyncResult)ar; Func primeNumberDelegate = (Func)ar.AsyncDelegate;
Nun müssen der Schalter wieder aktiviert und das Info-Label aktualisiert werden. Da die Callback-Methode in einem Arbeitsthread ausgeführt wird, darf das Programm, wie Sie ja bereits wissen, nicht direkt auf den Schalter und das Label zugreifen. Der Zugriff erfolgt über die Invoke-Methode des Dispatcher-Objekts, das die DispatcherEigenschaft eines Fensters oder Steuerelements zurückgibt: Listing 20.7:
Die Oberfläche müssen Sie über Invoke aktualisieren
20 21
Aktualisieren der Steuerelemente im Ende-Callback
22
this.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() => { this.lblInfo.Content = primeNumber.ToString(); this.btnCalculatePrimeNumber.IsEnabled = true; })); });
23
1151
Multithreading
20.3.4 Der asynchrone Aufruf BeginInvoke ruft die Methode asynchron auf und gibt ein IAsyncResultObjekt zurück
Die eigentliche Methode rufen Sie dann über die BeginInvoke- Methode des Delegaten auf. BeginInvoke wird vom Compiler dynamisch erzeugt und enthält zusätzlich die Argumente, die im Delegaten deklariert sind. In unserem Fall besitzt BeginInvoke deswegen an erster Stelle ein Argument vom Typ long. An diesem Argument übergeben Sie die Zahl, ab der die nächste Primzahl gesucht werden soll. BeginInvoke gibt eine Referenz auf ein Objekt zurück, das die Schnittstelle IAsyncResult implementiert. Wenn Sie diese Referenz in einer Variablen oder einem Feld speichern, können Sie über das referenzierte Objekt u. a. den Status der asynchronen Operation ermitteln und über die Eigenschaft IsCompleted abfragen, ob der asynchrone Aufruf zum Zeitpunkt der Abfrage beendet wurde. Damit können Sie ein Programm implementieren, bei dem der Anwender oder ein Timer (oder Thread) immer wieder nachschauen, ob der asynchrone Aufruf beendet wurde. In unserem Fall ist das aber nicht notwendig.
Den Callback-Delegaten übergeben Sie BeginInvoke am Argument callback. Am letzten Argument können Sie benutzerdefinierte Daten übergeben, die in der Callback-Methode ausgewertet werden können. Für das einfache Beispiel benötige ich dieses Argument nicht und übergebe an dieser Stelle null: Listing 20.8: Der eigentliche asynchrone Aufruf der Methode IAsyncResult asyncResult = primeNumberDelegate.BeginInvoke( startNumber, callback, null);
Und schon ist der asynchrone Aufruf von Methoden fertig.
20.3.5 Die Prozessorlast Wenn Sie sich während der Ausführung der Anwendung die Prozessorlast im Windows Task Manager anschauen, werden Sie bemerken, dass der Prozessor oder die Prozessoren sehr stark ausgelastet werden. Ihre Anwendung bleibt dabei aber gut benutzbar. Andere Anwendungen werden aber möglicherweise nicht mit ihrer üblichen Performance ausgeführt. Das liegt daran, dass die Berechnung der Primzahl sehr prozessorintensiv ist. Sie können die Prozessorlast erheblich verringern, indem Sie in der inneren Schleife den Thread mit Thread.Sleep(1) für eine Millisekunde pausieren. In diesem Fall wird die Berechnung aber natürlich auch wesentlich langsamer ausgeführt.
20.4
Einfaches Multithreading mit der BackgroundWorker-Klasse
Die Klasse BackgroundWorker aus dem Namensraum System.ComponentModel erleichtert die Programmierung von Threads, die auf die Benutzeroberfläche zugreifen. Mit dieser Klasse ersparen Sie sich die Deklaration und Verwendung von Delegaten für den Zugriff auf Steuerelemente für Status- oder Fortschrittsmeldungen und besitzen gleich noch eine Abbruchmöglichkeit, ein Ereignis für einen Fortschritt und ein Ereignis für das Ende des Threads. Eine BackgroundWorker-Instanz besitzt dazu einige Methoden, Eigenschaften und Ereignisse, die in Tabelle 20.1 beschrieben werden.
1152
Einfaches Multithreading mit der BackgroundWorker-Klasse
Methode / Eigenschaft / Beschreibung Ereignis RunWorkerAsync( [object argument ])
startet die asynchrone Ausführung. Am Argument argument können Sie ein beliebiges Objekt übergeben, das Sie in DoWork auswerten können.
DoWork
Dieses Ereignis wird nach dem Aufruf von RunWorkerAsync automatisch aufgerufen. In dem Ereignishandler führen Sie den asynchronen Programmcode aus. Über die Eigenschaft Argument des Ereignisargument-Objekts können Sie das an RunWorkerAsync ggf. übergebene Argument auslesen. Die Eigenschaft Cancel setzen Sie, wenn Sie die Ausführung abbrechen. Den Wert von Cancel können Sie im RunWorkerCompleted-Ereignis auswerten. In die Eigenschaft Result des Ereignisargument-Objekts können Sie ein Ergebnis schreiben, das Sie ebenfalls in RunWorkerCompleted auswerten können.
Tabelle 20.1: Wichtige Eigenschaften, Methoden und Ereignisse der BackgroundWorker-Klasse
12
13
Über diese Methode melden Sie innerhalb von DoWork threadsicher einen Fortschritt. Der Aufruf von ReportProgress resultiert in dem Aufruf des ProgressChanged-Ereignisses. Dieser Aufruf wird vom BackgroundWorker aber automatisch in den Thread umgeleitet, der den BackgroundWorker erzeugt hat (also in der Regel in den UI-Thread). Deswegen können Sie in dem Ereignishandler für dieses Ereignis ohne Weiteres auf Steuerelemente und Fenster/Formulare zugreifen (sofern der BackgroundWorker in dem Thread erzeugt wurde, der auch die Steuerelemente, Fenster bzw. Formulare erzeugt hat).
14
ProgressChanged
Dieses Ereignis wird nach einem Aufruf von ReportProgress aufgerufen. Aus der Eigenschaft ProgressPercentage des Ereignisargument-Objekts können Sie den übergebenen Prozentwert auslesen, die Eigenschaft UserState liefert den eventuell übergebenen Statuswert (wie z. B. eine Textmeldung).
16
CancelAsync
Diese Methode ermöglicht den Abbruch der asynchronen Operation. Nach dem Aufruf ist die Eigenschaft CancellationPending der BackgroundWorkerInstanz true. Diese Eigenschaft werten Sie in DoWork an geeigneter Stelle aus, um die Verarbeitung abzubrechen. Wenn Sie in DoWork abbrechen, setzen Sie die Eigenschaft Cancel des Ereignisargument-Objekts auf true, um RunWorkerCompleted darüber zu informieren, dass abgebrochen wurde.
ReportProgress( int percentProgress [, object userState])
RunWorkerCompleted
15
17
18
Dieses Ereignis wird aufgerufen, nachdem die asynchrone Verarbeitung beendet wurde. Über die Eigenschaft Error des Ereignisargument-Objekts können Sie ermitteln, ob in der asynchron ausgeführten Methode eine Ausnahme aufgetreten ist. Über Cancelled stellen Sie fest, ob die Verarbeitung abgebrochen wurde. In der Eigenschaft Result erhalten Sie das in DoWork übergebene Ergebnis der Operation.
19
20
Asynchrone Methoden, die auf die Benutzeroberfläche zugreifen und die einen Fortschritt melden, können Sie mit einer BackgroundWorker-Instanz wesentlich einfacher implementieren, als über einen asynchronen Aufruf oder einen Thread. Zur Demonstration verwende ich ein einfaches Beispiel, das eine Zahl hochzählt, das Ergebnis in einem Label ausgibt und eine ProgressBar aktualisiert (Abbildung 20.2).
21
22
Zur Implementierung dieser im Beispiel demonstrierten kompletten Verwendung aller Features der BackgroundWorker-Klasse benötigen Sie im Fenster ein Feld vom Typ BackgroundWorker. Da es sich um eine (Windows.Forms-)Komponente handelt, können Sie diese in einer Windows.Forms-Anwendung aus der Toolbar auf ein Formular ziehen. In einer WPF-Anwendung müssen Sie das Feld aber (in der zu der Zeit des Schreibens dieser Zeilen aktuellen WPF-Version) klassisch (was ja eigentlich unlogisch ist …) anlegen und initialisieren.
23
1153
Multithreading
Abbildung 20.2: Die BackgroundWorker-Beispielanwendung
Listing 20.9: Erzeugen und Initialisieren eines BackgroundWorker public partial class MainWindow : Window { /* Konstruktor. Initialisiert das Fenster. */ public MainWindow() { InitializeComponent(); } /* Der BackgroundWorker */ BackgroundWorker worker; /* Initialisiert das Fenster */ private void Window_Loaded(object sender, RoutedEventArgs e) { // BackgroundWorker erzeugen und initialisieren this.worker = new BackgroundWorker() { WorkerReportsProgress = true, WorkerSupportsCancellation = true, }; // Ereignisse zuweisen this.worker.DoWork += new DoWorkEventHandler(this.worker_DoWork); this.worker.ProgressChanged += new ProgressChangedEventHandler(this.worker_ProgressChanged); this.worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this.worker_RunWorkerCompleted); }
INFO
DoWork führt den Job aus
Neben der Zuweisung der Methoden für die drei Ereignisse DoWork, ProgressChanged und RunWorkerCompleted müssen Sie die Eigenschaften WorkerReportsProgress und WorkerSupportsCancellation beachten. Sind diese Eigenschaften false, erhalten Sie beim Aufruf von ReportProgress bzw. CancelAsync eine InvalidOperationException. In der Methode, die Sie dem DoWork-Ereignis zugewiesen haben, implementieren Sie die asynchron auszuführenden Anweisungen. Über e.Argument können Sie ein beliebiges Objekt auslesen, das Sie beim Starten des BackgroundWorkers übergeben können. Im unteren Beispiel wird an diesem Argument der letzte Index übergeben, bis zu dem die Demo-Schleife ausgeführt werden soll. Wenn Sie einen Abbruch ermöglichen, überprüfen Sie an gegebener Stelle, ob die Eigenschaft CancellationPending des übergebenen Ereignisargument-Objekts true ist. Ist dies der Fall, setzen Sie e.Cancel auf true und brechen die Ausführung ab. Einen Fortschritt können Sie über die ReportProgress-Methode der BackgroundWorker-Instanz melden. Dieser Methode können Sie einen Prozentwert und ein beliebiges Objekt (z. B. einen String) übergeben.
1154
Einfaches Multithreading mit der BackgroundWorker-Klasse
Listing 20.10: Die Arbeitsmethode des BackgroundWorker private void worker_DoWork(object sender, DoWorkEventArgs e) { // Den Worker referenzieren BackgroundWorker worker = (BackgroundWorker)sender; // Den übergebenen Maximalwert ermitteln long maxValue = Convert.ToInt32(e.Argument);
12
// Einen Job simulieren for (long i = 0; i < maxValue; i++) { // Auswerten, ob abgebrochen werden soll if (worker.CancellationPending) { e.Cancel = true; break; }
13
14
// Den Fortschritt melden int percentProgress = (int)(((i + 1) / (double)maxValue) * 100); this.worker.ReportProgress(percentProgress, "Durchlauf " + (i + 1) + "...");
15
// Kleine Pause zur Demo Thread.Sleep(10); }
16
}
Eine Ausnahmebehandlung müssen Sie in dieser Methode nicht unbedingt vorsehen. Zwar gilt hier wie bei Threads im Allgemeinen, dass Ausnahmen in asynchron ausgeführten Methoden nicht an die Benutzeroberfläche weitergegeben werden (was im Abschnitt »Ausnahmen in Threads und beim asynchronen Aufruf von Methoden« ab Seite 1176 noch behandelt wird). Der BackgroundWorker fängt Ausnahmen aber selbst ab, referenziert diese und gibt die Referenzen an die Eigenschaft Error des Ereignisargument-Objekts des RunWorkerCompleted-Ereignisses weiter.
Ausnahmen müssen Sie nicht in der Arbeitsmethode behandeln
17
18
Die Methode, die Sie mit dem ProgressChanged-Ereignis verknüpft haben, übernimmt die Ausgabe eines Fortschritts auf der Benutzeroberfläche. Die entsprechenden Daten lesen Sie aus dem Ereignisargument-Objekt aus:
19
Listing 20.11: Der Ereignishandler für das ProgressChanged-Ereignis
20
private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { // Den prozentualen Fortschritt auslesen this.progressBar.Value = e.ProgressPercentage;
21
// Die Statusdaten auslesen this.lblInfo.Content = e.UserState.ToString(); }
In der Methode für das RunWorkerCompleted-Ereignis werten Sie aus, ob innerhalb der asynchron ausgeführten Arbeitsmethode eine Ausnahme eingetreten ist. Zusätzlich dazu fragen Sie ab, ob die Ausführung abgebrochen wurde:
22
23
1155
Multithreading
Listing 20.12: Der Ereignishandler für das RunWorkerCompleted-Ereignis private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // Auwserten, ob ein Fehler aufgetreten ist oder ob abgebrochen wurde if (e.Error != null) { MessageBox.Show("Während der Ausführung des Jobs " + "ist ein Fehler aufgetreten: " + e.Error.Message); } else if (e.Cancelled) { MessageBox.Show("Abgebrochen"); } else { MessageBox.Show("Fertig"); } // Info-Steuerelemente zurücksetzen this.lblInfo.Content = null; this.progressBar.Value = 0; // Die Schalter wieder aktivieren this.btnStart.IsEnabled = true; this.btnCancel.IsEnabled = false; }
Nun müssen Sie den BackgroundWorker nur noch über die RunWorkerAsync-Methode starten, wobei Sie das Argument übergeben können, das Sie in DoWork auswerten: Listing 20.13: Starten des BackgroundWorker private void btnStart_Click(object sender, RoutedEventArgs e) { this.btnStart.IsEnabled = false; this.btnCancel.IsEnabled = true; this.worker.RunWorkerAsync(1000); }
Und schließlich müssen Sie noch die Methode für den Abbrechen-Schalter implementieren: Listing 20.14: Abbruch der asynchronen Ausführung private void btnCancel_Click(object sender, RoutedEventArgs e) { this.worker.CancelAsync(); }
20.5
Einfache Threads
Threads bieten wesentlich mehr Möglichkeiten, die asynchrone Ausführung zu steuern, als asynchron aufgerufene Methoden oder der BackgroundWorker. So können Sie für jeden Thread die Priorität bestimmen (was bei Threads, die den Prozessor stark auslasten, wichtig sein kann) und festlegen, ob es sich um einen Vorder- oder Hintergrundthread handelt.
1156
Einfache Threads
20.5.1 Threads erzeugen und starten Für die Ausführung eines Threads benötigen Sie zunächst eine Methode, die die Arbeit ausführen soll. Der Thread führt die Methode komplett aus. Normalerweise reicht es, eine Methode zu schreiben, die ganz normal von oben nach unten abgearbeitet wird. Einige Threads führen aber auch Methoden mit Endlos-Schleifen aus, die z. B. auf hereinkommende Daten warten und diese über ein Ereignis oder eine Warteschlange (Queue) dem UI-Thread (oder einem anderen Thread) melden.
Threads führen eine Methode aus
12
Thread-Methoden müssen dem Delegaten ThreadStart oder ParameterizedThreadStart (aus dem Namensraum System.Threading) entsprechen. ThreadStart definiert keine Argumente und keine Rückgabe. ParameterizedThreadStart besitzt ein Argument vom Typ Object:
13
public delegate void ParameterizedThreadStart(Object obj)
An diesem Argument übergibt die CLR ein Objekt, das Sie beim Start des Thread der Start-Methode (optional) übergeben. Damit haben Sie die Möglichkeit, der ThreadMethode Initialisierungsdaten zu übergeben. Eine Rückgabe können Sie nicht auswerten (was wegen des asynchronen Aufrufs auch gar nicht möglich wäre). Dieses Problem löse ich im Abschnitt »Das Ende von Threads signalisieren, Ergebnisse zurückgeben und Argumente einfacher übergeben« ab Seite 1160. Um einem verbreiteten Missverständnis vorzubeugen: Falls Sie mit einer normalen Methode arbeiten (nicht mit einer anonymen Methode oder einem Lambda-Ausdruck), spielt es keine Rolle, ob es sich um eine Instanz- oder eine Klassenmethode handelt. Beide Arten von Methoden werden auf nahezu die gleiche Weise nur ein einziges Mal im Arbeitsspeicher angelegt. Der Unterschied ist lediglich, dass auf Instanzmethoden über die Adresse der Instanz der Klasse oder Struktur zugegriffen wird und diese deshalb auf die Daten des jeweiligen Objekts zugreifen kann (bei Klassenmethoden wird über die Adresse der Klasse auf die Methoden zugegriffen). Auch wenn mehrere Threads dieselbe Methode ausführen, gibt es keine Probleme, sofern die Threads nicht auf globale Daten zugreifen. Jeder Thread besitzt schließlich einen eigenen Stack, auf dem alle Argumente und lokale Variablen abgelegt werden. Aufpassen müssen Sie lediglich, wenn Thread-Methoden statische Felder oder Eigenschaften lesen und/oder schreiben oder auf externe Ressourcen wie Dateien zugreifen. Dazu erfahren Sie mehr im Abschnitt »Den Zugriff auf globale Daten und Ressourcen sperren« ab Seite 1186.
14
15
16 INFO
17
18
19
Obwohl es natürlich auch möglich ist, einem Thread eine anonyme Methode oder einen Delegaten zu übergeben, setze ich im Beispiel eine normale Methode ein. Diese führt lediglich eine Schleife aus und gibt den Namen des Thread und den aktuellen Index an der Konsole aus. Der Name des Thread wird der Thread-Instanz später übergeben. Den Namen erhalte ich über die Name-Eigenschaft des aktuellen Thread, der über Thread.CurrentThread referenziert wird:
20 21
Listing 20.15: Eine einfache Thread-Methode, die dem Delegaten ParameterizedThreadStart entspricht
22
private static void ThreadMethod(object args) { long maxNumber = Convert.ToInt32(args); for (long i = 0; i < maxNumber; i++) { Console.Write("{0}:{1:00} ", Thread.CurrentThread.Name, i); Thread.Sleep(100); } }
23
1157
Multithreading
Falls Sie die Leerzeichen verwirren, die ich im String angebe, der Console.Write übergeben wird: Diese sorgen dafür, dass die Ausgabe an der Konsole besser aussieht. Die Klasse Thread repräsentiert einen Thread
Um einen Thread zu implementieren, erzeugen Sie eine Instanz der Klasse System.Threading.Thread und übergeben die Thread-Methode am Konstruktor. Wenn Sie eine normale Methode übergeben, müssen Sie dazu keine Instanz des ThreadStart- oder des ParameterizedThreadStart-Delegaten verwenden. Der Compiler erzeugt diese automatisch für Sie: Listing 20.16: Erzeugen der Thread-Instanz Thread thread1 = new Thread(ThreadMethod); ich hier (in ei
IsBackground legt fest, ob es sich um einen Hintergrundthread handelt
Über die Eigenschaft IsBackground legen Sie fest, ob es sich um einen Vorder- oder Hintergrundthread handeln soll. Die Voreinstellung ist false. Wenn Sie wollen, dass der Thread automatisch mit der Anwendung beendet wird, müssen Sie hier also true eintragen. Über die Eigenschaft Name können Sie dem Thread einen Namen geben. Im Beispiel habe ich diesen Namen an der Konsole ausgegeben. Der Thread-Name spielt allerdings auch beim Debugging eine große Rolle. Sie sollten Ihre Threads deswegen grundsätzlich benennen. Den Thread können Sie schließlich über die Start-Methode starten. Listing 20.17: Starten eines Hintergrundthread, der automatisch mit der Anwendung beendet wird thread1.IsBackground = true; thread1.Name = "Demo1"; thread1.Start(30);
Sie können auch noch einen zweiten Thread mit derselben Methode erzeugen, der dann parallel ausgeführt wird: Listing 20.18: Erzeugen eines zweiten Thread, der dieselbe Methode ausführt Thread thread2 = new Thread(ThreadMethod); thread2.Name = "Demo2"; thread2.IsBackground = true; thread2.Start(30);
Abbildung 20.3 zeigt das Ergebnis nach dem Beenden beider Threads. Abbildung 20.3: Konsolenanwendung mit zwei Beispiel-Threads
Wie Sie sehen, werden die beiden Threads abwechselnd ausgeführt. Dass das Ganze im Beispiel sehr synchron geschieht, ist allerdings in der Praxis eher selten der Fall. Es ist wahrscheinlicher, dass ein Thread einmal etwas länger ausgeführt wird als der andere und dann Zeit hat, mehrere Schleifendurchläufe auszuführen. Das hängt u. a. auch von der Priorität des Thread ab. In meinem Beispiel ist die Ursache, dass die
1158
Einfache Threads
100-ms-Pause Windows genug Zeit gibt, die Threads sauber abzuwechseln. Nehmen Sie die Pause weg, sind die Threads schon nicht mehr synchron (Abbildung 20.4). Abbildung 20.4: Konsolenanwendung mit zwei Beispiel-Threads, die ohne Pause ausgeführt werden
20.5.2 Threads debuggen
13
Visual Strudio macht das Debuggen von Threads sehr einfach. Zum einen können Sie in Thread-Methoden wie auch in anderen mit Haltepunkten arbeiten. Visual Studio hält beim Auftreten einer Ausnahme in einem Thread ebenfalls an etc.
14
In Multithreading-Anwendungen die Übersicht zu behalten, welcher Thread gerade ausgeführt wird, ist aber nicht so einfach. Visual Studio hilft dabei enorm, indem alle laufenden Threads angehalten werden, wenn nur einer der Threads im Debugger anhält. Probieren Sie dies in dem obigen Beispiel aus, indem Sie in der ThreadMethode einen Haltepunkt setzen.
Visual Studio hält beim Debuggen alle Threads an
Die laufenden Threads können Sie sich im Thread-Fenster anschauen, das Sie über den Befehl FENSTER / THREADS im DEBUGGEN-Menü erreichen. Dieses Fenster zeigt alle laufenden Threads mit deren ID (relativ uninteressant) und deren Name an. Da Sie den Namen eines Thread über die Name-Eigenschaft der Thread-Klasse einstellen können, haben Sie damit eine sehr gute Möglichkeit, die laufenden Threads zuzuordnen.
Threads können Namen vergeben werden
Vergeben Sie in der Praxis also allen Threads möglichst einen Namen, um diese später besser zuordnen zu können:
12
15
16
17
18 INFO
Thread thread1 = new Thread(ThreadMethod); thread1.IsBackground = true; thread1.Name = "Demo1"; thread1.Start(100);
19
Abbildung 20.5 zeigt das Thread-Fenster mit zwei benannten Arbeitsthreads (Demo1 und Demo2), nachdem Visual Studio in der Thread-Methode angehalten hat.
20 Abbildung 20.5: Das Thread-Fenster
21
22
23
1159
Multithreading
Wie Sie in Abbildung 20.5 sehen, enthält eine .NET-Anwendung mehrere Threads. Einer der zusätzlichen Arbeitsthreads führt die CLR aus, ein anderer den Garbage Collector. Die weiteren zusätzlichen Threads werden verwendet, weil die Anwendung im Visual-Studio-Prozess ausgeführt wird. Wichtig sind im Beispiel nur die unteren drei. Der Hauptthread der Anwendung (der UI-Thread) ist netterweise bereits von der CLR passend benannt worden. Demo1 und Demo2 sind die Namen der eigenen Threads. Neben der Übersicht über die aktuell laufenden Threads haben Sie in diesem Fenster noch die Möglichkeit, über einen Doppelklick auf einem Thread-Eintrag in den Thread zu wechseln, um diesen zu debuggen. Das funktioniert natürlich nur, wenn der Thread gerade ausgeführt wird. In der Praxis ist dies bei den komplexen Problemen, die Multithreading-Anwendungen verursachen können, eine große Hilfe. Besonders bei Deadlocks, die sehr schnell entstehen können, wenn Sie globale Objekte sperren oder mit Synchronisierungen arbeiten, ist das Thread-Fenster eine enorme Hilfe um herauszufinden, welche Threads sich gerade gegenseitig blockieren.
20.5.3 Das Ende von Threads signalisieren, Ergebnisse zurückgeben und Argumente einfacher übergeben In vielen Fällen müssen Sie erfahren, wann ein Thread beendet wurde. Das wäre z. B. notwendig, wenn Sie die Primzahlberechnung aus dem Abschnitt »Asynchrones Ausführen von Methoden« so umbauen wollen, dass diese in einem Thread ausgeführt wird. Sofern die Thread-Methode innerhalb der Fenster- oder Formular-Klasse deklariert ist, können Sie die Abschlussarbeiten natürlich einfach am Ende der ThreadMethode implementieren. Bei Thread-Methoden, die in einer separaten Klasse deklariert sind, müssen Sie jedoch anders vorgehen. Ein Thread kann sein Ende über ein Ereignis melden
Das Ende-Auswertungs-Problem lösen Sie, indem die Thread-Methode bei ihrem Ende einen Delegaten oder ein Ereignis aufruft. Zur Implementierung sollten Sie eine eigene Klasse für die Thread-Methode schreiben, die mit dem entsprechenden Ereignis für das Ende versehen wird. Das Ereignis kann auch gleich ein Ergebnis liefern. Wenn Sie schon einmal dabei sind, eine Klasse für den Thread zu definieren, können Sie auch gleich die Argumentübergabe an den Thread vereinfachen. Dazu implementieren Sie entsprechende Felder oder Eigenschaften, deren Werte am Konstruktor übergeben werden. Die Thread-Methode liest diese aus. Die Klasse wird damit auch typsicherer, da Sie auf das Object-Argument der Thread-Methode verzichten können. Listing 20.19: Klasse mit einer Methode, die in einem Thread ausgeführt werden soll und die bei ihrem Ende ein Ereignis aufruft public class PrimeNumberCalculator { /* EventArgs-Klasse für das CalculationCompleted-Ereignis */ public class CalculationCompletedEventArgs : EventArgs { public long? PrimeNumber = null; } /* Ereignis für das Ende der Berechnung */ public event EventHandler CalculationCompleted;
1160
Einfache Threads
/* Die Startnummer */ public long StartNumber { get; set; } /* Konstruktor */ public PrimeNumberCalculator(long startNumber) { this.StartNumber = startNumber; }
12
/* Methode zur Berechnung der nächsten Primzahl, die größer ist als die in der StartNumber-Eigenschaft definierte Zahl */ public void CalculatePrimeNumber() { for (long number = this.StartNumber; number { this.lblInfo.Content = e.PrimeNumber.ToString(); this.btnCalculatePrimeNumber.IsEnabled = true; })); }
INFO
Beachten Sie, dass das Ende-Ereignis wieder in einem anderen Thread ausgeführt wird, als dem UI-Thread. Der Zugriff auf die Steuerelemente und auf das Fenster (oder Formular) muss deswegen über die Invoke-Methode erfolgen. Dieses Problem löse ich (für eine andere Klasse) im Abschnitt »Das ereignisbasierte asynchrone Entwurfsmuster« ab Seite 1179 . Auf eine ähnliche Weise können Sie natürlich auch einen Fortschritt melden.
1162
Einfache Threads
Zusätzlich können Sie noch die Methode zum Starten des Thread in die Klasse integrieren, die die Thread-Methode beinhaltet. Nennen Sie diese dann so, dass am Namen erkannt wird, dass sie asynchron ausgeführt wird. Nach Microsoft-Konvention würde die Methode dann CalculatePrimeNumberAsync heißen. Dies macht die Verwendung der Klasse noch einfacher.
TIPP
20.5.4 Die Priorität
12
Jeder Thread besitzt eine Priorität, die in fünf Stufen einstellbar ist. Sie erreichen diese über die Eigenschaft Priority. Hier können Sie die selbsterklärenden Werte der ThreadPriority-Aufzählung einstellen: Highest, AboveNormal, Normal, BelowNormal und Lowest. Normal ist die Voreinstellung. Ein Thread mit einer höheren Priorität erhält einen größeren Anteil an der Zeitscheibe, als ein Thread mit einer niedrigeren. Dies bezieht sich aber nicht auf das gesamte System, sonder lediglich auf die Priorität, die dem Prozess zugeordnet ist. Sie können in einer Anwendung mit mehreren Threads also durch das Setzen der Priorität Highest für alle Threads nicht erreichen, dass diese insgesamt schneller ausgeführt werden. Tatsächlich sollte es keinen Unterschied machen, welche Priorität eingestellt ist, wenn alle Threads dieselbe verwenden.
13 Die Priorität bestimmt den Anteil an der Zeitscheibe, bezogen auf den Prozess
14
15
Innerhalb der Prozess-Priorität können Sie Ihre Threads aber über deren Priorität feineinstellen. Ein Thread zum Download einer Datei braucht vielleicht keine hohe Priorität. Der parallel laufende Thread, der eine Primzahl berechnet, aber vielleicht schon, um möglichst schnell fertig zu werden.
16
Als Demo habe ich in dem Beispiel von Seite 1157 die Priorität des ersten Thread auf Highest und die des zweiten auf Lowest gesetzt und in der Thread-Methode die Pause entfernt. Außerdem habe ich die Threads farblich unterschieden (was über das Setzen der Eigenschaften ForegroundColor und BackgroundColor der Console-Klasse möglich ist). Abbildung 20.6 zeigt das Ergebnis.
17
18 Abbildung 20.6: Zwei Threads werden mit einer unterschiedlichen Priorität ausgeführt
19
20 Obwohl das Programm mit dem zweiten Thread startet, erhält der erste insgesamt mehr Prozessor-Zeit und wird deshalb auch zuerst beendet. Das muss allerdings nicht immer so sein. Wenn Sie das Programm mehrfach starten, erhalten Sie vollkommen unterschiedliche Ergebnisse.
21
Die Prozess-Priorität Über die Eigenschaft PriorityClass des Process-Objekts für den aktuellen Prozess, das Sie über die Methode System.Diagnostics.Process.GetCurrentProcess erreichen, können Sie die Priorität des Prozesses einstellen. Dazu stehen Ihnen die folgenden Werte der ProcessPriorityClass-Aufzählung zur Verfügung: RealTime, High, AboveNormal, Normal, BelowNormal und Idle. Die Voreinstellung ist Normal. Über High können Sie erreichen, dass Ihrem Prozess relativ viel Prozessorzeit zugeordnet wird, ohne dass andere Prozesse zu stark darunter leiden. Die Priorität RealTime gibt Ihrem
Die Eigenschaft PriorityClass des Prozesses bestimmt dessen Priorität
1163
22
23
Multithreading
Prozess die meisten CPU-Ressourcen. Damit bleibt für andere Anwendungen – und auch für das Betriebssystem – nur noch sehr wenig CPU-Zeit übrig. High und RealTime sind für Realzeit-Anwendungen geeignet
Falls Sie Realzeit-Anwendungen entwickeln, die ohne Verzögerung arbeiten sollen, ist auf CPU-schwachen Maschinen die Prozessor-Priorität High an besten geeignet. Dabei sollten Sie aber natürlich beachten, dass Ihre Anwendung anderen Prozessen damit CPU-Zeit wegnimmt. Auf CPU-starken Maschinen können Sie auch RealTime verwenden. Das gesamte System wird damit aber erheblich verlangsamt (außer natürlich Ihre Anwendung) oder – bei CPU-schwachen Maschinen – sogar lahmgelegt. Sie sollten auch beachten, dass ein Thread Ihres Prozesses, der eine Endlosschleife ausführt, das gesamte Betriebssystem lahmlegen kann. Dem Anwender bleibt dann nur noch der Griff zum Ausschalter. Jetzt wissen Sie auch, wie Sie Anwendungen programmieren können, die das ganze System stilllegen ☺.
20.5.5 Threads abbrechen Abort bricht einen Thread ab
Threads können Sie über die Methode Abort hart abbrechen. Abort erzeugt im betreffenden Thread eine ThreadAbortException. Diese Ausnahme wird von der CLR abgefangen und ignoriert. Eine ThreadAbortException führt nicht dazu, dass die Anwendung beendet wird. Für eventuelle Aufräumarbeiten, die innerhalb der Thread-Methode notwendig sind, können Sie die ThreadAbortException innerhalb der Thread-Methode abfangen. Hier können Sie z. B. Dateien schließen, die der Thread geöffnet hat (alternativ können Sie dazu aber auch einen finally-Block verwenden, der auch beim Abbrechen eines Thread aufgerufen wird).
Der Thread wird an einem sicheren Punkt abgebrochen
Beim Aufruf von Abort wird der Thread nicht unbedingt sofort abgebrochen. Auf Einprozessor-Maschinen wartet Windows zunächst, bis die Zeitscheibe des Threads abgelaufen ist. Auf Mehrprozessor-Maschinen wird der Abbruch sofort initiiert. Danach wartet Windows noch, bis der Thread einen so genannten sicheren Punkt (Safe Point) erreicht hat. Ein solcher ist ein Punkt, an dem der Thread sicher abgebrochen werden kann. Sichere Punkte sind alle Anweisungen, die keine Objekte erzeugen und die keinen unverwalteten Code ausführen. Würde ein Thread mitten in einer Objekterzeugung oder während der Ausführung von unverwaltetem Code unterbrochen werden, wäre es ansonsten möglich, dass Speicherlöcher entstehen. Die Erzeugung eines Objekts besteht z. B. aus mehreren CIL-Code-Anweisungen. Die CLR muss u. a. Speicher reservieren, diesen Speicher initialisieren und einen Zeiger darauf in die verwendete Referenz schreiben. Würde der Thread vor dem Schreiben des Zeigers in die Referenz unterbrochen werden, würde die CLR das Objekt nicht kennen und der Garbage Collector könnte dieses dann auch nicht freigeben, wenn der Thread nicht weiter ausgeführt wird.
TIPP
Mit Abort sollten Sie in der Praxis vorsichtig umgehen. Abort ist immer ein harter Abbruch. Werten Sie in der Thread-Methode auf jeden Fall eine ThreadAbortException aus, bei deren Auftreten Sie Aufräumarbeiten ausführen können (aber nicht müssen). Statt Abort zu verwenden, ist die Verwendung einer Abbruch-Variablen, die in der Thread-Methode abgefragt wird, in der Praxis meist besser geeignet, da Sie dann genauer steuern können, wann abgebrochen wird. In einer Klasse, die die Thread-Methode enthält, implementieren Sie dazu ein privates Feld cancellationPending, das beim Start der Thread-Methode auf false gesetzt wird
1164
Einfache Threads
und über die Methode CancelAsync auf true gesetzt werden kann. Innerhalb der Thread-Methode überprüfen Sie cancellationPending auf true und brechen gegebenenfalls ab. Im Falle eines Abbruchs sollten Sie eine weitere nach außen schreibgeschützte Eigenschaft Cancelled auf true setzen, damit diese später ausgewertet werden kann. Das Beispiel im Abschnitt »Ein praxisnahes Beispiel« setzt dieses Muster um. Die Namen der Felder, Eigenschaften und Methoden habe ich übrigens so gewählt, dass diese der Microsoft-Konvention entsprechen (was Sie am BackgroundWorker erkennen). Die Thread-Klasse stellt neben Abort auch noch die Methoden Suspend und Resume zur Verfügung, über die Sie einen Thread anhalten und weiter ausführen können. Beide Methoden sind aber als veraltet gekennzeichnet. Das hat auch einen guten Grund, denn sie führen in der Praxis zu einigen Problemen. Verwenden Sie zur Steuerung eines Thread stattdessen andere Möglichkeiten wie ManualResetEvent- und AutoResetEventObjekte. Diese behandle ich im Abschnitt »Threads synchronisieren« ab Seite 1183.
12
13 INFO
14
Informationen zu Threads ermitteln oder festlegen 15
Die Thread-Klasse bietet einige Eigenschaften, über die Sie Informationen zu dem Thread erhalten oder auch festlegen können. Tabelle 20.2 beschreibt die wichtigsten. Eigenschaft
Beschreibung
CultureInfo CurrentCulture
referenziert ein CultureInfo-Objekt, das der Thread für Formatierungen und zur Lokalisierung bzw. für die Darstellung von Oberflächenelementen verwendet. Ein Thread übernimmt bei seiner Erzeugung automatisch das CultureInfo-Objekt des übergeordneten Threads. Sie können aber natürlich auch ein anderes einstellen.
CultureInfo CurrentUICulture Thread CurrentThread
Über diese statische Eigenschaft können Sie den Thread ermitteln, der die aktuelle Methode ausführt.
bool IsAlive
gibt an, ob der Thread noch ausgeführt wird.
bool IsBackground
gibt an, ob es sich um einen Hintergrundthread handelt. Ein Hintergrundthread wird beendet, wenn die Anwendung beendet wird.
bool IsThreadPoolThread
gibt an, ob der Thread aus einem Thread-Pool stammt.
int ManagedThreadId
gibt die eindeutige ID zurück, die die CLR dem Thread vergeben hat. Dabei handelt es sich nicht um die ID, die Windows einem Thread vergibt.
string Name
der Name des Thread
ThreadPriority Priority
gibt die Priorität des Thread an.
ThreadState ThreadState
gibt den Status des Thread mit den Werten der ThreadState-Aufzählung an (die teilweise redundant, veraltet oder für die interne Verwendung vorgesehen sind). Die relevanten Werte sind:
Tabelle 20.2: Die wichtigen Eigenschaften der Thread-Klasse
16
17
18
19
20 21
22
– Running: Der Thread wird ausgeführt – Unstarted: Der Thread wurde noch nicht gestartet
23
– Stopped: Der Thread wurde beendet – WaitSleepJoin: Der Thread wurde blockiert (z. B. über lock oder einen WaitHandle, siehe ab Seite 1186)
1165
Multithreading
20.5.6 Ein praxisnahes Beispiel Das folgende Beispiel demonstriert Multithreading praxisnah. Es ermöglicht dem Anwender drei Dateien gleichzeitig von einem Webserver herunterzuladen. Dieses relativ lange Beispiel führt in keine neuen Multithreading-Themen ein. Es setzt lediglich das bisher bekannte Wissen um. Sie können diesen Abschnitt also ruhig überspringen. Was allerdings bezogen auf das gesamte Buch neu ist, ist das Herunterladen von Dateien und ein kleiner Trick zum Zugriff auf die in einer Vorlage definierten Steuerelemente einer ListBox. Das Beispiel implementiert eine Klasse zum asynchronen Herunterladen von Dateien. Die WebClient-Klasse aus dem Namensraum System.Net bietet diese Möglichkeit allerdings bereits (inklusive Fortschritts-Ereignis). Ich habe die eigene Klasse aber aus zwei Gründen implementiert: Zum einen lässt sich damit Multithreading in der Praxis am besten zeigen. Zum anderen können Sie den Download in einer eigenen Klasse viel besser steuern. Für mein Codebook (und für die Praxis) habe ich diese Klasse z. B. so weiterentwickelt, dass diese den Download nach einem Abbruch seitens des Servers automatisch wiederholt ausführt (wobei die letzte gelesene Position berücksichtigt wird). Dieses Feature ist für Systeme, die nur über eine unzuverlässige oder zu langsame Internetverbindung verfügen, nicht zu unterschätzen.
DISC
Meine Implementierung der Download-Klasse finden Sie in dem Projekt »Dateien von einem Webserver über eine HttpWebRequest-Instanz downloaden« im Ordner ZusatzBeispiel in den Buch-Beispielen. Abbildung 20.7 zeigt die in diesem Abschnitt implementierte Beispiel-Anwendung.
Abbildung 20.7: Das praxisnahe Beispiel
Während des Downloads wird ein Fortschritt angezeigt werden. Der Anwender kann den Thread gezielt abbrechen. Nachdem der Thread beendet wurde, sollen eventuell aufgetretene Fehler oder ein Abbruch gemeldet werden.
1166
Einfache Threads
Die Klasse Download enthält zunächst alles, was für den Thread notwendig ist. Beachten Sie die Kommentare. Für eine separate Erläuterung habe ich keinen Platz im Buch. Nebenbei lernen Sie hier noch, wie Sie Dateien aus dem Internet herunterladen ☺. Das Herunterladen ist allerdings hier etwas komplexer implementiert als eigentlich notwendig. Der Grund dafür ist, dass der Fortschritt ausgewertet werden muss. Auf Seite 1199 finden Sie ein einfacheres Beispiel, das die WebClient-Klasse einsetzt.
12
INFO
Listing 20.21: Klasse für den Download einer Datei in einem Thread
13
/* Klasse für das DownloadCompleted-Ereignis */ public class DownloadCompletedEventArgs : EventArgs { /* Der Url des Downloads */ public Uri Url;
14
/* Der Name der lokal erzeugten Datei */ public string FileName; /* Nimmt eine Ausnahme auf, falls eine eingetreten ist */ public Exception Error;
15
/* Gibt an, ob abgebrochen wurde */ public bool Cancelled; }
16
/* Klasse zum Herunterladen einer Datei */ public class Download { /* Der Url der Datei */ public Uri Url { get; set; }
17
18
/* Der Ziel-Dateiname */ public string DestinationFileName { get; set; }
19
/* Ereignis für den Fortschritt des Downloads */ public event EventHandler DownloadProgress;
20
/* Ereignis für das Ende des Downloads */ public event EventHandler DownloadCompleted;
21
private bool cancellationPending = false; /* Methode, über die abgebrochen werden kann */ public void CancelDownload() { this.cancellationPending = true; }
22
/* Startet den Download */ public void DownloadAsync() {
23
1167
Multithreading
// Thread erzeugen und initialisieren. Der Download soll auf // jeden Fall zu Ende ausgeführt werden, weswegen IsBackground // auf false gesetzt wird (obwohl dies die Voreinstellung ist) Thread thread = new Thread(this.DownloadInternal); thread.IsBackground = false; // Den Thread starten thread.Start(); } /* Führt den Download aus */ private void DownloadInternal() { // Das Abbruch-Feld voreinstellen this.cancellationPending = false; // Felder für die Weitergabe an das DownloadFinished-Ereignis bool cancelled = false; Exception error = null; // Die Datei downloaden WebRequest request = null; WebResponse response = null; Stream responseStream = null; FileStream fileStream = null; try { // FileStream erzeugen fileStream = new FileStream(this.DestinationFileName, FileMode.Create); // WebRequest-Instanz für den Download erzeugen, die Antwort // anfordern und die Länge der Daten ermitteln request = WebRequest.Create(this.Url); response = request.GetResponse(); long fileSize = response.ContentLength; // Den Antwort-Stream ermitteln, diesen blockweise // lesen und in den Zielstream schreiben responseStream = response.GetResponseStream(); int bytesRead = 0; int totalBytesRead = 0; byte[] buffer = new byte[1024]; do { // Ermitteln, ob abgebrochen werden soll if (this.cancellationPending) { // Den Response-Stream, das WebResponse// Objekt und den Datei-Stream schließen responseStream.Close(); response.Close(); fileStream.Close(); // Den Abbruch melden cancelled = true; // und raus aus der Schleife break; } // Den Fortschritts-Delegate aufrufen if (this.DownloadProgress != null) { int percent = (int)((totalBytesRead / (float)fileSize) * 100);
1168
Einfache Threads
this.DownloadProgress(this, new ProgressChangedEventArgs( percent, this.Url)); } bytesRead = responseStream.Read(buffer, 0, 1024); totalBytesRead += bytesRead; fileStream.Write(buffer, 0, bytesRead); } while (bytesRead > 0); } catch (Exception ex) { // Die Exception ablegen error = ex; } finally { try { // Den Response-Stream, das WebResponse-Objekt // und den Datei-Stream schließen responseStream.Close(); response.Close(); fileStream.Close(); } catch { } }
12
13
14
15
// Das DownloadCompleted-Ereignis aufrufen if (this.DownloadCompleted != null) { this.DownloadCompleted(this, new DownloadCompletedEventArgs() { Url = this.Url, FileName = DestinationFileName, Cancelled = cancelled, Error = error }); }
16
17
18
} }
Die ListBox des WPF-Fensters wird mit Download-Instanzen gefüllt. Sie enthält ein DataTemplate für die ListBox-Einträge. Diese Vorlage enthält ein Label, das an die Eigenschaft Url.LocalPath des Download-Objekts gebunden ist, dass über diesen Eintrag dargestellt wird. Außerdem enthält die Vorlage eine ProgressBar:
19
Listing 20.22: Die ListBox des WPF-Fensters
20
21
22
23
1169
Multithreading
Bei der Betätigung des Schalters zum Herunterladen werden die Download-Instanzen erzeugt, an die ListBox gehängt, die Ereignisse zugewiesen und der Download gestartet: Listing 20.23: Start des Downloads private void btnStartDownload_Click(object sender, RoutedEventArgs e) { // Die drei URLs auslesen List urls = new List(); if (this.txtUrl1.Text != null) { urls.Add(new Uri(this.txtUrl1.Text)); } if (this.txtUrl2.Text != null) { urls.Add(new Uri(this.txtUrl2.Text)); } if (this.txtUrl3.Text != null) { urls.Add(new Uri(this.txtUrl3.Text)); } // Die Urls durchgehen foreach (var url in urls) { // Dateiname ermitteln string destinationFilename = Path.Combine( this.txtDestinationFolder.Text, url.Segments[url.Segments.Length - 1]); // Download erzeugen Download download = new Download() { Url = url, DestinationFileName = destinationFilename }; // In die ListBox schreiben this.lstDownloads.Items.Add(download); // Die Ereignisse zuweisen download.DownloadProgress += new EventHandler( this.download_DownloadProgress); download.DownloadCompleted += new EventHandler( this.download_DownloadCompleted); // Den Download starten download.DownloadAsync(); } }
Der Handler für das DownloadProgress-Ereignis sucht das entsprechende DownloadObjekt in der ListBox und aktualisiert den Wert der ProgressBar, die in den jeweiligen Eintrag verwaltet wird. Das Lokalisieren der ProgressBar ist leider relativ aufwändig und erfolgt über den visuellen Baum der Vorlage (was das Ganze relativ unsicher im Bezug auf neue WPF-Versionen macht). Beachten Sie, dass der Zugriff auf die ListBox wieder über die Invoke-Methode des Dispatcher-Objekts ausgeführt wird, da das Ereignis in dem Arbeitsthread aufgerufen wird.
1170
Einfache Threads
Listing 20.24: Der Handler für das DownloadProgress-Ereignis private void download_DownloadProgress(object sender, ProgressChangedEventArgs e) { // Ermitteln des Downloads Download download = (Download)sender; // Diesen in der ListBox suchen und die ProgressBar aktualisieren this.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() => { for (int i = 0; i < this.lstDownloads.Items.Count; i++) { if (((Download)this.lstDownloads.Items[i]) == download) { // Das ListBoxItem ermitteln ListBoxItem item = this.lstDownloads.ItemContainerGenerator .ContainerFromIndex(i) as ListBoxItem;
12
13
14
// Die ProgressBar suchen (was ein wenig "tricky" ist) DataTemplate dataTemplate = item.ContentTemplate; Border border = (Border)VisualTreeHelper.GetChild( item, 0); ContentPresenter cp = (ContentPresenter)border.Child; Grid grid = (Grid)dataTemplate.FindName( "downloadInfoGrid", cp); ProgressBar pbr = (ProgressBar)grid.FindName( "pbrDownload");
15
16
// Die ProgressBar aktualisieren pbr.Value = e.ProgressPercentage;
17
break; } } })); }
18
Der Handler für das Click-Ereignis des Schalters zum Abrechnen des aktuell selektierten Downloads ermittelt das jeweilige Download-Objekt und bricht den Download über die CancelDownload-Methode ab:
19 Listing 20.25: Abbruch eines Downloads private void btnCancelDownload_Click(object sender, RoutedEventArgs e) { // Ermitteln des Downloads if (this.lstDownloads.SelectedIndex > -1) { Download download = (Download)this.lstDownloads.SelectedValue; download.CancelDownload(); MessageBox.Show("Der Download '" + download.Url.AbsoluteUri + "' wird abgebrochen"); } else { MessageBox.Show("Es ist kein Download selektiert"); } }
20 21
22
23
1171
Multithreading
Fehlt nur noch der Handler für das DownloadCompleted-Ereignis, der das Ergebnis auswertet: Listing 20.26: Handler für das DownloadCompleted-Ereignis private void download_DownloadCompleted(object sender, DownloadCompletedEventArgs e) { // Ermitteln des Downloads Download download = (Download)sender; // Diesen in der ListBox suchen und die ProgressBar aktualisieren this.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() => { for (int i = 0; i < this.lstDownloads.Items.Count; i++) { if (((Download)this.lstDownloads.Items[i]) == download) { // Das ListBoxItem ermitteln ListBoxItem item = this.lstDownloads.ItemContainerGenerator .ContainerFromIndex(i) as ListBoxItem; // Die ProgressBar suchen (was ein wenig "tricky" ist) DataTemplate dataTemplate = item.ContentTemplate; Border border = (Border)VisualTreeHelper.GetChild( item, 0); ContentPresenter cp = (ContentPresenter)border.Child; Grid grid = (Grid)dataTemplate.FindName( "downloadInfoGrid", cp); ProgressBar pbr = (ProgressBar)grid.FindName( "pbrDownload"); // Die ProgressBar aktualisieren if (e.Error != null) { pbr.Value = 0; pbr.Background = Brushes.Red; } else if (e.Cancelled) { pbr.Value = 0; pbr.Background = Brushes.Orange; } break; } } })); // Fehler auswerten if (e.Error != null) { MessageBox.Show("Der Download von '" + download.Url.AbsoluteUri + "' ist fehlgeschlagen: " + e.Error.Message, "Download", MessageBoxButton.OK, MessageBoxImage.Error); } }
Wenn Sie dieses Programm testen, können Sie das Herunterladen von Dateien einmal starten und das Programm danach sofort beenden. Sie werden bemerken, dass der Prozess noch so lange weiterläuft, bis die Dateien komplett heruntergeladen sind.
1172
Die ThreadPool-Klasse
Das Beispiel enthält keine Fehlerbehandlung und erlaubt, den Download mehrfach zu starten. In der Praxis sollten Sie natürlich noch einige Vorkehrungen treffen, dass Ausnahmen abgefangen werden und dass der Download in eine Zieldatei nicht mehrfach gleichzeitig möglich ist. Das Problem mit der Aktualisierung der ProgressBar kann prinzipiell auch über Datenbindung gelöst werden. Dazu muss in der Download-Klasse aber eine Abhängigkeitseigenschaft für den Fortschritts-Prozentwert implementiert werden. Und über weitere WPF-Features (wie Trigger) könnte das Ganze noch weiter »vereinfacht« werden … Aber das würde den Rahmen dieses Buchs sprengen.
20.6
INFO
12 INFO
13
Die ThreadPool-Klasse
Die ThreadPool-Klasse verwaltet einen Pool von Threads, dessen Größe Sie einstellen können. Sie erlaubt die gemeinsame Benutzung (das Sharing) von Threads und deren Wiederverwendung.
ThreadPool verwaltet Threads in einem Pool
Der Grund für die Existenz dieser Klasse ist, dass Windows beim Erzeugen eines Thread etwas Zeit für die Initialisierung benötigt und jeder Thread und Speicher verbraucht (ca. 1 MB). In Anwendungen, die sehr viele Threads ausführen, vermindert ThreadPool potenzielle Probleme dadurch, dass im Pool nur eine angegebene maximale Anzahl an Threads verwaltet wird.
14
15
16
Die Verwendung von ThreadPool macht Sinn in Anwendungen mit vielen Threads, die sich meist im Ruhezustand befinden. Sie macht auch Sinn in Anwendungen, die potenziell sehr viele Threads ausführen. Ein Beispiel dafür sind Webserver, die pro Sitzung einen Thread verwalten. Würden in einer solchen Anwendung normale Threads eingesetzt, würde der Webserver bei vielen gleichzeitigen Sitzungen relativ schnell überlastet. Über einen Thread-Pool kann aber die maximale Anzahl an Threads bestimmt werden, womit eine Überlastung der Anwendung vermieden wird.
17
18
Die ThreadPool-Klasse hat aber auch einige Nachteile: ■ ■ ■
Sie können den Namen der Threads nicht bestimmen, was das Debugging erschwert, Sie können die Priorität nicht bestimmen und Sie können Pool-Threads nicht abbrechen (außer mit eigenen Techniken über Abbruch-Variablen).
19
20
ThreadPool-Threads sind zudem grundsätzlich immer Hintergrundthreads, was nicht unbedingt ein Nachteil ist. Sie können einen solchen Thread aber eben nicht als Vordergrundthread ausführen, um zu verhindern, dass der Thread mit der Anwendung beendet wird.
21
20.6.1 ThreadPool verwenden Die Verwendung der ThreadPool-Klasse ist einfach. Zum Hinzufügen einer Methode rufen Sie die QueueUserWorkItem-Methode auf. Diese Methode schreibt einen übergebenen Delegaten in die Warteschlange der ThreadPool-Klasse. Am Argument state können Sie optional Daten übergeben, die an die Thread-Methode weitergegeben werden. Die Thread-Methode muss dem Delegaten WaitCallback entsprechen, der folgendermaßen definiert ist:
QueueUserWorkItem fügt einen Delegaten hinzu
22
23
1173
Multithreading
public delegate void WaitCallback(Object state)
Listing 20.27 zeigt ein einfaches Beispiel. Listing 20.27: Beispiel für die Verwendung der ThreadPool-Klasse static void Main(string[] args) { ThreadPool.QueueUserWorkItem(Worker, 1); ThreadPool.QueueUserWorkItem(Worker, 2); ThreadPool.QueueUserWorkItem(Worker, 3); Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine(); } private static void Worker(object state) { Console.WriteLine(DateTime.Now.ToString("hh:mm:ss,fff") + " - " + data); // Kleine Pause, damit der Thread im Pool bleibt Thread.Sleep(5000); }
20.6.2 Die Anzahl der Threads im Pool Die voreingestellte Maximalzahl Threads variiert
ThreadPool verwendet per Voreinstellung laut der Dokumentation maximal 25 Threads pro Prozessor. Interessant ist, dass die GetMaxThreads-Methode für meinen Rechner (Pentium Dual Core E8400 3GHz) 500 maximale Arbeitsthreads zurückgegeben hat. Wahrscheinlich hängt die tatsächliche Anzahl der voreingestellten maximalen Threads neben der Anzahl auch von der Leistung der Prozessoren bzw. Prozessorkerne ab.
SetMaxThreads setzt die maximale Thread-Anzahl
Über die SetMaxThreads-Methode können Sie die Anzahl der maximalen Threads bestimmen. Dabei übergeben Sie am ersten Argument die maximale Anzahl der Arbeitsthreads. Am zweiten übergeben Sie die maximale Anzahl »asynchroner E/AThreads« im Pool. Diese Threads werden wahrscheinlich intern verwendet, was aber nicht dokumentiert ist. Per Voreinstellung handelt es sich hier um die doppelte Anzahl der Arbeitsthreads.
20.6.3 Die Arbeitsweise von ThreadPool ThreadPool optimiert die Erzeugung von Threads
ThreadPool geht folgendermaßen vor, um die Delegaten in der Warteschlange Threads zuzuordnen: ThreadPool erzeugt zunächst nur so viele Threads, wie als minimale Anzahl angegeben ist. Per Voreinstellung ist die minimale Anzahl auf die Anzahl der Prozessoren festgelegt. Über GetMinThreads können Sie die Mindestanzahl abfragen und über SetMinThreads setzen (wobei Sie allerdings eine Anzahl größer/gleich der Anzahl Prozessoren/Kerne angeben müssen, damit diese übernommen wird). Das Setzen der Minimalanzahl macht Sinn in Anwendungen, die eine große Anzahl von Threads ausführen.
Bei der Zuweisung der Delegaten in der Warteschlange werden zunächst die freien Threads im Pool verwendet. Sind dann noch Delegaten vorhanden, werden neue Threads nicht sofort erzeugt, sondern erst (laut der Dokumentation) nach einer halben Sekunde (was sich aber in zukünftigen Implementierungen des .NET Framework auch ändern kann). Damit stellt ThreadPool sicher, dass Threads, die in der
1174
Die ThreadPool-Klasse
Zwischenzeit beendet wurden, effizient wiederverwendet werden können. So geht es weiter, bis die maximale Anzahl an Threads erreicht ist. Threads, die sich im Leerlauf befinden, werden sofort wieder freigegeben, wobei allerdings die minimale Anzahl an Threads im Leerlauf bestehen bleibt. An der Ausgabe des obigen Beispiel können Sie dieses Verhalten relativ gut beobachten: Der dritte Thread wird nicht sofort gestartet, sondern (auf meinem System) ziemlich genau eine Sekunde später als die anderen beiden (Abbildung 20.8).
12 Abbildung 20.8: Demo für das Pooling der ThreadPool-Klasse mit Threads, die ca. fünf Sekunden laufen
13
14 Wenn Sie in der Thread-Methode die Pause wegnehmen (oder auf vielleicht 100 ms verringern), werden die Threads allerdings relativ schnell hintereinander gestartet, wodurch die Threads im Pool schnell wieder frei werden und wieder verwendet werden können (Abbildung 20.9).
15 Abbildung 20.9: Demo für das Pooling der ThreadPool-Klasse mit Threads, die ca. 100 Millisekunden laufen
Ist die maximale Anzahl an Arbeitsthreads erreicht und befinden sich noch Delegaten in der Warteschlange, werden diese erst dann einem Thread zugewiesen, wenn einer frei wird. ThreadPool eignet sich deswegen nicht für langlebige Threads.
16
17
18
20.6.4 Die Interne Verwendung der ThreadPool-Klasse ThreadPool wird auch intern von den folgenden .NET-Elementen verwendet: ■ ■ ■ ■
19
Asynchron aufgerufene Delegaten Die BackgroundWorker-Klasse Die Timer aus System.Threading und System.Timers WCF, Remoting, ASP.NET und Webdienst-Servern
20
20.6.5 Weitere interessante Features von ThreadPool, die hier nicht näher besprochen werden
21
Bei der Arbeit mit der ThreadPool-Klasse sind noch die folgenden Methoden interessant: ■
■ ■
22
RegisterWaitForSingleObject: Über diese Methode können Sie einen Delegaten in die Warteschlange schreiben, der auf das Signalisieren eines ebenfalls übergebenen WaitHandle wartet. UnsafeQueueUserWorkItem: Diese Methode erlaubt das Einstellen eines Delegaten in die Warteschlange, der eine Methode mit unsicherem Code referenziert. UnsafeRegisterWaitForSingleObject: Wie RegisterWaitForSingleObject, nur für Methoden mit unsicherem Code.
23
1175
Multithreading
20.7
Ausnahmen in Threads und beim asynchronen Aufruf von Methoden
Ausnahmen, die in einer Thread-Methode auftreten können, sollten Sie besondere Beachtung schenken. Das Problem in einer Thread-Methode ist nämlich das Folgende:
HALT
Ausnahmen, die in einer Thread-Methode oder in einer asynchron aufgerufenen Methode auftreten, werden nicht an den startenden Thread und leider auch nicht an die Anwendung weitergereicht. Die Weitergabe an den startenden Thread ist prinzipiell gar nicht möglich, da dieser nicht an der Stelle anhält, an der der Arbeitsthread aufgerufen wird. Wenn Sie im UI-Thread einen Arbeitsthread starten, kann der UIThread Ausnahmen, die im Arbeitsthread auftreten, nicht abfangen. Ausnahmen (außer ThreadAbortException und AppDomainUnloadedException), die in einem Arbeitsthread auftreten, führen in einer Windows.Forms- und einer WPFAnwendung dazu, dass die Anwendung beendet wird und Windows in einer für den Anwender verwirrenden Meldung meldet, diese würde nicht mehr funktionieren (Abbildung 20.10). Wenn Sie das einmal ausprobieren wollen, fügen Sie in die Thread-Methode der Primzahlberechnungs-Anwendung eine Anweisung ein, die eine Ausnahme verursacht: int i = 0; int j = 1 / i; // DivideByZeroException -Thread
Kompilieren Sie die Anwendung dann und starten Sie diese direkt unter Windows (nicht unter Visual Studio weil dort der Debugger an der Ausnahme anhält). Abbildung 20.10: Eine Ausnahme in einem Thread lässt die Anwendung abstürzen
Sie sollten in jeder Thread-Methode Ausnahmen behandeln
HALT
Leider werden auch die Anwendungs-Ereignisse ThreadException (Windows.Forms) bzw. DispatcherUnhandledException nicht aufgerufen, wenn in einem Thread eine Ausnahme eintritt. Deshalb und weil die Fehlersuche in einem solchen Fall enorm schwierig ist, sollten Sie in jeder Thread-Methode eine Ausnahmebehandlung vorsehen, die alle Anweisungen der Methode umfasst. Problematisch ist lediglich die Auswertung von Ausnahmen. Dazu haben Sie prinzipiell die folgenden Möglichkeiten: ■
1176
Sie geben eine Fehlermeldung aus. Dies macht jedoch nur dann Sinn, wenn der Thread definitiv von einer Oberfläche aus gestartet wird. Für viele Threads trifft das jedoch nicht zu: Diese werden von irgendwelche Methoden gestartet, die
Ausnahmen in Threads und beim asynchronen Aufruf von Methoden
■ ■
■
tief im Klassenmodell der Anwendung oder eine Klassenbibliothek verwaltet werden. Eine Fehlermeldung wäre in diesem Fall nicht angebracht, vor allen Dingen, wenn die Thread-Methode nicht in einer normalen Anwendung, sondern in einem Windows-Dienst oder einer Webanwendung ausgeführt wird. Das gilt auch für den Fall, dass die Thread-Methode mehrfach gestartet wird und demnach u. U. mehrere Fehlermeldungen ausgegeben werden würden. Sie schreiben einen Protokolleintrag. Diese Variante führt allerdings dazu, dass der Benutzer nichts davon mitbekommt, dass eine Ausnahme eingetreten ist. Sie implementieren die Thread-Methode in einer Klasse, die ein Error-Ereignis besitzt, das im Fall einer Ausnahme aufgerufen wird. Zusätzlich dazu protokollieren Sie aufgetretene Ausnahmen. Diese Variante ist die sicherste, da sie zum einen erlaubt, Threads unabhängig von der Benutzeroberfläche zu starten, und zum anderen die Wahrscheinlichkeit groß ist, dass Informationen zu aufgetretenen Ausnahmen nicht verloren gehen. Sie implementieren das Muster, das auch der BackgroundWorker einsetzt, indem Sie dem Ende-Ereignis eine Information darüber übergeben, ob eine Ausnahme aufgetreten ist. Zusätzlich dazu sollten Sie auch protokollieren. Ich verwende diesen Ansatz in meinem praxisnahen Beispiel ab Seite 1166.
12
13
14
15
Beim Protokollieren müssen Sie allerdings darauf achten, dass es vorkommen kann, dass mehrere Threads gleichzeitig in das Protokoll schreiben. Falls Sie nicht eine threadsichere Protokoll-Komponente wie log4net verwenden, müssen Sie den Zugriff auf das Protokoll sperren, solange ein Thread darauf zugreift. Dazu erfahren Sie mehr im Abschnitt »Den Zugriff auf globale Daten und Ressourcen sperren« ab Seite 1186. Bei der Auswertung von Ausnahmen sollten Sie beachten, dass beim Aufruf der Abort-Methode (die einen Thread abbricht, siehe im folgenden Abschnitt) eine ThreadAbortException geworfen wird. Diese ist dazu vorgesehen, in der Thread-Methode abgefangen und für Aufräumarbeiten verwendet zu werden. Sie müssen die ThreadAbortException nicht abfangen, da diese nicht dazu führt, dass das Programm von der CLR beendet wird. Wenn Sie allerdings generell nur Exception abfangen, erhalten Sie auch ThreadAbortException-Meldungen bzw. -Protokolleinträge.
Falls Sie mit finally-Blöcken arbeiten, müssen Sie beachten, dass diese bei Hintergrundthreads nicht aufgerufen werden, wenn der Thread deswegen abgebrochen wird, weil die Anwendung beendet wird. Dies ist natürlich sehr ungünstig in dem Fall, dass Sie in der Thread-Methode externe Ressourcen geöffnet haben, die im finally-Block geschlossen werden.
16
17 INFO
18
19
20
HALT
21
Dieses Problem können Sie leider auf eine einfache Weise nur dadurch lösen, dass Sie Threads mit finally-Blöcken als Vordergrundthread ausführen. Eine weitere Lösung ist, dass Sie beim Beenden der Anwendung (das Sie z. B. in einer WPF-Anwendung im Exit-Ereignis abfangen) die Abort-Methode des Thread aufrufen. Abort führt dazu, dass der finally-Block ausgeführt wird.
22
23
1177
Multithreading
Prinzipiell sieht das Abfangen von Ausnahmen in einer Thread-Methode so aus: Listing 20.28: Schema der Ausnahmebehandlung in einem Thread // Thread starten, der eine Ausnahme verursacht, die abgefangen wird Thread thread = new Thread(new ThreadStart(() => { try { // Den eigentlichen Job ausführen } catch (ThreadAbortException) { // Der Thread wurde abgebrochen: Aufräumarbeiten ausführen } catch (Exception ex) { // Ausnahme im Thread: Diese auswerten } })); thread.Start();
HALT
Im Internet kursiert auch der Trick, das alte .NET-1.0-Verhalten einzustellen, indem Sie die Konfigurationseinstellung legacyUnhandledExceptionPolicy auf 1 setzen. Das führt zwar dazu, dass Windows bei einer unbehandelten Ausnahme in einem Thread das Programm nicht mehr beendet. Die Anwendungs-Ereignisse ThreadException bzw. DispatcherUnhandledException werden dann aber trotzdem nicht aufgerufen. In diesem Fall würden Ausnahmen in Threads einfach ignoriert werden und unerkannt bleiben! Microsoft hatte einen guten Grund dafür, dieses Verhalten in .NET 2.0 zu ändern. Mit der Meldung, dass die Anwendung nicht mehr funktioniert, wird zumindest nicht einfach ignoriert, dass eine Ausnahme eingetreten ist.
20.8
Das ereignisbasierte asynchrone Entwurfsmuster und asynchrone Methoden
Das ereignisbasierte asynchrone Entwurfsmuster, dessen Name sich etwas komplex anhört, wird u. a. von der BackgroundWorker-Klasse implementiert. Sie können dies aber auch selbst implementieren, was deswegen nicht uninteressant ist, weil Sie damit einfach benutzbare Klassen erzeugen können, die Methoden im Threads ausführen und Ereignisse aufrufen. Der wesentliche Vorteil dieses Entwurfsmusters ist, dass die Ereignisse in den Thread umgeleitet werden, der die Methode aufgerufen hat, die den Thread gestartet hat. Damit entfällt im Ereignishandler der nervige Zwang, über Invoke auf Fenster, Formulare und Steuerelemente zugreifen zu müssen. Die älteren asynchronen Methoden einiger Klassen, deren Name den Präfix »Begin« trägt, sind ein weiteres Thema, das Sie grundsätzlich kennen sollten. Ich stelle beide Versionen, Methoden asynchron aufzurufen, hier grundlegend vor. Besonders für das im Original ziemlich komplexe ereignisbasierte asynchrone Entwurfsmuster fehlt im Buch der Platz (weswegen ich eine abgekürzte Form verwende).
1178
Das ereignisbasierte asynchrone Entwurfsmuster und asynchrone Methoden
20.8.1 Das ereignisbasierte asynchrone Entwurfsmuster Das im Original etwas komplexe ereignisbasierte asynchrone Entwurfsmuster wird z. B. von der BackgroundWorker- und anderen Klassen wie z. B. WebClient eingesetzt. Diese erlauben über Methoden, deren Name mit »Async« endet, die asynchrone Ausführung einer Aktion. Bei WebClient können Sie z. B. über die Methode DownloadFileAsync den asynchronen Download einer Datei starten. Ein entsprechendes Ereignis liefert dann das Ergebnis (bei WebClient ist das DownloadFileCompleted). Im Wesentlichen sieht das ereignisbasierte asynchrone Entwurfsmuster folgendermaßen aus: ■
■
■ ■
■
Das EAE wird bereits von einigen Klassen eingesetzt
12
Eine Klasse stellt über eine Methode eine Möglichkeit zur Verfügung, eine Aktion synchron auszuführen. Der Name der Methode entspricht der Aktion. Bei einer Klasse zum Herunterladen von Dateien würde diese z. B. »Download« heißen. Eine asynchrone Methode, deren Name mit dem Namen der synchronen Methode beginnt und mit »Async« endet, startet dieselbe Action asynchron (in einem Thread). Im obigen Beispiel würde diese Methode DownloadAsync heißen. Eine Methode, deren Name mit »Cancel« beginnt und mit dem Namen der Aktion endet, erlaubt den Abbruch der asynchronen Variante. Während des synchronen und asynchronen Ablaufs wird ein Fortschritts-Ereignis aufgerufen. Der Name dieses Ereignisses entspricht der Aktion mit dem Suffix »Progress«. Nach Beenden der asynchronen Methode wird ein Ende-Ereignis aufgerufen. Der Name des Ereignisses entspricht dem Namen der Aktion mit dem Suffix »Completed«. Diesem Ereignis ein Ereignisargument übergeben, das aussagt, ob in der asynchronen Methode eine Ausnahme eingetreten ist oder ob abgebrochen wurde.
Der wesentliche Vorteil dieses Musters ist, dass über einen kleinen Trick erreicht wird, dass die Ereignisse automatisch in den Thread umgeleitet werden, der die asynchrone Methode aufgerufen hat. Damit entfällt das in der Praxis aufwändige Umleiten von Aktualisierung der Oberfläche in den UI-Thread (sofern dieser die asynchrone Methode aufgerufen hat). Beim BackgroundWorker haben Sie diesen Vorteil bereits eingesetzt.
13
14
15
16
17 Der Vorteil ist, dass UI-Aktualisierungen einfach möglich sind
19
In diesem Kapitel habe ich im Abschnitt »Ein praxisnahes Beispiel« (Seite 1166) dieses Muster (rein zufällig ☺) bereits mit der Klasse Download vorbereitet. Die einzigen beiden Dinge, die in dieser Klasse noch fehlen, sind die Möglichkeit, den Download auch synchron auszuführen (was ich hier aber nicht implementiere) und das automatische Umleiten der Ereignisse in den Thread, der die Methode DownloadAsync aufgerufen hat. Und diesen wesentlichen (aber nicht besonders schwierigen) Trick zeige ich hier. Wenn Sie mit Klassen arbeiten, die das ereignisbasierte asynchrone Entwurfsmuster einsetzen, sollten Sie darauf achten, dass die Ereignishandler nicht zu viel Programmcode ausführen. Da der Aufruf der Ereignisse in den Thread umgeleitet wird, der die asynchrone Methode gestartet hat, blockiert der Arbeitsthread so lange, bis der Aufruf beendet ist. Ein effizientes Multithreading ist also nicht möglich, wenn Ereignishandler viel Zeit benötigen. Sollte dies in Ihren Programmen aber so sein, sollten Sie darüber nachdenken, in den Ereignishandlern mit asynchronem Methodenaufruf oder wieder mit Threads zu arbeiten.
18
20 21
22 HALT
23
1179
Multithreading
DISC
Auf der Buch-DVD finden Sie im Ordner Zusatz-Artikel den Artikel Das ereignisbasierte asynchrone Entwurfsmuster.pdf, der eine komplettere (allerdings noch keine nach Microsoft-Art komplette) Implementierung des ereignisbasierten asynchronen Entwurfsmusters beschreibt.
Der wesentliche Trick des ereignisbasierten asynchronen Entwurfsmusters Das EAE nutzt ein AsyncOperationObjekt, um Ereignisse umzuleiten
Um den Aufruf von Ereignissen in den Thread umzuleiten, der die asynchrone Version der Aktions-Methode gestartet hat, nutzt das ereignisbasierte asynchrone Entwurfsmuster ein AsyncOperation-Objekt, das in dieser Methode erzeugt und an die Thread-Methode weitergegeben wird. Dieses Objekt wird dann verwendet, um die Ereignisse in dem Thread aufzurufen, in dem das Objekt erzeugt wurde. Im originalen des ereignisbasierten asynchronen Entwurfsmuster ist die Weitergabe des AsyncOperation-Objekts etwas komplexer. Ich zeige hier aber eine einfache Lösung: Dazu erzeugen Sie dieses Objekt beim Aufruf der asynchronen Methode und geben es an die Thread-Methode weiter: Listing 20.29: Methode zum Starten des asynchronen Aufrufs mit einem AsyncOperation-Objekt zum Umleiten der Ereignisse public void DownloadAsync() { Thread thread = new Thread(this.DownloadInternal); thread.IsBackground = false; // AsyncOperation-Objekt für die Umleitung der Ereignisse // erzeugen AsyncOperation asyncOperation = AsyncOperationManager.CreateOperation(null); // Den Thread starten thread.Start(asyncOperation); }
Am Argument der CreateOperation-Methode können Sie ein Objekt übergeben, das der asynchronen Methode von außen übergeben wird. Dieses Objekt ist für die Weitergabe von Daten vorgesehen, die der spätere Benutzer der asynchronen Methode übergibt. Im kompletten ereignisbasierten asynchronen Entwurfsmuster ist dieses Argument vorgesehen und wird in der Thread-Methode an die Ereignisse weitergegeben. In meinem einfachen Beispiel ignoriere ich diese Möglichkeit aber. Die Thread-Methode muss natürlich entsprechend erweitert werden: private void DownloadInternal(object args) { // Das AsyncOperation-Objekt auslesen AsyncOperation asyncOperation = (AsyncOperation)args;
Über die Post-Methode des AsyncOperation-Objekts rufen Sie die Ereignisse auf. Dieser Methode übergeben Sie eine Instanz des Delegaten SendOrPostCallback, der folgendermaßen definiert ist: public delegate void SendOrPostCallback(Object state);
1180
Das ereignisbasierte asynchrone Entwurfsmuster und asynchrone Methoden
Das Argument state ist für die benutzerdefinierten Daten vorgesehen, die in diesem Beispiel ignoriert werden. So können Sie den Aufruf eines Ereignisses über die PostMethode relativ einfach in den Thread umleiten, in dem das AsyncOperation-Objekt erzeugt wurde: Listing 20.30: Umleiten von Ereignisaufrufen in den Thread, der die asynchrone Methode aufgerufen hat ...
12
if (this.DownloadProgress != null) { int percent = (int)((totalBytesRead / (float)fileSize) * 100); asyncOperation.Post(new SendOrPostCallback( userState => { this.DownloadProgress(this, new ProgressChangedEventArgs( percent, this.Url)); }), null); }
13
14
... if (this.DownloadCompleted != null) { asyncOperation.Post(new SendOrPostCallback( userState => { this.DownloadCompleted(this, new DownloadCompletedEventArgs() { Url = this.Url, FileName = DestinationFileName, Cancelled = cancelled, Error = error }); }), null); }
15
Der in der Praxis wirklich sehr große Vorteil ist, dass ein Programmierer, der eine solche Klasse einsetzt, die Handler für die Ereignisse ganz normal programmieren kann, oder sich um Thread-Probleme beim Zugriff auf Fenster, Formulare oder Steuerelement kommen zu müssen. Ich verzichte hier auf die Darstellung der relativ langen Methoden des Beispiels. Sie können sich vorstellen, dass Sie nun den Zugriff auf die Benutzeroberfläche ohne die Invoke-Methode eines Dispatcher-Objekts vornehmen können und damit die Klasse anwenden können wie eine ganz normale Klasse.
18
16
17
19
20
Auf der Buch-DVD finden Sie natürlich das komplett implementierte Beispiel.
20.8.2 Asynchrone Methoden
DISC
21
Viele ältere .NET-Klasse, die asynchrone Aktion erlauben, setzen noch nicht das ereignisbasierte asynchrone Entwurfsmuster ein, sondern besitzen Methoden, deren Name den Präfix »Begin« und »End« trägt. Die Begin-Methode startet die asynchrone Aktion, die »End«-Methode wartet darauf, dass der asynchrone Aufruf beendet ist, und gibt eventuelle Ergebnisse zurück. Der Begin-Methode können Sie einen AsyncCallback-Delegaten übergeben, der aufgerufen wird, wenn der asynchrone Vorgang beendet ist. Im Prinzip entspricht dies dem eigenen asynchronen Aufrufen von Methoden, nur dass dieser in Form zweier Methoden bereits in eine Klasse integriert ist.
22
23
1181
Multithreading
INFO
Die Verwendung der älteren asynchronen Methoden ist, verglichen mit moderneren Möglichkeiten wie dem ereignisbasierten asynchronen Entwurfsmuster und dem BackgroundWorker, doch relativ aufwändig und deswegen für die Praxis eher zweifelhaft. Das Prinzip der Verwendung dieser Methode ist das folgende: ■
■
Sie rufen die Begin-Methode auf und übergeben einen Callback-Delegaten. Außerdem übergeben Sie dieser Methode am Argument stateObject eine Instanz einer Struktur oder Klasse, die Informationen zum Aufruf enthält. In der Callback-Methode werten Sie das Status-Objekt aus und rufen die EndMethode auf.
Die FileStream-Klasse besitzt z. B. die Methoden BeginRead und EndRead. BeginRead startet das Lesen einer Datei. Sie müssen hier ein Byte-Array übergeben, in das die gelesenen Daten geschrieben werden. Dieses Byte-Array werten Sie in der CallbackMethode aus, weswegen Sie es in einem Status-Objekt definieren. Dieses wird BeginRead neben dem Callback-Delegaten übergeben. In der Methode des CallbackDelegaten wird das Statusobjekt ausgewertet, nachdem die EndRead-Methode aufgerufen wurde. Dazu benötigen Sie allerdings eine Referenz auf das Objekt, über das Sie die Begin-Methode aufgerufen haben. Dieses übergeben Sie idealerweise in dem Status-Objekt: Listing 20.31: Typische Verwendung der älteren asynchronen Methoden /* Struktur, die die Daten für das Laden einer Datei verwaltet */ private struct FileLoadData { public string FileName; public Byte[] FileData; public FileStream FileSteam; } /* Callback-Methode für den synchronen Aufruf */ private static void FileCallback(IAsyncResult result) { // Das FileLoadData-Objekt auslesen FileLoadData fileLoadData = (FileLoadData)result.AsyncState; // Den asynchronen Aufruf beenden fileLoadData.FileSteam.EndRead(result); // Das Ergebgnis auslesen Console.WriteLine("Das Laden von '" + fileLoadData.FileName + "' ist fertig"); Console.WriteLine("Die Daten haben eine Länge von " + fileLoadData.FileData.Length + " Byte"); } static void Main(string[] args) { string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "Los Roques.jpg"); // FileStream erzeugen FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
1182
Threads synchronisieren
// Die Datei asynchron in ein Byte-Array lesen FileLoadData fileLoadData = new FileLoadData() { FileName = fileName, FileData = new Byte[fileStream.Length], FileSteam = fileStream }; fileStream.BeginRead(fileLoadData.FileData, 0, (int)fileStream.Length, FileCallback, fileLoadData);
12
Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine(); }
13
20.9
Threads synchronisieren
Mit dem bisherigen Wissen können Sie einfache Multithreading-Anwendungen implementieren. Wenn Sie mit mehreren Threads arbeiten, wird in der Praxis aber die Synchronisierung wichtig. Dafür gibt es zwei Gründe: ■
■
14
Threads, die auf gemeinsame (globale) Daten oder Ressourcen zugreifen, müssen den Zugriff auf diese sperren, solange sie schreibend oder lesend zugreifen. Nur so kann verhindert werden, dass die Daten bzw. Ressourcen in einen ungültigen Status versetzt werden. In vielen Anwendungen arbeiten mehrere Threads, bei denen einer von einem Ergebnis abhängig ist, das ein anderer berechnet. Der zweite Thread muss dann darauf warten, dass der erste fertig ist. Der erste Thread signalisiert dem zweiten dann, dass er seine Berechnung beendet hat, und der zweite nimmt die Arbeit auf (während der erste vielleicht wieder neue Daten berechnet). Hier müssen Sie mit Techniken arbeiten, die den zweiten Thread solange blockieren, bis der erste signalisiert, dass er ein Ergebnis ermittelt hat.
15
16
17
18
Die Synchronisation von Threads kann in vier Bereiche eingeteilt werden: ■
■
■
■
Blockierende Methoden blockieren einen Thread so lange, bis ein anderer Thread beendet ist. Zu diesen Methoden gehören die Join-Methode der ThreadKlasse und die EndInvoke-Methode eines asynchron ausgeführten Delegaten. Blockierende Konstrukte erlauben die Definition eines Code-Blocks, der exklusiv nur von einem Thread ausgeführt werden kann. Damit können Sie Probleme lösen, die ansonsten entstehen würden, wenn mehrere Threads gleichzeitig auf dieselben Daten zugreifen. Das Schlüsselwort lock und die Klassen Monitor und Mutex gehören dazu. lock und Monitor blockieren nur innerhalb eines Prozesses, Mutex kann auch über mehrere Prozesse hinweg blockieren. Signalisierungs-Konstrukte erlauben, dass ein Thread darauf wartet, dass ein anderer ihm signalisiert, dass er seinen Job aufnehmen kann. .NET stellt diese Möglichkeiten über die von WaitHandle abgeleiteten Klassen ManualResetEvent und AutoResetEvent und die Methoden Wait und Pulse der Monitor-Klasse zur Verfügung. Nicht blockierende Synchronisations-Konstrukte erlauben die Definition von Daten, auf die mehrere Threads sicher zugreifen können, ohne synchronisiert werden zu müssen. In .NET gehören die Interlocked-Klasse und das volatileSchlüsselwort dazu.
19
20 21
22
23
Im Folgenden stelle ich alle Möglichkeiten grundlegend vor, wobei ich mehr Wert auf die Features lege, die in der Praxis am häufigsten verwendet werden.
1183
Multithreading
20.9.1 Threads blockieren Ein Thread kann auf einen anderen warten
Ein Thread kann sich selbst blockieren und darauf warten, dass ein anderer Thread fertig ausgeführt wird. Mit dem Aufruf der EndInvoke-Methode bei der asynchronen Ausführung von Methoden (Seite 1150) haben Sie bereits den UI-Thread so lange blockiert, bis der Thread, der die Methode asynchron ausführt, wirklich beendet ist. EndInvoke könnten Sie statt im Ende-Callback auch nach dem Aufruf von BeginInvoke aufrufen, um zu warten, bis die asynchrone Ausführung beendet ist: Listing 20.32: Asynchroner Aufruf ohne Callback mit Warten auf das Beenden der Methode // Asynchroner Aufruf ohne Callback IAsyncResult asyncResult = primeNumberDelegate.BeginInvoke( startNumber, null, null); // Weitere Anweisungen ... // Warten, bis der asynchrone Aufruf beendet ist primeNumberDelegate.EndInvoke(asyncResult);
Das macht aber in den meisten Fällen keinen Sinn, denn stattdessen könnten Sie die Methode auch normal (synchron) ausführen. In Sonderfällen haben Sie aber immerhin die Möglichkeit, den Thread, der die asynchrone Methode gestartet hat, so lange zu blockieren, bis der asynchrone Aufruf beendet ist. Join wartet darauf, dass ein anderer Thread beendet wird
Ähnliches erreichen Sie beim echten Multithreading über die Join-Methode einer Thread-Instanz. Wenn Sie diese Methode aufrufen, wartet der aufrufende Thread so lange, bis der Thread, auf dem die Methode aufgerufen wurde, beendet ist. Sinn macht die Verwendung von Join in meinen Augen: ■ ■
wenn ein Thread (z. B. der UI-Thread) mehrere Arbeitsthreads startet und darauf warten muss, dass alle Arbeitsthreads ihre Arbeit beendet haben und wenn ein Thread eine Methode quasi synchron aufrufen muss, dabei aber eingegebener Timeout nicht überschritten werden soll.
Der erste Fall ist einfach demonstriert. Angenommen eine Methode muss mehrere (PDF-)Dateien aus dem Internet herunterladen, die gemeinsam verarbeitet werden müssen. Würden diese synchron hintereinander heruntergeladen, würde der langsame Download die Verarbeitung wesentlich verlangsamen. Besser wäre es in diesem Fall, für den Download Threads zu verwenden. Da die Methode aber alle heruntergeladenen Dateien gleichzeitig verarbeiten muss (z. B. indem sie die PDFDateien hintereinander in ein Ergebnis PDF-Dokument schreibt), muss sie darauf warten, dass die Download-Threads mit ihrer Arbeit fertig sind. Dieses komplexe Beispiel implementierte ich hier aber nicht. Ich verwende ein abstraktes. Ich denke, Sie können sich vorstellen, wie Sie dies in der Praxis umsetzen: Listing 20.33: Demo für eine Anwendung der Join-Methode /* Thread-Methode zum Herunterladen von Dateien */ private static void Downloader(object args) { // Datei herunterladen (hier nur simuliert) Console.WriteLine("Lade " + args + " ..."); Thread.Sleep(3000); Console.WriteLine(args + " ist heruntergeladen"); }
1184
Threads synchronisieren
static void Main(string[] args) { Console.Title = "Threads blockieren"; // Arbeits-Threads erzeugen und starten Thread thread1 = new Thread(Downloader); Thread thread2 = new Thread(Downloader); Thread thread3 = new Thread(Downloader); thread1.Start("Datei 1"); thread2.Start("Datei 2"); thread3.Start("Datei 3");
12
Console.WriteLine("Warte auf das Ende der Arbeitsthreads ..."); thread1.Join(); thread2.Join(); thread3.Join(); Console.WriteLine("Verarbeite die Dateien ..."); ...
13
}
14
Wichtig ist, dass Sie Join auch einen Timeout übergeben können. Join wartet dann maximal die angegebene Zeit und gibt false zurück, wenn der Arbeitsthread in der Zwischenzeit nicht beendet werden konnte. So könnten Sie in dem obigen Beispiel verhindern, dass ein hängender Arbeitsthread das Programm stillgelegt. Den richtigen Timeout zu finden, ist in der Praxis aber nicht einfach. Falls Sie mit Timeouts arbeiten, empfehle ich dringend, diese konfigurierbar zu machen.
15
Timeouts können Sie auch für den zweiten Fall einsetzen. Damit können Sie einen Thread quasi synchron ausführen, aber dafür sorgen, dass dieser einen gegebenen Timeout nicht überschreitet:
16
Listing 20.34: Quasi synchroner Aufruf eines Arbeitsthread mit Timeout
17
// Arbeitsthread erzeugen und starten Thread thread = new Thread(new ThreadStart(() => { int i = 0; // Bewußt implementierte Endlosschleife while (i < 10) { Thread.Sleep(100); } })); thread.Start();
18
19
20
Console.WriteLine("Warte auf das Ende des Arbeitsthread ..."); if (thread.Join(TimeSpan.FromSeconds(3)) == false) { Console.WriteLine("Der Arbeitsthread wurde innerhalb des Timeout " + "nicht beendet");
21
// Den Thread (der vielleicht in einer Endlosschleife ausgeführt wird) // abbrechen thread.Abort(); }
22
In der Praxis ist dieses Feature nicht zu unterschätzen. Wenn Sie eine Methode eigentlich synchron aufrufen wollen, z. B. um das Ergebnis dieser Methode auszuwerten, aber gleichzeitig dafür sorgen wollen, dass ein gegebener Timeout nicht überschritten wird, ist ein Thread, auf den Sie über Join mit dem gegebenen Timeout warten, ideal geeignet.
23
1185
Multithreading
20.9.2 Den Zugriff auf globale Daten und Ressourcen sperren Häufig greift ein Thread auf Daten oder Ressourcen zu, auf die ein anderer Thread ebenfalls zugreift. In einem solchen Fall müssen Sie sicherstellen, dass nicht zwei Threads (mehr oder weniger) gleichzeitig mit den Daten oder Ressourcen arbeiten. Und dies betrifft sogar den Fall, dass der Zugriff nur in einer einzigen Anweisung erfolgt. Threads können jederzeit unterbrochen werden
Ein Thread kann nämlich auf der Ebene des CIL-Code prinzipiell jederzeit vom Betriebssystem unterbrochen werden. Wenn Sie zum Zugriff auf globale Daten oder Ressourcen nur eine einzige Anweisung verwenden, scheint das auf den ersten Blick nicht so schlimm zu sein. Leider können Sie damit aber auch Probleme verursachen (wie ich später noch beweise). Eine C#-Anweisung ist auf Ebene des CIL-Code nicht grundsätzlich atomar. Das Schreiben des int-Werts 42 in eine Variable besteht z. B. aus den Anweisungen ldc.i4.s 0x2a (42 auf den Stack legen) und stloc.0 (den obersten Stack-Wert in die Variable mit dem Index 0 lesen). Eine simple Anweisung wie i++ besteht sogar aus vier Anweisungen: ldloc.0 ldc.i4.1 add stloc.0
// // // //
Den Den Die Den
Wert der Variable am Index 0 auf den Stack legen Wert 1 auf den Stack laden auf dem Stack liegenden Werte addieren oberen Stack-Wert in die Variable am Index 0 schreiben
Auf der Ebene des CIL-Code ist .NET nicht mehr ganz so einfach. Glücklicherweise haben wir damit nicht viel zu tun … Außer beim Multithreading. Da kann es nämlich vorkommen, dass ein Thread an einer der Maschinencode-Anweisungen (nicht CIL-Anweisung!) unterbrochen wird, die eine C#-Anweisung ausmachen. Sie können sich vorstellen, dass dies zu massiven (logischen) Problemen führt, wenn zwei Threads gleichzeitig auf dieselben Daten zugreifen. Beim Zugriff auf einfache Typen gilt aber, dass der Zugriff atomar ist, wenn der Typ eine Größe aufweist, die kleiner/gleich der Breite des Betriebssystems bzw. der CPU ist. Auf einem 32-Bit-Betriebssystem sollte der Zugriff auf int-Variablen z. B. atomar sein. Atomar heißt hier, dass der Zugriff als eine Operation betrachtet wird, die vom Betriebssystem nicht unterbrochen werden darf. Beim Zugriff auf größere Typen sieht das Ganze aber schon wieder anders aus. Wenn Sie auf einem 32-Bit-System ein (privates oder öffentliches) long-Feld schreiben, handelt es sich auf der Ebene der Maschine um zwei atomare Befehle, da der long-Wert in zwei 32-Bit-Werte aufgeteilt wird. Hier kann es durchaus vorkommen, dass der Thread, der das Feld liest oder schreibt, unterbrochen wird. Wenn nun ein anderer Thread dasselbe Feld liest oder schreibt, entstehen ungültige Daten. Um dieses Problem zunächst zu visualisieren, habe ich eine einfache Konsolenanwendung geschrieben. Die Thread-Methode führt eine Schleife aus. In der Schleife werden der Name des aktuellen Thread und der Schleifenindex ausgegeben. Die erste Version des Programms führt noch nicht zu einem Problem: Listing 20.35: Einfaches Beispiel für das Zugriffs-Problem auf globale Daten und Ressourcen private static void Demo(object obj) { for (int i = 0; i < 30; i++) {
1186
Threads synchronisieren
Console.Write(Thread.CurrentThread.Name + "-" + i.ToString("000") + " "); } } static void Main(string[] args) { Console.Title = "Den Zugriff auf globale Daten sperren";
12
// Thread starten Thread thread1 = new Thread(Demo); thread1.Name = "T1"; thread1.IsBackground = true; thread1.Start();
13
// Weiteren Thread starten Thread thread2 = new Thread(Demo); thread2.Name = "T2"; thread2.IsBackground = true; thread2.Start();
14
Console.ReadLine(); }
15
Das Ergebnis dieser ersten Version ist in Ordnung (Abbildung 20.11): Abbildung 20.11: Das (noch) intakte Beispielprogramm
16
17 Eine kleine Veränderung bringt aber einiges durcheinander: Listing 20.36: Die leicht veränderte Thread-Methode
18
private static void Demo(object obj) { for (int i = 0; i < 100; i++) { Console.Write(Thread.CurrentThread.Name); Console.Write("-" + i.ToString("000") + " "); } }
19
Abbildung 20.12: Das Ergebnis der leicht veränderten Version
20 21
22
Weil das Schreiben an die Konsole nun in zwei Anweisungen ausgeführt wird, kann es vorkommen, dass ein Thread nach der ersten Write-Anweisung unterbrochen wird. Im Beispiel ist dies der Fall in der zweiten Zeile: Nach dem Schreiben des Thread-Namens wurde der erste Thread unterbrochen und der zweite ausgeführt.
23
Sie können sich vorstellen, dass dieses Problem in richtigen Anwendungen, bei denen Threads auf globale Daten zugreifen, ein massives Problem werden kann.
1187
Multithreading
INFO
Auf atomare Anweisungen im Zusammenhang mit 32- und 64-Bit-Werte gehe ich im Abschnitt »Atomare Anweisungen und die Klasse Interlocked« (Seite 1202) noch einmal ein.
Sperren über lock lock sperrt den Zugriff auf Objekte oder Typen
Die Lösung des Zugriffsproblems in Threads ist zunächst einfach (abgesehen von Deadlock-Problemen, die Sie ggf. damit verursachen können, siehe Seite 1191): Sie können innerhalb einer Thread-Methode das lock-Schlüsselwort verwenden, um den Zugriff auf ein Objekt oder einen Typ zu sperren, solange der Thread darauf zugreift. In unserem Fall ist die zu sperrende Ressource die Console-Klasse. Da nur Objekte oder Typen gesperrt werden können, müssen wir den Typ der ConsoleKlasse sperren (Console ist ja keine Instanz, sondern eine Klasse): Listing 20.37: Sperren des Zugriffs auf eine Ressource oder globale Daten über lock private static void Demo(object obj) { for (int i = 0; i < 100; i++) { lock (typeof(Console)) { Console.Write(Thread.CurrentThread.Name); Console.Write("-" + i.ToString("000") + " "); } } }
INFO
lock sollten Sie prinzipiell immer einsetzen, wenn Sie in einem Thread auf globale Daten (Daten, die in mehreren Methoden zur Verfügung stehen) oder auf Ressourcen zugreifen.
Das folgende Beispiel zeigt die Auswirkungen einer fehlenden Synchronisation bei Berechnungen. Zwei Threads inkrementieren in einer Schleife ein statisches Feld (bewusst in zwei Methoden, um zu zeigen, dass das Sperren in allen Methoden erfolgen muss): Listing 20.38: Beispiel für das Synchronisierungs-Problem bei Berechnungen /* Wird von den Threads berechnet */ private static int sum; /* Thread-Methode, die die Summe inkrementiert */ private static void Demo1() { for (int i = 0; i < 1000000; i++) { Program.sum++; } } /* Weitere Thread-Methode, die die Summe inkrementiert */ private static void Demo2() { for (int i = 0; i < 1000000; i++) { Program.sum++;
1188
Threads synchronisieren
} } static void Main(string[] args) { // Zwei Threads starten Thread thread1 = new Thread(Demo1); thread1.Start(); Thread thread2 = new Thread(Demo2); thread2.Start();
12
// Auf die beiden Threads warten thread1.Join(); thread2.Join();
13
// Die Summe ausgeben Console.WriteLine(Program.sum); }
14
Das Ergebnis dieses Programms variiert von Aufruf zu Aufruf. Mit viel Glück kommt 2000000 heraus (was das korrekte Ergebnis wäre). Bei den meisten Aufrufen des Programms kommen aber vollkommen falsche Werte heraus (wie z. B. 1076305). Wenn Sie dieses Problem nachvollziehen, sollten Sie ein wenig mit der Anzahl der Durchläufe in den Schleifen experimentieren. Threads mit Schleifen, die sehr prozessorintensiv und schnell sind, werden häufig komplett ausgeführt und nicht durch andere Threads unterbrochen (weil die Zeitscheibe für das komplette Ausführen ausreicht). In meinen Tests wurden beide Threads bei einer Anzahl Durchläufe, die deutlich kleiner war als 1000000, immer sequentiell nacheinander ausgeführt, was bewirkte, dass das Ergebnis stimmte. Sie können auch Thread.Sleep(1) in die Schleife einbauen, um eine höhere Bearbeitungszeit zu simulieren. Dann werden die Threads auf jeden Fall mitten in der Schleife durch den jeweils anderen Thread unterbrochen.
15 INFO
16
17
18
Sperren über ein separates Sperr-Objekt Den Zugriff auf das statische Feld müssen Sie hier sperren. Da lock nur Referenztypen oder Type-Instanzen erlaubt, können Sie die int-Variable aber nicht direkt sperren. Der Trick ist, dass Sie dazu ein separates Sperr-Objekt verwenden:
19
Listing 20.39: Sperren über ein separates Sperr-Objekt
20
/* Sperr-Objekt */ private static object lockObject = new object(); /* Thread-Methode, die die Summe inkrementiert */ private static void Demo1() { for (int i = 0; i < 1000000; i++) { lock (Program.lockObject) { Program.sum++; } } }
21
22
23
/* Weitere Thread-Methode, die die Summe inkrementiert */ private static void Demo2() { for (int i = 0; i < 1000000; i++)
1189
Multithreading
{ lock (Program.lockObject) { Program.sum++; } } }
INFO
In diesem Zusammenhang ist es interessant, dass Sie von Object, der Basisklasse aller Typen, eine Instanz erzeugen können. Das wusste ich bis vor kurzem auch noch nicht ☺. Das ist sehr hilfreich für das Sperren von Daten und Ressourcen, die nicht über einen Referenztyp referenziert werden. Wenn Sie das Beispiel einmal ausprobieren und in einer der Methoden die Sperrung wegnehmen, werden Sie sehen, dass das Ergebnis dann in vielen Fällen wieder falsch ist.
Was Sie bei der Verwendung von lock beachten sollten Bei der Verwendung des lock-Schlüsselworts sollten Sie einige Punkte beachten: ■
■
■
■ ■
■
1190
Beim Zugriff auf 32-Bit-Zahlvariablen müssen Sie eigentlich nicht sperren. Auf einem 32-Bit-Betriebssystem sollte das Lesen und Schreiben von 32-Bit-Werten atomar sein. Ich bin da allerdings nicht ganz so sicher, immerhin besteht das Schreiben eines Integer-Werts in eine Variable aus zwei Maschinencode-Anweisungen. Und beim Multithreading habe ich gelernt, sehr vorsichtig zu sein … Beim Zugriff auf 64-Bit-Zahlvariablen (und andere Typen) müssen Sie prinzipiell sperren, da der Zugriff auf diese nicht atomar ist. Sie können für 64-Bit-Zahlvariablen aber statt einer Sperrung das effizientere Lesen und Schreiben über die Klasse Interlocked verwenden (siehe Seite 1202). Sie sollten nur so wenig sperren wie möglich. Wenn Sie Anweisungen mit in den gesperrten Bereich aufnehmen, die mit den gesperrten Daten oder Ressourcen nichts zu tun haben, führt das dazu, dass andere Threads länger auf die Daten bzw. Ressourcen warten müssen als notwendig. Im obigen Beispiel wäre es z. B. ein Fehler (der aber in der ersten Version sogar mir unterlaufen ist ☺), den lockBlock um die jeweilige Schleife zu legen. Damit würde der Zugriff für die gesamte Dauer der Methode blockiert. Da aber der jeweils andere Thread vom Betriebssystem trotzdem Zeit zugeteilt bekommt, verbringt er diese mit sinnlosem Warten. Beim Sperren müssen Sie sehr darauf achten, dass Sie keine Deadlocks produzieren. Mehr dazu erfahren Sie ab Seite 1191. Das Sperren kostet recht viel Zeit. Die Methode im Beispiel wird nun wesentlich langsamer ausgeführt (ca. 95 ms im Vergleich zu ca. 2,7 ms). Wie hoch der Performanceverlust ist, hängt natürlich davon ab, wie Sie sperren. In unserem Beispiel könnte die Performance dadurch erhöht werden, dass der lock-Block oberhalb der Schleife angelegt wird. Dann kann aber zwischenzeitlich kein anderer Thread auf die Eigenschaft zugreifen. lock macht nur dann Sinn, wenn alle Threads damit arbeiten, die auf dieselben Daten oder Ressourcen zugreifen. Diese müssen außerdem natürlich dasselbe Objekt bzw. denselben Typ zum Sperren einsetzen. Greift ein Thread auf ein in einem anderen Thread gesperrtes Objekt zu, ohne lock zu verwenden, wird dieser nicht blockiert und Sie haben wieder dasselbe Problem wie zuvor.
Threads synchronisieren
■
Wenn Sie in einem Thread (über die Invoke-Methode) auf ein Formular, Fenster oder ein Steuerelement zugreifen, dürfen Sie den Zugriff nicht sperren. Ansonsten laufen Sie Gefahr, dass Sie damit einen Deadlock produzieren.
lock und die Monitor-Klasse Die Arbeitsweise von lock wird klarer, wenn Sie sich anschauen, was der Compiler daraus macht. Aus
lock verwendet die Monitor-Klasse
lock(Program.lockObject) { // Programmcode }
12
13
wird try {
14
System.Threading.Monitor.Enter(Program.lockObject); // Programmcode } finally { System.Threading.Monitor.Exit(Program.lockObject); }
15
Das Sperren erfolgt also über die Klasse Monitor aus dem Namensraum System. Threading. Ein Monitor ist in diesem Zusammenhang ein Überwachungsgerät, das den Zugriff auf Objekte überwacht.
16
Die statische Methode Enter fügt dem Monitor ein Objekt oder einen Typ hinzu (der ja auch über ein Objekt – eine Instanz der Type-Klasse – repräsentiert wird). Wenn ein anderer Thread Enter mit demselben Objekt aufruft, blockiert Enter so lange, bis das Objekt über die Exit-Methode freigegeben wird. Falls Sie an Stelle von lock die Monitor-Klasse verwenden (was u. U. Sinn macht, denn Monitor stellt erweiterte Features wie die TryEnter-Methode zur Verfügung), ist es wichtig, dass Sie das Sperren in einem catch-Block und das Entsperren im finally-Block vornehmen. Damit stellen Sie sicher, dass die Daten oder die Ressourcen auf jeden Fall wieder entsperrt werden, auch wenn eine Ausnahme eintritt. Falls Sie nicht so vorgehen, wird der Zugriff nicht wieder freigegeben, wenn vor dem Freigeben eine Ausnahme eintritt. Ein anderer Thread, der gerade auf die Freigabe der Daten wartet, wird dann nicht weiter ausgeführt – der Thread hängt in einem so genannten Deadlock.
17
18 INFO
19
20
Race Conditions und Deadlocks Race Conditions sind Bedingungen, bei denen ein Thread mit Daten arbeitet, dieser durch einen anderen Thread unterbrochen wird, der die Daten verändert, und der erste Thread dann mit den veränderten Daten fehlerhaft weiterarbeitet. Race Conditions verhindern Sie über das Sperren von gemeinsam verwendeten Daten oder Ressourcen.
Race Conditions werden durch Sperren verhindert
Das Sperren von Daten oder Ressourcen und das Blockieren von Threads (die zwei Synchronisations-Grundtechniken) kann aber einen Deadlock zur Folge haben. Bei einem solchen blockieren sich zwei Threads gegenseitig. Thread 1 sperrt z. B. Ressource A und wartet dann auf Ressource B. Thread 2 sperrt die Ressource B und wartet auf die Ressource A. Beide Threads können nicht weiterarbeiten, weil jeder auf den anderen wartet.
Das Sperren kann Deadlocks verursachen
21
22
1191
23
Multithreading
Das folgende Beispiel soll dieses Problem erläutern. In einer Konsolenanwendung verwenden zwei Threads die Object-Referenzen lockObject1 und lockObject2 zum Sperren. Thread 1 blockiert erst lockObject1 und danach lockObject2. Thread 2 arbeitet umgekehrt: Listing 20.40: Demo für einen Deadlock object lockObject1 = new object(); object lockObject2 = new object(); Thread thread1 = new Thread(new ThreadStart(() => { for (int i = 0; i < 100; i++) { Console.WriteLine("Thread 1 wartet auf lockObject1 ..."); lock (lockObject1) { Console.WriteLine("Thread 1 hat lockObject1 gesperrt"); Console.WriteLine("Thread 1 wartet auf lockObject2 ..."); lock (lockObject2) { Console.WriteLine("Thread 1 hat lockObject2 gesperrt"); Console.WriteLine("Thread 1: " + i); Thread.Sleep(10); } Console.WriteLine("Thread 1 hat lockObject2 entsperrt"); } Console.WriteLine("Thread 1 hat lockObject1 entsperrt"); } })); thread1.Start(); Thread thread2 = new Thread(new ThreadStart(() => { for (int i = 0; i < 100; i++) { Console.WriteLine("Thread 2 wartet auf lockObject2 ..."); lock (lockObject2) { Console.WriteLine("Thread 2 hat lockObject2 gesperrt"); Console.WriteLine("Thread 2 wartet auf lockObject1 ..."); lock (lockObject1) { Console.WriteLine("Thread 2 hat lockObject1 gesperrt"); Console.WriteLine("Thread 2: " + i); Thread.Sleep(10); } Console.WriteLine("Thread 2 hat lockObject1 entsperrt"); } Console.WriteLine("Thread 2 hat lockObject2 entsperrt"); } })); thread2.Start();
Wird das Programm ausgeführt, hängt es nach kurzer Zeit in einem Deadlock (Abbildung 20.13). Deadlocks treten unberechenbar auf
1192
Wann ein Deadlock auftritt, hängt sehr von den Threads und den Umgebungsbedingungen ab. Es kann auch sein, dass ein Deadlock nicht immer auftritt (aber wenn, dann natürlich meist beim Kunden ☺). Weil solche Fehler nur sehr schwer zu lokalisieren sind und möglicherweise gar nicht erkannt werden (bei Hintergrundthreads, die mit der Anwendung beendet werden), sollten Sie beim Sperren sehr vorsichtig
Threads synchronisieren
sein. Bei Vordergrundthreads führt ein Deadlock sogar dazu, dass die Anwendung nicht beendet werden kann und abgeschossen werden muss. Abbildung 20.13: Das Programm hängt in einem Deadlock fest
12
13
14
Deadlocks sollten Sie versuchen zu vermeiden, indem Sie möglichst wenig Objekte sperren und die Sperrung möglichst schnell wieder aufheben. Leider ist die Ursache in der Praxis nicht immer so offensichtlich wie im obigen Beispiel.
15 Was bei der Suche nach Deadlock-Ursachen enorm hilft, ist das Thread-Fenster von Visual Studio. Halten Sie das Programm dazu explizit an (über DEBUGGEN / ALLE UNTERBRECHEN) und öffnen Sie das Thread-Fenster. Doppelklicken Sie auf den Threads, um zu sehen, welche hängen. Wenn Sie den Threads Namen vergeben haben, hilft das natürlich bei der Suche. Wenn Ihre Threads potenziell Deadlock-gefährdet sind, können Sie statt lock auch Monitor.TryEnter verwenden (natürlich in Zusammenhang mit Monitor.Exit). Dieser Methode können Sie einen Timeout übergeben. Wenn TryEnter false zurückgibt, wurde die Sperre innerhalb des Timeout nicht aufgehoben. In diesem Fall können Sie den Fehler protokollieren und den Thread abbrechen.
TIPP
16
Deadlocks können mit Monitor. TryEnter abgefangen werden
17
18
Das Beispiel »Deadlocks abfangen« auf der Buch-DVD zeigt, wie Sie dies implementieren können. Der Programmcode ist leider zu lang für eine Darstellung im Buch. An der Adresse research.microsoft.com/~birrell/papers/ThreadsCSharp.pdf finden Sie einen Artikel, der neben dem allgemeinen Multithreading (aus eher technischer Sicht) auch Deadlocks sehr gut beschreibt.
DISC
19
REF
20
20.9.3 Prozessübergreifendes Sperren mit einem Mutex Ein Mutex ist der Monitor-Klasse ähnlich (obwohl er von WaitHandle abgeleitet ist, deren andere Ableitungen nicht zum Sperren, sondern zum Synchronisieren eingesetzt werden). Er erlaubt aber das Sperren von Ressourcen über Prozessgrenzen hinweg. Ein Mutex ist ein betriebssystemweites Sperr-Objekt, dem Sie einen auf das gesamte System eindeutigen Namen zuweisen. Jeder Prozess kann einen solchen Mutex erzeugen oder einen bereits vorhandenen holen.
Ein Mutex erlaubt das Sperren über Prozessgrenzen
22
Ein Thread kann einen Mutex über den Aufruf von WaitOne1 versuchen zu sperren. WaitOne blockiert allerdings, wenn der Mutex zurzeit gesperrt ist. Der Thread, der den 1
21
23
Der eigenartige Name dieser Methode stammt von der Basisklasse WaitHandle. Dort ist WaitOne eigentlich dazu vorgesehen, auf ein Signal zu warten, das von einem anderen Thread gesetzt wird.
1193
Multithreading
Mutex gesperrt hat, gibt diesen dann nach der Arbeit mit der gemeinsamen Ressource über ReleaseMutex wieder frei. Der andere Thread nimmt dann seine Arbeit auf, wobei der Mutex wieder gesperrt wird.
Das Prinzip ist also ziemlich einfach. Zunächst erzeugen oder holen Sie einen Mutex. Am Konstruktor übergeben Sie den Namen. Hier sollten Sie einen GUID verwenden (den Sie über das Menü EXTRAS / GUID ERSTELLEN in Visual Studio erzeugen können), damit der Name betriebssystemweit eindeutig ist. Existiert bereits ein Mutex mit dem übergebenen Namen, erhalten Sie eine Referenz auf diesen. Ansonsten wird ein neuer Mutex erzeugt. Am ersten Argument können Sie bestimmen, ob Ihr Prozess den Mutex direkt bei der Anforderung sperrt. In der Praxis macht die Übergabe von true hier keinen Sinn. Über die Methode WaitOne warten Sie dann darauf, dass der Mutex freigegeben wird (oder ist). Dabei können Sie einen Timeout übergeben, der festlegt, wie lange WaitOne maximal wartet. In diesem Fall können Sie über die Rückgabe von WaitOne ermitteln, ob der Mutex innerhalb des Timeout freigegeben wurde. Nach der Arbeit mit der Ressource rufen Sie dann ReleaseMutex auf. Das Ganze sollte in einem try-finally-Konstrukt erfolgen, damit der Mutex auf jeden Fall freigegeben wird.
HALT
Bei der Arbeit mit Mutex-Objekten ist es einerseits sehr hilfreich, dass das Betriebssystem einen gesperrten Mutex automatisch zurücksetzt, wenn der Prozess korrekt beendet wird. Wird der Prozess, der den Mutex gesperrt hat, aber abgeschossen, wirft WaitOne eine AbandonedMutexException. Diese steht dafür, dass der andere Thread den Mutex beim Beenden nicht korrekt freigegeben hat. Diese Ausnahme müssen Sie direkt beim Aufruf von WaitOne abfangen. Danach können Sie auf die Ressource zugreifen, sollten aber beachten, dass die Ressource u. U. ungültig sein kann. In meinen Tests funktionierte das auch, wenn mehrere Anwendungen auf den Mutex gewartet haben: Die Ausnahme wurde nur in einer der Anwendungen geworfen (die als erste den Mutex erhielt). Listing 20.41: Typische Verwendung eines Mutex try { Console.WriteLine("Warte auf den Mutex ..."); // Den Mutex sperren bzw. auf die Freigabe warten try { mutex.WaitOne(); } catch (AbandonedMutexException) { Console.WriteLine("Der Mutex ist verwaist. " + "Das macht aber nichts :-)"); } // Die Arbeit mit der externen Ressource ausführen Console.WriteLine("Arbeite mit der Ressource ..."); Thread.Sleep(3000); Console.WriteLine("Fertig"); } finally { // Den Mutex freigeben mutex.ReleaseMutex(); }
1194
Threads synchronisieren
Mutex-Objekte sollten Sie nur dann einsetzen, wenn mehrere Prozesse auf eine Ressource zugreifen. Da ein Mutex langsamer arbeitet als die Monitor-Klasse, verwenden Sie zum Sperren innerhalb eines Prozesses aber besser lock (oder Monitor direkt). Außerdem müssen Sie beachten, dass alle Prozesse, die auf die zu sperrende Ressource zugreifen, auch denselben Mutex verwenden. Ansonsten funktioniert die Sperrung nicht (ähnlich wie bei der Monitor-Klasse, wenn eine Methode Monitor.Enter gar nicht oder mit einem anderen Objekt verwendet).
Ein Mutex kann auch für einen netten Trick verwendet werden: Damit können Sie verhindern, dass eine Anwendung mehrfach gestartet werden kann:
Verhindern eines Mehrfachstarts
Listing 20.42: Verwendung eines Mutex, um zu verhindern, dass eine Anwendung mehrfach gestartet werden kann
12
13
// Mutex erzeugen string mutexName = "DD036A70-AABC-4123-B9D6-FB63C98C6E17"; Mutex mutex = new Mutex(false, mutexName);
14
// Überprüfen, ob der Mutex gesperrt ist if (mutex.WaitOne(TimeSpan.FromSeconds(5), false) == false) { Console.WriteLine("Eine andere Instanz dieser Anwendung " + "wird bereits ausgeführt"); Console.WriteLine("Beenden mit Return"); Console.ReadLine(); return; }
15
16
Console.WriteLine("Anwendung wird ausgeführt"); Console.WriteLine("Beenden mit Return"); Console.ReadLine();
17
20.9.4 Signalisierungs-Konstrukte Signalisierungs-Konstrukte erlauben die Synchronisierung von Threads über ein Verfahren, das einer Ampel ähnlich ist, die in mehreren Threads beobachtet wird. Ein Thread kann die Ampel auf Rot schalten. Andere Threads, die die Ampel beobachten, müssen dann warten, bis der eine Thread die Ampel wieder auf Grün schaltet (Gelb gibt es dabei nicht ☺).
18
19
.NET bietet die folgenden Klassen zur Implementierung einer Signalisierung: ManualResetEvent, AutoResetEvent, Mutex und Semaphore. Alle diese Klassen sind von der Basisklasse WaitHandle abgeleitet. Auf ManualResetEvent und AutoResetEvent gehe ich im Folgenden näher ein. Mutex und Semaphore erläutere ich nur grundlegend.
20
WaitHandle und EventWaitHandle 21
Die Klasse WaitHandle-Klasse ist die gemeinsame Basisklasse der im Folgenden beschriebenen Synchronisierungs-Klassen. Diese Klasse kapselt die vom WindowsAPI auf der Ebene des Betriebssystems verwendeten Synchronisierung-Handles. Ein WaitHandle-Objekt ist so etwas wie eine Ampel, allerdings nur mit zwei Farben: Rot und Grün. Diese »Ampel« steht auf Grün, wenn der WaitHandle signalisiert ist. Sie steht auf Rot, wenn er unsignalisiert ist. Das ist für viele Entwickler etwas verwirrend, weil signalisiert im normalen Sprachgebrauch für ein Stopp-Signal steht (also für Rot). Ein WaitHandle signalisiert anderen Threads aber, dass diese nun weiterarbeiten können (nicht, dass diese anhalten sollen).
Ein WaitHandle kann signalisiert oder unsignalisiert sein
22
23
1195
Multithreading
WaitHandle selbst ist abstrakt. Von WaitHandle sind die Klassen EventWaitHandle, Mutex und Semaphore abgeleitet. EventWaitHandle ist die Basisklasse für ManualResetEvent und AutoResetEvent. Alle diese Klassen implementieren das konkrete Verhalten eines WaitHandle auf unterschiedliche Weise. Das Signalisieren und Unsignalisieren erfolgt in den verschiedenen Klassen durch unterschiedliche Methoden. Ich beschreibe das Verhalten hier an der EventWaitHandle-Klasse, deren Ableitungen für das Sperren innerhalb eines Prozesses am häufigsten verwendet werden.
Ein WaitHandle-Objekt kann in mehreren Threads beobachtet und verwendet werden. Diese Threads benötigen dazu eine Referenz auf das Objekt, welche deswegen meist in einem privaten Feld verwaltet wird, auf das alle Thread-Methoden Zugriff haben. Ein Thread, der mit dem EventWaitHandle-Objekt arbeitet, ruft dessen ResetMethode auf, um den EventWaitHandle auf Rot zu schalten. Reset gibt einen booleschen Wert zurück, der aussagt, ob das Unsignalisieren erfolgreich war. In welchen Fällen das Unsignalisieren nicht erfolgreich sein könnte, ist allerdings nicht dokumentiert. In meinen Versuchen konnten auch mehrere Threads gleichzeitig dasselbe ManualResetEvent unsignalisieren, ohne dass Reset false zurückgegeben hätte. Andere Threads (oder bei einem Mutex auch Prozesse) rufen an der Stelle, an der sie auf den EventWaitHandle warten müssen, dessen WaitOne-Methode auf. WaitOne blockiert den Thread so lange, bis der EventWaitHandle signalisiert ist. Nach meinen Versuchen kann jeder Thread den EventWaitHandle wieder auf Grün schalten (also signalisieren), indem er dessen Set-Methode aufruft. Dies ist ebenfalls nicht dokumentiert, genau wie die Fälle, in denen Set false zurückgibt, weil das Signalisieren fehlgeschlagen ist. Üblicherweise signalisiert aber der Thread, der den EventWaitHandle auch unsignalisiert hat. Die anderen warten in der Regel über WaitOne und befinden sich deswegen im Leerlauf. Es ist jedoch auch möglich, dass ein Thread den EventWaitHandle unsignalisiert und danach darauf wartet, dass ein anderer Thread diesen signalisiert. In den anderen Threads hebt WaitOne nach dem Aufruf von Set die Blockade auf und diese werden weiter ausgeführt. Der Unterschied zwischen ManualResetEvent und AutoResetEvent ist hier allerdings, dass bei AutoResetEvent nur ein WaitOne-Aufruf die Blockade aufhebt. Der EventWaitHandle wird danach sofort wieder auf den unsignalisierten Status zurückgesetzt. WaitHandle-Objekte sind ideal für die Synchronisierung von Threads geeignet. Sie werden immer dann eingesetzt, wenn ein Thread auf einen anderen warten muss, z. B. weil der andere Berechnungen ausführt und die Zwischenergebnisse weitergibt.
Ein Thread, der WaitOne aufruft und bei dem diese Methode blockiert, wird in den Status WaitSleepJoin versetzt. Dieser Status führt zu keiner spürbaren Belastung des Prozessors, weil er vom Betriebssystem verwaltet wird. Erst wenn der WaitHandle signalisiert wird, führt das Betriebssystem den Thread weiter aus. Deswegen sind WaitHandle-Objekte für die Synchronisierung von Threads wesentlich besser geeignet als andere denkbare Lösungen, wie z. B. eine boolesche Variable, die in einem Thread gesetzt und in einem anderen abgefragt wird.
1196
Threads synchronisieren
Signalisieren mit ManualResetEvent und AutoResetEvent Die Klassen ManualResetEvent und AutoResetEvent sind konkrete Implementierungen der EventWaitHandle-Klasse. Beide arbeiten nach dem oben beschriebenen Muster. Der Unterschied ist, dass AutoResetEvent den WaitHandle automatisch wieder unsignalisiert, wenn in einem Thread, der auf den WaitHandle wartet, WaitOne weiter ausgeführt wird. Bei ManualResetEvent muss der WaitHandle manuell wieder unsignalisiert werden, wenn die anderen Threads wieder gesperrt werden sollen. Wenn mehrere Threads auf einen EventWaitHandle warten, wird bei AutoResetEvent nach dem Signalisieren genau einer weiter ausgeführt, die anderen müssen weiter warten. Welchen Sinn das hat, erläutere ich nach dem folgenden Beispiel zur ManualResetKlasse.
ManualResetEvent und AutoResetEvent implementieren einen WaitHandle für einen Prozess
12
13
Die Arbeitsweise eines WaitHandle demonstriert das folgende Beispiel. Es nutzt einen ManualResetEvent, um drei Arbeitsthreads vom UI-Thread aus zu einem definierten Zeitpunkt zu starten. Dem ManualResetEvent-Konstruktor wird false übergeben, damit der WaitHandle zunächst nicht signalisiert ist. Nach dem Erzeugen der Threads ruft der UI-Thread Set auf, um den Arbeitsthreads zu signalisieren, dass sie ihre Arbeit aufnehmen können:
14
15
Listing 20.43: Verwendung eines ManualResetEvent, um Threads zu einem definierten Zeitpunkt zu starten /* WaitHandle für die Synchronisation */ private static ManualResetEvent waitHandle = new ManualResetEvent(false);
16
/* Arbeits-Methode */ private static void Worker() { // Warten darauf, dass der WaitHandle signalisiert wird waitHandle.WaitOne();
17
// Den Job ausführen for (int i = 0; i < 10; i++) { Console.WriteLine(i); Thread.Sleep(100); }
18
19
} static void Main(string[] args) { // Drei Threads erzeugen Thread[] workers = { new Thread(Worker), new Thread(Worker), new Thread(Worker) };
20
// Die Threads starten workers[0].Start(); workers[1].Start(); workers[2].Start();
21
// Eine Ausgabe vornehmen Console.WriteLine(); Console.WriteLine("Beenden mit Return");
22
// Den WaitHandle signalisieren waitHandle.Set();
23
Console.ReadLine(); }
1197
Multithreading
In der Praxis ergeben sich für die Verwendung von ManualResetEvent natürlich auch andere Verwendungen, bei denen das ManualResetEvent-Objekt auch wieder unsignalisiert wird. Für dieses komplexe Thema ein einfaches Beispiel zu finden ist aber sehr schwierig. Das Prinzip sollte auch mit dem obigen Beispiel relativ klar werden. Wahrscheinlich hilft auch das folgende, mehr praxisorientierte Beispiel, das ein AutoResetEvent-Objekt einsetzt. Doch vor diesem Beispiel möchte ich eine Warnung weitergeben, die Microsoft in der Dokumentation beschreibt:
HALT
»Es ist nicht gewährleistet, dass bei jedem Aufruf der Set-Methode ein Thread von einem EventWaitHandle freigegeben wird, bei dem der Zurücksetzmodus auf EventResetMode.AutoReset festgelegt ist. Wenn zwei Aufrufe zu schnell aufeinander folgen, sodass der zweite Aufruf vor der Freigabe eines Threads erfolgt, wird nur ein Thread freigegeben. Der zweite Aufruf wird vollständig ignoriert. Wenn beim Aufruf von Set keine Threads warten und das EventWaitHandle bereits signalisiert ist, hat der Aufruf ebenfalls keine Auswirkungen.« Gehen Sie also sehr vorsichtig mit AutoResetEvent um. Wenn nur ein Thread den AutoResetEvent signalisiert, sollte in der Praxis eigentlich kein Problem auftreten. Sie müssen dann aber dafür sorgen, dass mehrere Aufrufe nicht zu schnell hintereinander ausgeführt werden. Wobei unklar ist, was »zu schnell« ist. Im Notfall hilft eine kleine Pause mit Thread.Sleep(100) vor dem Aufruf der Set-Methode.
Das Warteschlangen-Muster mit AutoResetEvent AutoResetEvent wird häufig für Warteschlangen eingesetzt
AutoResetEvent wird in der Praxis häufig für ein spezielles Programmiermuster eingesetzt: Ein Haupt-Arbeitsthread ermittelt dabei Informationen, indem er z. B. Dateien aus dem Internet herunterlädt. Er schreibt jede fertig ermittelte Information in eine Warteschlange (Queue oder Queue). Ein oder mehrere andere Threads warten in einer Endlosschleife darauf, dass die Warteschlange gefüllt wird. Der HauptArbeitsthread setzt ein AutoResetEvent-Objekt auf signalisiert, sobald er etwas in die Queue geschrieben hat. Der erste Thread, dessen WaitOne-Methode nun nicht mehr blockiert, sperrt die Queue (die natürlich auch im Haupt-Arbeitsthread gesperrt wird), liest alle Informationen möglichst schnell aus, entsperrt die Queue wieder und verarbeitet danach die Informationen. Da der WaitHandle sofort automatisch zurückgesetzt wird, nachdem WaitOne nur für einen Thread nicht mehr blockiert, warten die anderen Threads geduldig weiter, bis der Haupt-Arbeitsthread wieder etwas in die Warteschlange geschrieben hat.
Auf diese Art können Sie das Ermitteln von Informationen einem Thread überlassen, der aber nicht mit der eventuell aufwändigen Verarbeitung belastet wird. So können Sie z. B. bei einem Internet-Download die zur Verfügung stehende Bandbreite optimal ausnutzen. In der Praxis verarbeitet meist aber nur ein Verarbeitungs-Thread die Informationen. Mehrere Verarbeitungs-Threads können aber u. U. die Performance steigern, wenn die Verarbeitung langsame externe Ressourcen verwendet. Ein anderer Einsatz für mehrere Informationsverarbeitungs-Threads wäre, dass diese unterschiedliche Informationen verarbeiten. Aber so weit will ich hier nicht gehen. Das folgende Beispiel zeigt das Warteschlangen-Muster an einem praxisorientierten Beispiel. Es verwendet einen Thread, der Dateien herunterlädt, und einen weiteren, der fertig heruntergeladene Dateien verarbeitet.
1198
Threads synchronisieren
Zunächst sind ein paar Deklarationen notwendig. Wichtig ist die AutoResetEventInstanz, die in dem statischen Feld waitHandle verwaltet wird. Listing 20.44: Deklarationen zur Implementierung des Warteschlangen-Musters zum Download von Dateien /* Verwaltet Informationen zu einem Download */ private class DownloadInfo { public Uri Url; public string DestinationFolder; }
12
13
/* Queue für die Übergabe der heruntergeladenen Dateien */ an den Verarbeitungs-Thread */ private static Queue queue = new Queue(); /* WaitHandle zur Synchronisierung des Downloadund des Verarbeitungs-Threads */ private static AutoResetEvent waitHandle = new AutoResetEvent(false);
14
Die Methode Downloader wird später in einem Thread aufgerufen. Sie erwartet eine Auflistung von DownloadInfo-Objekten, die sie durchgeht und für jedes die entsprechende Datei herunterlädt. Sobald eine Datei fertig heruntergeladen ist, wird diese in die Warteschlange geschrieben und das AutoResetEvent-Objekt signalisiert:
15
Listing 20.45: Methode zum Herunterladen von mehreren Dateien in einem Thread
16
private static void Downloader(object args) { // Die Infos zu den herunterzuladenden Dateien auslesen und durchgehen List downloadInfos = (List)args; foreach (var downloadInfo in downloadInfos) { // Eine Info ausgeben Console.WriteLine("Lade die Datei '" + downloadInfo.Url.AbsoluteUri + " ... ");
17
18
// Den Dateinamen ermitteln string fileName = Path.Combine(downloadInfo.DestinationFolder, downloadInfo.Url.Segments[ downloadInfo.Url.Segments.Length - 1]);
19
// Die Datei herunterladen WebClient webClient = new WebClient(); webClient.DownloadFile(downloadInfo.Url.AbsoluteUri, fileName);
20
// Eine Info ausgeben Console.WriteLine("Habe die Datei '" + downloadInfo.Url.AbsoluteUri + " heruntergeladen");
21
// Das Ergebnis in die Queue schreiben lock (queue) { queue.Enqueue(fileName); }
22
// Signalisieren, dass die Queue Informationen enthält waitHandle.Set();
23
} }
1199
Multithreading
Die Methode Processor wird später ebenfalls in einem Thread ausgeführt. Sie wartet in einer Endlosschleife darauf, dass der Download-Thread das AutoResetEventObjekt signalisiert, liest dann die Warteschlange aus und verarbeitet die Dateien: Listing 20.46: Methode zur Verarbeitung der heruntergeladenen Dateien private static void Processor() { while (true) { // Auf den WaitHandle warten waitHandle.WaitOne(); // Die Queue möglichst schnell auslesen List fileNames = new List(); lock (queue) { while (queue.Count > 0) { fileNames.Add(queue.Dequeue()); } } // Das Ergebnis verarbeiten foreach (var fileName in fileNames) { // Eine Info ausgeben Console.WriteLine("Verarbeite die Datei '" + fileName + " ..."); // Die Datei verarbeiten Process.Start(fileName); } } }
In der Anwendung müssen nun nur noch die herunterzuladenden Dateien bestimmt und die Threads gestartet werden: Listing 20.47: Starten der beiden Threads static void Main(string[] args) { // Ein paar herunterzuladende Dateien bestimmen string destinationFolder = Path.GetTempPath(); List downloadInfos = new List() { new DownloadInfo() { Url = new Uri( "http://www.juergen-bayer.net/artikel/HTML/HTML-4.pdf"), DestinationFolder = destinationFolder}, new DownloadInfo() { Url = new Uri( "http://www.juergen-bayer.net/artikel/SQL/SQL.pdf"), DestinationFolder = destinationFolder}, new DownloadInfo() { Url = new Uri( "http://www.juergen-bayer.net/artikel/Internet-" + "Grundlagen/Internet-Grundlagen.pdf"), DestinationFolder = destinationFolder} }; // Den Verarbeitungs-Thread starten (der zunächst nur wartet) Thread processorThread = new Thread(Processor); processorThread.Start();
1200
Threads synchronisieren
// Den Download-Thread starten Thread downloaderThread = new Thread(Downloader); downloaderThread.Start(downloadInfos); Console.WriteLine(); Console.WriteLine("Beenden mit Return"); Console.ReadLine(); }
Und fertig ist das gar nicht mehr so komplex aussehende Warteschlangen-Muster mit einem AutoResetEvent. In der Praxis müssen Sie natürlich noch ein wenig nacharbeiten. So bleibt der Verarbeitungs-Thread nach dem Ende des Download-Thread aktiv. Das ist aber eigentlich gar nicht schlimm, denn das Warten auf den WaitHandle kostet kaum Prozessorzeit. Trotzdem sollten Sie diesen Thread ggf. abbrechen, wenn der Download beendet ist. Andererseits könnten Sie ihn auch für den Fall laufen lassen, dass später weitere Downloads ausgeführt werden, die ebenfalls verarbeitet werden müssen.
12
13
14
Weitere Signalisierungs-Möglichkeiten, die nicht näher besprochen werden 15
.NET bietet noch einige Möglichkeiten, Signalisierungs-Konstrukte zu implementieren, die in der Praxis eher selten eingesetzt werden. Dazu gehören: ■
■
■
■
Die Möglichkeiten, über den Konstruktor der EventWaitHandle-Klasse (die die Basisklasse für AutoResetEvent und ManualResetEvent ist) prozessübergreifende WaitHandle-Objekte zu erzeugen. Die statischen Methoden WaitAny, WaitAll und SignalAndWait der WaitHandleKlasse. WaitAny wartet darauf, dass einer der in einem Array übergebenen WaitHandle-Objekte signalisiert ist, WaitAll wartet darauf, dass alle übergebenen WaitHandle-Objekte signalisiert werden. WaitAll ist sehr zweifelhaft, weil diese Methode nur in Anwendungen funktioniert, die ein »Multi-ThreadedApartment« verwenden. WPF- und Windows.Forms-Anwendungen können aber nur dann zuverlässig mit COM-Komponenten (und dazu gehört bereits der Datei-öffnen- und der Datei-schließen-Dialog) kommunizieren, wenn diese in einem »Single Threaded Apartment« laufen (weswegen die Main-Methode mit dem STAThreadAttribute-Attribut gekennzeichnet ist). Die Methode SignalAndWait mag in speziellen Fällen sinnvoll sein. Dieser Methode übergeben Sie zwei WaitHandle-Objekte. Sie signalisiert das erste und wartet auf das zweite. Das Ganze geschieht in einer atomaren Operation, die nicht unterbrochen werden kann. Über die statischen Methoden Wait, Pulse und PulseAll der Monitor-Klasse können Sie eine benutzerdefinierte Signalisierung implementieren, über die Sie die Features der Klassen ManualResetEvent, AutoResetEvent, Semaphore genau wie die Methoden WaitAll und WaitAny nachempfinden können. Die Anwendung dieser Methoden ist jedoch in der Praxis sehr schwierig und für andere Programmierer nur sehr schwer nachzuvollziehen. Die Klasse Semaphore lässt eine am Konstruktor vorgegebene Anzahl von Threads über WaitOne »eintreten« und blockiert, sobald diese Anzahl erreicht ist. Im Gegensatz zu lock und zu einem Mutex kann allerdings jeder Thread Release aufrufen, um sich aus der Semaphore zu entfernen. Damit können Sie erreichen, dass nicht zu viele Threads gleichzeitig einen bestimmten Code ausführen.
16
17
18
19
20 21
22
23
1201
Multithreading
20.9.5 Atomare Anweisungen und die Klasse Interlocked C#-Anweisungen sind nicht unbedingt atomar
Wie ich bereits zu Anfang dieses Kapitels gezeigt habe, sind C#-Anweisungen nicht unbedingt atomar. Eine C#-Anweisung kann in mehrere einzelne MaschinencodeAnweisungen umgesetzt werden, bei denen die Gefahr besteht, dass der Thread unterbrochen wird. Dieses Problem gilt sogar für einfache Anweisungen wie das Schreiben oder Lesen einer Variablen. Auf einem 32-Bit-Betriebssystem ist das Schreiben eines 32-BitWerts allerdings atomar. Beim Schreiben oder Lesen einer int-Variablen kann der Thread nicht mittendrin unterbrochen werden. Bei größeren Variablen ist es allerdings schon nicht mehr unbedingt der Fall. Auf einem 32-Bit-Betriebssystem besteht z. B. das Schreiben eines 64-Bit-Variablen aus zwei Schreibvorgängen (in je ein 32-Bit-Register). Dabei kann der Thread durchaus unterbrochen werden. Dies gilt allerdings nicht, wenn es sich um ein echtes 64-BitBetriebssystem handelt.
Interlocked bietet Methoden für 64-Bit-Werte
Beim Schreiben von 64-Bit-Werten kann es also zu massiven Problemen kommen, z. B. wenn Thread A die ersten 32 Bit schreibt, Thread B diese überschreibt und Thread A dann die zweiten 32 Bit schreibt. Der resultierende Wert ist natürlich vollkommen falsch. Ein ähnliches Problem habe ich bereits für die Verwendung des Operators ++ gezeigt (der ja aus zwei C#-Anweisungen besteht). Um mit 64-Bit-Zahlwerten arbeiten zu können, ohne sperren zu müssen, bietet nun die Klasse Interlocked einige Methoden, die mit Sicherheit atomar ausgeführt werden: Listing 20.48: Die wichtigsten Methoden der Klasse Interlocked static long number = 0; static void Demo() { // Threadsicheres Addieren long sum = Interlocked.Add(ref number, 10); // Inkrementieren und Dekrementieren Interlocked.Increment(ref number); Interlocked.Decrement(ref number); // Wert lesen Console.WriteLine(Interlocked.Read(ref number)); // Den alten Wert lesen und einen neuen schreiben Console.WriteLine(Interlocked.Exchange(ref number, 42)); // Variable nur dann aktualisieren, wenn ihr Wert einem // definierten entspricht Interlocked.CompareExchange(ref number, 123, 42); }
Die Verwendung der Interlocked-Methoden mag zwar etwas eigenartig aussehen, der Vorteil ist aber, dass diese Operationen wesentlich schneller ausgeführt werden, als wenn gesperrt wird.
1202
Timer
20.10 Timer Die Verwendung eines Timers ähnelt dem Multithreading. Die Grundidee der Verwendung eines Timers ist allerdings die Ausführung einer Methode in immer wiederkehrenden Intervallen. Die einfachste denkbare Aufgabe eines Timers ist die Ausgabe der aktuellen Zeit in einem Label o. Ä. Timer existieren in drei Varianten: ■
■
■
System.Threading.Timer: System.Threading.Timer ist ein einfacher Basis-Timer, der über den Konstruktor initialisiert wird. Er ruft in dem übergebenen Intervall sein Tick-Ereignis auf. Das Intervall kann über die Change-Methode geändert werden. Ein solcher Timer wird (im Gegensatz zu einem Windows.Forms-Timer) immer in dem Thread ausgeführt, in dem er erzeugt und gestartet wurde. Er kann dadurch in einem Arbeitsthread erzeugt und verwendet werden und ist deshalb für die Verwendung in Multithreading-Umgebungen geeignet. Außerdem ist die Genauigkeit eines System.Timers-Timers höher als eines Windows.Forms-Timers, da er eine andere Architektur verwendet. System.Timers.Timer: Die Klasse System.Timers.Timer verwendet einen System.Threading.Timer und erweitert diesen im Wesentlichen um die Möglichkeit, auf einem Formular als Komponente angelegt zu werden. Er besitzt eine Intervall-Eigenschaft, ein Elapsed-Ereignis, eine Enabled-Eigenschaft, eine Start- und eine Stop-Methode und eine AutoReset-Eigenschaft. Um problemlos auf ein Formular oder Fenster zugreifen zu können, besitzt ein solcher Timer die Eigenschaft SynchronizingObject. (die beim Ziehen der Komponente auf ein Formular automatisch gesetzt wird). Wenn Sie einen System.Timers.TimerTimer verwenden und im Elapsed-Ereignis (das bei dieser Art Timer ja in einem Thread ausgeführt wird) auf ein Formular oder Fenster zugreifen, können Sie SynchronizingObject auf dieses setzen um sich den Umweg über Invoke zu sparen. System.Windows.Forms.Timer (aus der Toolbox im Register ALLE WINDOWS FORMS): Ein solcher Timer nutzt die WM_TIMER-Nachricht, die dem UI-Thread in regelmäßigen Abständen gesendet wird. Deshalb kann ein System.WindowsTimer sinnvollerweise nur aus dem UI-Thread heraus erzeugt und gestartet werden2. Dafür kann die Methode, die mit dem Tick-Ereignis verknüpft ist, aber problemlos auf das Formular und seine Steuerelemente zugreifen. Ein Problem solcher Timer ist, dass die Nachricht WM_TIMER gemeinsam mit WM_PAINT die niedrigste Priorität in der Nachrichtenwarteschlange einer Anwendung besitzt. WM_TIMER- und WM_PAINT-Nachrichten werden immer ganz nach hinten verschoben und u. U. nach der Abarbeitung der wichtigen Nachrichten gemeinsam ausgeführt. Außerdem entspricht die Genauigkeit eines solchen Timers lediglich 55 Millisekunden. Für wichtige und zeitkritische Aufgaben können Sie einen Windows.Forms-Timer also nicht verwenden.
12
13
14
15
16
17
18
19
20 21
22
23 2
Sie können einen Windows.Forms-Timer zwar auch in einem Arbeitsthread erzeugen und starten, die Tick-Methode wird dann aber nie ausgeführt, weil der Arbeitsthread keine UI-Nachrichten, und damit natürlich auch nicht die WM_TIMER-Nachricht, erhält.
1203
Multithreading
20.11 Weitere interessante Themen beim Multithreading Für einige interessante Themen blieb in diesem Kapitel kein Platz. Dazu gehören: ■
■ ■
Die Synchronized-Eigenschaft vieler Auflistungen, die ein threadsicheres Objekt zurückliefert, das die Auflistung kapselt. Dieses Objekt kapselt alle Methoden und Eigenschaften und verpackt den Aufruf in eine Sperrung über die Monitor-Klasse. Das Sperren ganzer Methoden über das Attribut MethodImpl (MethodImplOptions. Synchronized). Diese Möglichkeit hilft manchmal bei der Fehlersuche. Das (in der Praxis selten genutzte) Schlüsselwort volatile, das an einem Feld angegeben werden kann. Dieses Schlüsselwort teilt dem Compiler mit, dass der dafür sorgen muss, dass der Prozessor Zugriffsanweisungen auf das Feld nicht aus Optimierungsgründen umsortieren darf. Mit volatile können Sie theoretisch auf Felder in mehreren Threads zugreifen. In der Praxis ist die Verwendung von volatile aber nicht allzu einfach und kann zu Problemen führen. So sollte volatile nur eingesetzt werden, wenn: ein Feld von mehreren Threads verwendet wird, nur ein Thread schreibend auf das Feld zugreift ■ und ansonsten keine Synchronisierung-Mechanismen verwendet werden. Verwenden Sie stattdessen lieber eine Synchronisierung über lock oder die Interlocked-Klasse. Multithreading ist schon kompliziert genug. ■ ■
■
■
Die Methoden SetData und GetData der Thread-Klasse, über die Sie Daten global für einen Thread verwalten können. Diese Daten können Sie in allen Methoden verwenden, die vom Thread aufgerufen werden. Die (in .NET 3.5 neue) Klasse ReaderWriterLockSlim, über die Sie erreichen können, dass nur ein Thread in eine Ressource schreiben, aber mehrere diese lesen können.
Ich hoffe, dass Sie auch ohne die fehlenden Features einen guten Überblick und ein Verständnis für das Multithreading in der Praxis erhalten haben.
1204
Inhalt
21
Der Aufruf von APIFunktionen und das Arbeiten mit COM-Komponenten 12
Auch in modernen .NET-Zeiten mit immer mehr Möglichkeiten sind in einigen Fällen der Aufruf von API-Funktionen oder die Arbeit mit COM-Komponenten in einer Anwendung sinnvoll oder sogar notwendig. Das betrifft z. B. den relativ einfachen Fall, dass Sie den Windows-Ordner eines Systems herausfinden wollen. Oder dass Sie eine klassische DLL eines externen Herstellers einsetzen müssen, die vielleicht in C++geschrieben wurde. Aber auch viele spezielle Tipps und Tricks setzen API-Funktionen ein.
13
Die Arbeit mit COM-Komponenten kommt in der Praxis allerdings schon häufiger vor, denn Microsoft Office stellt seine Funktionalität nach außen leider auch in der 2007er-Version immer noch lediglich über COM-Komponenten zur Verfügung (wenn Sie in Office-Anwendungen programmieren, können Sie allerdings auch – über VSTO1 – mit .NET arbeiten).
15
14
16
Dieses Kapitel behandelt deshalb die grundlegende Arbeit mit API-Funktionen in klassischen DLL-Dateien (am Beispiel des Windows-API) und mit COM-Komponenten (am Beispiel von Office).
17
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■
Grundlagen zum Aufruf von API-Funktionen Das Umsetzen von Datentypen zwischen .NET und dem Windows-API Die Übergabe und Rückgabe von booleschen Werten, Strings und Strukturen Umgehen mit API-Fehlern (Grobe) Übersicht über das COM-Modell COM-Komponenten mit früher Bindung verwenden COM-Komponenten mit später Bindung verwenden
18
19
20 Ich beschreibe in diesem Kapitel nur die Grundlagen der Arbeit mit API-Funktionen und COM-Komponenten, nicht die Anwendungsmöglichkeiten in der Praxis (die viel zu vielfältig wären).
INFO
21
Wenn Sie mit API-Funktionen – besonders solchen, die von externen Herstellern stammen – arbeiten müssen, helfen die in diesem Kapitel behandelten Grundlagen dabei, die Funktionen zum einen überhaupt aufrufen zu können und zum anderen Fehler zu vermeiden. Wenn Sie allerdings nicht mit API-Funktionen arbeiten müssen, können Sie den API-Abschnitt ruhig erst einmal übergehen.
22
23
Visual Studio Tools for Office
1205
Index
1
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Die Arbeit mit COM-Komponenten kann genauso wichtig sein, wenn Sie mit externen COM-Komponenten arbeiten müssen (was sehr selten ist) oder (was häufiger vorkommt) Office-Anwendungen fernsteuern wollen (z. B. um über Word einen Brief zu schreiben, dessen Aufbau in einer Word-Vorlage definiert ist). In diesem Fall helfen die hier beschriebenen Grundlagen. Ich gehe aber nicht auf das Objektmodell der Office-Anwendungen ein, weil dieses den Rahmen des Buchs sprengen würde.
INFO
Der Zugriff auf Office-Anwendungen über COM ist extrem langsam. Wenn Sie Word-, Excel- und PowerPoint-Dokumente performant bearbeiten wollen, müssen Sie externe Komponenten verwenden. Die kommerziellen Komponenten von Aspose (www.aspose.com) sind dazu sehr gut geeignet (aber leider auch recht teuer).
21.1 Das Windows-API ist die Basis aller WindowsProgramme
Das Windows-API
Das Windows-API liefert mit (je nach Version) wahrscheinlich um die 1000 Funktionen und einigen wenigen COM-Komponenten die Basis für die Programmierung unter Windows. Bei der Programmierung unter Windows rufen die Funktionen und Methoden einer Programmiersprache auf der untersten Ebene meist Windows-APIFunktionen auf (was natürlich auch beim .NET Framework der Fall ist). Obwohl das .NET Framework sehr umfangreich ist, müssen Sie in einigen Fällen Windows-API-Funktionen direkt aufrufen, nämlich dann, wenn die gesuchte Funktionalität nicht in den Klassen des .NET Framework, aber eben im API zu finden ist. Die folgenden Abschnitte klären einige dazu wichtige grundsätzliche Dinge.
REF
Die offizielle Dokumentation des Windows-API finden Sie in der Visual-Studio-Hilfe unter WIN32- UND COM-ENTWICKLUNG / DEVELOPMENT GUIDES / WINDOWS API / WINDOWS API REFERENCE.
21.1.1 API-Funktionen werden über PInvoke aufgerufen
Der Aufruf von API-Funktionen über PInvoke
API-Funktionen werden über PInvoke (Platform Invocation = Plattform-Aufruf) aufgerufen. PInvoke ermöglicht den Aufruf von Funktionen in unverwalteten DLLs, wie eben denen des Windows-API, und steht über die Klassen des Namensraums System.Runtime.InteropServices zur Verfügung. API-Funktionen deklarieren Sie mit dem DllImport-Attribut (dessen Typ die Klasse DllImportAttribute ist), wobei Sie im Konstruktor den Dateinamen der DLL-Datei übergeben. Die DLLs des Windows-API können Sie ohne Pfad angeben, da diese im Windows-Systemordner gespeichert sind. Optional können Sie die Felder der DllImportAttribute-Klasse (siehe Tabelle 21.1) in der Attribut-Deklaration setzen, um die Ausführung der Funktion zu steuern. Die Funktion deklarieren Sie dann mit dem extern-Modifizierer. Die Deklaration muss statisch erfolgen: Eine einfache Deklaration sieht dann z. B. so aus: Listing 21.1:
Typische Deklaration einer API-Funktion
[DllImport("winmm.dll", SetLastError=true)] public static extern int PlaySound(string pszSound, long hmod, int fdwSound);
1206
Das Windows-API
In diesem Beispiel wird die API-Funktion PlaySound aus der DLL winmm.dll deklariert. Das Feld SetLastError des DllImport-Attributs wird auf true gesetzt, damit die Funktion bei einem Fehler den Fehlercode speichert (siehe im Abschnitt »Umgehen mit API-Fehlern« ab Seite 1217). Die deklarierte API-Funktion können Sie innerhalb von C# dann aufrufen wie eine C#-Methode (allerdings ohne Klasse): Listing 21.2:
12
Aufruf einer API-Funktion
const int SND_FILENAME = 0x00020000; PlaySound("c:\\windows\\media\\chimes.wav", 0, SND_FILENAME);
13 Die Deklaration einer API-Methode können Sie in den meisten Fällen recht einfach über die Wiki-Webseite www.pinvoke.net herausfinden. REF
14
Das Problem des Herausfindens der verwendeten Konstanten (im Beispiel SND_FILENAME) kläre ich im Abschnitt »API-Konstanten-Werte« (Seite 1216). Im DllImport-Attribut können Sie einige Eigenschaften angeben, um den Aufruf der Funktion zu steuern. Tabelle 21.1 beschreibt die aktuell wichtigen (die für Windows 98 und Me sind nicht enthalten). Eigenschaft
Bedeutung
CallingConvention gibt mit einem Wert der CallingConvention-Aufzählung die Aufrufkonvention der Funktionen in der DLL an. Möglich sind die Werte CDecl, FastCall, StdCall, ThisCall und Winapi. Die in DLLs fast immer eingesetzte Aufrufkonvention StdCall ist die Voreinstellung.
15 Tabelle 21.1: Eigenschaften der DllImportAttributeKlasse
17
Aufrufkonventionen legen fest, wie die Argumente und die Rückgabe auf dem Stack abgelegt werden (von rechts nach links oder links nach rechts) und wer für das Aufräumen des Stack verantwortlich ist (der Aufrufer oder die aufgerufene Methode). Falle Sie eine DLL eines externen Herstellers verwenden, sollten Sie die Aufrufkonvention kennen, die diese einsetzt. Für das Windows-API ist StdCall die passende Aufrufkonvention. CharSet
16
18
bestimmt mit einem Wert der CharSet-Aufzählung, wie Zeichenketten an die Funktion übergeben werden:
19
– Ansi: Zeichenketten werden als 8-Bit-ASCII-Zeichen übergeben, – Auto: Zeichenketten werden im Format des Zielsystems übergeben,
20
– None: veralteter Wert, der Ansi entspricht, – Unicode: Zeichenketten werden als 16-Bit-Unicode-Zeichen übergeben.
EntryPoint
ExactSpelling
Die Voreinstellung ist Ansi. Diese Eigenschaft ist wichtig bei Funktionen, die mit Strings arbeiten. Dazu erfahren Sie mehr im Abschnitt »Strings übergeben« ab Seite 1211.
21
An diesem Feld können Sie den Originalnamen der Funktion angeben, wenn Sie diese unter einem anderen Namen importieren wollen. Ein anderer Einsatz ist die Angabe der Ordinalzahl der Funktion (mit einem führenden #-Zeichen) für den Fall, dass Sie deren Namen nicht kennen.
22
Diese boolesche Eigenschaft gibt an, ob dem Namen der Funktion je nach Einstellung in CharSet automatisch ein »A« bzw. »W« angehängt wird. Damit können Sie festlegen, dass die zu CharSet passende Funktion aufgerufen wird. Die Standardeinstellung ist false.
23
1207
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Tabelle 21.1: Eigenschaften der DllImportAttribute-Klasse (Forts.)
Eigenschaft
Bedeutung
PreserveSig
Diese (spärlich und verwirrend dokumentierte …) Eigenschaft ist für Funktionen interessant, die HRESULT-Werte zurückgeben. Ein HRESULT-Wert (der nur von COMMethoden und COM-zugehörigen API-Funktionen zurückgegeben wird) ist ein 32-BitInteger-Wert, der einen Status meldet. Er enthält Informationen zum Schweregrad, zum Kontext, zum Verursacher und zum Status selbst. Einige HRESULT-Werte melden Fehler, S_OK (0) meldet eine fehlerfreie Ausführung. Daneben existieren weitere Konstanten wie S_TRUE oder S_FALSE, die einen Nicht-Fehler-Status melden. Bei COM-Methoden oder -Funktionen, die einen HRESULT-Wert zurückgeben, können Sie mit PreserveSig = false (true ist die Voreinstellung) festlegen, dass die Methode/Funktion bei einem HRESULT-Wert, der einen Fehler darstellt (was wohl am Schweregrad-Anteil des Werts erkannt wird) eine Ausnahme generiert. Die Signatur der Funktion bzw. Methode wird so verändert, dass kein HRESULT, sondern ein int-Wert zurückgegeben wird. Die Rückgabe enthält dann HRESULT-Statuswerte, die keinen Fehler darstellen. Bei einer fehlerfreien Ausführung können Sie den Status also aus dem zurückgegebenen int-Wert auslesen. Die Voreinstellung dieser Eigenschaft ist true.
SetLastError
21.1.2 .NET-Typen müssen in die entsprechenden API-Typen umgesetzt werden
Viele API-Funktionen geben einen booleschen Wert zurück, der lediglich mit false darüber informiert, dass ein Fehler aufgetreten ist. Wenn Sie SetLastError auf true setzen (die Voreinstellung ist false), wird die API-Funktion aufgefordert, beim Eintreten eines Fehlers die Funktion SetLastError aufzurufen, die den Fehlercode für den aufrufenden Thread innerhalb des Windows-Systems zwischenspeichert. Über Marshal.GetLastWin32Error können Sie diesen Fehler dann auslesen (siehe im Abschnitt »Umgehen mit API-Fehlern« ab Seite 1217).
Umsetzen von Datentypen (Marshalling)
Bei der Deklaration einer API-Funktion müssen Sie die Datentypen so angeben, dass diese mit den Datentypen der Funktion identisch sind. Geben Sie falsche Datentypen an, resultiert dies beim Aufruf der Funktion in einer Ausnahme oder im schlimmsten Fall in einem Programmabsturz (weil die Funktion Speicherbereiche verwendet, die nicht dem Programm gehören). Beim Aufruf einer API-Funktion werden C#-Datentypen in API-Datentypen umgesetzt. Dieser Vorgang wird als Marshalling bezeichnet (die beste Übersetzung des Begriff »to marshal« ist wohl »geleiten«). Die meisten Datentypen der normalerweise in C oder C++ geschriebenen Funktionen besitzen eine direkte Entsprechung in C# und können deswegen ohne weitere Vorkehrungen eingesetzt werden. Einige APIFunktionen verwenden aber auch Datentypen, die in C# keine Entsprechung besitzen. Daneben kann es sein, dass C#-Datentypen bei der Weitergabe an die Funktion prinzipiell in mehrere C++-Datentypen umgesetzt werden können. Dies ist z. B. beim Datentyp string der Fall, der in die C++-Typen LPStr2, LPWStr3, LPTStr4 oder BStr5 umgesetzt werden kann. Diese Typen besitzen ein Default-Marshallingverhalten. Der string-Typ wird z. B. standardmäßig in einen BStr umgesetzt. Das Marshallingverhalten der einzelnen Typen beschreibt Microsoft in der .NET Framework-Dokumentation unter der Überschrift BLITFÄHIGE UND NICHT BLITFÄHIGE TYPEN (suchen Sie nach dieser Überschrift in der Hilfe). 2 3 4 5
1208
LPStr = Long Pointer to String = Nullterminierter ASCII-String LPWStr = Long Pointer to Wide String = Nullterminierter Unicode-String LPTStr = Nullterminierter String, der plattformabhängig als ASCII- oder Unicode-String verwaltet wird BStr = Basic String = Nullterminierter Unicode-String mit einem Präfix, der die Länge des Strings speichert
Das Windows-API
Über das MarshalAs-Attribut können Sie das Marshallingverhalten von C#-Datentypen beeinflussen. Im Konstruktor dieses Attributs können Sie mit einem Wert der Aufzählung UnmanagedType den Zieldatentyp angeben. So können Sie z. B. bei der Deklaration einer API-Funktion, die mit Zeichenketten arbeitet, angeben, dass ein string je nach Zielsystem als LPStr oder LPWStr übergeben werden soll: Listing 21.3:
MarshalAs bestimmt das Marshalling
Verwendung des MarshalAs-Attributs
12
[DllImport ("Kernel32.dll", SetLastError=true, CharSet=CharSet.Auto] public static extern int GetDiskFreeSpaceEx( [MarshalAs(UnmanagedType.LPTStr)] string lpDirectoryName, ref ulong lpFreeBytesAvailable, ref ulong lpTotalNumberOfBytes, ref ulong lpTotalNumberOfFreeBytes);
13
Die Originaldeklaration der Funktion sieht übrigens so aus: BOOL GetDiskFreeSpaceEx( LPCTSTR lpDirectoryName, PULARGE_INTEGER lpFreeBytesAvailable, PULARGE_INTEGER lpTotalNumberOfBytes, PULARGE_INTEGER lpTotalNumberOfFreeBytes );
// // // //
14
directory name bytes available to caller bytes on disk free bytes on disk
15
Da C# für die meisten C++-Datentypen direkte Entsprechungen besitzt, ist ein explizites Marshalling nur selten notwendig, weswegen ich an dieser Stelle auf die Auflistung der Konstanten der UnmanagedType-Aufzählung verzichte. Problematisch wird das Ganze bei der Übergabe von Strings, Arrays und Strukturen.
21.1.3
16
Die .NET-Entsprechungen der API-Typen 17
Wenn Sie eine API-Funktion in C# deklarieren wollen und nur die originale Dokumentation dieser Funktion besitzen, müssen Sie die API-Typen in passende C#-Typen umwandeln. Microsoft beschreibt die Entsprechungen der API-Typen in der .NET Framework-Dokumentation unter der Überschrift DATENTYPEN FÜR DEN PLATTFORMAUFRUF. Tabelle 21.2 fasst diese Informationen zusammen. API-Typ C#-Typ
Bemerkung
HANDLE
Bei der Programmierung mit dem Windows-API arbeiten Sie häufig mit Handles. Ein Handle repräsentiert ein Windows-Objekt wie z. B. eine geöffnete Datei, ein Fenster oder ein Steuerelement. In einer Windows.Forms-Anwendung können Sie den Handle eines Formulars oder Steuerelements aus dessen Eigenschaft Handle auslesen.
System. IntPtr
18 Tabelle 21.2: C#-Entsprechungen der gängigen Windows-APITypen
19
20
In einer WPF-Anwendung erhalten Sie den Handle eines Fensters folgendermaßen: System.IntPtr windowHandle = new System.Windows.Interop. WindowInteropHelper(window).Handle; Da WPF-Steuerelemente von WPF gezeichnet werden, besitzen diese keinen Windows-Handle.
21 22
API-Funktionen, die Windows-Objekte öffnen oder erzeugen, geben den Handle auf dieses Objekt zurück. API-Funktionen, die Objekte bearbeiten, erwarten den Handle in einem Argument. Der dazu verwendete Typ HANDLE ist in der Datei wtypes.h als Zeiger auf void (void*) definiert, also als 32- oder 64-Bit-Integer-Wert (je nach Plattform), der die Adresse eines Speicherbereichs mit einem beliebigen Datentyp verwaltet. Der Typ IntPtr wird automatisch in einen solchen Zeiger gemarshallt.
23
1209
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Tabelle 21.2: C#-Entsprechungen der gängigen Windows-APITypen
API-Typ C#-Typ
Bemerkung
BYTE
byte
SHORT
short
WORD
ushort
INT
int
UINT
uint
LONG
int
LONG ist als 32-Bit-Integer-Wert mit Vorzeichen definiert und wird deswegen nicht durch den 64 Bit großen long-Typ repräsentiert.
BOOL
int
Der im API vier Byte große BOOL-Typ kann nicht direkt in den nur ein Byte großen C#-bool-Typ umgesetzt werden. Deshalb sollten Sie für diesen Typ int verwenden. Sie können jedoch auch den bool-Typ marshallen (siehe im Abschnitt »Boolesche Argumente, Felder und Rückgabewerte« ab Seite 1210).
DWORD
uint
ULONG
uint
ULONG ist als 32-Bit-Integer-Wert ohne Vorzeichen definiert und wird deswegen nicht durch den 64 Bit großen ulong-Typ repräsentiert.
CHAR, LPSTR, LPWSTR, LPCWSTR
string oder System. Text. StringBuilder
Strings müssen korrekt gemarshallt werden, da diese in verschiedenen Varianten auftreten können. Funktionen, die einen String zurückgeben, müssen mit einem StringBuilder-Objekt aufgerufen werden. Informationen dazu finden Sie in den Abschnitten »Strings übergeben« (Seite 1211) und »Die Rückgabe von Strings« (Seite 1213).
FLOAT
float
DOUBLE
double
Viele Typen in API-Funktionen beginnen mit einem LP (z. B. LPWORD). LP steht für Long Pointer (32- oder 64-Bit-Zeiger). Ein solcher Typ ist also ein Zeiger auf den eigentlichen Typen. LPWORD ist z. B. deklariert als WORD*. Handelt es sich bei diesem Typen um einen, dessen C#-Entsprechung ein Werttyp ist, müssen Sie entsprechende Argumente By Reference deklarieren (siehe im Abschnitt »Referenzargumente« ab Seite 1214). Strings, die ja Referenztypen sind, werden allerdings anders behandelt (wie ich in den Abschnitten »Strings übergeben« und »Die Rückgabe von Strings« erläutere). Die komplette Liste der Windows-API-Typen finden Sie in der Visual-Studio-Hilfe, indem Sie nach »"Windows Data Types"« suchen (inklusive der Anführungszeichen). REF
21.1.4 Boolesche Werte werden in dem C-Typ BOOL übergeben
1210
Boolesche Argumente, Felder und Rückgabewerte
API-Funktionen, die boolesche Argumente oder Rückgabewerte besitzen oder die boolesche Felder in Strukturen erwarten, arbeiten mit dem C-Typ BOOL. Dieser Typ ist (warum auch immer …) definiert als 4-Byte-Wert, der den Wert 0 für false und einen Wert ungleich 0 für true speichert. Ein C#-bool-Typ verwaltet true und false zwar prinzipiell auf dieselbe Weise, ist aber nur ein Byte groß. Laut der Dokumentation wird bool in einen Wert der Größe 1, 2 oder 4 Byte (!) gemarshallt, der true in -1 oder 1 konvertiert. Wann welche Speichergröße verwendet wird, ist leider nicht dokumentiert (bzw. habe ich die entsprechende Stelle nicht gefunden). Wenn Sie
Das Windows-API
boolesche Argumente oder Strukturfelder deklarieren, sollten Sie diese also über das MarshalAs-Attribut explizit in den Typen BOOL marshallen. Ein Beispiel ist die Struktur SHFILEOPSTRUCT, die von der Funktion SHFileOperation verwendet wird: Listing 21.4:
Beispiel für das Marshallen eines booleschen Typs
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)] public struct SHFILEOPSTRUCT { public IntPtr hwnd; public uint wFunc; public string pFrom; public string pTo; public short fFlags; [MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted; public IntPtr hNameMappings; public string lpszProgressTitle; }
Den Rückgabewert von Funktionen, die einen BOOL-Wert zurückgeben, können Sie nicht marshallen. Diese Funktionen sollten Sie stattdessen mit einem int als Rückgabewert deklarieren und beim Aufruf überprüfen, ob dieser ungleich 0 ist. In einigen Beispielen ist hingegen zu sehen, dass dort der bool-Typ verwendet wird. Scheinbar ist es kein Problem, dass der 4-Byte-BOOL-Wert bei der Rückgabe auf den 1-Byte-boolTypen zugewiesen wird. Bei meinen Versuchen sind bisher auf jeden Fall noch keine Probleme aufgetreten. Trotzdem sollten Sie sich an die Microsoft-Empfehlung halten und BOOL-Rückgaben als int deklarieren. Damit stellen Sie sicher, dass auch eventuell mögliche Rückgabewerte, die den Byte-Bereich verlassen, korrekt ausgewertet werden.
21.1.5
12
13
14 Rückgabewerte können nicht gemarshallt werden
15
16
17
Strings übergeben
Funktionen, die Strings übergeben bekommen oder zurückgeben, sind häufig in drei Varianten im Windows-API enthalten. Eine Variante, deren Name mit »A« endet, arbeitet mit ASCII-Strings, eine andere, deren Name mit »W« (Wide String) endet, arbeitet mit Unicode-Strings. Die dritte Variante, deren Namen kein Zeichen angehängt ist, verwendet automatisch die vom System benutzte Zeichencodierung. Das System selbst speichert Zeichenketten entweder als ASCII (Windows 95, 98) oder als Unicode (NT, 2000, XP, Vista etc.). Sie können unter Windows 95 z. B. auch die WVersion einer Funktion aufrufen (falls Sie jemals für dieses uralte Betriebssystem programmieren müssen ☺). Der übergebene Unicode-String wird dann aber intern nach ASCII konvertiert, was natürlich Zeit kostet. Umgekehrt können Sie unter Windows XP auch die A-Version aufrufen, was sogar zwei Konvertierungen verursacht (einmal vom Unicode-String im Programm nach ASCII, dann in der Funktion von ASCII nach Unicode).
Strings werden auf modernen Systemen als Unicode gespeichert
Sicher gehen Sie, wenn Sie einfach die Version ohne angehängtes Zeichen verwenden und bei der Deklaration im DllImport-Attribut Charset=CharSet.Auto angeben. Dann stellen Sie sicher, dass die neutrale Variante der Funktion aufgerufen und die Zeichencodierung je nach Zielsystem automatisch bestimmt wird. Die explizite Angabe des CharSet-Felds ist notwendig, da die Voreinstellung dieses Feldes CharSet.Ansi ist. Über das Feld ExactSpelling des DllImport-Attributs können Sie übrigens mit true festlegen, dass je nach Einstellung des CharSet-Felds (auf Ansi oder Unicode) explizit die A- oder die W-Version aufgerufen wird, was aber wohl kaum notwendig sein sollte.
Charset = CharSet .Auto bestimmt die Version automatisch
18
19
20
21 22
23
1211
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Strings sind problematisch
Ein weiteres Problem ist das Marshalling von Strings. Standardmäßig wird der String-Typ in einen BStr (Basic String) umgewandelt. Das hat übrigens den Grund, dass BStr-Strings in COM (für das PInvoke auch verwendet wird) ausschließlich verwendet werden. Ein solcher String verwaltet je nach der CharSet-Einstellung ASCIIoder Unicode-Zeichen, wird mit einem 0-Zeichen abgeschlossen und besitzt als Präfix einen 32-Bit-Integer-Wert, der die Länge des Strings verwaltet. Eine BStr-Variable ist ein Zeiger auf den Anfang des Strings (nicht auf den Anfang des Längen-Werts). Deshalb ist er mit einem LPWStr bzw. LPStr kompatibel. Da ein BStr Unicode- oder ASCII-Zeichen verwalten kann, ist er auch beim Aufruf von API-Funktionen ideal geeignet: Listing 21.5:
(Implizites) Marshalling eines Strings in einen BStr
[DllImport ("Kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] public static extern int GetDiskFreeSpaceEx( string lpDirectoryName, ref ulong lpFreeBytesAvailable, ref ulong lpTotalNumberOfBytes, ref ulong lpTotalNumberOfFreeBytes);
Da das Argument lpDirectoryName die Standard-Marshalling-Einstellung verwendet, wird es als BStr gemarshallt. Die Einstellung von CharSet auf CharSet.Auto bewirkt, dass je nach Zielsystem ASCII- oder Unicode-Zeichen übergeben werden. Der BStr wird entsprechend passend als ASCII- oder Unicode-BStr übergeben. Diese vereinfachende Deklaration müsste eigentlich immer funktionieren. Die vier Byte Overhead des BStr gegenüber einem normalen String lassen sich dabei verschmerzen. MarshalAs kann auch Strings speziell umsetzen
Alternativ können Sie auch über das MarshalAs-Attribut dafür sorgen, dass Strings korrekt übergeben werden. Dazu stehen Ihnen die Konstanten LPStr, LPWStr und LPTStr (Marshalling je nach Zielsystem als LPStr oder LPWStr) der UnmanagedType-Aufzählung zur Verfügung, die Sie im Konstruktor übergeben. Idealerweise setzen Sie eine Kombination von CharSet=CharSet.Auto im DllImport-Attribut mit [MarshalAs(UnmanagedType.LPTStr)] ein. Damit sorgen Sie dafür, dass unter Windows 98 ein LPStr und unter Windows NT, 2000, XP, Vista etc. ein LPWStr übergeben wird: Listing 21.6:
Marshallen von Strings über MarshalAs
[DllImport ("Kernel32.dll", SetLastError=true, CharSet=CharSet.Auto] public static extern int GetDiskFreeSpaceEx( [MarshalAs(UnmanagedType.LPTStr)] string lpDirectoryName, ref ulong lpFreeBytesAvailable, ref ulong lpTotalNumberOfBytes, ref ulong lpTotalNumberOfFreeBytes);
Häufig werden API-Funktionen mit String-Argumenten in Beispielen auch folgendermaßen deklariert: Listing 21.7:
Alternative Deklaration einer API-Funktion, der ein String übergeben wird
[DllImport ("Kernel32.dll", SetLastError=true)] public static extern int GetDiskFreeSpaceEx( [MarshalAs(UnmanagedType.LPStr)] string lpDirectoryName, ref ulong lpFreeBytesAvailable, ref ulong lpTotalNumberOfBytes, ref ulong lpTotalNumberOfFreeBytes);
Diese Variante funktioniert ebenfalls, da Strings als ASCII übergeben und in einen (ASCII-)LPStr gemarshallt werden.
1212
Das Windows-API
21.1.6
Die Rückgabe von Strings
Einige API-Funktionen geben Informationen als String zurück. Dazu gehört z. B. die Funktion GetWindowsDirectory, die den Namen des Windows-Ordners ermittelt. Diese Funktionen geben Strings nicht als Rückgabewert, sondern in Argumenten zurück. Der Grund dafür liegt darin, dass einige Programmiersprachen wie z. B. Visual Basic 6 mit dem in C++ üblichen Zeiger auf ein char-Array nichts anfangen können. Also werden Strings in den Speicherbereich übergebener String-Variablen geschrieben. GetWindowsDirectory ist im Original folgendermaßen deklariert:
Die Rückgabe von Strings ist nicht einfach auszuwerten
12
UINT GetWindowsDirectory( LPTSTR lpBuffer, // buffer for Windows directory UINT uSize // size of directory buffer );
13
Das Argument lpBuffer erwartet einen Zeiger (LP = Long Pointer) auf einen plattformabhängigen String (TStr). Damit die Funktion weiß, wie viele Zeichen geschrieben werden können, wird diese Anzahl am Argument uSize übergeben. An diesem Argument können Sie aber nun keine C#-string-Instanz übergeben, da Strings in C# unveränderbar6 sind. Die Lösung ist die Verwendung einer StringBuilder-Instanz (aus dem Namensraum System.Text). Eine solche kann bei der Instanzierung ausreichend groß vorinitialisiert werden, behält den für den String reservierten Speicherbereich so lange, bis sie vom Garbage Collector zerstört wird, und wird so gemarshallt, dass ein Zeiger auf den gespeicherten String übergeben wird. Die C#Deklaration der GetWindowsDirectory-Funktion sieht also so aus: Listing 21.8:
14
15
16
Deklaration einer API-Funktion, die einen String zurückgibt
[DllImport ("Kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] public static extern uint GetWindowsDirectory( StringBuilder lpBuffer, uint uSize);
17
Beim Aufruf der Funktion müssen Sie die StringBuilder-Instanz ausreichend groß vorinitialisieren, sodass der Speicherbereich für den String reserviert wird. Sie müssen ein Zeichen mehr reservieren, als der String maximal groß werden kann, da APIFunktionen Strings immer mit einem 0-Zeichen abschließen. Am Argument uSize übergeben Sie dann die Länge des Strings.
18
19 Falls Sie die StringBuilder-Instanz nicht ausreichend groß initialisieren oder einen zu hohen Wert in uSize übergeben, resultiert dies darin, dass die API-Funktion versucht in einen Speicherbereich zu schreiben, der nicht zum StringBuilder-Objekt gehört. Im günstigsten Fall gehört der Speicher nicht zu Ihrem Programm, was in einem Windows-Ausnahmefehler (mit der Meldung »Der Vorgang written konnte nicht ausgeführt werden«) und einem Programmabsturz resultiert. Im ungünstigsten Fall gehört der Speicher zu Ihrem Programm und die Funktion überschreibt den Wert anderer Variablen oder Eigenschaften, was zu erheblichen und schwer findbaren Fehlern führt.
HALT
20
21 22
API-Funktionen mit String-Rückgabe geben die Länge des ermittelten Strings zurück. Daran können Sie erkennen, ob überhaupt ein String ermittelt wurde. Der Aufruf der GetWindowsDirectory-Funktion sieht also so aus: 6
23
Der Speicherbereich, den eine string-Instanz belegt, kann tatsächlich nicht verändert werden. Wenn Sie einem String Zeichen anfügen, legt die CLR einen neuen Speicherbereich an, kopiert den alten String dort hinein und hängt die neuen Zeichen hinten an. Ähnlich geht die CLR vor, wenn Sie eine Stringvariable mit einem neuen Wert versehen.
1213
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Listing 21.9:
Aufruf einer API-Funktion, die einen String zurückgibt
StringBuilder buffer = new StringBuilder(261); if (GetWindowsDirectory(buffer, 261) > 0) { Console.WriteLine("Windows-Ordner: {0}", buffer.ToString()); } else { Console.WriteLine("Fehler bei der Ermittlung des Windows-Ordners"); }
21.1.7 Referenzargumente müssen bei Werttypen mit ref deklariert werden
Referenzargumente
Einige Funktionen wie z. B. GetUserName geben auch andere Daten als Strings in Referenzargumenten zurück. An der Deklaration erkennen Sie dies meist daran, dass das Argument mit einem Typ deklariert ist, der mit LP (Long Pointer) beginnt. GetUserName wird z. B. im Original so dokumentiert: BOOL GetUserName( LPTSTR lpBuffer, LPDWORD nSize );
// name buffer // size of name buffer
Handelt es sich bei diesen Argumenten um C#-Werttypen, müssen diese mit ref deklariert werden, damit die Übergabe per Referenz erfolgt. Handelt es sich hingegen um Referenztypen (was aber eigentlich nicht vorkommt), werden die Argumente natürlich nicht mit ref deklariert. Die C#-Deklaration der GetUserName-Funktion sieht also folgendermaßen aus: Listing 21.10: C#-Deklaration einer API-Funktion mit Referenzargument [DllImport ("Advapi32.dll", SetLastError=true)] public static extern int GetUserName( StringBuilder lpBuffer, ref uint nSize);
Beim Aufruf müssen Sie bei diesem Beispiel zunächst wieder eine StringBuilderInstanz für den zurückgegebenen String erzeugen. Da das Argument nSize mit ref deklariert ist, muss an diesem Argument eine passende Variable übergeben werden. Bei GetUserName muss diese Variable vor dem Aufruf auch mit der Größe des Strings im StringBuilder-Objekt initialisiert werden: Listing 21.11: Aufruf einer API-Funktion mit Referenzargument StringBuilder userName = new StringBuilder(1024); uint size = 1024; if (GetUserName(userName, ref size) != 0) { Console.WriteLine(userName.ToString()); } else { Console.WriteLine("Fehler beim Aufruf von GetUserName"); }
Nach dem Aufruf enthält die Variable in diesem Beispiel die Länge des ermittelten Strings, was aber für das Beispiel vollkommen unwichtig ist.
1214
Das Windows-API
21.1.8
Die Übergabe von Strukturen und feste Strings
Viele API-Funktionen arbeiten mit Strukturen. Einige Strukturen besitzen Felder, die Zeichenketten verwalten. Da Strukturen in API-Funktionen immer eine definierte Größe besitzen müssen, sind String-Felder mit einer festen Länge definiert. Die Funktion GetVersionEx, die Informationen zur Windows-Version zurückliefert, ist ein Beispiel dafür. Diese Funktion ist im Original folgendermaßen deklariert:
Viele API-Funktionen arbeiten mit Strukturen
12
BOOL GetVersionEx( LPOSVERSIONINFO lpVersionInfo // version information );
GetVersionEx erwartet eine Struktur vom Typ OSVERSIONINFO, die folgendermaßen deklariert ist:
13
typedef struct _OSVERSIONINFO{ DWORD dwOSVersionInfoSize; DWORD dwMajorVersion; DWORD dwMinorVersion; DWORD dwBuildNumber; DWORD dwPlatformId; TCHAR szCSDVersion[128]; } OSVERSIONINFO;
14
15
Diese Struktur müssen Sie in C# nachbilden. Dazu müssen Sie zunächst im StructLayout-Attribut festlegen, dass die Felder der Struktur (anders als normalerweise unter .NET) sequenziell im Speicher angelegt werden. Dazu übergeben Sie diesem Attribut im Konstruktor den Wert LayoutKind.Sequential. Optional können Sie zusätzlich im Feld CharSet angeben, in welcher Form Zeichenketten übergeben werden.
StructLayout legt die Reihenfolge der Member fest
Feste Zeichenketten werden dann als string deklariert. Über das MarshalAs-Attribut legen Sie aber fest, dass der String By Value übergeben wird, indem Sie im Konstruktor den Wert UnmanagedType.ByValTStr übergeben, und definieren über das Feld Size die feste Größe des Strings. Das Marshalling als ByValTStr bewirkt übrigens auch, dass die Zeichenkette je nach Einstellung des CharSet-Feldes des StructLayout-Attributs als ASCII- oder Unicode-String übergeben wird. Die Deklaration der OSVERSIONINFO-Struktur sieht in C# also so aus:
Feste Zeichenketten werden speziell deklariert
16
18
Listing 21.12: Eine typische C-Struktur in C#
19
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] public struct OSVERSIONINFO { public uint dwOSVersionInfoSize; public uint dwMajorVersion; public uint dwMinorVersion; public uint dwBuildNumber; public uint dwPlatformId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string szCSDVersion; }
Die Größe fester Strings wird immer in Byte angegeben. Im Beispiel ist der String 128 Byte groß. Deshalb müssen Strings in Strukturen immer als ASCII-String gemarshallt werden. Würden Sie diese als Unicode-String marshallen, wäre das Feld für den String zu klein definiert und der Aufruf der Funktion würde zum API-Fehler 122 (»Der an einen Systemaufruf übergebene Datenbereich ist zu klein«) führen. Im Beispiel habe ich deswegen im StructLayout-Attribut explizit CharSet=CharSet.Ansi angegeben (obwohl das die Voreinstellung ist).
17
20
21 22 HALT
23
1215
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
GetVersionEx wird dann folgendermaßen deklariert: Listing 21.13: Beispiel für die Deklaration einer Funktion mit Struktur-Argument [DllImport("kernel32.Dll")] public static extern int GetVersionEx(ref OSVERSIONINFO lpVersionInfo);
Das Argument lpVersionInfo wird mit ref deklariert, weil die Struktur per Referenz übergeben wird. Wenn Sie sich die originale Deklaration der Funktion anschauen, erkennen Sie dies daran, dass der Datentyp LPOSVERSIONINFO mit LP beginnt. Natürlich hilft auch die Dokumentation der Funktion bei der Ermittlung der Art der Übergabe. Wenn Sie die Funktion dann aufrufen wollen, müssen Sie zunächst eine Instanz der Struktur erzeugen: OSVERSIONINFO vi = new OSVERSIONINFO();
Den festen String müssen Sie nicht erzeugen, das erledigt die CLR automatisch. Sie müssen allerdings in einem dafür vorgesehenen Feld der Struktur deren tatsächliche Speichergröße angeben, damit die API-Funktion die Felder der Struktur korrekt auswerten kann. Die Speichergröße ermitteln Sie über Marshal.SizeOf: vi.dwOSVersionInfoSize = Marshal.Sizeof(typeof(OSVERSIONINFO));
Nun können Sie die Funktion aufrufen: Listing 21.14: Aufruf einer API-Funktion, der eine Struktur übergeben wird if (GetVersionEx(ref vi) != 0) { Console.WriteLine("Windows-Version: {0}.{1}.{2}", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber); Console.WriteLine("Service-Pack: {0}", vi.szCSDVersion); } else { Console.WriteLine("Fehler beim Aufruf von GetVersionEx"); }
21.1.9 Das Ermitteln von API-Konstanten ist ein Problem
API-Konstanten-Werte
An den Argumenten von API-Funktionen werden häufig Konstanten übergeben. Diese werden in der Dokumentation der jeweiligen Funktion beschrieben. Die Funktion SHFileOperation, die für verschiedene Datei- und Ordneroperationen verwendet werden kann, erwartet am einzigen Argument z. B. eine SHFILEOPSTRUCT-Struktur, in deren Feld wFunc die auszuführende Operation definiert wird. In der Dokumentation der Struktur finden Sie eine Erläuterung der Bedeutung der Konstanten. Die Konstante FO_COPY führt z. B. dazu, dass eine Datei oder ein Ordner kopiert wird, FO_DELETE führt zum Löschen einer Datei bzw. eines Ordners. Das Problem ist nun, dass die Dokumentation nicht die Werte der Konstanten angibt. Diese müssen Sie aber kennen, um die Konstanten deklarieren zu können. In den meisten Fällen hilft eine Suche im Internet. Pinvoke.net listet die meisten (oder alle?) Konstantenwerte auf. Die wichtigen »WM«-Konstanten (für Fenster-Nachrichten) finden Sie z. B. an der Adresse www.pinvoke.net/default.aspx/Constants.WM. In
1216
Das Windows-API
vielen Fällen hilft auch eine Suche bei Google. Die Wahrscheinlichkeit ist recht groß, dass die gesuchte Konstante in irgendeinem Artikel oder Newsgroup-Beitrag vorkommt. Eine andere Lösung des Problems ist das Nachschauen in den Headerdateien, die mit Visual C++ oder Visual C++.NET mitgeliefert werden. In irgendeiner dieser Headerdateien sind die gesuchten Konstanten wahrscheinlich (aber nicht zwingend) deklariert. Wenn Sie C++.NET installiert haben, finden Sie die Headerdateien des Windows-SDK normalerweise im Ordner C:\Programme\Microsoft Visual Studio 9.0\VC\Include. Suchen Sie über die Windows-Suchfunktion in allen Dateien mit der Endung .h nach einer der Konstanten, deren Wert Sie benötigen. Eigenartigerweise sind die von SHFileOperation verwendeten Konstanten aber ab Visual Studio 2008 nicht mehr enthalten (diese befanden sich in der Datei ShellAPI.h).
12
13
21.1.10 Umgehen mit API-Fehlern Windows-API-Funktionen generieren grundsätzlich keine Ausnahmen, sondern liefern einen numerischen Fehlercode zurück. Einige Funktionen, wie z. B. SHFileOperation, liefern diesen Code direkt als Rückgabewert. Andere Funktionen, wie z. B. GetDiskFreeSpaceEx, geben einen BOOL-Wert zurück (einen Wert ungleich Null bei Erfolg, Null beim Auftreten eines Fehlers). Diese Funktionen rufen intern meist die API-Funktion SetLastError auf, die den Fehlercode Windows-intern speichert. Ein C++-Programmierer würde dann GetLastError aufrufen, um diesen Fehlercode auszulesen. Ein C#-Programmierer sollte dazu allerdings besser die Methode GetLastWin32Error der Marshal-Klasse aus dem Namensraum System.Runtime.InteropServices verwenden. Der Grund liegt darin, dass das .NET Framework intern weitere API-Funktionen aufrufen kann, die den Fehler zurücksetzen könnten. GetLastWin32Error stellt sicher, dass Sie den bei Ihrem API-Aufruf gesetzten Fehler erhalten.
14 API-Funktionen liefern einen Fehlercode
15
16
17
Voraussetzung dafür, dass die API-Funktion SetLastError aufruft, ist allerdings, dass Sie das Feld SetLastError im DllImport-Attribut auf true setzen.
18
Das folgende Beispiel deklariert die Funktion GetDiskFreeSpaceEx, über die Sie Größen-Informationen zu einem Laufwerk auslesen können: [DllImport ("Kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] public static extern int GetDiskFreeSpaceEx( string lpDirectoryName, ref ulong lpFreeBytesAvailable, ref ulong lpTotalNumberOfBytes, ref ulong lpTotalNumberOfFreeBytes);
19
20
Wenn Sie beim Aufruf z. B. ein nicht vorhandenes Laufwerk übergeben: ulong userSpace = 0, totalSize = 0, totalSpace = 0; if (GetDiskFreeSpaceEx("x:", ref userSpace, ref totalSize, ref totalSpace) == 0) {
21
gibt diese Funktion 0 zurück.
22
In diesem Fall können Sie den API-Fehler auslesen: int apiError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
In diesem Fall resultiert der Fehlercode 3, der für den Fehler »Das System kann den angegebenen Pfad nicht finden« steht.
23
1217
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
21.1.11 Umsetzen von API-Fehlercodes in passende Fehlerbeschreibungen Fehlercodes können (meist) umgesetzt werden
Beim Einsatz von API-Funktionen nutzt der numerische Fehlercode, den viele Funktionen zurückliefern, weder dem Programmierer noch dem Anwender. Sie können zwar den Fehlercode in der API-Dokumentation nachlesen (wenn Sie diesen finden ☺), der Aufwand dazu ist aber für den Programmierer und erst recht für den Anwender eindeutig zu hoch. Professionelle Programme sollten statt des Fehlercodes eine aussagekräftige Fehlermeldung liefern. Und das ist prinzipiell möglich (funktioniert aber nicht immer). Den von einer APIFunktion direkt zurückgegebenen oder über GetLastWin32Error ermittelten Fehlercode können Sie (meist) über die API-Funktion FormatMessage in eine Fehlerbeschreibung umwandeln. Eine kleine Einschränkung ist, dass einige Funktionen (wie z. B. SHFileOperation) leider einen speziellen, oft undokumentierten Fehlercode zurückgeben, den Sie mit FormatMessage nicht sinnvoll umsetzen können. Für APIFunktionen, die beim Eintritt eines Fehlers intern SetLastError aufrufen, sollte FormatMessage allerdings problemlos funktionieren. Lesen Sie gegebenenfalls in der Dokumentation der aufgerufenen API-Funktion nach. Die Deklaration der FormatMessage-Funktion sieht folgendermaßen aus: Listing 21.15: Deklaration der API-Funktion FormatMessage [DllImport("Kernel32.dll")] private static extern int FormatMessage(int dwFlags, IntPtr lpSource, int dwMessageId, int dwLanguageId, StringBuilder lpBuffer, int nSize, string [] Arguments);
Am Argument dwFlags können Sie spezielle Flags übergeben, die die Funktionsweise von FormatMessage beeinflussen. Für die Auswertung der meisten API-Fehler reicht die Angabe der Konstante FORMAT_MESSAGE_FROM_SYSTEM (0x1000), die bewirkt, dass die Beschreibung aus der System-Meldungs-Tabelle ausgelesen wird. Ist die Fehlermeldung nicht in einer System-DLL, sondern in einer nicht zum System gehörenden DLL-Datei gespeichert, müssen Sie das Flag FORMAT_MESSAGE_FROM_HMODULE (0x0800) angeben, um die Meldungs-Tabelle eines »Moduls« (der DLL-Datei) auszulesen. In diesem Fall geben Sie am Argument lpSource den Windows-Handle zum Modul an. Diesen Handle können Sie über die API-Funktion GetModuleHandle ermitteln, die folgendermaßen deklariert wird: [DllImport("kernel32.dll")] static extern IntPtr GetModuleHandle(string lpFileName);
GetModuleHandle setzt voraus, dass die DLL-Datei in den Prozessraum der Anwendung geladen ist, was aber der Fall ist, wenn Ihr Programm eine Funktion der DLLDatei aufruft (die ja den auszuwertenden Fehler verursacht).
Beim Auslesen der Systemtabelle übergeben Sie am Argument lpSource den Wert(IntPtr)0 (laut der Dokumentation eigentlich null, aber das ist bei der Deklaration als IntPtr nicht möglich). Das Argument lpSource ist in der Funktion als Zeiger auf void (spezieller C-Datentyp, der mit allen anderen Datentypen belegt werden kann) deklariert. Deshalb können Sie in C++an diesem Argument entweder einen Zeiger auf eine int-Variable übergeben, die den Handle des Moduls verwaltet, oder einen Zeiger auf einen C-String, der die zu formatierende Meldung
1218
Das Windows-API
speichert. Da für uns nur die erste Variante interessant ist, habe ich dieses Argument als IntPtr (Zeiger auf int) deklariert. Am Argument dwMessageId übergeben Sie dann den Fehlercode. In dwLanguageId können Sie eine Sprach-Id für die Sprache der Meldung angeben. Wenn Sie 0 angeben, erfolgt die Ausgabe in der Systemsprache. Der Wert 0x0409 steht z. B. für das amerikanische Englisch. Voraussetzung dafür, dass die Meldung in einer anderen Sprache ausgegeben wird, ist, dass diese Sprache auch installiert bzw. verfügbar ist. Die verwendbaren Konstanten finden Sie in der SDK-Dokumentation, wenn Sie nach »Language Identifiers« suchen.
12
Am Argument lpBuffer übergeben Sie ein ausreichend groß dimensioniertes StringBuilder-Objekt (aus dem Namensraum System.Text). In nSize übergeben Sie die Initialgröße dieses Objekts. Achten Sie darauf, dass das StringBuilder-Objekt auch wirklich so groß initialisiert ist, wie Sie in nSize angeben, da ansonsten ein WindowsAusnahmefehler droht, wenn die API-Funktion in einen undefinierten Speicherbereich schreibt.
13
14
Das letzte Argument ist für unsere Zwecke nicht interessant: FormatMessage kann ähnlich String.Format auch Zeichenketten mit festgelegten Platzhaltern formatieren, in die die Inhalte des Arrays eingetragen werden. Übergeben Sie hier einfach null.
15
Das Auslesen der Meldung eines API-Fehlers, die in der Systemtabelle gespeichert ist, sieht folgendermaßen aus:
16
Listing 21.16: Auswerten eines API-Fehlercodes, dessen Meldung in der System-Meldungstabelle gespeichert ist int apiError = Marshal.GetLastWin32Error();
17
// StringBuilder erzeugen StringBuilder errorMessage = new StringBuilder(1024);
18
// Beschreibung für den Fehlercode ermitteln und damit eine Ausnahme werfen if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, IntPtr(0), apiError, 0, errorMessage, 1024, null) > 0) { throw new Exception(message.ToString()); } else { throw new Exception("API-Fehler " + apiError); }
19
20
Listing 21.17 zeigt das Auslesen einer Fehlermeldung für den Fall, dass diese nicht in der System-Meldungstabelle gespeichert ist (was bei DLL-Dateien der Fall ist, die nicht zum eigentlichen System gehören). Das Beispiel ruft die Funktion InternetGetConnectedState aus der DLL wininet.dll auf, die den aktuellen Status der Internetverbindung zurückgibt:
21
Listing 21.17: Auswerten eines API-Fehlercodes, dessen Meldung in einer speziellen DLL-Datei gespeichert ist
22
/* Deklaration der Funktion InternetGetConnectedState */ [DllImport("wininet.dll")] public static extern int InternetGetConnectedState(out int flags, int reserved);
23
...
1219
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
int flags; if (InternetGetConnectedState(out flags, 0) == 0) { int apiError = Marshal.GetLastWin32Error(); // Den Text des Fehlers aus der Datei wininet.dll auslesen und // damit eine Ausnahme werfen IntPtr hModule = GetModuleHandle("wininet.dll"); StringBuilder message = new StringBuilder(1024); if (FormatMessage(FORMAT_MESSAGE_FROM_HMODULE, hModule, apiError, 0, message, 1024, null) > 0) { throw new Exception(message.ToString()); } else { throw new Exception("API-Fehler " + apiError); } }
21.2 COM ist das alte Modell zum Wiederverwenden von Programmcode
Arbeiten mit COM-Komponenten
COM (Component Object Model) ist die veraltete Microsoft-Komponenten-Technologie, die in .NET-Programmen durch Klassenbibliotheks-Assemblys ersetzt ist. COM-Komponenten spielen aber auch heute noch eine Rolle, besonders wenn es darum geht, Office-Programme fernzusteuern. Word, Excel, Outlook und Co. stellen (wenigstens bis zur Version 2003) ihre Funktionalität ausschließlich über solche Komponenten zur Verfügung. Eine COM-Komponente speichert ähnlich einer Assembly Klassen (als einfache Klassen oder als Steuerelemente), die in Anwendungen, die die Komponente referenzieren, verwendet werden können. Die wesentlichen Unterschiede zu .NET-Assemblys sind, dass COM-Komponenten in der Windows-Registry registriert sein müssen und nur einmal (in genau einer Version) auf dem Rechner vorkommen können. Daneben setzen COM-Komponenten spezielle COM-Datentypen ein, die u. U. in .NET-Typen konvertiert werden müssen. Eine weitere Besonderheit ist, dass die Methoden von COM-Klassen nicht überladen werden können und mit optionalen Argumenten arbeiten, für die ein .NET-Programm einen speziellen Wert eintragen muss, wenn diese nicht belegt werden sollen. Sehr ausführliche Informationen zur Verwendung von COM-Komponenten in .NET finden Sie an der Adresse www.codeproject.com/dotnet/cominterop.asp.
REF
COM verwendet frühe oder späte Bindung
COM-Komponenten können mit früher oder später Bindung verwendet werden. Frühe Bindung bedeutet, dass dem Compiler zum Kompilierungszeitpunkt alle verwendeten Typen bekannt sind. Damit kann er den Aufruf von Methoden bzw. den Zugriff auf Eigenschaften (Eigenschaften werden im COM-Modell allerdings immer über Methoden zum Setzen und Lesen implementiert) fest in die Assembly einbetten und beim Kompilieren Syntaxprüfungen vornehmen. Bei der späten Bindung, die im folgenden Rezept behandelt wird, werden die Methodenaufrufe erst zur Programmlaufzeit ermittelt. Methodenaufrufe sind mit früher Bindung ein wenig schneller als mit später Bindung (was aber in der Praxis meist unerheblich ist). Der größte Vorteil ist, dass in Visual Studio IntelliSense mit früher Bindung funktioniert und dass alle verwendeten Typen und Aufzählungen bekannt sind. Ein Nachteil der frühen Bindung ist, dass
1220
Arbeiten mit COM-Komponenten
Programme, die eine bestimmte Version einer COM-Komponente referenzieren, mit älteren und manchmal auch mit neueren Versionen nicht unbedingt laufen. Wenn Sie z. B. eine .NET-Anwendung entwickeln, die Outlook einsetzt, um Mails zu versenden, diese unter der Verwendung von Outlook 2007 programmieren und das Programm später auf einem Rechner ausführen, auf dem Outlook 2003 installiert ist, kann dies zu einer Ausnahme bei der Erzeugung oder der Verwendung der OutlookInstanz führen (z. B. mit der Meldung »Das COM-Objekt mit der CLSID {…} ist ungültig oder wurde nicht registriert«).
12
Microsoft empfiehlt, Programme immer mit der niedrigstmöglichen Version einer COM-Komponente zu entwickeln, damit bei der Ausführung des Programms keine Versions-Probleme auftreten. Manchmal ist das aber einfach nicht möglich, z. B. wenn Sie auf Ihrem Computer Office 2003 einfach nicht installiert haben, das Programm beim Kunden aber unter Office 2003 ausgeführt werden soll. Um diese Probleme zu lösen, können Sie die späte Bindung verwenden (Seite 1224).
21.2.1
13
14
COM-Komponenten mit früher Bindung verwenden
Zur Verwendung der frühen Bindung benötigen Sie eine »Hüll«7- oder InteropAssembly, die die COM-Typen in .NET-Typen umsetzt. Diese Assembly können Sie dann in einem C#-Programm referenzieren, um die COM-Klassen zu verwenden. Sie müssen die Hüll-Assembly aber nicht selbst programmieren (was sehr aufwändig wäre). Um diese Assembly zu erzeugen können Sie in Visual Studio 2008 einfach eine Referenz auf die COM-Komponente anlegen, wie es Abbildung 21.1 für die Komponente Microsoft Scripting Runtime zeigt (die nicht wirklich benötigt wird, aber ich brauchte ein einfaches Beispiel …).
Frühe Bindung verwendet eine Interop-Assembly
15
16
17 Abbildung 21.1: Anlegen eines Verweises auf die COM-Komponente Microsoft Scripting Runtime
18
19
20
21 22
23 7
Englisch für »Umschlag«
1221
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Visual Studio 2008 erzeugt die für die COM-Komponente (die u. U. von anderen COM-Komponenten abhängig ist) notwendigen Interop-Assemblys automatisch, legt diese im Ordner für die binären Dateien des Projekts ab (normalerweise bin/debug bzw. bin/release) und referenziert diese für das Projekt. Im Programm können Sie COM-Klassen (fast) verwenden wie normale .NET-Klassen. Das folgende Beispiel setzt die FileSystemObjectClass-Klasse ein (die eigentlich FileSystemObject heißt, was aber auch ein schlechter Name ist, weil eine Klasse kein Objekt ist …): Listing 21.18: Anwendung einer COM-Klasse // FileSystemObjectClass-Instanz erzeugen Scripting.FileSystemObjectClass fso = new Scripting.FileSystemObjectClass(); // Laufwerk referenzieren Scripting.Drive drive = fso.GetDrive("c:"); // Informationen zum Laufwerk ausgeben Console.WriteLine("Freier Platz: {0}", drive.FreeSpace); Console.WriteLine("Größe: {0}", drive.TotalSize);
INFO
Microsoft empfiehlt für die Kommunikation mit Office-Anwendungen die Verwendung der vordefinierten Primären Interop-Assemblys (Primary Interop Assemblies oder PIAs). Sie finden diese für Office 2003 und 2007 im Ordner Visual Studio Tools for Office\PIA im Programmordner von Visual Studio. Sie können die PIAs aber auch bei Microsoft herunterladen. Der Download-Link ist sehr komplex und wird sich wahrscheinlich in Zukunft ändern. Suchen Sie bei www.microsoft.com/downloads nach »Primary Interop Assemblies«, um diese zu finden.
Optionale Argumente von COM-Methoden Viele COMMethoden besitzen optionale Argumente
Viele Methoden von COM-Klassen arbeiten mit optionalen Argumenten, die in echten COM-Anwendungen (wie Visual-Basic-6- oder Office-Programmen) oder auch in Visual Basic.NET einfach weggelassen werden können. Für weggelassene Argumente verwendet die COM-Methode dann in der Regel Voreinstellungen. Ein Beispiel dafür ist die Methode Open der Documents-Auflistung der Klasse Word.ApplicationClass aus der COM-Komponente Microsoft Word x.x Object Library. Abbildung 21.2 zeigt die Syntax dieser Methode im Visual-Basic-Editor von Word 20038. Optionale Argumente werden bei dieser Syntax mit eckigen Klammern gekennzeichnet. Der Datentyp der Argumente ist Variant, ein dem Object-Typ ähnlicher Typ, der alle anderen Typen aufnehmen kann.
Abbildung 21.2: Eine COM-Methode mit optionalen Argumenten im Word-Visual-BasicEditor
8
1222
OK, Word 2003 ist alt. Aber Office 2007 ist in meinen Augen zum einen sehr Anwender-unfreundlich und stürzte (in der ersten Version) zum anderen beim Schreiben meiner Bücher und Artikel andauernd ab … Ich bin zwar innovativ. Aber Office 2003 funktioniert hervorragend und (mittlerweile) auch beim Schreiben von Büchern (mit vielen Seiten) stabil.
Arbeiten mit COM-Komponenten
Eine weitere Besonderheit vieler COM-Methoden wie Open ist, dass die Argumente als Referenzargumente deklariert sind (was am Fehlen des Schlüsselworts ByVal erkennbar ist). Die Referenzübergabe wurde von Microsoft in vielen COM-Methoden deswegen bevorzugt, weil damit die Übergabe von Variablen performanter ausgeführt wird als bei einer By-Value-Übergabe. Für den eigentlichen Zweck – die Rückgabe von Werten – wird By-Reference im COM-Modell kaum eingesetzt.
Viele Argumente werden per Referenz übergeben
Abbildung 21.3 zeigt die Syntax der Open-Methode der Documents-Auflistung der Word-AppicationClass9-Klasse in Visual Studio. Der Variant-Datentyp wurde in den passenden Object-Typ umgewandelt und die Argumente sind aufgrund der Referenzübergabe mit ref deklariert. Optionale Argumente sind, da diese in .NET nicht möglich sind, nicht vorhanden.
12
13 Abbildung 21.3: Eine COM-Methode mit optionalen Argumenten in Visual Studio
14
15 Wenn Sie eine Methode mit optionalen Argumenten wie die Open-Methode aufrufen, können Sie an Stelle der optionalen Argumente natürlich einen passenden Wert eintragen, was in der Praxis aber sehr mühselig und meist auch unnötig ist. Stattdessen können Sie einfach den speziellen Wert System.Reflection.Missing.Value einsetzen, der beim Aufruf von COM-Methoden dafür steht, dass an einem optionalen Argument nichts übergeben wird.
Missing .Value wird für nicht definierte optionale Argumente eingesetzt
16
17
Da die Argumente der Open-Methode als Object und By-Reference deklariert sind, können Sie diesen Wert jedoch nicht direkt einsetzen, sondern müssen dazu eine Object-Variable verwenden, wie es Listing 21.19 zeigt. In diesem Beispiel wird das Dokument hitchhiker.doc im Ordner der Anwendung geöffnet.
18
Listing 21.19: Öffnen eines Word-Dokuments mit der Übergabe optionaler Argumente // Word-Instanz erzeugen und sichtbar schalten Microsoft.Office.Interop.Word.Application word = new Microsoft.Office.Interop.Word.ApplicationClass(); word.Visible = true;
19
// Variable für den Wert Missing.Value für nicht belegte // optionale Argumente object missing = Missing.Value;
20
// Dokument öffnen string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); object fileName = Path.Combine(appPath, "Hitchhiker.doc"); word.Documents.Open(ref fileName, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);
21 22
23 9
Die eigentlich in der COM-Komponente Application heißt. In der Interop-Assembly wurden aber für diese Klasse die Schnittstelle Application und die implementierende Klasse ApplicationClass angelegt.
1223
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
21.2.2
COM-Komponenten mit später Bindung verwenden
Frühe Bindung führt zu Problemen, wenn die Klassen-ID geändert wurde
Die frühe Bindung von COM-Komponenten führt manchmal zu Problemen. Das ist z. B. dann der Fall, wenn Sie eine Anwendung entwickeln, die Office-2003-Programme fernsteuert, die aber beim Kunden unter Office 97 ausgeführt werden soll. Da Microsoft zwischen Office 97 und Office 2000/XP/2003 die CLSIDs10 vieler Klassen geändert hat, findet die Anwendung die CLSID der 2003-Klassen nicht auf dem Office-97-Rechner und meldet eine Ausnahme.
Die späte Bindung kann die Probleme lösen
Wenn Sie solche Probleme erwarten, können Sie statt der frühen Bindung auch die späte Bindung verwenden. Bei dieser Art der Bindung erzeugen Sie keine Referenz auf eine Interop-Assembly, sondern ermitteln die zu verwendenden Typen zur Laufzeit des Programms. Wenn Sie sicherstellen, dass Sie nur Typen und deren Methoden und Eigenschaften verwenden, die in allen relevanten Versionen der COMKomponente verfügbar sind (und dieselbe Signatur besitzen), kann Ihre Anwendung problemlos unter den verschiedenen Versionen der COM-Komponente ausgeführt werden. Das ist besonders dann interessant, wenn Ihre Anwendung auf mehreren Rechnern ausgeführt werden soll, die unterschiedlich ausgestattet sind.
INFO
Leider ist dies in der Praxis problematisch, da zwischen verschiedenen Versionen einer COM-Komponente häufig auch die Methodensignaturen geändert wurden. Wenn Sie dann über die späte Bindung eine Methode mit zu wenig oder zu vielen Argumenten aufrufen, resultiert dies in einer Ausnahme.
Späte Bindung basiert auf Reflektion
Für die späte Bindung erzeugen Sie zunächst eine Type-Instanz für die COM-Klasse. Dazu können Sie die Methode GetTypeFromProgID der Type-Klasse verwenden. Dieser Methode übergeben Sie die Prog-Id11 der COM-Klasse, die üblicherweise die Form Komponentenname.Klassenname besitzt. Zur Erzeugung einer Word-Instanz setzen Sie z. B. die ProgId Word.Application ein. Alternativ können Sie auch die Methode GetTypeFromCLSID verwenden, wenn Sie die CLSID der COM-Klasse kennen. Dann gehen Sie allerdings wieder die Gefahr ein, dass diese auf einem anderen Rechner mit einer älteren oder neueren Version der COM-Komponente geändert ist und folglich nicht gefunden wird. Verwenden Sie also besser die ProgId, falls die COM-Klasse mit einer solchen registriert ist.
Activator .CreateInstance erzeugt eine Instanz
Wenn Sie den Typ erzeugt haben, können Sie eine Instanz über die Methode CreateInstance der Activator-Klasse erzeugen, der Sie den Typen übergeben. CreateInstance gibt eine Referenz auf ein Object zurück, da dem Programm ja nicht bekannt ist, um welchen Typ es sich wirklich handelt. Listing 21.20 zeigt, wie das für eine Instanz von Word programmiert wird.
10 Eine CLSID (Class Id) ist ein GUID-Wert (Global Unique Identifier), der eine COM-Klasse weltweit eindeutig identifiziert. COM-Klassen sind über ihre CLSID in der Registry registriert. Die CLSID wird bei der frühen Bindung in der Wrapper-Assembly mit der Wrapper-Klasse verknüpft, sodass das Programm beim Erzeugen einer Instanz der Wrapper-Klasse an Hand der CLSID das COM-ServerProgramm über die Registry ausfindig machen kann. 11 Die meisten COM-Klassen sind neben der CLSID auch mit einer ProgId (Program Id) in der Registry eingetragen, die ein für Menschen besser lesbares Format in der Form Komponentenname.Klassenname besitzt. Bei der Erzeugung eines .NET-Typs über eine ProgId ermittelt das Programm in der Registry zunächst über die ProgId die CLSID der Klasse, über die CLSID dann die Server-Anwendung und fordert diese auf, eine Instanz der Klasse zu erzeugen.
1224
Arbeiten mit COM-Komponenten
Listing 21.20: Erzeugen einer Word-Instanz über die späte Bindung Type wordType = null; object wordObject = null; try { // .NET-Typ für die COM-Klasse Word.Application erzeugen wordType = Type.GetTypeFromProgID("Word.Application");
12
// Instanz dieses Typs erzeugen wordObject = Activator.CreateInstance(wordType); } catch (Exception ex) { Console.WriteLine("Fehler beim Erzeugen der Word-Instanz: " + ex.Message); return; }
13
14
Eigenschaften setzen und Methoden aufrufen Nun kommt der etwas komplexere Teil, nämlich das Setzen von Eigenschaften und der Aufruf von Methoden. Da der COM-Typ im .NET-Programm nicht bekannt ist (und über eine Object-Referenz referenziert wird), können Sie Eigenschaften und Methoden nicht direkt verwenden. Dazu benutzen Sie einen Teil von Reflektion, den dynamischen Aufruf von Eigenschaften und Methoden über die Methode InvokeMember des Typen. Dieser Methode übergeben Sie am ersten Argument den Namen der Eigenschaft bzw. der Methode. Am zweiten Argument definieren Sie die Art der Bindung an das entsprechende Mitglied der COM-Klasse. Dazu verwenden Sie die Konstanten der BindingFlags-Aufzählung aus dem Namensraum System.Reflection. Diese Konstanten, die Sie mit | kombinieren können, geben an, nach welchen Elementen der COM-Klasse der Reflektions-Mechanismus suchen soll. Die für COM wichtigsten Konstanten finden Sie in Tabelle 21.3. Konstante
Bedeutung
GetField
gibt an, dass der Wert eines einfachen Feldes (einer Eigenschaft ohne set-/get-Zugriffsmethoden) gelesen werden soll
GetProperty
gibt an, dass der Wert einer Eigenschaft (mit set-/get-Zugriffsmethoden) gelesen werden soll
IgnoreCase
gibt an, dass die Groß- und Kleinschreibung des Namens nicht berücksichtigt werden soll
Instance
gibt an, dass nur Instanzeigenschaften und Methoden in die Suche einbezogen werden
Über InvokeMember erhalten Sie Zugriff auf Eigenschaften und Methoden
16
17
Tabelle 21.3: Die wichtigsten Konstanten der BindingFlagsAufzählung für die späte Bindung an COM-Klassen
gibt an, dass nur nach öffentlichen Mitgliedern mit dem angegebenen Namen gesucht werden soll
SetField
gibt an, dass der Wert eines einfachen Feldes (einer Eigenschaft ohne set-/get-Zugriffsmethoden) gesetzt werden soll
SetProperty
gibt an, dass der Wert einer Eigenschaft (mit set-/get-Zugriffsmethoden) gesetzt werden soll
Static
gibt an, dass nur statische Eigenschaften und Methoden in die Suche einbezogen werden
18
19
20
InvokeMethod gibt an, dass eine Methode aufgerufen werden soll Public
15
21 22
23
1225
Der Aufruf von API-Funktionen und das Arbeiten mit COM-Komponenten
Am dritten Argument können Sie eine Instanz einer von der abstrakten Klasse Binder abgeleiteten Klasse übergeben, über die Sie spezielle Angaben zu Bindungen machen können, die nicht ohne weiteres aufgelöst werden können. Das kann z. B. dann der Fall sein, wenn die Klasse überladene Varianten einer Methode besitzt und Sie entscheiden müssen, welche dieser Varianten aufgerufen werden soll. Da diese und andere Spezialitäten bei COM-Klassen (meines Wissens nach) nicht vorkommen (bzw. nicht möglich sind), können Sie an diesem Argument in der Regel einfach null übergeben. Am vierten Argument übergeben Sie die Object-Variable, die die Referenz auf das COM-Objekt verwaltet. Das fünfte Argument übernimmt beim Setzen von Eigenschaften den Eigenschaftswert und beim Aufruf von Methoden die Argumente der Methode in Form eines Object-Arrays. Beim Lesen von Eigenschaften und beim Aufruf von Methoden gibt InvokeMember den Wert der Eigenschaft bzw. den Rückgabewert der Methode als Object-Wert zurück. Listing 21.3 setzt dieses Wissen am Beispiel der Fernsteuerung von Word ein. Das Programm setzt zunächst die Visible-Eigenschaft der neu erzeugten Word-Instanz auf true, um Word sichtbar zu schalten. Danach wird ein neues Dokument erzeugt. Mit früher Bindung könnte dazu die Add-Methode der Documents-Auflistung direkt aufgerufen werden. Mit der späten Bindung ist das jedoch nicht möglich, da der Name Documents.Add nicht direkt aufgelöst werden kann. Deshalb ermittelt das Programm zunächst eine Referenz auf die Documents-Auflistung über die Abfrage der entsprechenden Eigenschaft in die Variable documents. Über diese Referenz ruft das Programm dann die Add-Methode auf. Diese Methode besitzt vier optionale Argumente, über die u. a. die zu verwendende Dokumentenvorlage spezifiziert werden kann. Da ein normales Dokument erzeugt werden soll, übergibt Listing 21.3 keine Argumente. Wie in COM üblich verwendet die Add-Methode damit die Voreinstellungen für die Argumente. Über die TypeText-Methode der Selection-Eigenschaft schreibt das Programm dann einen Text in das Dokument. Um das Dokument auszudrucken, referenziert das Programm schließlich das aktuelle Dokument über die ActiveDocument-Eigenschaft der Word-Instanz und ruft deren PrintOut-Methode auf. Über die Close-Methode wird das Dokument dann geschlossen, wobei am ersten Argument false übergeben wird, um das Dokument zu schließen, ohne die Änderungen zu speichern. // Visible-Eigenschaft setzen wordType.InvokeMember("Visible", BindingFlags.Public | BindingFlags.SetProperty, null, wordObject, new object[] { true }); // Documents-Auflistung referenzieren ... object documents = wordType.InvokeMember("Documents", BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty, null, wordObject, null); // ... und deren Add-Methode ohne Argumente aufrufen documents.GetType().InvokeMember("Add", BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, documents, null); // Die Methode TypeText der Selection-Eigenschaft aufrufen object selection = wordType.InvokeMember("Selection", BindingFlags.Public | BindingFlags.GetProperty, null, wordObject, null); selection.GetType().InvokeMember("TypeText", BindingFlags.Public |
1226
Arbeiten mit COM-Komponenten
BindingFlags.Instance | BindingFlags.InvokeMethod, null, selection, new object[] { "Hallo, das ist ein Text, der von außen kommt" }); // Das aktive Dokument referenzieren und ausdrucken object document = wordType.InvokeMember("ActiveDocument", BindingFlags.Public | BindingFlags.GetProperty, null, wordObject, null); document.GetType().InvokeMember("PrintOut", BindingFlags.Public | BindingFlags.InvokeMethod, null, document, null);
12
Auf die weitere Arbeit mit Word (und anderen Office-Anwendungen) kann ich hier leider nicht eingehen. Die Grundlagen zum Zugriff auf COM-Komponenten (und API-Funktionen) kennen Sie nun aber. Viel Spaß beim eigenen Ausprobieren ☺.
13
14
15
16
17
18
19
20
21 22
23
1227
Inhalt
22
Assemblys, Reflektion und Anwendungsdomänen 12
In den vorhergehenden Kapiteln (und den Zusatz-Artikeln auf meiner Website) haben Sie eine (hoffentlich) gute Übersicht über die Programmierung in .NET erhalten, die in der Praxis am häufigsten benötigt wird (bis auf die Webprogrammierung, die in diesem Buch nicht behandelt wird).
13
14
In diesem vorletzten Kapitel gehe ich nun auf einige Themen ein, die in der Praxis ebenfalls wichtig sein können, für deren komplette Besprechung in diesem Buch aber kein Platz bleibt.
15
Dieses Kapitel behandelt die einzelnen Themen wegen des notorischen Platzmangels des Buchs nur noch in einer abgekürzten, beschreibenden Form. Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■
16
Assembly-Signierung und Assembly-Namen Erzeugen von nativen Abbildern für Assemblys Assemblys, die nicht (direkt) im Anwendungsordner verwaltet werden Anwendungsdomänen und deren Bedeutung Grundlagen zur Reflektion
17
Wichtige weitere Möglichkeiten beim Umgang mit Assemblys
18
In der Praxis reicht das Wissen, das dieses Buch zum Umgang mit Assemblys in den vorhergehenden Kapiteln vermittelt hat, in vielen Fällen aus. Sie wissen prinzipiell, was eine Assembly ist (Kapitel 1), können diese erzeugen und referenzieren, wissen, was der Global Assembly Cache ist (ebenfalls Kapitel 1), und kennen die Bedeutung von Satelliten-Assemblys (für die Lokalisierung, siehe Kapitel 15).
19
22.1
20
Sie haben aber noch einige weitere Möglichkeiten, die ich hier kurz vorstelle. Dazu gehören: ■ ■ ■
21
Das Signieren einer Assembly mit einem starken Namen, das Vorkompilieren von Assemblys in den nativen Abbild-Cache, Assemblys, die nicht im Anwendungsordner direkt verwaltet werden und das Installieren von Assemblys im GAC.
22
In diesem Abschnitt behandle ich die ersten drei Punkte. Den letzten Punkt halte ich für die Praxis für nicht relevant, da es in meinen Augen nicht wirklich Sinn macht, eigene Assemblys im GAC zu verwalten.
23
1229
Index
■
Assemblys, Reflektion und Anwendungsdomänen
22.1.1 Assemblys können mit einem starken Namen signiert werden
Das Signieren und der Name einer Assembly
Signieren einer Assembly bedeutet, diese mit einer Signatur zu versehen, die aus einem öffentlichen und einem privaten Schlüssel besteht. Der öffentliche Schlüssel kann später aus der Assembly ausgelesen werden. Der private Schlüssel wird nicht in der Assembly gespeichert und kann deswegen auch nicht ausgelesen werden. Er ist aber zur Erstellung der Signatur notwendig, die in der Assembly neben dem öffentlichen Schlüssel gespeichert ist. Prinzipiell können Sie Assemblys auf zwei Arten signieren: Über ein Schlüsselpaar, das in einer speziellen Datei verwaltet wird, und/oder über ein »richtiges« Zertifikat, das üblicherweise von einer Vertrauensstelle stammt. Die zweite Variante habe ich bereits in Kapitel 16 für ClickOnce-Anwendungen angesprochen. Die erste Variante, die einen starken Namen erzeugt, behandle ich hier.
Die Signatur über ein Schlüsselpaar Die Signatur basiert auf der asymmetrischen Verschlüsselung
Die Signatur über ein Schlüsselpaar basiert auf einer asymmetrischen Verschlüsselung. Bei einer asymmetrischen Verschlüsselung können Daten mit dem privaten Schlüssel verschlüsselt und mit dem öffentlichen Schlüssel entschlüsselt werden und umgekehrt. Beim Versand von Nachrichten verwendet der Sender den öffentlichen Schlüssel des Empfängers, um die Daten zu verschlüsseln. Nur der Besitzer des zum öffentlichen Schlüssels gehörenden privaten Schlüssels (und ein gut ausgestatteter Hacker1 …) kann die Daten wieder entschlüsseln. Der private Schlüssel bleibt, wie der Name schon sagt, privat (oder »geheim«). Der öffentliche Schlüssel wird öffentlich bekannt gemacht. Für Signaturen wird der umgekehrte Weg gewählt. Dabei wird zunächst ein Hashcode berechnet. Bei der Signierung von .NET Assemblys mit einem Schlüsselpaar ist das ein aus den Assembly-Daten berechneter, eindeutiger Code. Dieser wird mit dem privaten Schlüssel verschlüsselt und als Signatur abgelegt. Beim Laden einer solchen Assembly liest die CLR die Signatur und den öffentlichen Schlüssel aus und entschlüsselt die Signatur (womit sie den Hashcode erhält). Dann berechnet sie den Hashcode der Assembly selbst und vergleicht beide Werte. Unterscheiden diese sich, stammt die Assembly nicht von dem Hersteller, dessen öffentlicher Schlüssel angegeben ist, oder wurde nachträglich verändert. In diesem Fall weist die CLR die Assembly mit einer Ausnahme ab. Dieses Verfahren ist sicher, weil der private Schlüssel zum Verschlüsseln des Hashcode bekannt sein und dieser eben zum öffentlichen Schlüssel passen muss. So kann über eine Signatur mit einem Schlüsselpaar (die auch als Signatur mit einem starken Namen bezeichnet wird) zum einen der Hersteller einer Assembly ermittelt werden. Zum anderen wird damit verhindert, dass veränderte oder schadhafte Assemblys eingeschleust werden. Der öffentliche Schlüssel einer mit einem Schlüsselpaar signierten Assembly wird z. B. für die im GAC installierten Assemblys im GAC-Manager ausgegeben, den Sie im Windows-Ordner über den ASSEMBLY-Eintrag öffnen können. Dieser zeigt allerdings nicht den vollständigen öffentlichen Schlüssel an, sondern nur ein abgekürztes (wahrscheinlich eindeutiges) Token.
1
1230
Je nach Größe und Qualität der verwendeten Schlüssel können prinzipiell alle VerschlüsselungsVerfahren geknackt werden.
Wichtige weitere Möglichkeiten beim Umgang mit Assemblys
Abbildung 22.1: Der GAC-Manager zeigt u. a. den Token des öffentlichen Schlüssels der im GAC installierten Assemblys an
12
13
14 Der starke Name Eine Assembly, die über ein Schlüsselpaar signiert ist, erhält automatisch einen starken Namen. Der starke Name einer Assembly besteht aus dem Basisnamen, der Version, der Kultur (die im Assembly-Attribut AssemblyCulture angegeben ist), dem öffentlichen Schlüssel und der Signatur. Der Name einer Assembly hat eine Bedeutung bei der Referenzierung in Projekten und bei der Installation im GAC.
Der starke Name beeinflusst die Referenzierung
16
Das Signieren von Assemblys mit einem Schlüsselpaar hat aber im Großen und Ganzen mehrere Bedeutungen bzw. Auswirkungen:
1.
2.
3. 4.
5.
15
Das Signieren einer Assembly mit einem Schlüsselpaar ermöglicht, diese einem Hersteller zuzuordnen. Andersherum kann ein Hersteller beweisen, dass Assemblys von ihm entwickelt wurden, weil nur er den privaten Schlüssel besitzt, ohne den eine Signatur mit demselben öffentlichen Schlüssel nicht möglich ist. Das Signieren einer Assembly mit einem Schlüsselpaar verhindert das Laden ausgetauschter Assemblys (mit demselben Namen) und das Laden nachträglich veränderter Assemblys. Das Signieren einer Assembly stattet diese mit einem starken Namen aus, ohne den eine Installation im GAC nicht möglich ist. Wenn eine Assembly eine andere Assembly referenziert, die einen starken Namen besitzt, bezieht sich die Referenzierung per Voreinstellung auf genau diesen starken Namen. Wird die referenzierte Assembly durch eine neue ersetzt und diese weist einen anderen starken Namen auf (z. B. weil die Version erhöht wurde), resultiert die Ausführung des Programms in einer FileLoadException mit der Meldung, dass die Assembly mit dem Namen »Basisname, Version=Version, Kultur=Kultur, PublicKeyToken=Token des öffentlichen Schlüssels« nicht gefunden wurde. Auf hoch gesicherten Systemen (wie z. B. in staatlichen Einrichtungen) ist es u. a. möglich, über die Codezugriffssicherheit nur Assemblys zuzulassen, die einen bestimmten öffentlichen Schlüssel aufweisen. Damit kann gezielt gesteuert werden, welchen Herstellern vertraut wird. Auf solchen Systeme werden Assemblys, die von unvertrauten Herstellern stammen, durch die CLR nicht ausgeführt.
17
18
19
20
21
22 23
1231
Assemblys, Reflektion und Anwendungsdomänen
6. Eine Assembly mit einem starken Namen kann per Voreinstellung keine Assem-
7.
blys referenzieren, die keinen starken Namen besitzen. Dies ist ebenfalls ein Sicherheitsfeature, das verhindert, dass ein Hacker Assemblys einschleusen kann. In der Praxis kann dies aber auch problematisch werden, nämlich dann, wenn eine Anwendung zwar mit einem starken Namen signiert wird, diese aber externe Assemblys referenzieren soll, die keinen solchen besitzen. In der Praxis wird dieses Problem häufig dadurch gelöst, dass die externen Komponenten im Quellcode vorliegen (was z. B. bei den meisten Open-Source-Komponenten der Fall ist), selbst kompiliert und dabei mit einem speziell dafür vorgesehenen Schlüsselpaar signiert werden. Bei der Referenzierung einer Assembly mit einem starken Namen können Sie in Visual Studio über die Option SPEZIFISCHE VERSION bestimmen, dass genau diese Version benötigt wird. Diese in meinen Augen vollkommen unsinnige Option bewirkt – wenn sie eingeschaltet ist –, dass Visual Studio referenzierte Assemblys nicht mehr findet, sobald deren Version geändert wurde. Die Voreinstellung ist normalerweise false. In Visual Studio 2005 hatten ich und viele andere Programmierer aber auch häufig das Problem, dass manchmal bei der Referenzierung einer Assembly mit einem starken Namen SPEZIFISCHE VERSION per Voreinstellung auf true gesetzt wurde.
Umbiegen der Version für die Assembly-Bindung Die Version können Sie umbiegen
Die Berücksichtigung der Version bei der Suche nach einer referenzierten Assembly können Sie aber über eine Konfiguration auch aufweichen, sodass z. B. Assemblys mit einer höheren Version (aber ansonsten gleichen Werten im starken Namen) zugelassen werden. Ich gehe hier nicht näher auf dieses in der Praxis eher selten eingesetzte Feature ein. Ein Beispiel sagt aber mehr als 1000 Worte: Listing 22.1:
Umbiegen der Version einer referenzierten Assembly
Der Stern im oldVersion-Attribut steht dafür, dass alle Versionen, die älter sind als die in newVersion angegebene, umgebogen werden.
TIPP
1232
In diesem Zusammenhang ist natürlich interessant, wie Sie den öffentlichen Token einer Assembly ermitteln. Dazu können Sie ganz einfach den .NET Reflector von Lutz Roeder verwenden (www.aisto.com/roeder/dotnet). Ziehen Sie die Assembly auf den Reflector und wählen Sie diese aus. Im unteren Bereich zeigt der Reflector den kompletten (starken) Namen an. Den Schlüsseltoken können Sie dann in die Zwischenablage kopieren.
Wichtige weitere Möglichkeiten beim Umgang mit Assemblys
An der Adresse www.thescarms.com/dotNet/assembly.aspx finden Sie weitere Informationen zur Assembly-Ladestrategie der CLR. REF
Der Name einer Assembly In einigen Situationen, besonders bei der Arbeit mit Reflektion, müssen Sie den Namen einer Assembly angeben. Der Name einer Assembly kann, wie Sie ja wissen, ein einfacher (schwacher) Name sein, aber auch ein starker. Ein schwacher Name ist einfach nur der Basisname. Bei einem starken Namen sieht das Format aber etwas komplexer aus, da der starke Name mehr Informationen beinhaltet. Der starke Name der System.dll von .NET sieht z. B. so aus:
12
13
System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Wie Sie sehen, enthält der Name einer Assembly mit starkem Namen den Basisnamen, die Version, die Kultur und den Token des öffentlichen Schlüssels des Assembly-Herstellers.
14
Um den Namen einer Assembly herauszufinden, verwenden Sie einfach den .NET Reflector (siehe im vorherigen Abschnitt).
15
Assemblys über Visual Studio direkt signieren Visual Studio ermöglicht das direkte Signieren einer Assembly beim Kompilieren (eigentlich ermöglicht dies bereits der Compiler …). Dazu benötigen Sie eine Datei, die den privaten und den öffentlichen Schlüssel der Signatur enthält. Visual Studio erlaubt aber auch die Erzeugung einer solchen Datei. Diese wird als »Starker-NameSchlüssel-Datei« (Strong name key file) bezeichnet und besitzt deswegen die Dateiendung .snk.
Mit Visual Studio können Sie Assemblys direkt signieren
17
Das Signieren erfolgt über das Register SIGNIERUNG in den Eigenschaften des entsprechenden Projekts. Wählen Sie hier die Option ASSEMBLY SIGNIEREN. Über die ComboBox unter SCHLÜSSELDATEI MIT STARKEM NAMEN AUSWÄHLEN können Sie eine bereits vorhandene .snk-Datei zur Signatur auswählen. Falls Sie (oder die Firma, für die Sie arbeiten) noch nicht im Besitz einer solchen sind, können Sie diese auch neu erstellen. Beim Erstellen einer neuen Datei sollten Sie diese mit einem Kennwort schützen, sodass der private Schlüssel nicht ausgelesen werden kann2. In diesem Zusammenhang ist wichtig, dass Sie nicht jede Assembly mit einer neuen Schlüsseldatei verschlüsseln, da der private und der öffentliche Schlüssel per Zufall erzeugt werden. Erzeugen Sie die Schlüsseldatei nur ein einziges Mal und verwenden Sie diese danach für alle weiteren Assemblys. Die Schlüsseldatei selbst muss natürlich geheim gehalten werden. In Situationen, wo die Schlüsseldatei so geheim ist, dass ein Entwickler diese zum Signieren nicht verwenden kann, kann er die verzögerte Signierung verwenden (die im nächsten Abschnitt behandelt wird).
16
18
19
20
HALT
21
Die Schlüsseldatei wird von Visual Studio immer in den Projektordner kopiert, was ich bei der Verwendung einer Schlüsseldatei für alle Assemblys einer Firma für nicht besonders glücklich gelöst halte. Auf diese Weise ist der Austausch der Schlüsseldatei für den Notfall (falls diese in falsche Hände geraten ist) besonders schwierig.
22
Wenn Sie ein Projekt kompilieren, dem eine Schlüsseldatei zugeordnet ist, wird dieses automatisch mit einem starken Namen versehen.
23
2
Ich weiß: Ein Hacker mit dem notwendigen Wissen kann u. U. auch geschützte Dateien knacken …
1233
Assemblys, Reflektion und Anwendungsdomänen
Assemblys verzögert signieren Assemblys können nachträglich signiert werden
Für den Fall, dass beim Kompilieren einer Assembly die Schlüsseldatei nicht vorliegt (weil diese geheim gehalten wird), kann die so genannte verzögerte Signierung verwendet werden. Dabei wird die Schlüsseldatei, die den privaten und den öffentlichen Schlüssel enthält, den Entwicklern nicht bekannt gemacht. Während der Entwicklung einer Anwendung oder einer Klassenbibliothek wird diese lediglich über den öffentlichen Schlüssel signiert. Auf der Entwicklungsmaschine muss dazu allerdings eingestellt werden, dass Assemblys, die nur mit einem öffentlichen Schlüssel signiert sind, ausgeführt werden dürfen. Eine auf diese Art kompilierte Anwendung kann aber auf einem System, auf dem die Ausführung der inkomplett signierten Assembly nicht speziell erlaubt ist, nicht ausgeführt werden (das Laden einer solchen Assembly führt zu einer FileLoadException). In einem späteren Build-Prozess werden die Assemblys dann mit der eigentlichen Schlüsseldatei signiert und können ausgeliefert werden. Das (leider relativ komplexe) Vorgehen dazu ist Folgendes:
STEPS
1.
Zur verzögerten Signierung benötigen Sie zunächst eine Datei, die den öffentlichen Schlüssel des zur Signierung verwendeten Schlüsselpaars enthält. Diese kann die Person, die für die Signatur-Datei verantwortlich ist, über das .NETTool sn.exe (das Teil des Windows-SDK ist und mit Visual Studio im Ordner C:\Programme\Microsoft SDKs\Windows\v6.0A\bin installiert wird) folgendermaßen aus der Schlüsseldatei extrahieren: sn -p Schlüsselpaar-Datei Öffentlicher-Schlüssel-Ergebnis-Datei
Die eigentliche Schlüsseldatei kann übrigens auch über sn.exe erzeugt werden: sn -k Schlüsselpaar-Datei
2.
3.
Die Datei mit dem öffentlichen Schlüssel geben Sie in einem Projekt als Schlüsseldatei an. Zusätzlich dazu schalten Sie die Einstellung NUR VERZÖGERTE SIGNIERUNG ein. Beim Kompilieren wird der öffentliche Schlüssel in das Manifest der Assembly übertragen. Außerdem reserviert der Compiler Platz für die später erzeugte Signatur. Assemblys, die für die verzögerte Signierung vorbereitet sind, können zwar in einem Visual-Studio-Projekt referenziert werden, beim Starten der Anwendung wird aber eine FileLoadException generiert. Deswegen müssen diese auf der Entwicklungsmaschine zur Verwendung freigegeben werden, was folgendermaßen geschieht: sn -Vr Assemblyname.dll
Dies sollten Sie allerdings nur auf Entwicklungsmaschinen ausführen, da die Freigabe einer nicht komplett signierten Assembly natürlich ein Sicherheitsrisiko darstellt. 4.
5.
Auf der Entwicklungsmaschine kann nun ganz normal entwickelt werden. Auch die Erhöhung der Versionsnummer der referenzierten, teilweise signierten Assembly ist kein Problem. Vor der Auslieferung müssen die nur teilweise signierten Assemblys nun mit der richtigen Signatur versehen werden (was dann die Person oder der Build-Prozess macht, der für die Erstellung des Setup zuständig ist). Dafür wird wieder sn.exe verwendet: sn -R Assemblyname.dll Schlüsselpaar-Dateiname.snk
1234
Wichtige weitere Möglichkeiten beim Umgang mit Assemblys
Die Anwendung kann dann mit den neu signierten Assemblys ausgeliefert werden. Und das Schöne ist: Es funktioniert ☺.
22.1.2
Vorkompilieren von Assemblys in den nativen Abbild-Cache
Assemblys bestehen ja, wie Sie wissen, aus CIL-Code. Wenn die CLR eine Assembly lädt, wird diese dynamisch und »just in time« in Maschinencode kompiliert. Dieses Kompilieren bei Bedarf hat den wesentlichen Vorteil, dass der Maschinencode auf den jeweiligen Rechner optimiert werden kann.
Assemblys können in ein natives Abbild kompiliert werden
Der Nachteil ist allerdings, dass das erste Ausführen einer Methode immer etwas länger dauert als alle folgenden Ausführungen, weil der entsprechende CIL-Code zunächst in Maschinencode kompiliert werden muss. In den meisten Anwendungen spielt dieser kleine Nachteil keine große Rolle, weil die zum Kompilieren benötigte Zeit (auf einer modernen Maschine) so gering ist, dass der Benutzer davon gar nichts mitbekommt.
13
14
Der »Native Image Generator« (ngen.exe) In speziellen Situationen ist es sinnvoll oder notwendig, auch den ersten Aufruf einer Methode mit der optimalen Performance auszuführen. In diesem Fall hilft das Tool ngen.exe, der »Native Image Generator«. ngen.exe ist in der Lage, eine komplette Anwendung, inklusive aller referenzierten Assemblys, vorzukompilieren und in den so genannten nativen Abbild-Cache (Native Image Cache) zu speichern.
12
ngen.exe erzeugt ein natives Abbild
15
16
Wenn die CLR eine Assembly lädt, überprüft sie zunächst, ob die Assembly im nativen Abbild-Cache gespeichert ist, und lädt in diesem Fall diese. Dabei gelten natürlich dieselben Regeln wie beim normalen Laden einer Assembly.
17
Die Verwendung von ngen.exe ist an sich einfach: ngen [Anwendungsname.exe | Assemblyname.dll]
Die so installierten Assemblys finden Sie im Ordner assembly\NativeImages_ v2.0.50727_32 im Windows-Ordner (oder ähnlich, je nach System). Diesen Ordner können Sie per Voreinstellung in Windows aber nicht direkt öffnen. Verwenden Sie dazu die Konsole oder einen alternativen Dateimanager wie den (hervorragenden) Total Commander. In diesem Ordner sind übrigens auch die nativen Abbilder der .NET-Assemblys gespeichert.
18
19
TIPP
20
Zu bedenken sind nur zwei Dinge: ■
■
ngen.exe macht nur dann Sinn, wenn dieses Tool auf der Maschine ausgeführt wird, auf der die Anwendung später ausgeführt wird. Wenn Sie ngen.exe auf der Entwicklungsmaschine ausführen, bringt das für die ausgelieferte Anwendung gar nichts, da in deren nativem Abbild-Cache keine kompilierte Version der Anwendungs-Assemblys gefunden wird. Bei einem Update der Anwendung oder von einzelnen Assemblys ist das native Abbild veraltet und wird nicht mehr verwendet. Deswegen sollte ngen.exe nach einem Update immer erneut aufgerufen werden.
21
22 23
In der Praxis sollten Sie den Aufruf von ngen.exe in das Setup integrieren, da Sie die Ausführung nicht vom Anwender erwarten können. Die Integration ist jedoch leider nicht allzu einfach (es sei denn, Sie verwenden professionelle Setup-Werkzeuge wie
1235
Assemblys, Reflektion und Anwendungsdomänen
InstallShield). Für ein mit Visual Studio erzeugtes Setup können Sie eine benutzerdefinierte Aktion verwenden, die beim Setup ngen.exe startet. Die in der Praxis nicht einfachen benutzerdefinierten Aktionen wurden bereits in Kapitel 16 besprochen.
Tipp für eine automatische Installation im nativen Abbild-Cache Nur als Tipp zu verstehen ist das folgende Programm, das die eine Anwendung im nativen Abbild-Cache installiert (die Integration in ein Setup müssen Sie allerdings selbst vornehmen ☺): Listing 22.2:
Programmgesteuerte Installation einer Anwendung in den nativen Abbild-Cache
[DllImport("mscoree.dll", CharSet = CharSet.Unicode)] static extern int GetCORSystemDirectory( [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pbuffer, int cchBuffer, out int dwlength); ... // Assembly-Image erzeugen (über ngen) string installFolder = base.Context.Parameters["InstallFolder"]; StringBuilder frameworkPath = new StringBuilder(1024); int length; GetCORSystemDirectory(frameworkPath, frameworkPath.Capacity, out length); string ngenPath = Path.Combine(frameworkPath.ToString(), "ngen.exe"); string exePath = Path.Combine(installFolder, Path.GetFileName(Assembly.GetExecutingAssembly().Location)); ProcessStartInfo psi = new ProcessStartInfo(ngenPath, " install \"" + exePath + "\""); try { Process process = Process.Start(psi); process.WaitForExit(); } catch (Exception ex) { throw new Exception("Fehler bei der Installation " + "in den nativen Abbild-Cache:" + ex.Message, ex); }
Den Kontext-Parameter InstallFolder müssen Sie im Setup für die benutzerdefinierte Aktion übergeben. Informationen dazu finden Sie in Kapitel 16.
INFO
Interessant ist, dass eine Anwendung sich scheinbar auch selber in den nativen Abbild-Cache installieren kann. Dies würde aber nur dann Sinn machen, wenn beim Start der Anwendung überprüft werden könnte, ob alle von der Anwendung verwendeten Assemblys nicht bereits in der aktuellen Version im nativen Abbild-Cache kompiliert gespeichert sind.
Weitere Features von ngen.exe ngen.exe hat noch mehr Features, die ich hier nicht vorstellen kann. Lesen Sie in der Visual-Studio-Dokumentation nach, wenn Sie mehr erfahren wollen. Suchen Sie dazu nach »The Performance Benefits of NGen« (eigenartigerweise in Englisch, im Internet gibt es auch eine deutsche Version: msdn.microsoft.com/de-de/library/ bb978898.aspx).
1236
Reflektion
22.1.3
Assemblys, die nicht im Anwendungsordner direkt verwaltet werden
In einigen Fällen sollen Assemblys bei der Installation beim Kunden nicht in demselben Ordner gespeichert werden wie die Anwendung. Dies macht z. B. dann Sinn, wenn die Anwendung sehr viele Assemblys verwendet und diese die Übersicht über die eigentlich wichtigen Dateien der Anwendung erschweren. Sinnvoll ist in diesem Fall, die von der Anwendung referenzierten Assemblys in einem Unterordner zu verwalten (z. B. dem bin-Ordner).
Assemblys können auch in Unteroder anderen Ordnern verwaltet werden
Das Vorgehen dazu ist prinzipiell einfach: Sie müssen der CLR lediglich mitteilen, wo diese zusätzlich nach Assemblys suchen soll. Und das geht (natürlich) über die Konfiguration: Listing 22.3:
12
13
Konfiguration von zusätzlichen (Unter-)Ordnern, in denen die CLR nach Assemblys suchen soll
14
15
Im privatePath-Attribut können Sie einen oder mehrere Pfade angeben, die Sie durch Semikolons trennen.
16
Ich hatte in der Praxis damit aber massive Probleme. In vielen Tests funktionierte die Umleitung selbst für einfachste Anwendungen nicht.
17 INFO
Bei Problemen mit der Lokalisierung von Assemblys können Sie den Registry-DWord32-Wert HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\EnableLog auf 1 setzen (den Sie meist noch erzeugen müssen). In diesem Fall protokolliert die CLR die Versuche, Assemblys zu lokalisieren (in der Standardausgabe, die Sie an der Konsole sehen). Setzen Sie den Wert aber danach wieder auf 0, da das Protokollieren Zeit kostet.
22.2
18 TIPP
19
Reflektion
Reflektion (Reflection) ist ein Feature von .NET, das es erlaubt, Typen und Assemblys in der Laufzeit auszuwerten und in der Laufzeit dynamisch zu erzeugen. Das hört sich auf den ersten Blick etwas verwirrend an. Aber Reflektion ist ein wichtiger Bestandteil der (Hardcore-)Programmierung mit .NET. In normalen Anwendungen haben Sie mit Reflektion meist nicht viel zu tun (außer, dass Sie die Type-Klasse häufiger verwenden, weil einige Methoden eine Type-Instanz erwarten). In speziellen Anwendungen ist Reflektion aber unverzichtbar.
20 Reflektion erlaubt die Auswertung und Erzeugung von Typen und Assemblys in der Laufzeit
21
22
Eine typische Anwendung von Reflektion sind Anwendungen, die mit einem PluginMechanismus arbeiten. Diese laden spezielle Plugin-Assemblys in der Laufzeit und führen Methoden aus, die in Typen dieser Assemblys definiert sind. Plugin-Assemblys sind Assemblys, deren Typen eine bestimmte Schnittstelle erfüllen, die in der Anwendung bekannt ist. Die Anwendung sucht in einem bestimmten Ordner nach Assemblys, lädt diese dynamisch, sucht (per Reflektion) nach Typen, die die Schnitt-
23
1237
Assemblys, Reflektion und Anwendungsdomänen
stelle erfüllen, und führt die in der Schnittstelle definierten Methoden aus. Auf diese Weise können Anwendungen nachträglich mit neuen Funktionen versehen werden (wie dies auch bei Visual-Studio-Add-Ins, .NET-Reflector-Add-Ins und vielen anderen Plugin- oder Add-In-Techniken verwendet wird). Eine andere Anwendung von Reflektion sind Tools, die dynamisch Assemblys erzeugen und diese ausführen. Ein einfaches Beispiel ist die Integration einer Script-Möglichkeit in Anwendungen: Sie ermöglichen dem Anwender, für Teile der Anwendung Programme in Quellcodeform anzugeben, erzeugen darauf per Reflektion einen neuen Typ und führen diesen dann aus. So etwas macht natürlich nur in sehr komplexen Situationen Sinn. Ich habe z. B. einmal einen eigenen Berichtsgenerator und -Designer entwickelt, der dem Anwender erlaubt, Felder auf dem Bericht anzulegen, die mit Programmcode definiert wurden. In der Laufzeit – bei der Auswertung des Berichts – habe ich diese Felder ausgelesen, aus dem Code einen Typ erzeugt und diesen ausgeführt. Aber keine Angst: In diesem Kapitel erzeuge ich nur ein Programm, das einfache, vom Anwender eingegebene Ausdrücke wie 1 + 1 auswertet ☺. Ich kann Reflektion aber hier nicht in der Tiefe behandeln. Ich zeige deshalb nur die grundlegenden Dinge. INFO
22.2.1 Type und Assembly sind die Basis
Evaluieren von Typen und Assemblys
Die Basis der Reflektion sind die Klassen System.Type und System.Reflection.Assembly. Type liefert Informationen zu einem Typ und ermöglicht das dynamische Erzeugen eine Instanz und den dynamischen Zugriff auf die Felder, Eigenschaften und Methoden. Assembly liefert Informationen zu einer Assembly (inklusive deren Typen).
Evaluieren von Typen Eine Type-Instanz erhalten Sie auf eine von vier verschiedenen Weisen: Die von Object geerbte Methode GetType liefert eine Type-Instanz für den Typ eines Objekts: Type type1 = DateTime.Now.GetType();
Der typeof-Operator liefert eine Type-Instanz für einen Typ: Type type2 = typeof(DateTime);
Die Methode GetType einer Assembly-Instanz liefert einen in der repräsentierten Assembly gespeicherten Typ: Type type3 = Assembly.GetExecutingAssembly().GetType( "Mut.Demos.Demo");
Schließlich können Sie eine Type-Instanz noch über den voll qualifizierten Namen (inklusive Assembly) erzeugen (was aber in der Praxis eher selten verwendet wird): Listing 22.4:
Ermitteln einer Type-Instanz
Type type4 = Type.GetType("System.DateTime, mscorlib, " + "Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
Type besitzt eine Vielzahl an Eigenschaften und Methoden. Über die Eigenschaften können Sie Informationen zum Typ ermitteln. Listing 22.5 liest die wichtigsten aus.
1238
Reflektion
Listing 22.5:
Basisinformationen zum Typ ermitteln
Console.WriteLine("Name: " + type4.Name); Console.WriteLine("Namensraum: " + type4.Namespace); Console.WriteLine("Öffentlich: " + type4.IsPublic); Console.WriteLine("Serialisierbar: " + type4.IsSerializable); Console.WriteLine("Werttyp: " + type4.IsValueType); Console.WriteLine("Kann von außen verwendet werden: " + type4.IsVisible);
Die Methoden GetField, GetProperty und GetMethod ermitteln ein FieldInfo-, PropertyInfo- bzw. PropertyInfo-Objekt mit Informationen für ein Feld, eine Eigenschaft und eine Methode, deren Name Sie übergeben. Die Methoden GetFields, GetProperties und GetMethods ermitteln Informationen für mehrere Felder, Eigenschaften bzw. Methoden in Form eines Array der entsprechenden Informations-Klassen. So können Sie z. B. alle öffentlichen Eigenschaften eines Typs auslesen und zu diesen Informationen ermitteln: Listing 22.6:
Verschiedene Methoden liefern Informationen zu Feldern, Eigenschaften und Methoden
foreach (PropertyInfo propertyInfo in type4.GetProperties()) { Console.WriteLine("Name: " + propertyInfo.Name); Console.WriteLine("Typ: " + propertyInfo.PropertyType); Console.WriteLine("Lesbar: " + propertyInfo.CanRead); Console.WriteLine("Schreibbar: " + propertyInfo.CanWrite); Console.WriteLine(); }
■
Public: Gibt an, dass öffentliche Member zurückgegeben werden sollen
■
NonPublic: Gibt an, dass nicht-öffentliche Member zurückgegeben werden sollen
■
Instance: Gibt an, dass Instanz-Member zurückgegeben werden sollen
■
Static: Gibt an, dass statische Member zurückgegeben werden sollen
13
14
Auslesen von Informationen zu allen öffentlichen Eigenschaften eines Typs
Sie können diesen Methoden aber auch eine Kombination von BindingFlags-Werten übergeben, die definieren, welche Member Sie auslesen wollen. Per Voreinstellung (ohne Definition der BindingFlags) lesen Sie nur öffentliche Member aus. Reflektion erlaubt aber auch, nicht öffentliche Felder, Eigenschaften und Methoden auszulesen und sogar, auf diese zuzugreifen. Die wichtigsten BindingFlags-Werte sind (für unseren Fall):
12
15
16 BindingFlags definieren, welche Member in die Suche einbezogen werden
17
18
19
So können Sie z. B. Informationen für alle Felder eines Typs ermitteln, auch wenn diese nicht öffentlich sind:
20 Listing 22.7:
Ermitteln von Informationen zu allen Feldern eines Typs
foreach (FieldInfo fieldInfo in type4.GetFields( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) { Console.WriteLine("Name: " + fieldInfo.Name); Console.WriteLine("Typ: " + fieldInfo.FieldType); Console.WriteLine("Statisch: " + fieldInfo.IsStatic); Console.WriteLine("Öffentlich: " + fieldInfo.IsPublic); Console.WriteLine("Privat: " + fieldInfo.IsPrivate); Console.WriteLine(); }
21
22 23
1239
Assemblys, Reflektion und Anwendungsdomänen
INFO
Beachten Sie, dass Sie in diesem Fall BindingFlags.Instance und/oder BindingFlags.Static angeben müssen. Wenn Sie keine dieser Werte angeben, werden keine Member zurückgegeben. Zur Vervollständigung des Beispiels liest Listing 22.8 noch Informationen zu allen öffentlichen Methoden aus: Listing 22.8: Auslesen von Informationen zu allen öffentlichen Methoden eines Typs foreach (MethodInfo methodInfo in type4.GetMethods()) { Console.WriteLine("Name: " + methodInfo.Name); Console.WriteLine("Rückgabetyp: " + methodInfo.ReturnParameter.ParameterType); Console.WriteLine("Öffentlich: " + methodInfo.IsPublic); Console.WriteLine("Privat: " + methodInfo.IsPrivate); Console.WriteLine("Statisch: " + methodInfo.IsStatic); Console.WriteLine("Virtuell: " + methodInfo.IsVirtual); Console.WriteLine("Konstruktor: " + methodInfo.IsConstructor); Console.WriteLine("Generisch: " + methodInfo.IsGenericMethod); Console.WriteLine(); }
Das Ermitteln von Typinformationen kann noch ein wenig komplexer sein, da Sie z. B. auch Arrays und generische Typen berücksichtigen, Basisklassen ermitteln und die Parameter einer Methode auslesen können. Ich denke aber, für einen Einblick in das Auslesen von Informationen zu Typen (das in der Praxis – außer beim .NET Reflector ☺ – nur sehr selten benötigt wird) reichen die gegebenen Beispiele aus.
Evaluieren von Assemblys Assemblys können ebenfalls evaluiert werden
Assemblys (die auch dynamisch geladen werden können) lassen sich genauso einfach evaluieren wie Typen. Die Assembly-Klasse bietet zum Ermitteln einer AssemblyReferenz zunächst einige Methoden: ■ ■
■ GetName liefert u. a. die Version
GetAssembly ermittelt die Assembly, die den übergebenen Typ verwaltet Load lädt eine Assembly auf eine ähnliche Art, wie die CLR dies macht (die Assembly muss also für die CLR erreichbar sein). Dieser Methode können Sie u. a. den (vollen) Namen der zu ladenden Assembly übergeben. LoadFrom lädt eine Assembly aus einer Datei. Diese Assembly muss der CLR nicht bekannt sein, weil der komplette Dateipfad angegeben wird.
Über eine Assembly-Referenz können Sie über verschiedene Eigenschaften grundsätzliche Informationen auslesen. Wichtig ist dabei die Methode GetName, die (anders als der Name vermuten lässt) Zugriff auf den vollen (starken) Namen der Assembly gibt, der unter anderem auch die Version und die Kultur beinhaltet: Listing 22.9:
Auslesen von Basisinformationen zu einer Assembly
// Die Assembly ermitteln, die den Typ String enthält Assembly assembly = Assembly.GetAssembly(typeof(string)); // Einige wichtige Informationen ausgeben Console.WriteLine("Speicherort: " + assembly.Location); AssemblyName assemblyName = assembly.GetName(); Console.WriteLine("Einfacher Name: " + assemblyName.Name); Console.WriteLine("Voller Name: " + assemblyName.FullName); Console.WriteLine("Version: " + assemblyName.Version); Console.WriteLine("Kultur: " + assemblyName.CultureInfo.EnglishName);
1240
Reflektion
Über die Methode GetType können Sie einen bestimmten Typ aus der Assembly auslesen, GetTypes gibt alle Typen zurück (auch interne): Listing 22.10: Auslesen aller Typen einer Assembly foreach (Type type in assembly.GetTypes()) { Console.WriteLine(type.Name); }
12
Über die Type-Referenzen können Sie dann natürlich wieder die einzelnen Typen evaluieren.
13
22.2.2 Dynamisches Instanzieren und Verwenden von Typen Viel wichtiger als das Auslesen von Informationen sind das dynamische Instanzieren von Typen und der dynamische Zugriff auf die Typmember. In der Praxis kommt dieses zwar wie das Auslesen von Informationen relativ selten vor, aber in spezifischen Situationen machen das dynamische Instanzieren und/oder der dynamische Zugriff durchaus Sinn. Einfache Beispiele dafür zu finden, ist allerdings fast unmöglich. In der Praxis wird Reflektion meist in relativ komplexen Programmen eingesetzt. Deshalb müssen Sie sich an dieser Stelle mit der abstrakten Darstellung der grundsätzlichen Techniken begnügen.
14
15
16
Dynamisches Instanzieren von Typen am Beispiel eines Plugin-Systems Das dynamische Instanzieren eines Typs ist natürlich immer nur dann notwendig, wenn Ihr Programm den Typ nicht kennt. Das ist dann der Fall, wenn ein Programm eine Assembly dynamisch lädt (was über Assembly.Load relativ einfach ist). Diese Technik wird in der Praxis häufig für Plugin-Systeme verwendet. Um auf die dynamisch erzeugten Instanzen zuzugreifen, wird dabei allerdings häufig nicht Reflektion eingesetzt, sondern der Weg über eine Schnittstelle.
17
18
Und diesen zeige ich nun einfach hier. Dabei lernen Sie auch grundsätzlich, wie Plugin-Systeme entwickelt werden. Der Trick eines Plugin-Systems ist, dass die Anwendung und die Plugin-Assemblys eine gemeinsame weitere Assembly referenzieren, die eine Schnittstelle enthält. Diese Schnittstelle definiert die Felder, Eigenschaften und Methoden, die von der Anwendung für jede einzelne Plugin-Assembly gelesen, geschrieben bzw. aufgerufen werden sollen. Da diese Schnittstelle in der Anwendung bekannt ist, kann diese die Plugin-Assembly dynamisch laden und Typen suchen, die die Schnittstelle implementieren (was natürlich auch über Reflektion geschieht). Für alle gefundenen Typen kann die Anwendungs-Assembly dann eine Instanz erzeugen, diese eine Referenz vom Typ der Schnittstelle zuweisen und über die Schnittstelle dann mit den Instanzen arbeiten.
Plugin-Systeme basieren auf einer Schnittstelle
19
20
21
Als Demo definiere ich eine sehr einfache Schnittstelle in einem separaten Klassenbibliothek-Projekt (mit Namen Jb.Demos.Plugins.Interfaces.dll):
22
Listing 22.11: Die Schnittstelle in der Plugin-Schnittstellen-Assembly
23
namespace Jb.Demos.Plugins.Interfaces { public interface IDemoPlugin {
1241
Assemblys, Reflektion und Anwendungsdomänen
/* Soll die Arbeit des Plugin ausführen */ string Work(string info); } }
Ein weiteres Klassenbibliothek-Projekt, das ein erstes Plugin darstellen soll, referenziert diese Assembly und implementiert die Schnittstelle (in einer beliebigen Klasse): Listing 22.12: Implementierung der Plugin-Schnittstelle in einer Plugin-Assembly namespace Jb.Demos.Plugins.DemoPlugin1 { public class Demo : IDemoPlugin { public string Work(string info) { Console.WriteLine("Hallo, ich bin das " + "Demo-Plugin 1. Ich bearbeite " + info + " ..."); return info.ToUpper(); } } }
Die eigentliche Anwendung referenziert die Schnittstellen-Assembly ebenfalls. Die Anwendung schaut beim Start in einem vorgegebenen Ordner nach, ob dieser Assemblys mit Typen enthält, die die Schnittstelle implementieren. Das ist bisher noch nichts wirklich Neues, da dazu einfach Reflektion verwendet wird (ok, das Neue ist die Methode GetInterface, die eine Schnittstelle eines Typs ermittelt): Listing 22.13: Suchen nach Assemblys mit Typen, die eine gegebene Schnittstelle implementieren static void Main(string[] args) { Console.Title = "Demo-Anwendung"; // Im Unterordner 'Plugins' nach Plugin-Assemblys suchen string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); string pluginFolder = Path.Combine(appPath, "Plugins"); DirectoryInfo di = new DirectoryInfo(pluginFolder); foreach (var fi in di.GetFiles("*.dll")) { // Versuchen, die Datei als Assembly zu laden try { Assembly assembly = Assembly.LoadFrom(fi.FullName); // Die Typen der Assembly durchgehen foreach (var type in assembly.GetTypes()) { // Überprüfen, ob der Typ die Plugin-Schnittstelle // implementiert Type interfaceType = type.GetInterface( "Jb.Demos.Plugins.Interfaces.IDemoPlugin"); if (interfaceType != null) { // Der Typ implementiert die Plugin-Schnittstelle: // Diesen den Plugins hinzufügen AddPlugin(type); } } } catch (Exception ex)
1242
Reflektion
{ // Die Datei kann nicht geladen werden. Wahrscheinlich // handelt es sich nicht um eine Assembly Console.WriteLine("Fehler beim Laden von '{0}': {1}", fi.Name, ex.Message); } } }
Die AddPlugin-Methode muss nun noch implementiert werden. Diese Methode macht die eigentliche Arbeit. Sie erzeugt eine Instanz des Typs und hängt diesen an eine Auflistung an, die die Plugins verwaltet.
Activator .CreateInstance erzeugt eine Instanz
Die Erzeugung einer Instanz aus einem Typ kann über die CreateInstance-Methode der Activator-Klasse erfolgen: Listing 22.14: Erzeugen einer Instanz eines Typs
12
13
14
/* Verwaltet die Plugins */ private static List plugins = new List();
15
/* Fügt der Anwendung ein Plugin hinzu */ private static void AddPlugin(Type type) { // Eine Instanz des Typs erzeugen ... IDemoPlugin plugin = (IDemoPlugin)Activator.CreateInstance(type);
16
// ... und diese der Auflistung anhängen plugins.Add(plugin); }
17
CreateInstance können Sie am zweiten Object[]-Argument auch Argumente für den Konstruktor übergeben, falls dies notwendig sein sollte.
Der Rest ist jetzt reine Programmierung. In einer WPF- oder Windows.Forms-Anwendung können Sie die Plugin-Auflistung z. B. durchgehen, für jedes Plugin (das für diesen Fall über eine Eigenschaft auch einen Namen liefern sollte) und einen Menüeintrag im PLUGINS-Menü erzeugen. Falls Sie dies implementieren, leiten Sie dazu eine eigene Klasse von MenuItem (WPF) bzw. ToolStripMenuItem (Windows.Forms) ab, die Sie um ein Feld vom Typ der PluginSchnittstelle erweitern. Referenzieren Sie das Plugin in diesem Feld, wenn Sie den Menüeintrag anlegen. Dann können Sie dieses im Click-Ereignis sehr einfach (über eine Umwandlung des Arguments sender in Ihre MenuItem-Klasse) referenzieren und ausführen.
18
19 TIPP
20
21
In meiner kleinen Beispielanwendung gehe ich die geladenen Plugins aber einfach nur in der Main-Methode durch und rufe die Demo-Methode auf: foreach (IDemoPlugin plugin in plugins) { Console.WriteLine(plugin.Work("Hallo Plugin")); }
22
Das einzige Problem dieser Variante ist, dass die geladenen Plugin-Assemblys nicht wieder entladen werden können. Dieses Problem können Sie aber über eine separate Anwendungsdomäne lösen (siehe Seite 1248).
23
1243
Assemblys, Reflektion und Anwendungsdomänen
Dynamischer Zugriff auf Typ-Instanzen Der dynamische Zugriff auf Typ-Instanzen (ohne Schnittstelle, wie im vorigen Beispiel!) ist mit Reflektion ebenfalls kein Problem. Sie können auf alle Member eines Objekts zugreifen, auch auf nicht öffentliche! Das ist in Sonderfällen sogar interessant, aber doch eher selten anzutreffen. Das dynamische Verwenden öffentlicher Member eines Objekts wird schon eher, aber ebenfalls nur in sehr speziellen Sonderfällen eingesetzt. Ich brauchte dieses Feature einmal bei der Entwicklung einer eigenen Datenbindung, bei der beliebige Objekte an eine Oberfläche gebunden wurden. Aber alleine dies hier zu beschreiben, würde schon zu viel Platz wegnehmen. Also zeige ich nur die Grundlagen (wobei mir gerade einfällt: Habe ich nicht bereits in Kapitel 21 gezeigt, wie Sie bei der späten COM-Bindung dynamisch auf Typ-Instanzen zugreifen? Ich arbeite zu viel …): FieldInfo und PropertyInfo erlauben das Lesen und Schreiben von Feldern bzw. Eigenschaften
Den Wert eines Feldes können Sie über das FieldInfo-Objekt lesen und schreiben, das das Feld repräsentiert. Für Eigenschaften gilt Ähnliches für das PropertyInfoObjekt, das für die Eigenschaft steht. Die Methode GetValue liefert den Wert, die Methode SetValue schreibt den Wert. Beiden müssen Sie eine Referenz auf das Objekt übergeben. GetValue gibt den gelesenen Wert natürlich als Object zurück, SetValue erwartet den zu schreibenden Wert am zweiten Object-Argument. Beim Zugriff auf Eigenschaften erwarten beide Methoden (in der von mir verwendeten Variante ohne BindingFlags) zusätzlich am letzten Argument ein optionales Object-Array für indizierte Eigenschaften (also für Indexer), an dem Sie die IndexWerte übergeben können. Für einfache Eigenschaften übergeben Sie hier null. Das folgende Beispiel setzt eine einfache Klasse ein: public class Person { public string FirstName; public string LastName { get; set; } public int GetAge(DateTime baseDate) { return baseDate.Year - 1979; } }
Beachten Sie, dass in diesem Beispiel FirstName ein Feld und LastName eine Eigenschaft ist. Wenn Sie lediglich eine Object-Referenz auf eine Instanz dieses Typs haben, müssen Sie per Reflektion darauf zugreifen. In dem folgenden Beispiel muss ich diesen Umstand simulieren: Listing 22.15: Dynamisches Lesen und Schreiben von Feldern und Eigenschaften // Ein Demo-Objekt erzeugen Object obj = new Person() { FirstName = "Zaphod" }; // Den Typ ermitteln Type type = obj.GetType(); // Ein Feld lesen FieldInfo fi = type.GetField("FirstName"); if (fi != null) { object fieldValue = fi.GetValue(obj);
1244
Reflektion
Console.WriteLine(fieldValue); } else { Console.WriteLine("Das Feld wurde nicht gefunden"); } // Eine Eigenschaft schreiben PropertyInfo pi = type.GetProperty("LastName"); if (pi != null) { pi.SetValue(obj, "Beeblebrox", null); } else { Console.WriteLine("Die Eigenschaft wurde nicht gefunden"); }
12
13
Wichtig ist in diesem Zusammenhang, dass Sie überprüfen, ob GetField bzw. GetProperty ein Objekt zurückgegeben haben. Diese Methoden erzeugen nämlich keine Ausnahme, wenn der übergebene Name nicht existiert.
14
Wegen der Object-Referenzen können bei den beim Schreiben implizit und beim Lesen ggf. explizit verwendeten Konvertierungen natürlich Ausnahmen auftreten, die Sie in der Praxis abfangen sollten.
15 HALT
Methoden aufrufen
16
Der Aufruf von Methoden ist prinzipiell genauso einfach. Dazu verwenden Sie die InvokeMember-Methode des Type-Objekts, die (wie viele Methoden bei der Reflektion) in mehreren Überladungen vorliegt. Die einfachste ist die folgende:
17
Object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, Object target, Object[] args)
Das Argument name definiert den Namen der Methode. invokeAttr sind die schon bekannten BindingFlags, über die Sie definieren, in welchen Bereichen (private, öffentliche, statische und/oder Instanzmethoden) nach dem Namen gesucht werden soll. Zusätzlich müssen Sie beim Zugriff auf Methoden hier BindingFlags. InvokeMethod übergeben, um den Zugriff auf eine Eigenschaft (die auch über InvokeMember gelesen und geschrieben werden kann) zu unterscheiden.
18
19
Das Argument binder wird scheinbar für den Aufruf von speziell überladenen Methoden verwendet. Dieses (sehr schlecht dokumentierte) Argument können Sie normalerweise ignorieren3 und an dieser Stelle null übergeben (womit der StandardBinder verwendet wird).
20
target ist beim Aufruf von Instanzmethoden das Objekt. Beim Aufruf von statischen Methoden übergeben Sie hier null. Am letzten Argument übergeben Sie die Argumente der Methode (natürlich mit den korrekten, am jeweiligen Argument erwarteten Typen).
21
22
Falls die Methode eine Rückgabe hat, werten Sie diese über die Object-Rückgabe von InvokeMember aus. Da InvokeMember eine Ausnahme wirft, wenn keine entsprechende Methode (oder Eigenschaft) gefunden wird, müssen Sie diese abfangen:
23 3
Ich hatte bisher auf jeden Fall noch keine Probleme ohne die Übergabe eines speziellen BinderObjekts, auch nicht beim Aufruf von überladenen Methoden.
1245
Assemblys, Reflektion und Anwendungsdomänen
Listing 22.16: Aufruf einer Methode per Reflektion try { object result = type.InvokeMember("GetAge", BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public, null, obj, new Object[] { DateTime.Now }); if (result != null) { Console.WriteLine(Convert.ToInt32(result)); } else { Console.WriteLine("Die Rückgabe der Methode ist " + "unerwarteterweise null"); } } catch (Exception ex) { Console.WriteLine("Fehler beim Aufruf der Methode: " + ex.Message); }
22.2.3 Dynamisches Erzeugen von Typen und Assemblys Reflektion erlaubt das dynamische Erzeugen von Assemblys
Wenn Sie einmal Programmcode dynamisch ausführen müssen (z. B. zur Programmierung eines einfachen Rechners, in dem der Anwender eine Rechenoperation als String eingeben kann), können Sie einen komplizierten und einen einfachen Weg gehen. Der komplizierte ist die Verwendung der Klassen im Namensraum System.Reflection.Emit. Diese Klassen ermöglichen die dynamische Erzeugung von Assemblys. Leider müssen Sie hier aber CIL-Code angeben, was für die Praxis also kaum zu gebrauchen ist. Besser wäre es, den C#-Quellcode für eine Assembly in einem String speichern und diesen in eine Assembly kompilieren zu können, die dann schließlich im Programm per Reflektion ausgeführt wird. Und das ist kein Problem. Sie müssen lediglich wissen, wie es geht. Und das zeige ich hier ☺. Über eine Instanz der CodeDomProvider-Klasse aus dem Namensraum System. CodeDom.Compiler können Sie beliebigen Quellcode in einer von .NET unterstützten Sprache in eine Assembly kompilieren und die erzeugten Methoden dynamisch ausführen. Die Assembly muss dazu nicht als Datei erzeugt werden, sondern kann lediglich im Speicher existieren. Der Quellcode muss natürlich den Regeln von .NET entsprechen, sollte also einen Namensraum und in diesem zumindest eine Klasse oder Struktur mit einer Methode beinhalten. Bei der dynamischen Ausführung der Methode können Sie (natürlich) Argumente übergeben und einen eventuellen Rückgabewert auswerten. Wie Sie dies programmieren, zeige ich an einem einfachen Beispiel. Zunächst müssen Sie natürlich den Quellcode zusammenstellen. Um dieses Beispiel so einfach wie möglich zu halten, verwende ich eine Methode, die lediglich zwei Zahlen addiert und das Ergebnis zurückgibt: Listing 22.17: Ein einfacher, im Programm definierter Beispiel-Quellcode string string string string
1246
assemblyNamespace = "DynamicCode"; className = "Demo"; methodName = "Add"; source = "namespace " + assemblyNamespace +
Reflektion
"{" + " class " + className + " {" + " public double " + methodName + "(" + " double value1, double value2)" + " {" + " return value1 + value2;" + " }" + " }" + "}";
12
Je nach Programmiersprache (ich verwende hier natürlich C#) müssen Sie sich nicht um Zeilenumbrüche kümmern, da die Compiler für die Sprachen mit ZeilenendeZeichen (C#, J#) diese sowieso ignorieren. In dem Beispiel habe ich deswegen auch keine Zeilenumbrüche im Code-String eingesetzt und den Quelltext lediglich so zusammengestellt, dass er für Sie verständlich ist.
13
Beim Aufbau des Quellcodes haben Sie alle Möglichkeiten, die die Sprache bietet. Sie müssen lediglich beachten, dass Sie die Methode(n) später über Reflektion aufrufen und deswegen den Namensraum, die Klasse und den Namen und die Signatur der Methode kennen müssen. Wenn Sie Quellcode ausführen wollen, der vom Anwender eingegeben wird, müssen Sie diese Namen also festlegen. Dem Anwender können Sie in diesem Fall lediglich die Möglichkeit bieten, den Inhalt der Methode(n) anzugeben. Zum Kompilieren des Quellcodes benötigen Sie eine Instanz der CodeDomProviderKlasse, die Sie über die statische CreateProvider-Methode erhalten. Dieser Methode übergeben Sie einen String, der die zu verwendende Sprache angibt. "C#" steht dabei natürlich für C#. Über die CompileAssemblyFromSource-Methode der CodeDomProvider-Instanz kompilieren Sie den Quellcode. Dabei übergeben Sie am ersten Argument eine Instanz der Klasse CompilerParameters und am zweiten den Code.
14
15
CodeDomProvider erlaubt das Kompilieren von Quellcode
16
17
Die CompilerParameters-Instanz verwaltet alle Parameter, die dem Compiler übergeben werden sollen. Die Eigenschaft GenerateInMemory legt fest, dass die Assembly im Speicher erzeugt werden soll. Über GenerateExecutable legen Sie fest, ob Sie eine ausführbare (Exe-)Datei oder eine Klassenbibliothek kompilieren. IncludeDebugInformation bestimmt, ob Debuginformationen mit in die Assembly aufgenommen werden sollen. Die letzte für unser Vorhaben wichtige Eigenschaft ist ReferencedAssemblies, über deren Add-Methode Sie Referenzen zu allen in der dynamischen Klasse benötigten Assemblys hinzufügen können.
18
19
CompileAssemblyFromSource gibt ein CompilerResults-Objekt zurück, über das Sie später eventuelle Fehler auswerten bzw. die erzeugte Assembly erreichen.
20
Listing 22.18: Dynamisches Kompilieren von Quellcode in eine In-Memory-Assembly
21
// Compiler erzeugen CodeDomProvider compiler = CodeDomProvider.CreateProvider("C#"); // Input-Parameter für den Compiler definieren CompilerParameters compilerParams = new CompilerParameters(); compilerParams.GenerateInMemory = true; compilerParams.GenerateExecutable = false; compilerParams.IncludeDebugInformation = true; compilerParams.ReferencedAssemblies.Add("system.dll");
22 23
// Assembly erzeugen CompilerResults compilerResults = compiler.CompileAssemblyFromSource(compilerParams, source);
1247
Assemblys, Reflektion und Anwendungsdomänen
Errors enthält eventuelle Fehler
Nach dem Kompilieren sollten Sie über die Errors-Eigenschaft des CompilerResultsObjekts überprüfen, ob Fehler aufgetreten sind. Die Eigenschaft Line der in dieser Auflistung referenzierten CompilerError-Objekte speichert die Zeile, in der der Fehler aufgetreten ist. Aus der Eigenschaft Column können Sie Spalten auslesen und ErrorText verwaltet die Fehlermeldung. Sind keine Fehler aufgetreten, erreichen Sie die erzeugte In-Memory-Assembly über die Eigenschaft CompiledAssembly. Der Rest ist reine Reflektion: Das Aufrufen einer Methode in einer Assembly: Listing 22.19: Fehlerauswertung und dynamischer Aufruf der erzeugten Methode // Auf eventuelle Fehler abfragen if (compilerResults.Errors.Count == 0) { // Den erzeugten Typ laden Type type = compilerResults.CompiledAssembly.GetType( assemblyNamespace + "." + className, true); // Eine Instanz der Klasse erzeugen object classInstance = Activator.CreateInstance(type); // Die Methode aufrufen object[] args = { 10, 11 }; object result = type.InvokeMember(methodName, BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public, null, classInstance, args); // Das Ergebnis ausgeben Console.WriteLine("Das Ergebnis: " + Convert.ToDouble(result)); } else { // Fehler auswerten for (int i = 0; i < compilerResults.Errors.Count; i++) { Console.WriteLine("Zeile " + compilerResults.Errors[i].Line + ", Spalte " + compilerResults.Errors[i].Column + ": " + compilerResults.Errors[i].ErrorText); } }
Alles Weitere (also die Anwendung dieser Technik) liegt bei Ihnen ☺.
HALT
Wenn Sie Assemblys dynamisch erzeugen, bleiben diese auch nach der Verwendung im Arbeitsspeicher. Um dieses Problem zu lösen, sollten Sie das Erzeugen und Verwenden in einer separaten Anwendungsdomäne vornehmen, die nach der Verwendung entladen wird.
22.3 Anwendungsdomänen isolieren Teile einer Anwendung
1248
Anwendungsdomänen
Anwendungsdomänen (Application domains) sind ein Feature der CLR zur Isolation von Teilen einer .NET-Anwendung. Jede .NET-Anwendung enthält zunächst lediglich eine Anwendungsdomäne, die Default-Anwendungsdomäne. Alle referenzierten Assemblys werden in diese Anwendungsdomäne geladen. In einigen Fällen ist es aber sinnvoll oder notwendig, geladene Assemblys von anderen geladenen Assemblys komplett zu trennen. In diesen Fällen können Sie der Anwendung weitere Anwendungsdomänen hinzufügen, in die Sie die anderen Assemblys laden. Damit sind die separat geladenen Assemblys komplett von den anderen getrennt. Die
Anwendungsdomänen
Assemblys in den verschiedenen Anwendungsdomänen der Anwendung können sich gegenseitig nicht beeinflussen. Warum dies in manchen Situationen Sinn macht, kläre ich gleich. Was mich aber in Bezug auf den Begriff »Anwendungsdomäne« immer verwirrt hat, ist das »Domäne« in diesem Begriff. Bevor ich Anwendungsdomänen genauer kannte, habe immer gedacht, eine Anwendungsdomäne würde mehrere Anwendungen beinhalten. Aber das ist falsch. Richtig ist: Eine Anwendung kann mehrere Anwendungsdomänen beinhalten. Eine Domäne ist in der Grundbedeutung ein »Herrschaftsbereich«. Eine Anwendungsdomäne »herrscht« über ihre geladenen Assemblys, aber nicht über die, die in andere Anwendungsdomänen derselben Anwendung geladen sind.
22.3.1
■
■
13
Der Sinn von Anwendungsdomänen
Anwendungsdomänen haben den Sinn, geladene Assemblys voneinander zu isolieren. Eine .NET-Anwendung wird zunächst nur in einer Anwendungsdomäne ausgeführt. Eine Anwendung kann aber bei Bedarf auch weitere Anwendungsdomänen erzeugen, Assemblys in diese laden und ausführen. Dieses Feature wird in der Praxis eher selten benötigt. Die Gründe für einen Einsatz mehrerer Anwendungsdomänen sind folgende: ■
12
Anwendungsdomänen isolieren Assemblys
15
Wenn eine Anwendung separate (Plugin-)Assemblys in der Laufzeit dynamisch lädt, per Reflektion Instanzen der Typen erzeugt und diese verwendet, tritt ohne eine separate Anwendungsdomäne ein Problem auf: In .NET einmal geladene Assemblys können nicht wieder aus der Anwendungsdomäne entladen werden. Eine separate Anwendungsdomäne löst das Probleme, denn sie kann komplett entladen werden. In Anwendungen, die in einem Prozess mehrere Quasi-Anwendungen verwalten, werden Anwendungsdomänen zur prozessähnlichen Isolation der einzelnen Quasi-Anwendungen voneinander verwendet. Eine solche Anwendung ist z. B. ein Webserver wie der IIS, der in einem Hosting-Prozess gleich mehrere Webs verwalten kann. Spezielle Stress-Test-Anwendungen, die z. B. eine Webanwendung oder einen Webdienst über mehrere gleichzeitig ausgeführte Threads testen sollen, setzen häufig Anwendungsdomänen ein, um darauf verzichten zu können, threadsicheren Code zu verwenden. Da Anwendungsdomänen voneinander isoliert sind, können mehrere Threads in separaten Anwendungsdomänen sich nicht gegenseitig beeinflussen.
Was aber leider nicht besonders gut isoliert ist, sind Ausnahmen, die in einer Anwendungsdomäne auftreten. Diese müssen in der Anwendungsdomäne abgefangen werden, die die andere erzeugt und verwendet. Ansonsten wird die HauptAnwendungsdomäne mit der üblichen Fehlermeldung beendet, wenn in einer anderen Anwendungsdomäne eine Ausnahme eintritt. Führt die andere Anwendungsdomäne einen Thread aus, der keine Ausnahmebehandlung besitzt, und tritt in diesem eine Ausnahme ein, führt das in einigen (!) Fällen dazu, dass die HauptAnwendungsdomäne abstürzt.
14
16
17
18
19
20
21
HALT
22 23
Sie sollten also das Erzeugen und Anwenden von Anwendungsdomänen grundsätzlich immer in einem try-catch-Block vornehmen. Besonders auch in Thread-Methoden, die in Anwendungsdomänen ausgeführt werden, sollten Sie eine Ausnahmebehandlung
1249
Assemblys, Reflektion und Anwendungsdomänen
intern vorsehen. In den Beispielen dieses Abschnitts verzichte ich allerdings aus Platzund Übersichtsgründen darauf.
DISC
Eine Demonstration des Verhaltens bei Fehlern finden Sie in Form des Projekts »Fehler-Demo« in dem Buchbeispielen. Starten Sie das Beispiel direkt unter Windows und Sie werden feststellen, dass das Programm beim mehrfachen Starten in einigen Fällen abstürzt (wegen einer Ausnahme in einem Thread).
22.3.2 Anwendungsdomänen erzeugen Das Erzeugen einer Anwendungsdomäne ist einfach. Dazu rufen Sie die CreateDomain-Methode der AppDomain-Klasse auf, der Sie einen Namen für die neue Anwendungsdomänen übergeben: AppDomain newAppDomain = AppDomain.CreateDomain("Test");
In der erzeugten Anwendungsdomäne können Sie dann ausführbare Assemblys (.exe-Dateien) laden oder Programmcode ausführen.
22.3.3 Ausführen einer ausführbaren Assembly in der Anwendungsdomäne ExecuteAssembly lädt eine Anwendung
Eine ausführbare Assembly können Sie über die Methode ExecuteAssembly ausführen, der Sie den Dateinamen übergeben. Die Methode ExecuteAssemblyByName macht Ähnliches, dieser Methode übergeben Sie allerdings den Assembly-Namen. Die Assembly muss in diesem Fall für die CLR erreichbar sein. Das Problem dieser Variante ist die Steuerung der aufgerufenen Anwendungs-Assembly, da der aufrufende Code an den Einsprungpunkt der Anwendung (der MainMethode) gebunden ist. Sie können zur Steuerung Befehlszeilenargumente übergeben. Eine andere Möglichkeit sind spezielle Anwendungsdomänen-Eigenschaften, die Sie der Anwendungsdomäne über die SetData-Methode übergeben und in der Anwendungsdomäne über die GetData-Methode lesen. Diese Möglichkeit behandle ich im Abschnitt »Datenaustausch zwischen Anwendungsdomänen« (Seite 1253). Das folgende Beispiel startet die ausführbare Datei Test.exe in der neuen Anwendungsdomäne und übergibt an den Befehlszeilenargumenten das aktuelle Datum und einen Integer-Wert. Befehlszeilenargumente können Sie der ExecuteAssemblyMethode in einer der Überladungen am dritten Argument in Form eines StringArrays übergeben. Diese Überladung erwartet am zweiten Argument einen »Nachweis« für die auszuführende Anwendung (in Form einer Instanz der EvidenceKlasse). Ein Nachweis kann z. B. sein, dass die gestartete Assembly mit einer bestimmten digitalen Signatur versehen ist. Für unseren Fall können Sie dieses Argument ignorieren und null übergeben (auch in der Praxis würde ich hier nichts übergeben und mich auf die Codezugriffssicherheit verlassen, die natürlich auch beim Laden von Assemblys in separate Anwendungsdomänen greift). Listing 22.20: Ausführen einer ausführbaren Assembly in einer Anwendungsdomäne string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); string exeFilename = Path.Combine(appPath, "Test.exe"); newAppDomain.ExecuteAssembly(exeFilename, null, new string[] {DateTime.Now.ToString(), "42"});
1250
Anwendungsdomänen
Zur Vervollständigung des Beispiels folgt die Main-Methode der gestarteten Anwendungs-Assembly, die die Argumente auswertet: Listing 22.21: Die Main-Methode der gestarteten Anwendungs-Assembly static void Main(string[] args) { Console.WriteLine("Hallo, ich bin die andere Anwendung."); Console.WriteLine(); Console.WriteLine("Hier sind die übergebenen Argumente:");
12
// Auswerten der Befehlszeilenargumente foreach (var arg in args) { Console.WriteLine(arg); }
13
}
14
22.3.4 Entladen von Anwendungsdomänen Anwendungsdomänen können Sie sehr einfach komplett über die Unload-Methode der AppDomain-Klasse entladen:
15
AppDomain.Unload(newAppDomain);
Das Entladen entfernt alle in der Anwendungsdomäne geladenen Assemblys. In Anwendungen, die relativ selten sehr große Assemblys einsetzen müssen, können Sie eine Anwendungsdomäne deswegen auch verwenden, um diese nur bei Bedarf zu laden und nach der Verwendung aus dem Speicher zu entfernen. Das Entladen einer Anwendungsdomäne ist übrigens auch kein Problem, wenn die Anwendungsdomäne eine Instanz eines Referenztyps erzeugt und diesen (über den Datenaustausch, siehe Seite 1253) an eine andere Anwendungsdomäne weitergibt. Beim Datenaustausch zwischen Anwendungsdomänen werden keine Referenzen weitergegeben: Referenztypen werden serialisiert und auf der anderen Seite deserialisiert. Sie erhalten also bei der Weitergabe zwei Instanzen (was natürlich auch berücksichtigt werden muss).
16
17 INFO
18
19
22.3.5 Ausführen von eigenem Programmcode in einer separaten Anwendungsdomäne Die zweite Möglichkeit, Programmcode in einer separaten Anwendungsdomänen auszuführen, ist der Aufruf der DoCallBack-Methode der AppDomain-Klasse. Diese Methode ruft einfach eine Callback-Methode auf, die Sie definieren. Innerhalb dieser Methode können Sie beliebig programmieren. Hier können Sie z. B. Threads starten, die von den Threads in anderen Anwendungsdomänen unabhängig ausgeführt werden, Assemblys dynamisch laden oder Assemblys dynamisch erzeugen. Das einzige, worauf Sie achten sollten, ist, dass die Methode statisch sein sollte. Die Methode ist damit nicht an eine Instanz gebunden (die immer zu einer bestimmten Anwendungsdomäne gehört) und kann deswegen problemlos in mehreren Anwendungsdomänen verwendet werden. Falls Sie eine Instanzmethode verwenden (was prinzipiell auch möglich ist), würde die CLR für die Kommunikation zwischen den dann verschiedenen Anwendungsdomänen Remoting-Mechanismen verwenden, die die Performance vermindern würden.
DoCallBackMethode ruft eine Methode auf
20
21
22 INFO
23
1251
Assemblys, Reflektion und Anwendungsdomänen
In Listing 22.22 zeige ich lediglich ein einfaches Beispiel. DoCallBack wird eine Instanz des CrossAppDomainDelegate-Delegaten übergeben, der keine Argumente und Rückgabe definiert. Das Beispiel verwendet eine separate Methode, weil die zum einen die Übersicht fördert und zum anderen eine statische Methode verwendet werden kann: Listing 22.22: Ausführen von Programmcode in einer separaten Anwendungsdomäne /* Methode, die in der separaten Anwendungsdomäne ausgeführt wird */ private static void Demo() { Console.WriteLine("Hallo. Ich werde in der " + "Anwendungsdomäne '" + AppDomain.CurrentDomain.FriendlyName + "' " + "ausgeführt"); } static void Main(string[] args) { // Neue Anwendungsdomäne erzeugen AppDomain newAppDomain = AppDomain.CreateDomain("Demo"); // Die Callback-Methode aufrufen newAppDomain.DoCallBack(Program.Demo); // Die Anwendungsdomäne entladen AppDomain.Unload(newAppDomain); }
Im folgenden Abschnitt setze ich DoCallBack sinnvoll ein.
22.3.6 Dynamisches Erzeugen und Verwenden von Typen einer Assembly in einer separaten Anwendungsdomäne Anwendungsdomänen werden häufig für dynamisch geladene oder dynamisch erzeugte Assemblys eingesetzt
1252
Der häufigste Einsatz separater Anwendungsdomänen ist wahrscheinlich das Erzeugen und Verwenden von Typen einer dynamisch geladenen (siehe Seite 1241) oder dynamisch erzeugten Assemblys (Seite 1246). Der wesentliche Vorteil ist hier, dass Sie einfach die Anwendungsdomäne entladen können, um alle geladenen oder erzeugten Assemblys aus dem Speicher zu entfernen. Sinn macht das natürlich nur, wenn die geladenen oder erzeugten Assemblys nur selten verwendet werden. In meinem Plugin-Beispiel könnte eine separate Anwendungsdomäne Sinn machen, wenn die Plugins nur selten verwendet werden und/oder sehr große sind. Dies erfordert aber eine erweiterte Programmierung, da Sie die Plugin-Assemblys dann zweimal laden müssen: einmal beim Programmstart, um zu ermitteln, welche Plugins vorhanden sind, und noch einmal, wenn das Plugin ausgeführt werden soll. Deswegen müsste das Plugin-Beispiel so erweitert werden, dass statt einer Referenz auf die Instanz der einzelnen Plugin-Klassen lediglich Informationen verwaltet werden, wo diese gefunden werden. Beim Aufruf eines Befehls zum Starten eines Plugins müsste die Anwendung dann eine erneute Anwendungsdomäne erzeugen, die Assembly neu laden, eine Instanz des Typs erzeugen und diese ausführen. Ein weiteres Problem dieses Ansatzes ist der Austausch von Informationen zwischen den Anwendungsdomänen. Dazu können Sie nicht einfach statische oder Instanzfelder oder -Eigenschaften verwenden, da der Sinn von Anwendungsdomänen ja ist, dass diese voneinander isoliert sind. Auch ein
Anwendungsdomänen
statisches Feld, das in einer Klasse in einer Anwendungsdomäne beschrieben wird, kann in einer anderen nicht ausgelesen werden. Zur Kommunikation müssten Sie die über SetData und GetData erreichbaren Anwendungsdomänen-Eigenschaften verwenden. Da ich denke, dass Plugin-Systeme in der Praxis wahrscheinlich eher nicht so entwickelt werden, verzichte ich auf ein Beispiel. Beim dynamischen Erzeugen von Programmcode ist das Ganze wesentlich einfacher, weil Sie dies in der Methode vornehmen können, die Sie DoCallBack zuweisen. Auf ein komplettes Beispiel verzichte ich jedoch hier auch, da dieses zu viel Platz wegnimmt. Das Prinzip ist aber sehr einfach: Sie erzeugen die dynamische Assembly in der Callback-Methode, die Sie dem DoCallBack-Ereignis einer neuen Anwendungsdomäne zuweisen:
12
13
Listing 22.23: Verwenden einer Anwendungsdomäne zum dynamischen Erzeugen einer Assembly /* Die Methode zum Erzeugen von Programmcode wird in der separaten Anwendungsdomäne ausgeführt */ private static void ExecuteCode() { // Programmcode zum dynamischen Erzeugen einer Assembly. // Siehe Seite _Ref197832608 ... }
14
15
static void Main() { // Anwendungsdomäne erzeugen, die Callback-Methode ausführen // und die Anwendungsdomäne wieder entladen AppDomain dynamicCodeDomain = AppDomain.CreateDomain("Code"); dynamicCodeDomain.DoCallBack(Program.ExecuteCode); AppDomain.Unload(dynamicCodeDomain); }
16
17
Ein entsprechendes Projekt finden Sie in den Buchbeispielen.
22.3.7 Datenaustausch zwischen Anwendungsdomänen
18 DISC
Der Datenaustausch zwischen Anwendungsdomänen ist nicht über statische oder Instanzfelder eines Typs möglich. Der Sinn von Anwendungsdomänen ist aber, dass diese gegenüber anderen Anwendungsdomänen wie ein Prozess isoliert sind. Globale, statische Felder oder Eigenschaften, die innerhalb einer Anwendungsdomäne in allen geladenen Assemblys gültig sind, können in einer anderen zwar verwendet werden. Es handelt sich dabei aber um separate Speicherbereiche, die nur innerhalb der jeweiligen Anwendungsdomäne gültig sind.
Datenaustausch ist nicht über statische oder InstanzMember möglich
Zum Austausch von Daten zwischen Anwendungsdomänen können Sie aber Anwendungsdomänen-Eigenschaften verwenden, die Sie über die SetData-Methode einer AppDomain-Instanz schreiben und über die GetData-Methode lesen.
SetData und GetData erlauben den Austausch von Daten
19
20
Listing 22.24 demonstriert dies und beweist gleich noch, dass statische Felder und Eigenschaften (genau wie Instanz-Member) in Anwendungsdomänen in eigenen Speicherbereichen verwaltet werden. Das Beispiel erzeugt die Callback-Methode dieses Mal über einen Lambda-Ausdruck.
21
22 23
1253
Assemblys, Reflektion und Anwendungsdomänen
Listing 22.24: Beweis, dass statische Felder und Eigenschaften in Anwendungsdomänen separat verwaltet werden, und Einsatz von Anwendungsdomänen-Eigenschaften // Statisches-Demo-Feld public static int DemoNumber; static void Main(string[] args) { // Das statische Feld setzen Program.DemoNumber = 42; // Das statische Feld auslesen Console.WriteLine("Anwendungsdomäne: " + AppDomain.CurrentDomain.FriendlyName); Console.WriteLine("Statisches Feld: " + Program.DemoNumber); Console.WriteLine(); // Anwendungsdomäne erzeugen AppDomain demoDomain = AppDomain.CreateDomain("Demo"); // Eine Anwendungsdomänen-Eigenschaft setzen demoDomain.SetData("OtherDemoNumber", 42); // Programmcode ausführen demoDomain.DoCallBack(new CrossAppDomainDelegate(() => { Console.WriteLine("Anwendungsdomäne: " + AppDomain.CurrentDomain.FriendlyName); // Das statische Feld auslesen Console.WriteLine("Statisches Feld: " + Program.DemoNumber); // Hier wird 0 ausgegeben, weil Anwendungsdomänen komplett // voneinander getrennt ausgeführt werden! // Die Anwendungsdomänen-Eigenschaft auslesen int otherDemoNumber = Convert.ToInt32( AppDomain.CurrentDomain.GetData("OtherDemoNumber")); Console.WriteLine("Anwendungsdomänen-Eigenschaft: " + otherDemoNumber); // 42 })); // Die Anwendungsdomäne wieder entladen AppDomain.Unload(demoDomain); }
INFO
Wichtig in diesem Zusammenhang ist, dass auch Anwendungsdomänen-Eigenschaften nur für die jeweilige Anwendungsdomäne gelten. Sie müssen deswegen die jeweilige Referenz auf die Anwendungsdomäne verwenden, um solche Eigenschaften korrekt austauschen zu können. Das Setzen eine Anwendungsdomänen-Eigenschaft über eine Referenz auf die eine Anwendungsdomäne und das Lesen über eine Referenz auf die andere Anwendungsdomäne ist nicht möglich (weil es sich in diesem Fall um verschiedene Sätze von Anwendungsdomänen-Eigenschaften handelt). Abbildung 22.2 zeigt das Ergebnis des Programms.
Abbildung 22.2: Das Datenaustausch-Demoprogramm
1254
Anwendungsdomänen
Ein wichtiger Punkt ist auch die Übergabe von Referenztypen, denn diese werden bei der Übergabe serialisiert und auf der anderen Seite deserialisiert. Dies hat zwei Konsequenzen: Zum einen muss der entsprechende Typ serialisierbar (mit dem Serializable-Attribut gekennzeichnet) sein. Zum anderen erhalten Sie in der anderen Anwendungsdomäne eine Kopie des Objekts. Das sollten Sie bei der Arbeit mit Anwendungsdomänen immer im Auge behalten.
INFO
12
Der Grund für dieses ist übrigens einmal, dass damit ermöglicht wird, dass Anwendungsdomänen auch dann entladen werden können, wenn die eine ReferenztypInstanz an die andere Anwendungsdomäne übergeben hat. Wird eine Referenz übergeben, wäre das Entladen deswegen nicht möglich, weil das Objekt ja noch im Speicher referenziert würde. Ein anderer Grund ist natürlich die Isolation von Anwendungsdomänen. Auf diese Weise kann eine Anwendungsdomäne nicht durch eine Verwendung eines Objekts in der anderen Anwendungsdomäne Fehler verursachen.
13
14
22.3.8 Optimieren des Ladeverhaltens von Assemblys 15
Per Voreinstellung lädt jede Anwendungsdomäne alle referenzierten Assemblys separat. Dazu gehören alle .NET-Assemblys, vorkompilierte native Abbilder und alle von der Anwendungsdomäne dynamisch nachgeladenen Assemblys. Dies kann in größeren Anwendungen zum Performance-Problem werden.
16
Über das LoaderOptimization-Attribut, das Sie an die Main-Methode der Anwendung anfügen, können Sie das Ladenverhalten aber auch optimieren: ■
■
17
[LoaderOptimization(LoaderOptimization.MultiDomainHost)]: Mit dieser Einstellung werden die Standard-.NET-Assemblys, native Abbilder und bereits vom Just In Time Compiler kompilierte Assemblys domänen-neutral verwaltet und somit für alle Anwendungsdomänen der Anwendung nur einmal geladen. [LoaderOptimization(LoaderOptimization.MultiDomainHost)]: Über diese Einstellung werden alle geladenen Assemblys domänen-neutral verwaltet (was allerdings diejenigen ausschließt, die außerhalb des CLR-Assembly-LadeMechanismus geladen werden). Wenn Sie Anwendungsdomänen einsetzen, um Assemblys wieder aus dem Speicher entfernen zu können, ist diese Einstellung nicht ideal.
18
19
22.3.9 Weitere Features, die hier nicht näher besprochen werden
20
Bei der Arbeit mit Anwendungsdomänen haben Sie noch mehr Möglichkeiten, als ich in diesem Kapitel darstellen konnte. Dazu gehören:
21
■
Die Verwendung von Threads in einer Anwendungsdomäne. Über diese Technik können Sie innerhalb einer Anwendung mehrere Threads laufen lassen, die komplett voneinander isoliert sind. Das Vorgehen dazu ist einfach: Sie erzeugen in der Callback-Methode, die Sie DoCallBack übergeben, einen neuen Thread und führen diesen aus. Dabei müssen Sie allerdings in der Thread-Methode auf jeden Fall eine Ausnahmebehandlung unterbringen. Eine unbehandelte Ausnahme führt ansonsten (nicht unbedingt in allen Fällen!) zu einem Absturz der Anwendung.
22 23
1255
Assemblys, Reflektion und Anwendungsdomänen
■
■
■ ■
Die Verwendung separater Konfigurationsdateien für die Anwendungsdomäne über die ConfigurationFile-Eigenschaft einer AppDomainSetup-Instanz, die Sie CreateDomain übergeben. Die Angabe zusätzlicher Pfade, in denen Assemblys gespeichert sind, die von der Anwendungsdomäne verwendet werden (über die Eigenschaften PrivateBinPath und PrivateBinPathProbe einer AppDomainSetup-Instanz, die Sie CreateDomain übergeben). Das Auflösen von Assembly-Ladeproblemen über das AssemblyResolve-Ereignis. Die Kommunikation zwischen Anwendungsdomänen über Intra-Prozess-Remoting, bei der eine Anwendungsdomäne in einer anderen über Remoting eine Instanz eines Typs erzeugt und verwendet.
Für die Praxis sind diese Punkte aber meist nicht allzu wichtig. Ich denke, das in diesem Kapitel vermittelte Wissen reicht in den meisten Fällen aus. Und ansonsten wissen Sie ja, dass noch etwas mehr möglich ist.
1256
Inhalt
23
Sicherheitsgrundlagen 12
Damit das Buch mit Sicherheit beendet wird, habe ich dieses Thema für dieses letzte Kapitel im Buch aufgespart. Leider kann ich dieses sehr umfangreiche Thema aber nur grundlegend behandeln. Besonders die sehr komplexe Codezugriffssicherheit kann ganze Kapitel füllen. Außerdem gehören die Sicherheits-Features von .NET nicht unbedingt zu denen, die sehr einfach anzuwenden sind ☺.
13
14
Die Sicherheit unter .NET betrifft einige Bereiche: ■ ■ ■ ■ ■ ■ ■ ■ ■
die Codezugriffssicherheit, das eigene Verschlüsseln von Daten, das Verschlüsseln von Abschnitten der Konfigurationsdatei einer Anwendung, die Verwendung sicherer Strings (in Form der nicht einfach anzuwendenden SecureString-Klasse), die von einem Hacker nicht ausgelesen werden können, der Schutz einer Assembly gegen nachträgliche Manipulationen (über einen starken Namen, siehe Kapitel 22), der Schutz einer Assembly gegen das Auslesen des Quellcodes (über Obfuskatoren), die Verwendung von Membership-Features in einer Anwendung, über die Sie Benutzer-Logins verwalten können, die Verhinderung von Angriffen auf eine Datenbank (Verhindern von SQL Injection) und die Verhinderung von Angriffen auf eine Webanwendung (Verhindern von Cross Site Scripting).
15
16
17
18
19
Um alle diese Punkte zu besprechen, reicht der verfügbare Platz im Buch bei weitem nicht aus. Ich behandle deswegen lediglich die am häufigsten eingesetzten ersten drei Punkte.
20
Die Stichworte dieses Kapitels sind: ■ ■ ■ ■ ■ ■ ■
Grundlagen der Codezugriffssicherheit Einfache Ver- und Entschlüsselung mit Windows-Mitteln Sichere Hashcodes erzeugen Grundlagen der Verschlüsselung Symmetrische Verschlüsselung Asymmetrische Verschlüsselung mit RSA Verschlüsseln mit dem Elliptic Curve Diffie-Hellman-Algorithmus Digitale Signaturen
21
22
23
1257
Index
■
Sicherheitsgrundlagen
23.1
Codezugriffssicherheit
Codezugriffssicherheit (Code Access Security, CAS) ist das Basis-Sicherheit-Feature von .NET, an dem keine Assembly vorbeikommt. Codezugriffssicherheit ist im Prinzip zunächst einfach, kann aber schnell auch – bei der Programmierung von Anwendungen, die Codezugriffssicherheit auf spezielle Weise einsetzen – sehr komplex werden. Ich beschreibe in diesem Abschnitt die Grundlagen der Codezugriffssicherheit und zeige einige Prinzipien für die Anwendungsentwicklung auf hoch gesicherten Systemen. Ich gehe aber nicht auf alle Möglichkeiten der Codezugriffssicherheit ein.
23.1.1
Die Arbeitsweise der Codezugriffssicherheit
Jede .NET-Assembly wird von der CLR unter zwei Sicherheitssystemen ausgeführt: der Codezugriffssicherheit und der normalen Windows-Sicherheit. Die Windows-Sicherheit bestimmt, welche Rechte der Benutzer hat, unter dem die Anwendung ausgeführt wird. Normale Windows-Benutzer (keine Administratoren) dürfen z. B. nicht auf das Windows-Verzeichnis schreibend zugreifen. Diese Rechte gelten natürlich auch für .NET-Anwendungen, vollkommen unabhängig von der Codezugriffssicherzeit. Codezugriffssicherheit basiert auf Berechtigungen und Nachweisen
Bei .NET-Assemblys kommt die Codezugriffssicherheit dann noch hinzu. Diese ist in Form von Regeln auf jedem System definiert, die aussagen, welche Berechtigungen (englisch: Permissions) eine Assembly besitzt. Codezugriffssicherheit basiert auf einzeln definierten Berechtigungen wie z. B. der Berechtigung, auf Dateien zuzugreifen, die zu Berechtigungssätzen (Permission sets) zusammengefasst werden können. Beim Laden einer Assembly teilt die CLR dieser einzelne Berechtigungen und/oder Berechtigungssätze zu. Die Assembly darf dann alle Aktionen ausführen, die innerhalb der ihr zugeteilten Berechtigungen erlaubt sind. Wird eine Aktion ausgeführt, die nicht erlaubt ist, resultiert eine SecurityException. Die Frage ist nun, wie die CLR die Berechtigungen einer Assembly ermittelt. Die Antwort auf diese Frage ist: über einen so genannten Nachweis (Englisch: Evidence). Ein solcher kann einer der folgenden sein: ■
■
■
1258
Die Assembly enstammt einer der konfigurierten Zonen der Codezugriffssicherheit. Vordefiniert sind die Zonen Lokal, Intranet und Internet, Vertraute Assemblys und Eingenschränkt vertraute Assemblys. Eine Assembly, die lokal gestartet wird oder die der Zone der vertrauten Assemblys angehört, erhält per Voreinstellung alle Berechtigungen (ihr wird voll vertraut). Assemblys, die aus dem Intranet oder Internet geladen werden, erhalten eingeschränkte Berechtigungssätze zugeteilt. So darf per Voreinstellung eine aus dem Internet geladene Assembly Dateien nicht schreiben. Lesen darf sie diese nur dann, wenn das Öffnen über einen OpenFileDialog geschieht (sodass der Benutzer involviert ist). Eine aus dem Intranet geladene Assembly darf Dateien genauso wenig direkt lesen und schreiben, neben dem Lesen über einen OpenFileDialog ist einer solchen Assembly aber auch das Schreiben über einen SaveFileDialog möglich. Eine Assembly ist mit einem bestimmten digitalen Zertifikat signiert. Dieser Nachweis wird häufig auf hoch gesicherten Systemen verwendet, bei denen die Zone Lokal stark eingeschränkt wird. Nur Assemblys, die mit dem Zertifikat signiert sind, wird auf diesen Systemen voll vertraut. Eine Assembly besitzt einen starken Namen mit einem bestimmten öffentlichen Schlüssel. Dieser Nachweis wird ebenfalls häufig auf hoch gesicherten Systemen eingesetzt, um lediglich Assemblys von bestimmten Herstellern alle oder bestimmte Berechtigungen zu erteilen.
Codezugriffssicherheit
■
Eine Assembly enstammt einem angegebenen Ort, wie z. B. einer URL. Dieser Nachweis kann benutzt werden, um bei normal eingeschränktem Zugriff für Assemblys aus dem Intranet oder Internet bestimmten Assemblys, die dem angegebenen Ort entsprechen, besondere Berechtigungen zu erteilen.
Darüber hinaus bestehen noch weitere Möglichkeiten, dass eine Assembly einen Nachweis erbringt, dass ihr bestimmte Berechtigungen zustehen.
12
Codegruppen Die einzelnen Möglichkeiten, Assembly-Nachweise zu erbringen, werden als Codegruppen bezeichnet. Eine Codegruppe mappt eine Mitglieds-Bedingung (z. B. Zone = "Lokal" oder Öffentlicher Schlüssel Token = "b77a5c561934e089") auf einen benannten Berechtigungssatz (der natürlich auch innerhalb der Codezugriffssicherheit konfiguriert ist).
Codegruppen mappen MitgliedsBedingungen auf Berechtigungssätze
Codegruppen stehen zudem auf drei Ebenen zur Verfügung: der Ebene der Maschine, der Ebene des Benutzers und der Ebene des Unternehmens. Vordefiniert ist lediglich die Ebene der Maschine. Ein Administrator kann aber für einzelne Benutzer oder für ein Unternehmen die verwendeten Codegruppen individuell konfigurieren.
13
14
15
Codezugriffssicherheit in der Praxis In der Praxis wird eine Administration der Codezugriffssicherheit mit Einschränkungen der Berechtigungen für lokal ausgeführte Assemblys, speziellen Nachweisen und verschiedenen Codegruppen auf den einzelnen Ebenen nach meinen Erfahrungen aber lediglich in sehr speziellen Umgebungen umgesetzt, die hoch gesicherte Systeme erfordern. In meiner Praxis habe ich dies erst ein einziges Mal erlebt, nämlich bei einer Schulung für Programmierer bei der Bundeswehr. In solchen Umgebungen wie der Bundeswehr oder anderen staatlichen Einrichtungen ist es natürlich klar, dass die einzelnen Systeme speziell gesichert werden müssen.
In der Praxis wird Codezugriffssicherheit in »normalen« Umgebungen kaum konfiguriert
17
18
In allen »normalen« Firmen, für die ich bisher gearbeitet habe, wurde aber die Konfiguration der Codezugriffssicherheit nicht angepasst1. Auf einem normal konfigurierten System ist lediglich die Zonen-Sicherheit konfiguriert. Für alle anderen muss ein Administrator die Codezugriffssicherheit auf dem jeweiligen System anpassen. Dazu steht aber mit Caspol.exe (was für »Code access security policy tool« steht) leider nur ein kompliziert anzuwendendes BefehlszeilenWerkzeug zur Verfügung. Das im .NET-2.0-Framework-SDK noch enthaltene, wesentlich einfacher anzuwendende Plugin für die Microsoft Management Konsole (mscorcfg.msc) ist aus unerfindlichen Gründen weder in dem eingeschränkten Windows-SDK enthalten, das mit Visual Studio installiert wird, noch in der vollen Version des Windows-SDK. Zudem läuft die alte Version leider nach der Installation des .NET Framework 3.5 nicht mehr und kann auch scheinbar nicht ohne Probleme nachinstalliert werden.
16
Per Voreinstellung ist lediglich die Zonen-Sicherheit konfiguriert
19
20
21
Ich würde gerne einmal erfahren, was Microsoft sich dabei gedacht hat, das ohnehin schon komplexe Thema Codezugriffssicherheit durch das Fehlen dieses wichtigen Tools noch schwieriger zu machen.
22
23 1
Was in der Praxis teilweise auch daran liegt, dass Administratoren die Codezugriffssicherheit nicht kennen, dass die Administration zu aufwändig ist oder dass eine Einschränkung der Berechtigungen in der Praxis dazu führt, dass Anwendungen nicht mehr ausgeführt werden können.
1259
Sicherheitsgrundlagen
23.1.2 Caspol.exe erlaubt die Administration auf Steinzeit-Art
Die Administration der Codezugriffssicherheit
Die Administration der Codezugriffssicherheit ist wie gesagt leider nicht mehr so einfach, wie sie einmal war. Seit Microsoft aus vollkommen unklaren Gründen entschieden hat, das enorm hilfreiche MMC-Plugin mscorcfg.exe nicht mehr zu unterstützen, muss zur Administration das Kommandozeilen-Werkzeug Caspol.exe verwendet werden. Dieses ist Bestandteil der normalen .NET Framework-Installation und wird normalerweise im Ordner C:\Windows\Microsoft.NET\Framework\v2.0.50727 verwaltet. Sie können auch die VISUAL STUDIO 2008-EINGABEAUFFORDERUNG über das Startmenü starten, in der bereits ein Pfad zu diesem Ordner angelegt ist. Caspol.exe ist sehr komplex. Da an dieser Stelle der Platz fehlt, dieses (nervige) Tool zu beschreiben, verweise ich auf die Dokumentation. Ich zeige lediglich, wie Sie die Codegruppen und Berechtigungssätze der Maschine auflisten: Caspol -m –l
Ein typisches Problem in Intranetumgebungen ist, dass Assemblys aus dem Intranet nicht mit vollen Rechten ausgeführt werden. Dieses Problem können Sie für einzelne Anwendungen lösen, indem Sie eine neue Codegruppe erzeugen, der voll vertraut wird. Als Nachweis geben Sie den Ort der Anwendung an: CasPol.exe -m -ag 1 -url file://Rechnername/Freigabename/* FullTrust -name Codegruppenname
Diese Anweisung erzeugt die neue Codegruppe Codegruppenname, die unterhalb der Codegruppe All_Code (die Codegruppe mit dem Index 1) angelegt wird. All_Code ist so etwas wie eine Kategorie-Codegruppe, der per Voreinstellung alle StandardCodegruppen untergeordnet sind. Als Nachweis für die Codegruppe wird der angegebene UNC-Pfad verwendet. Den Assemblys, die diesem Pfad entstammen, wird voll vertraut. In einer Batch-Datei (DOS-Befehlsdatei mit der Endung .bat) können Sie die folgenden Anweisungen verwenden, um die Ausführung zu erleichtern: Listing 23.1:
DOS-Anweisungen zum Erstellen einer Codegruppe für Assemblys aus dem Intranet
SET appPath=file://Rechnername/Freigabename SET codegroupName=Codegruppenname PATH=%windir%\microsoft.net\framework\v2.0.50727\ CasPol.exe -pp off CasPol.exe -m -ag 1 -url %appPath%/* FullTrust -name %codegroupName% CasPol.exe -pp on pause
HALT
Achten Sie beim Erstellen der Batchdatei, dass beim Setzen der Batch-Variablen (appPath und codegroupName) vor und hinter dem Zuweisungsoperator (=) keine Leerzeichen angegeben sind. Diese werden ansonsten in den Namen der Variable oder in den zugewiesenen Wert übernommen. Eine im Internet ebenfalls kursierende Lösung ist, der Intranet-Zone den Berechtigungssatz FullTrust zuweisen: Caspol.exe -m -chggroup LocalIntranet_Zone FullTrust
1260
Codezugriffssicherheit
Diese Lösung funktioniert aber seit dem .NET Framework 2.0 nicht mehr bzw. nicht mehr in allen Fällen (auf jeden Fall nicht in meinen Tests, bei denen Assemblys aus dem Intranet nach der Zuweisung des FullTrust-Berechtigungssatzes zur IntranetZone immer noch lediglich eingeschränkte Rechte besaßen).
23.1.3
Codezugriffssicherheit auf Assembly-Ebene
Auf der Assembly-Ebene wird die Codezugriffssicherheit im .NET Framework dadurch gewährleistet, dass alle .NET-Typen, die Aktionen ausführen, die unter die Codezugriffssicherheit fallen können, die entsprechende Berechtigung anfordern. Dies geschieht auf eine von zwei Arten: ■
■
12
13
Über das Erzeugen einer Instanz einer von System.Security.CodeAccessPermission abgeleiteten Klasse (z. B. FileIOPermission), der am Konstruktor der Wert System.Security.Permissions.PermissionState.Unrestricted übergeben wird. Damit wird die uneingeschränkte Berechtigung angefordert, die entsprechende Aktion auszuführen. Dieses Feature wird als imperative Sicherheit bezeichnet, oder über ein entsprechendes Attribut, dessen Name mit dem Namen der entsprechenden Klasse beginnt und mit »Attribute« endet. Mit dem Attribut werden Methoden, Eigenschaften und/oder ganze Typen gekennzeichnet. Dieses Feature wird als deklarative Sicherheit bezeichnet.
14
15
Wird diese Berechtigungsanforderung nicht erfüllt, weil der Assembly nicht die entsprechende Berechtigung zugeteilt wurde, generiert die CLR eine SecurityException. Dabei wird auch überprüft, ob alle Assemblys, die sich in einer höheren Aufrufebene befinden, diese Berechtigung ebenfalls besitzen. So kann es nicht vorkommen, dass eine Assembly, die eine Berechtigung nicht besitzt, diese über eine andere geladene Assembly erhält, deren Typen sie verwendet.
16
17
In speziellen Fällen können Sie Ihre Methoden, Eigenschaften, Typen und Assemblys auch selbst mit einer Berechtigungsanforderung ausstatten. Wenn Sie auf der Assembly-Ebene alle Berechtigungen anfordern, die Ihre Assembly benötigt, hat das den Vorteil, dass die CLR beim Laden der Assembly direkt überprüfen kann, ob diese die angeforderten Berechtigungen besitzt. Für den Benutzer ist die dann direkt beim Start geworfene SecurityException besser als eine, die erst im späteren Verlauf der Anwendung geworfen wird. Listing 23.2:
18
19
Deklaratives Anfordern einer Datei-IO-Berechtigung auf Assembly-Ebene
20
[FileIOPermission(SecurityAction.Demand)]
Das Ganze ist aber recht komplex, weil Sie nicht nur festlegen können, dass Ihre Assembly (oder Ihr Typ, Ihre Methode oder Ihre Eigenschaft) eine Berechtigung anfordert. Sie können Berechtigungen z. B. auch explizit ablehnen (um zu verhindern, dass referenzierte Assemblys diese erhalten). Listing 23.3:
21
Deklaratives explizites Ablehnen einer Datei-IO-Berechtigung auf Assembly-Ebene
22
[FileIOPermission(SecurityAction.Deny)]
Das sind aber nur zwei von vielen Möglichkeiten, mit CAS zu arbeiten. Leider fehlt mir hier der Platz, um auf dieses komplexe Thema einzugehen. In meiner Praxis hat sich noch nie die Anforderung ergeben, diese speziellen Sicherheitstechniken einzusetzen.
23
1261
Sicherheitsgrundlagen
An der Adresse www.codeproject.com/KB/security/UB_CAS_NET.aspx finden Sie einen guten Artikel, der sich mit diesem Thema beschäftigt. REF
23.2
Verschlüsseln und Entschlüsseln von Daten
Zur eigenen Verschlüsselung von Daten haben Sie vier Möglichkeiten: Sie können die Windows-Verschlüsselung nutzen, aus Daten einen Hashcode berechnen und Daten symmetrisch und asymmetrisch verschlüsseln. Im Folgenden gehe ich auf diese Möglichkeiten grundlegend ein.
23.2.1
Grundlagen
Bei der Verschlüsselung werden im Allgemeinen drei Bereiche unterschieden: ■ ■ ■
Hashcodes, symmetrische Verschlüsselungen und asymmetrische Verschlüsselungen.
Da diese häufig in Kombination eingesetzt werden, sollten Sie die Grundlagen kennen. Deswegen gehe ich hier auf die wesentlichen Dinge ein.
Schlüssel Schlüssel sind die Basis der Verschlüsselung
Verschlüsselungs-Algorithmen setzen (bis auf einige Hashcode-Algorithmen) Schlüssel ein, deren Werte in der Verschlüsselungslogik berücksichtigt werden. Ein einfacher Algorithmus würde z. B. den Zeichencode jedes Zeichens eines zu verschlüsselnden Strings mit dem Zeichencode des korrespondierenden Schlüssel-Strings addieren und das Ergebnis speichern. Bei den modernen Verschlüsselungs-Algorithmen ist dieser Vorgang natürlich wesentlich komplexer. Trotzdem spielt neben dem Verschlüsselungsalgorithmus auch die Länge des Schlüssels eine große Rolle für die Qualität des Ergebnisses. In vielen Fällen können Sie die Länge in einem vorgegebenen Bereich selbst bestimmen. Große Schlüssel führen zu einer höheren Sicherheit, aber auch zu einer langsameren Ver- und Entschlüsselung.
Hashcodes Hashcodes sind eindeutige Codes für Daten
Ein Hashcode ist ein Code, der Daten eindeutig in einer abgekürzten Form identifiziert. Hashcodes werden über öffentlich bekannte Hashing-Algorithmen berechnet. Einige davon arbeiten ohne, andere mit Schlüssel. Der berechnete Code besitzt meist eine festgelegte Länge (z. B. 128 oder 256 Bit). Aus einem Hashcode können unter keinen Umständen die eigentlichen Daten zurückgerechnet werden. Wird aber auf dieselben Daten derselbe Hashing-Algorithmus mit demselben Schlüssel (falls der Algorithmus mit einem Schlüssel arbeitet) und derselben Hashcode-Länge angewendet, resultiert immer derselbe Hashcode. Die Wahrscheinlichkeit, dass unterschiedliche Daten zu demselben Hashcode führen, ist äußerst gering2.
2
1262
Wenn der Hashcode kürzer ist als die Daten, kann es theoretisch vorkommen, dass Kollisionen auftreten (siehe de.wikipedia.org/wiki/Hashcode).
Verschlüsseln und Entschlüsseln von Daten
Eine Anwendung für Hashcodes ist die sichere Speicherung von Passwörtern, die von Anwendern zur Authentifizierung eingegeben werden sollen, in einer Konfigurationsdatei, Datenbank o. Ä. Gespeichert werden dann nicht die Passwörter, sondern deren Hashcodes. In einem Login-Fenster wird zur Authentifizierung das vom Anwender eingegebene Passwort in den korrespondierenden Hashcode umgerechnet und mit dem gespeicherten verglichen. Sind beide Hashcodes identisch, ist das Passwort korrekt. Natürlich können Sie für einen solchen Zweck auch einen Verschlüsselungs-Algorithmus verwenden. Hashcodes sind aber einfacher zu errechnen und bieten die Sicherheit, dass die Daten nicht entschlüsselt werden können.
Hashcodes werden zur Speicherung von Passwörtern verwendet …
Eine andere Anwendung für Hashcodes sind komplexe Verschlüsselungsverfahren, bei denen aus den zu verschlüsselnden Daten ein Hashcode berechnet wird, der den verschlüsselten Daten beigelegt wird. Der Empfänger kann aus den entschlüsselten Daten wieder einen Hashcode berechnen und diesen mit dem mitgelieferten vergleichen. Sind beide Hashcodes gleich, wurden die Daten mit einer recht hohen Wahrscheinlichkeit nicht verändert.
… und für komplexe Verschlüsselungsverfahren
12
13
14
Symmetrische Verschlüsselungsverfahren Symmetrische (und asymmetrische) Verschlüsselungsverfahren erlauben neben dem Ver- auch das Entschlüsseln von Daten. Symmetrische Verschlüsselungsverfahren setzen einen einzigen Schlüssel mit einer meist festgelegten Länge für das Verund das Entschlüsseln ein. Das Problem dabei ist die Weitergabe des Schlüssels: Gelangt der Schlüssel in falsche Hände, können die Daten problemlos entschlüsselt werden (sofern das Verfahren bekannt ist). Der Schlüssel muss also geheim gehalten werden (und wird deshalb als geheimer Schlüssel bezeichnet). Beispiele für symmetrische Verfahren sind RC2 (64 Bit Schlüsselgröße), DES (64 Bit), TripleDES (192 Bit), AES (256 Bit), IDEA (128 Bit) und CAST (128 oder 256 Bit).
Symmetrische Verschlüsselungsverfahren arbeiten mit einem Schlüssel
15
16
17
Asymmetrische Verschlüsselungsverfahren Asymmetrische Verfahren wie RSA und DSA setzen unterschiedliche Schlüssel meist mehr oder weniger variabler Länge für das Ver- und das Entschlüsseln der Daten ein. Die Schlüssel stehen in einer mathematischen Beziehung, die meist auf Primzahlen basiert, und bilden ein Schlüsselpaar. Einer der Schlüssel wird geheim gehalten und daher als privater Schlüssel bezeichnet. Der andere Schlüssel wird als öffentlicher Schlüssel bezeichnet und kann problemlos öffentlich, z. B. über E-Mail, versendet werden. Diese Verfahren werden deshalb auch als Public-Key-Verfahren bezeichnet.
Asymmetrische Verschlüsselungsverfahren setzen zwei Schlüssel ein
19
Asymmetrische Verschlüsselungsverfahren erlauben, Daten über den öffentlichen Schlüssel zu verschlüsseln, die dann nur über den privaten Schlüssel wieder entschlüsselt werden können. Umgekehrt können Daten, die über den privaten Schlüssel verschlüsselt wurden, nur über den öffentlichen Schlüssel wieder entschlüsselt werden (deswegen heißt das Ganze auch »asymmetrisch« ☺).
20
21
Beim sicheren Versenden von Daten muss der Sender also nur den öffentlichen Schlüssel des Empfängers kennen, um die Daten so verschlüsseln zu können, dass prinzipiell nur der Empfänger die Daten wieder entschlüsseln kann. Die andere Richtung, also das Verschlüsseln mit dem privaten und das Entschlüsseln mit dem öffentlichen Schlüssel, wird für digitale Signaturen verwendet. Digitale Signaturen sollen sicherstellen, dass unverschlüsselt gesendete Daten beim Empfänger nicht ausgewertet werden, wenn diese von einem »Man in the Middle3« ausgelesen und in veränderter Form weitergesendet wurden. 3
18
22 Digitale Signaturen werden auch asymmetrisch verschlüsselt
Einem Angreifer, der sich in die Kommunikation zwischen zwei Partnern eingeschleust hat.
1263
23
Sicherheitsgrundlagen
Dazu wird aus den zu übertragenden Daten ein Hashcode berechnet. Dieser wird mit dem privaten Schlüssel des Senders verschlüsselt. Der sich daraus ergebende Wert wird als Signatur mit den Daten übertragen. Der Empfänger entschlüsselt die Signatur mit dem öffentlichen Schlüssel des Senders und vergleicht den so ermittelten Hashcode mit einem selbst berechneten. Sind beide gleich, kann davon ausgegangen werden, dass die Daten nicht verändert wurden.
Hybride Verfahren Alle Verschlüsselungsverfahren haben ihre Vor- und Nachteile: ■ ■ ■
Hybride Verfahren heben alle Nachteile auf
Hashcodes können nicht zurückgerechnet werden Symmetrische Verschlüsselungsverfahren sind effizient und sicher, aber in Bezug auf die Schlüsselweitergabe unsicher Asymmetrische Verschlüsselungsverfahren sind in Bezug auf die Schlüsselweitergabe sicher, sind aber langsam und können meist nur auf sehr kleine Daten angewendet werden (wenige hundert Bytes).
Die ultimative Lösung für eine sichere Datenübertragung setzt alle drei Verfahren ein: Die eigentlichen Daten werden symmetrisch mit einem möglichst langen Schlüssel verschlüsselt. Dieser Schlüssel wird asymmetrisch mit dem öffentlichen Schlüssel des Empfängers verschlüsselt. Die Daten und der verschlüsselte Schlüssel werden in einer Nachricht oder separat gesendet (was die Sicherheit noch erhöht). Der Empfänger kann den Schlüssel für die symmetrische Verschlüsselung über seinen privaten Schlüssel (für die asymmetrische Verschlüsselung) entschlüsseln und damit auch die eigentlichen Daten.
Hashcodes helfen, manipulierte Daten zu erkennen
Trotz der Verschlüsselung kann es vorkommen, dass die Daten auf dem Weg zum Empfänger manipuliert werden (von einem so genannten »Man in the Middle»). Das kann einmal dadurch passieren, dass die Daten durch einen Hacker entschlüsselt, verändert und wieder verschlüsselt wurden. Eine andere Möglichkeit ist aber auch, dass einfach die verschlüsselten Daten verändert wurden. Um beim Entschlüsseln der Daten erkennen zu können, ob eine Veränderung vorgenommen wurde, wird vor dem Versenden häufig zusätzlich über die Daten ein Hashcode berechnet, der der Nachricht mitgeliefert oder – idealerweise – separat versendet wird. Der Empfänger kann den Hashcode auslesen und mit einem selbst berechneten vergleichen. Sind die Hashcodes identisch, kann der Empfänger (relativ) sicher sein, dass die Daten auf ihrem Weg nicht verändert wurden. Da auch selbst dabei noch Manipulationen möglich sind, werden in der Praxis statt Hashcodes digitale Signaturen verwendet, die den Hashcode selbst wieder asymmetrisch verschlüsseln.
Digitale Signaturen Digitale Signaturen sichern gegen Manipulationen
Das symmetrische Verschlüsseln und der Schlüsselaustausch über eine asymmetrische Verschlüsselung reichen noch nicht aus, um einen Datenaustausch wirklich sicher zu machen. Die Daten können immer noch von einem »Man in the Middle« unerkannt verändert werden, auch wenn zusätzlich Hashcodes mitgeliefert werden (die ggf. vom Man in the Middle auch neu erzeugt werden können). Um Veränderungen der Daten mit einer großen Sicherheit erkennen zu können, werden versendete Daten signiert. Dazu wird, basierend auf den zu versendenden Daten, über eine asymmetrische Verschlüsselung und einen privaten Schlüssel eine
1264
Verschlüsseln und Entschlüsseln von Daten
Signatur erzeugt, die nur mit dem zugehörigen öffentlichen Schlüssel wieder entschlüsselt werden kann. Die Signatur enthält einen Hashcode der Daten, den der Empfänger mit dem Hashcode vergleichen kann, den er selbst berechnet hat. Sind die Hashcodes nicht gleich, wurden die Daten verändert. Ein Hacker kann die Signatur nun nicht mehr fälschen, da er den privaten Schlüssel des Versenders nicht kennt, über den die Signatur erzeugt wurde. Setzt er eine Signatur mit einem anderen privaten Schlüssel ein, kann diese beim Empfänger mit dem öffentlichen Schlüssel des Senders nicht entschlüsselt werden. Der Empfänger erkennt also auf jeden Fall, wenn die Daten verändert wurden.
12
23.2.2 Windows-Verschlüsselung
13
Windows bietet mit dem Data Protection API (DAPI) Möglichkeiten, Dateien und Daten recht einfach zu verschlüsseln (und damit auch recht unsicher). Die Daten werden dabei mit einem Schlüssel verschlüsselt, der entweder aus dem Konto des Benutzers generiert wird oder mit einem Schlüssel, der der Maschine zugeordnet ist.
14
Die einfachste Anwendung dieser Technik sind die Methoden Encrypt und Decrypt der File-Klasse, über die Sie eine Datei mit dem Benutzer-Schlüssel ver- und entschlüsseln können: Listing 23.4:
15
Verschlüsseln einer Datei mit der Windows-Verschlüsselung
16
string fileName = ... File.Encrypt(fileName);
Vista zeigt auf diese Art verschlüsselte Dateien im Explorer in grüner Schrift an. Ist eine Datei unter einem Benutzerkonto verschlüsselt, kann der Benutzer, unter dem die Datei verschlüsselt wurde, diese unter Vista problemlos öffnen. Aber auch das Lesen und Schreiben im Programm ist möglich, wenn derselbe Benutzer die Anwendung ausführt, der die Dateien verschlüsselt hat. Über die Klasse ProtectedData aus dem Namensraum System.Security.Cryptography können Sie das Data Protection API auch direkt nutzen. Zur Verwendung dieser Klasse müssen Sie die Assembly System.Security.dll referenzieren.
17
Das DAPI können Sie auch direkt nutzen
19
Über die Methode Protect verschlüsseln Sie Daten, die Sie als Byte-Array übergeben. Unprotect entschlüsselt diese. Neben den Daten müssen Sie am dritten Argument noch einen DataProtectionScope übergeben, der aussagt, ob die Verschlüsselung auf Benutzer- oder Maschinenebene erfolgen soll. Am zweiten Argument (optionalEntropy) können Sie ein zusätzliches Byte-Array übergeben, das bei der Verschlüsselung berücksichtigt wird (ein Zusatz-Schlüssel). Ich übergebe im Beispiel hier null (weil ich von dieser einfachen Art Verschlüsselung nichts halte): Listing 23.5:
18
20
21
Ver- und Entschlüsseln mit dem DAPI
// Daten mit dem DAPI verschlüsseln byte[] originalData = { 1, 2, 3 }; byte[] encryptetData = ProtectedData.Protect( originalData, null, DataProtectionScope.CurrentUser);
22
// Daten mit dem DAPI entschlüsseln byte[] decryptetData = ProtectedData.Unprotect( encryptetData, null, DataProtectionScope.CurrentUser);
23
1265
Sicherheitsgrundlagen
HALT
Problematisch ist bei der benutzerbezogenen Art der Verschlüsselung unter XP, dass die Datei nur wieder mit genau demselben Benutzer-Konto entschlüsselt werden kann. Für den Fall, dass das System wiederhergestellt werden muss, sind die Daten verloren: Auch wenn Sie ein Konto mit demselben Benutzernamen und Passwort anlegen, können die Daten prinzipiell nicht mehr entschlüsselt werden. Unter Vista muss der Anwender seinen Schlüssel sichern, damit nach einer Neuinstallation des Systems seine verschlüsselten Dateien entschlüsselt werden können. Vista erinnert bei einem ungesicherten Schlüssel immer wieder daran, den Schlüssel zu sichern. In der Taskbar finden Sie bei einem ungesicherten Schlüssel normalerweise ein entsprechendes Symbol, über das Sie das Sichern des Schlüssels starten können.
23.2.3 Das Ver- und Entschlüsseln von Strings Das Problem aller Verschlüsselungs-Techniken ist das Ver- und Entschlüsseln von Strings. Diese müssen zum Verschlüsseln in ein Byte-Array gelesen und beim Entschlüsseln aus einem Byte-Array erzeugt werden. Und das ist in der Praxis leider nicht so einfach. Das Problem dabei ist die Codierung. Normalerweise müssten Sie für Strings UTF-8 verwenden. UTF-8 hat aber zwei Probleme: Zum einen werden in einigen Fällen Codierungs-Start-Bytes an den Anfang des String gesetzt. Zum anderen wird in UTF-8 ein Zeichen dynamisch in ein bis sieben Bytes verwaltet. Bei der Ver- und Entschlüsselung von Strings treten deswegen die verschiedensten Probleme auf. Im Fall des DAPI wurde in meinem Test beim Entschlüsseln mit UTF-8 eine Ausnahme mit der sehr eigenartigen Meldung »Das Kennwort kann nicht aktualisiert werden. Der Wert, der als neues Kennwort angegeben wurde, entspricht nicht den Kennwortrichtlinien der Domäne« erzeugt. Deswegen habe ich im Beispiel die zu dem ersten Byte des Unicode-Zeichensatzes kompatible Codierung ISO-8859-1 verwendet (die ich beim Ver- und Entschlüsseln von Strings zur Sicherheit immer einsetze). Wenn Sie diesen Zeichensatz verwenden, sollten Sie beachten, dass damit keine Unicode-Zeichen mit einem Wert größer 255 möglich sind. Listing 23.6: Ver- und Entschlüsseln eines Strings (leider über ISO-8859-1, weil UTF-8 zu einem Fehler führt) // Verschlüsseln eines Strings Encoding encoding = Encoding.GetEncoding("ISO-8859-1"); string originalString = "Das ist ein Test"; byte[] originalStringData = encoding.GetBytes(originalString); byte[] encryptetStringData = ProtectedData.Protect( originalStringData, null, DataProtectionScope.CurrentUser); string encryptedString = encoding.GetString(encryptetStringData); // Entschlüsseln eines Strings encryptetStringData = encoding.GetBytes(encryptedString); byte[] decryptetStringData = ProtectedData.Unprotect( encryptetStringData, null, DataProtectionScope.CurrentUser); string decryptedString = encoding.GetString(decryptetStringData);
1266
Verschlüsseln und Entschlüsseln von Daten
23.2.4 Hashcodes berechnen Hashcodes erzeugen Sie über spezielle Hashing-Klassen, die das .NET Framework im Namensraum System.Security.Cryptography zur Verfügung stellt. Diese Klassen implementieren die verschiedenen von .NET unterstützten Algorithmen und ermöglichen die Erzeugung von Hashcodes aus Byte-Arrays oder Streams. Alle Hashing-Klassen sind (mehr oder weniger direkt) von der abstrakten Klasse HashAlgorithm abgeleitet. Für die Hashing-Algorithmen ohne Schlüssel reicht eine Referenz von diesem Typ aus. Die Hashing-Algorithmen mit Schlüssel sind von KeyedHashAlgorithm abgeleitet. KeyedHashAlgorithm definiert die zusätzliche Eigenschaft Key.
HashAlgorithm und KeyedHashAlgorithm sind die Basis
13
Zudem existieren weitere abstrakte Basisklassen wie z. B. SHA1 oder MD5, die keine Implementierung beinhalten. Die Namen der davon abgeleiteten Klassen enden teilweise auf »Managed«, was darauf hinweist, dass die Implementierung in verwaltetem Code existiert (z. B. SHA1Managed). Namen mit der Endung »ServiceProvider« (z. B. bei der Klasse SHA1CryptoServiceProvider) weisen darauf hin, dass die Klassen lediglich Hüllen für entsprechende Klassen des Microsoft-Crypto-API sind. Daneben existieren auch Implementierungen mit der Endung »Cng« (z. B. SHA1Cng). Diese Endung steht für »Cryptography Next Generation« (CNG). CNG ist ein neues Kryptographie-Framework unter Windows Vista, mit dem Vorteil, dass der KryptographieProvider (z. B. bei einem Vista-Update) ausgetauscht werden kann, ohne dass ein Programmcode davon negativ betroffen ist (angeblich …).
14
15
16
Die Create-Methode HashAlgorithm und alle abgeleiteten Klassen stellen die statische Methode Create zur Verfügung, die in den spezialisierteren Klassen überschrieben wird. Dieser Methode übergeben Sie einen String, der den zu verwendenden Algorithmus beschreibt. Die möglichen Strings sind in der Dokumentation der Create-Methode von HashAlgorithm dokumentiert. Über HashAlgorithm.Create("SHA512")« erzeugen Sie z. B. eine Instanz für den SHA-Algorithmus mit einer Schlüssellänge von 512 Bit.
Create erzeugt eine »Standard«Instanz
17
18
Für die spezialisierten Klassen, die die Create-Methode von HashAlgorithm erben, können Sie nur noch Strings übergeben, die zu der Klasse passende Algorithmen beschreiben. In anderen Fällen erhalten Sie eine InvalidCastException.
19
Die abstrakten Methoden auf der letzten Ebene fügen zudem eine Create-Methode ohne Argument hinzu. Über diese Methode erzeugen Sie laut der (wenig helfenden) Dokumentation eine Instanz der »Standardimplementierung« des jeweiligen Verfahrens. Nach meinen Erfahrungen ist dies die »ServiceProvider«-Implementierung, falls eine vorhanden ist.
20
Schließlich überschreiben die konkreten Klassen die Create-Methoden. Damit können Sie dann auch eine Instanz dieser speziellen Implementierung des Algorithmus erzeugen. Unsinnige Aufrufe wie z. B. MD5Cng.Create("SHA1") resultieren in einer InvalidCastException. Die konkreten Klassen lassen an dem String-Argument der Create-Methode nur noch ihren speziellen String zu (was die Anwendung dieser Variante der Create-Methode dann sinnlos macht). Ich bin bei der Verwendung der Create-Methode etwas skeptisch. Damit können Sie nicht steuern, welche Implementierung Sie wirklich erhalten. Ich arbeite lieber mit den konkreten Klassen, die eine Erzeugung über den Konstruktor erlauben. Wenn Sie die CNG-Varianten verwenden wollen, können Sie Create auch gar nicht verwenden (es sei denn, es existiert nur eine CNG-Variante, aber wer weiß, wie das in Zukunft aussieht …).
12
21
22
23
INFO
1267
Sicherheitsgrundlagen
Die Create-Methode hat aber den Vorteil, dass Sie den Algorithmus dynamisch bestimmen und damit z. B. dem Anwender überlassen können. Tabelle 23.1 beschreibt die jeweiligen abstrakten Basisklassen und deren Implementierungen. Diese Tabelle beschreibt gleich noch die maximale Größe des Schlüssels (nach eigenen Versuchen), damit in der Praxis keine Probleme auftreten ☺. Tabelle 23.1: Die .NET-Klassen zum Erzeugen von Hashcodes
HALT
1268
Basisklasse
Implementierungen
Beschreibung
Maximale Schlüsselgröße
MD5
MD5CryptoServiceProvider MD5Cng
MD5 (Message Digest 5) mit 128 Bit Länge
Kein Schlüssel
Direkt von HashAlgorithm abgeleitet
RIPEMD160
RIPEMD-Verfahren mit 160 Bit Kein Schlüssel Länge
SHA1
SHA1Managed SHA1Cng
SHA1 (Secure Hash Algorithm 1) Kein Schlüssel mit 160 Bit Länge
SHA256
SHA256Managed SHA256Cng
SHA1 mit 256 Bit Länge
Kein Schlüssel
SHA384
SHA384Managed SHA384Cng
SHA1 mit 384 Bit Länge
Kein Schlüssel
SHA512
SHA512Managed SHA512Cng
SHA1 mit 512 Bit Länge
Kein Schlüssel
HMAC
HMACMD5
MD5 mit 128 Bit Länge und Schlüssel
64 Byte
HMAC
HMACRIPEMD160
RIPEMD160 mit 160 Bit Länge und Schlüssel
64 Byte
HMAC
HMACSHA1
SHA1 mit 160 Bit und Schlüssel 64 Byte
HMAC
HMACSHA256
SHA256 mit 256 Bit und Schlüssel
64 Byte
HMAC
HMACSHA384
SHA384 mit 384 Bit und Schlüssel
128 Byte
HMAC
HMACSHA512
SHA512 mit 512 Bit und Schlüs- 128 Byte sel
Direkt von KeyedHashAlgorithm abgeleitet
MACTripleDES
TripleDES mit 64 Bit Länge und Schlüssel
24 Byte
Laut der Dokumentation werden die CNG-Varianten unter Windows Vista, Windows XP SP2 und Windows Server 2003 unterstützt. Auf meinem Windows XP SP2 erhielt ich aber eine PlatformNotSupportedException. Das ist auch logisch, denn CNG ist ja schließlich ein neues Kryptographie-Framework unter Vista.
Verschlüsseln und Entschlüsseln von Daten
Die verschiedenen Implementierungen des SHA1-Algorithmus verwenden eine unterschiedliche Hashcode-Größe. Ein größerer Hashcode, dessen Berechnung ein wenig mehr Zeit kostet, bringt eine größere Sicherheit gegenüber Doppelgängern. Die Größe des Hashcodes können Sie übrigens aus der Eigenschaft HashSize auslesen. Die Klassen, deren Name mit HMAC (Hash-based Message Authentication Code) beginnt, verwenden einen privaten Schlüssel, der mit den Daten vermischt wird. Das Ergebnis wird über das verwendete Hashing-Verfahren in einen Hashcode umgewandelt, der Hashcode wird wieder mit dem privaten Schlüssel vermengt und die Hashfunktion wird ein zweites Mal auf diese Datenmenge angewendet. Auf diese Weise erzeugte Hashcodes sind sehr sicher und werden üblicherweise für das Signieren von Nachrichten verwendet.
12
13
Die Klasse MACTripleDES arbeitet wahrscheinlich ähnlich (was aber nicht dokumentiert ist): MAC steht für Message Authentication Code.
14
Hashcodes ermitteln Die Verwendung der Hashing-Klassen ist sehr einfach: Sie erzeugen eine Instanz und rufen zur Erzeugung des Hashcodes die Methode ComputeHash auf. Dieser Methode können Sie ein Byte-Array oder einen Stream übergeben. Sie erhalten dann ein ByteArray mit den Daten des Hashcodes zurück.
Zur Ermittlung eines Hashcodes rufen Sie ComputeHash auf
Bei den Algorithmen, die mit einem Schlüssel arbeiten (die HMAC-Klassen und die Klasse MACTripleDES), können Sie zudem vor der Erzeugung des Hashcodes in der Eigenschaft Key den Schlüssel definieren. Dieser Schlüssel wird bei jeder Erzeugung einer Instanz dieser Klassen per Zufall neu erzeugt. Für den Vergleich zweier Hashcodes müssen Sie den Schlüssel also zwischenspeichern und beim zweiten Ermitteln des Hashcodes setzen. Außerdem darf der Schlüssel eine vom Algorithmus bestimmte Maximalgröße nicht überschreiten. Leider ist die Maximalgröße nicht dokumentiert. Ich habe die folgenden Maximalgrößen ermittelt:
Key verwaltet bei Algorithmen mit Schlüssel den Schlüssel
■ ■ ■
16
17
HMACMD5, HMACRIPEMD160, HMACSHA1, HMACSHA256: 64 Byte, HMACSHA384, HMACSHA512: 128 Byte MACTripleDES: 24 Byte.
18
19
Das folgende Beispiel erzeugt einen Hashcode für eine Datei über das HMAC-SHA-512Verfahren. Als Schlüssel (128 Bit) wird ein String verwendet (weil das praxisorientierter ist). Der Schlüssel wird über die Codierung ISO-8859-1 in ein Byte-Array umgerechnet, damit (bei 16 Zeichen) 16 Byte herauskommen und keine Daten verloren gehen. Schließlich wandelt das Beispiel noch den Hashcode in einen String um, wobei wieder die Codierung ISO-8859-1 verwendet wird, um Probleme zu vermeiden: Listing 23.7:
15
20
Typisches Erzeugen eines Hashcodes
21
// Datei in einen Stream lesen string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "DontPanic.gif"); using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) { // Einen Schlüssel für den Hashcode aus einem String berechnen Encoding encoding = Encoding.GetEncoding("ISO-8859-1"); // 16 Zeichen = 128 Bit in ISO-8859-1 string hashCodeKeyString = "k4odf#+ä@sd+ksxh"; byte[] hashCodeKey = encoding.GetBytes(hashCodeKeyString);
22
23
1269
Sicherheitsgrundlagen
// Für die Daten einen Hashcode berechnen HMACSHA512 hashAlgorithm = new HMACSHA512(hashCodeKey); byte[] hashcode = hashAlgorithm.ComputeHash(fs); // Den Hashcode in einen String umwandeln string hashCodeString = encoding.GetString(hashcode); // Mit dem Hashcode arbeiten ... }
Abbildung 23.1 zeigt die etwas erweiterte Beispielanwendung, die den Schlüssel, den Hashcode und den Hashcode-String an der Konsole ausgibt: Abbildung 23.1: Die Beispielanwendung zum Erzeugen eines Hashcodes
23.2.5 Daten symmetrisch verschlüsseln Symmetrisch verschlüsselte Daten werden über dieselben Schlüssel und IVs wieder entschlüsselt
Das symmetrische Verschlüsseln von Daten ist die sicherste und schnellste Form der Verschlüsselung (allerdings nicht im Bezug auf die Schlüsselweitergabe). Bei einer symmetrischen Verschlüsselung werden Daten über einen Schlüssel und einen Initialisierungsvektor über besondere Verfahren so verschlüsselt, dass das Entschlüsseln ohne Schlüssel und Initialisierungsvektor (je nach Verfahren) sehr aufwändig bis nahezu unmöglich ist. Ein Initialisierungsvektor (kurz: IV) ist so etwas wie ein zusätzlicher Schlüssel, der mit in der Verschlüsselung verwendet wird. Der Initialisierungsvektor ist dazu vorgesehen, beim Senden verschiedener Daten immer wieder geändert, aber der Nachricht öffentlich mitgegeben zu werden. Da unterschiedliche Initialisierungsvektoren zu unterschiedlichen Ergebnissen führen, wird das Auslesen des geheimen Schlüssels durch einen Austausch des Initialisierungsvektors erheblich erschwert. Zum Entschlüsseln der Daten werden derselbe Schlüssel und Initialisierungsvektor verwendet wie zum Verschlüsseln, weswegen diese Verfahren als symmetrisch bezeichnet werden.
EXKURS
Bei symmetrischen Verschlüsselungsverfahren werden zudem verschiedene Chiffrier-Modi (Cipher modes) unterschieden. Block-Chiffrierungen wie CBC (Cipher Block Chaining), CFB (Cipher Feedback) und ECB (Electronic Codebook) verschlüsseln einen festen Block von Daten in einen Block gleicher Länge. Stream-Chiffrierungen verschlüsseln einzelne Bit-Gruppen, typischerweise, indem diese mit dem Schlüssel bitweise mit XOR verknüpft werden. Die Blockgröße von Block-Chiffrierungen können Sie in den .NET-Klassen einstellen. Die Arbeitsweise der von .NET unterstützten Modi werden sehr gut in der Dokumentation der CipherMode-Aufzählung erläutert. Bei allen Block-Chiffrierungen werden die Daten blockweise über den Schlüssel neu berechnet und im Ergebnis abgelegt. Der beliebte CBC-Modus nutzt für die Verschlüsselung zusätzlich den zuvor verschlüsselten Block. Weil für den ersten Block kein verschlüsselter Block
1270
Verschlüsseln und Entschlüsseln von Daten
zur Verfügung steht, arbeitet CBC mit dem Initialisierungsvektor. Wenn Sie Daten im (übrigens unter .NET voreingestellten) CBC-Modus ver- und entschlüsseln, müssen Sie also neben dem Schlüssel auch den Initialisierungsvektor angeben. Die Länge des Initialisierungsvektors muss der Blockgröße entsprechen. Da der letzte Block meist nicht der definierten Blockgröße entspricht, wird dieser häufig mit Bytes aufgefüllt. Bei diesem so genannten Padding werden ebenfalls verschiedene Modi eingesetzt. PKCS7 speichert dazu für jedes Byte den Wert der Anzahl der insgesamt hinzuzufügenden Bytes, der andere Modus Zeros speichert einfach 0-Bytes. PKCS7 ist unter .NET die Voreinstellung.
12
13 Die .NET-Klassen zum symmetrischen Verschlüsseln Für eine symmetrische Verschlüsselung stellt Ihnen der Namensraum System.Security.Cryptography einige abstrakte Basisklassen und einige davon abgeleitete konkrete Implementierungen zur Verfügung. Die Klasse SymmetricAlgorithm ist die gemeinsame Basisklasse aller Klassen für eine symmetrische Verschlüsselung. Eine Referenz dieses Typs reicht aus, um mit den konkreten Klassen arbeiten zu können.
14
15
Von SymmetricAlgorithm ist für jeden Algorithmus je eine weitere abstrakte Klasse abgeleitet und von diesen schließlich eine oder zwei konkrete Klassen. Die Klasse Aes ist z. B. die abstrakte Basisklasse für die Klassen AesCryptoServiceProvider und AesManaged.
16
Die Klassen mit der Endung »Managed« sind wie bereits bei den Hashing-Klassen komplett in verwaltetem Code implementiert. Klassen mit der Endung »ServiceProvider« sind lediglich Hüllen für Klassen des nativen Windows-Kryptographie-API (Crypto API). CNG-Varianten der Klassen für symmetrische Verschlüsselungen existieren scheinbar (noch) nicht. Über die Create-Methode von SymmetricAlgorithm können Sie eine Instanz einer der Klassen verfügbaren Algorithmen erzeugen. Dazu übergeben Sie einen String mit dem Basisnamen (z. B. »Rijndael« für den Rijndael-Algorithmus). Die konkreten Klassen stellen eine weitere Create-Methode ohne Argument zur Verfügung (die natürlich eine Instanz der jeweiligen Klasse erzeugt). Da diese Klassen von SymmetricAlgorithm erben, besitzen sie verwirrenderweise auch die Create-Variante mit String-Argument. Diese ist aber sinnlos, da sie nur den für den jeweiligen Algorithmus gültigen String akzeptiert.
17 Die CreateMethode der abstrakten Klassen erzeugt eine optimale Instanz
19
20
Tabelle 23.2 beschreibt die abstrakten Basisklassen und die konkreten Ableitungen für die einzelnen Algorithmen. In der letzten Spalte sind die möglichen Schlüsselund Blockgrößen angegeben. Basisklasse
Konkrete Klassen
Beschreibung
Schlüssel- und Blockgröße
SymmetricAlgorithm
Alle konkreten Verschlüsselungs-Klassen
Basisklasse für alle symmetrischen Verschlüsselungsverfahren
-
Aes
AesCryptoServiceProvider AesManaged
repräsentiert den AESAlgorithmus.
Schlüssel: 128, 192 und 256 Bit
18
Tabelle 23.2: Die .NET-Klassen für symmetrische Verschlüsselungen
21
22
23
Blockgröße: 128 Bit
1271
Sicherheitsgrundlagen
Tabelle 23.2: Die .NET-Klassen für symmetrische Verschlüsselungen
Basisklasse
Konkrete Klassen
Beschreibung
Schlüssel- und Blockgröße
DES
DESCryptoServiceProvider
repräsentiert den DESAlgorithmus.
Schlüssel: 64 Bit
RC2CryptoServiceProvider
repräsentiert den RC2Algorithmus
Schlüssel: 40 bis 128 Bit in 8-BitSchritten
RC2
Blockgröße: 64 Bit
Blockgröße: 64 Bit Rijndael
TripleDES
INFO
RijndaelManaged
TripleDESCryptoServiceProvider
repräsentiert den Rijndael- Algorithmus
Schlüssel: 128, 192 und 256 Bit
repräsentiert den TripleDES-Algorithmus.
Schlüssel: 128 und 192 Bit
Blockgröße: 128, 192 und 256 Bit
Blockgröße: 64 Bit
Beachten Sie, dass Sie die gültigen Größen für den Schlüssel und die Blockgröße (wonach sich auch der IV richtet) aus den Eigenschaften LegalKeySizes und LegalBlockSizes auslesen können. Ich habe dies für Tabelle 23.2 gemacht, weswegen sich die hier angegebenen Werte von der Dokumentation unterscheiden können.
Das Verschlüsseln Zum Verschlüsseln benötigen Sie einen Schlüssel und einen IV
Zum Verschlüsseln benötigen Sie den Schlüssel und den Initialisierungsvektor. Dieser wird per Zufall erzeugt, wenn Sie eine Instanz der jeweiligen Klasse erzeugen. Den Schlüssel erhalten Sie über die Eigenschaft Key, den Initialisierungsvektor über die Eigenschaft IV. Beide referenzieren ein Byte-Array. Über das Erzeugen einer Instanz können Sie also (einmalig) Zufallswerte erzeugen, die Sie dann für die Verschlüsselung einsetzen. Da ich Strings für Schlüssel bevorzuge, weil ich diese (z. B. zur Übertragung per E-Mail) etwas handlicher finde, erzeuge ich einmal (vor der eigentlichen Programmierung) Strings für den Schlüssel und den IV: Listing 23.8: Einmaliges Ermitteln eines Zufalls-Schlüssels und -IV // Verschlüsselungs-Instanz erzeugen SymmetricAlgorithm symmetricAlgorithm = Rijndael.Create(); // Die Zufallswerte des Schlüssels und des // Initialisierungsvektors auslesen byte[] key = symmetricAlgorithm.Key; byte[] iv = symmetricAlgorithm.IV; // Diese in einen String umwandeln Encoding encoding = Encoding.GetEncoding("ISO-8859-1"); string keyString = encoding.GetString(key); string ivString = encoding.GetString(iv);
INFO
1272
Beim Umsetzen der Byte-Arrays in Strings verwende ich die mit Unicode in den ersten acht Bit pro Zeichen kompatible Codierung ISO-8859-1. Der Grund dafür ist, dass die Verwendung von UTF-8 oder einer anderen Unicode-Codierung zu eigenartigen Fehlern beim Ver- oder Entschlüsseln führt.
Verschlüsseln und Entschlüsseln von Daten
Diese Strings verwende ich dann als Schlüssel und IV für die Verschlüsselung und die Entschlüsselung der Daten. Für das Buch setze ich allerdings einfachere Strings ein, bei denen nicht die Gefahr besteht, dass die vielen Sonderzeichen im Druck Probleme machen.
Verschlüsselt wird über einen ICryptoStream
Das Verschlüsseln ist dann leider etwas aufwändig: Dazu erzeugen Sie über die CreateEncryptor-Methode zunächst ein Verschlüsselungs-Objekt (das die ICryptoTransform-Schnittstelle implementiert. Dieses übergeben Sie einem CryptoStream, der die Daten eines ebenfalls übergebenen anderen Stream über das Verschlüsselungs-Objekt verschlüsselt. Zum Verschlüsseln müssen Sie am letzten Argument des CryptoStream-Konstruktors noch CryptoStreamMode.Write angeben, damit der CryptoStream weiß, dass er in den anderen Stream schreiben soll:
12
13
Listing 23.9: Verschlüsseln eines Strings in eine Datei // Schlüssel und Initialisierungsvektor (IV) als String definieren. // Bei Rijndael müssen der Schlüssel 32 und der IV 16 Byte groß sein. string keyString = "sh@lhei@rhiewnjkehje@whehkd@sf+@" string ivString = "340odlfxf9t034l4";
14
// Verschlüsselungs-Instanz erzeugen SymmetricAlgorithm symmetricAlgorithm = Rijndael.Create();
15
// Die Schlüssel-Strings in Byte-Arrays umwandeln und // zuweisen Encoding encoding = Encoding.GetEncoding("ISO-8859-1"); symmetricAlgorithm.Key = encoding.GetBytes(keyString); symmetricAlgorithm.IV = encoding.GetBytes(ivString);
16
// Die zu verschlüsselnden Daten zusammenstellen string originalString = "Das ist ein Test-String"; byte[] data = encoding.GetBytes(originalString);
17
// In einen (File)Stream verschlüsseln string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); string fileName = Path.Combine(appPath, "EncryptedData.bin"); ICryptoTransform encryptor = symmetricAlgorithm.CreateEncryptor(); using (FileStream baseStream = new FileStream(fileName, FileMode.Create, FileAccess.Write)) { using (CryptoStream cryptoStream = new CryptoStream( baseStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(data, 0, data.Length); } }
Vor dem Ver- oder Entschlüsseln können Sie über die Eigenschaften BlockSize, FeedbackSize, Padding und Mode die Blockgröße, die Feedback-Größe (was immer das auch ist …), das Padding und den Chiffrier-Modus einstellen (wobei der Initialisierungsvektor zu der Blockgröße passen muss). Beim Entschlüsseln muss genau dieselbe Einstellung verwendet werden, wie beim Verschlüsseln. Ansonsten erhalten Sie eine CryptographicException mit der wenig sprechenden Meldung »Zeichenabstände sind ungültig und können nicht entfernt werden«. Beim Datenaustausch mit Fremdsystemen müssen Sie diese Werte also kennen.
18
19
20
21 HALT
22
23
1273
Sicherheitsgrundlagen
Das Entschlüsseln Das Entschlüsseln läuft ebenfalls über einen CryptoStream
Das Entschlüsseln ist ähnlich komplex wie das Verschlüsseln. Dazu erzeugen Sie über die CreateDecryptor-Methode ein Entschlüssel-Objekt, das Sie einem neu erzeugten CryptoStream übergeben, der die verschlüsselten Daten aus dem ebenfalls übergebenen Stream liest. Am letzten Argument des CryptoStream-Konstruktors müssen Sie nun CryptoStreamMode.Read übergeben, damit der CryptoStream weiß, dass er aus dem übergebenen Stream lesen soll. Das Problem, das Sie beim Lesen lösen müssen, ist, dass ein CryptoStream nicht erlaubt, die Länge des Streams auszulesen (Length wirft eine NotSupportedException), weil er diese nicht ermitteln kann. Sie müssen den Stream also häppchenweise auslesen. Das ist dummerweise gar nicht so einfach. Ich verwende als kleinen Trick einen MemoryStream. Statt des MemoryStream können Sie natürlich auch jeden anderen Stream verwenden, in den Sie die entschlüsselten Daten schreiben wollen. Den MemoryStream werte ich dann als String aus: Listing 23.10: Das leider etwas komplexe symmetrische Entschlüsseln ICryptoTransform decryptor = symmetricAlgorithm.CreateDecryptor(); using (FileStream baseStream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) { using (CryptoStream cryptoStream = new CryptoStream( baseStream, decryptor, CryptoStreamMode.Read)) { // Die vom CryptoStream gelesenen Daten in einen // MemoryStream schreiben byte[] buffer = new byte[1048576]; // 1 MB Puffer using (MemoryStream memoryStream = new MemoryStream()) { int count; do { count = cryptoStream.Read(buffer, 0, buffer.Length); memoryStream.Write(buffer, 0, count); } while (count > 0); // Den MemoryStream in ein Byte-Array schreiben byte[] decryptedData = new byte[memoryStream.Length]; memoryStream.Position = 0; memoryStream.Read(decryptedData, 0, (int)memoryStream.Length); // Das Byte-Array in einen Strimg konvertieren string decryptedString = encoding.GetString(decryptedData); Console.WriteLine("Entschlüsselter String: " + decryptedString); } } }
23.2.6 Daten asymmetrisch verschlüsseln Die asymmetrische Verschlüsselung basiert auf abstrakten Klassen
1274
Das asymmetrische Verschlüsseln ist in .NET erstaunlich einfach. Dazu stehen zunächst wieder einige abstrakte Klassen zur Verfügung. Die von der gemeinsamen Basisklasse AsymmetricAlgorithm abgeleiteten abstrakten Klassen besitzen wie schon bei der symmetrischen Verschlüsselung eine Create-Methode, die die eine Instanz der »Standardimplementierung« zurückgibt.
Verschlüsseln und Entschlüsseln von Daten
Basisklasse
Konkrete Klasse(n) Beschreibung
Schlüssellänge
AsymmetricAlgorithm Alle konkreten Klassen Basisklasse für alle Klassen, die für die asymmetrische eine asymmetrische VerschlüsseVerschlüsselung lung implementieren DSA
RSA
ECDiffieHellman
ECDsa
DSACryptoServiceProvider
repräsentiert den DSA-Algorithmus (Digital Signature Algorithm). DSA wird ausschließlich für digitale Signaturen verwendet.
512 bis 1024 Bit in 64Bit-Schritten
RSACryptoServiceProvider
repräsentiert den RSA-Algorithmus. RSA ist sicherer als DSA, wurde aber (mit kleinen Schlüsselgrößen) bereits mehrfach geknackt (Siehe citeseer.ist.psu.edu/514527.html). Für eine hohe Sicherheit sind große Schlüssel ab 1024 Bit zu empfehlen. 2048-Bit-Schlüssel sind für die Zukunft wohl ausreichend groß dimensioniert.
384 bis und 16384 Bit in 8-Bit-Schritten, wenn der Microsoft Enhanced Cryptographic Provider installiert. Ansonsten nur bis 512 Bit.
ECDiffieHellmanCng
ECDsaCng
Tabelle 23.3: Die Klassen für asymmetrische Verschlüsselungen
12
13
14
15
repräsentiert den Elliptic Curve 256, 384 und 521 Bit Diffie-Hellman-Algorithmus (ECDH). Dieser schon recht alte Algorithmus ist schwierig zu knacken, weil dazu eine logarithmische Rückrechnung vorgenommen werden muss.
16
repräsentiert den ECDSA-Algo256 und 384 Bit rithmus (Elliptic Curve Digital Signature Algorithm).
18
17
19 Beachten Sie, dass die »Cng«-Varianten zum »Cryptography Next Generation Framework« gehören, das erst ab Vista und Windows Server 2003 zur Verfügung steht (die Dokumentation behauptet allerdings, die entsprechenden Klassen würden auch unter Windows XP SP2 zur Verfügung stehen, aber auf meinen Windows XP SP2 erhielt ich beim Versuch der Instanzierung eine TargetInvocationException).
Die Klassen zur asymmetrischen Verschlüsselung sind leider recht »durcheinander«. Die abstrakten Basisklassen stellen entweder keine Methode zur Ver- und Entschlüsselung zur Verfügung (weswegen die Verwendung der Create-Methode auf einer abstrakten Basisklasse keinen Sinn macht), oder diese werfen, wie im Fall von RSA, eine NotSupportedException. Damit sind Sie in der Praxis schon einmal gezwungen, mit den konkreten Klassen zu arbeiten.
HALT
20
21 INFO
22
23
Zum anderen verwenden die ECDiffieHellman- und die ECDsa-Klasse eine andere Grundtechnik als DSA und RSA. Darauf gehe ich im Abschnitt »Verschlüsseln mit dem Elliptic-Curve-Diffie-Hellman-Algorithmus« (Seite 1278) ein.
1275
Sicherheitsgrundlagen
Mit RSA ver- und entschlüsseln Da DSA nur für das digitale Signieren vorgesehen ist und ECDH eine andere Technik verwendet, zeige ich zunächst das Ver- und Entschlüsseln mit RSA. RSA benötigt zwei Schlüssel-XMLDokumente
Dazu müssen Sie zunächst einmal die Schlüssel erzeugen, die Sie verwenden wollen. Wie schon bei der symmetrischen Verschlüsselung generiert die jeweilige Klasse beim Erzeugen einer Instanz Zufalls-Schlüssel (allerdings just in time, weil das Generieren Zeit benötigt). Diese Schlüssel können Sie für RSA und DSA über die ToXmlString-Methode in XML-Form auslesen und z. B. in Dateien speichern. Die Schlüsselgröße (die bei asymmetrischen Algorithmen variabel ist) sollten Sie allerdings zuvor über die Eigenschaft KeySize bestimmen. Beachten Sie aber, dass größere Schlüssel zu einer langsameren Ver- und Entschlüsselung führen und dass nur bestimmte Schlüsselgrößen möglich sind (siehe Tabelle 23.3). Listing 23.11: Erzeugen von Schlüsseldateien string appPath = Path.GetDirectoryName( Assembly.GetEntryAssembly().Location); // Instanz der Klasse für den Verschlüsselungs-Algorithmus erzeugen AsymmetricAlgorithm asymmetricAlgorithm = RSA.Create(); // Die Schlüsselgröße bestimmen asymmetricAlgorithm.KeySize = 2048; // XML-String für den öffentlichen Schlüssel erzeugen string publicKeyXml = asymmetricAlgorithm.ToXmlString(false); // XML-String für den privaten und öffentlichen Schlüssel erzeugen string privateAndPublicKeyXml = asymmetricAlgorithm.ToXmlString(true); // Speichern als XML-Dateien string privateAndPublicKeyFileName = Path.Combine(appPath, "PrivateAndPublicKey.xml"); string publicKeyFileName = Path.Combine(appPath, "PublicKey.xml"); File.WriteAllText(privateAndPublicKeyFileName, privateAndPublicKeyXml); File.WriteAllText(publicKeyFileName, publicKeyXml);
Zum Verschlüsseln rufen Sie Encrypt auf
Zum Verschlüsseln von Daten über RSA benötigen Sie eine Referenz vom Typ der konkreten Klasse, da die abstrakten Klassen keine Methoden zum Ver- und Entschlüsseln zur Verfügung stellen. Zum Verschlüsseln über RSA erzeugen Sie also eine Instanz der RSACryptoServiceProvider-Klasse. Den öffentlichen Schlüssel des Empfängers (den Sie in Form einer entsprechenden XML-Datei vorliegen haben) weisen Sie über die FromXmlStringMethode zu. Verschlüsseln können Sie dann über die Encrypt-Methode, aber:
HALT
Encrypt erwartet neben den zu verschlüsselnden Daten am zweiten Argument einen booleschen Wert, der aussagt, ob die Verschlüsselung mit »OAEP-Padding« (auch: PKCS#1 Version 2) ausgeführt wird oder nicht (dann wird PKCS#1 Version 1.5 verwendet). OAEP-Padding kann nur auf Windows-Systemen ab XP verwendet werden.
RSA ist auf eine Maximallänge der zu verschlüsselnden Daten eingeschränkt.Versuchen Sie, zu große Daten zu verschlüsseln, erhalten Sie eine CryptographicException mit der wenig sagenden Meldung »Ungültige Länge«.
1276
Verschlüsseln und Entschlüsseln von Daten
Die Maximallänge wird vom verwendeten Padding bestimmt, aber auch von dem verwendeten »Modulus« und der Länge eines Hashcodes. Beim OAEP-Padding berechnet sich laut der Dokumentation die Maximallänge folgendermaßen: Modulusgröße -2 -2 * Hashgröße Beim PKCS#1-Version-1.5-Padding berechnet sich die Maximalgröße so:
12 Modulusgröße -11 Der »Modulus« einer RSA-Verschlüsselung ist ein nach dem Modulo-Verfahren berechneter Wert für den verwendeten Schlüssel. Dieses Verfahren wird bei Wikipedia erläutert: en.wikipedia.org/wiki/Modular_arithmetic.
13
Die Modulusgröße können Sie über die RSA-Parameter ermitteln, die Sie in Form einer RSAParameters-Instanz über die ExportParameters-Methode exportieren können. Die Eigenschaft Modulus verwaltet ein Byte-Array mit den Modulo-Werten. In meinem Fall war die Größe dieses Arrays 128 Byte. Die RSA-Parameter können Sie auch neu definieren oder verändern und über die ImportParameters-Methode importieren. Allerdings hatte ich damit bisher keinen Erfolg …
14
15
Wie Sie allerdings die Hashgröße ermitteln oder definieren können, ist nicht dokumentiert. RSA kann also nur Daten bis zu einer Maximalgröße verschlüsseln. Durch reines Ausprobieren habe ich ermittelt, dass Sie mit dem OAEP-Padding maximal 87 Byte und mit dem PKCS#1-Version-1.5-Padding maximal 117 Byte verschlüsseln können. Dies habe ich unter Windows Vista und unter XP nachvollzogen. Ob das aber grundsätzlich so ist, ist mir unklar.
16 RSA ist auf eine maximale Länge eingeschränkt
Weil RSA in der Praxis hauptsächlich eingesetzt wird, den Schlüssel einer symmetrischen Verschlüsselung zu verschlüsseln, ist diese Einschränkung nicht weiter schlimm. Die auch mit dem OAEP-Padding mögliche Schlüsselgröße von 696 Bit wird bei der symmetrischen Verschlüsselung nicht benötigt.
17
18
19
Listing 23.12 zeigt das Verschlüsseln eines Byte-Arrays, das als Schlüssel für die symmetrische Verschlüsselung verwendet werden soll. Das Ergebnis konvertiere ich allerdings in einen String, um diesen z. B. neben den einer E-Mail anzufügen, und aus diesem wieder in ein Byte-Array:
20
Listing 23.12: Asymmetrisches Verschlüsseln über den öffentlichen Schlüssel mit RSA // Die zu verschlüsselnden Daten als Byte-Array definieren byte[] symmetricEncryptionKey = { 115, 104, 64, 108, 104, 101, 105, 64, 114, 104, 105, 101, 119, 110, 106, 107, 101, 104, 106, 101, 64, 119, 104, 101, 104, 107, 100, 64, 115, 102, 43, 64, };
21
// RSA-Instanz erzeugen und den öffentlichen Schlüssel zuweisen RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); rsa.FromXmlString(File.ReadAllText(publicKeyFileName));
22
// Mit OAEP-Padding verschlüsseln byte[] encryptedData = rsa.Encrypt(symmetricEncryptionKey, true);
23
// Die verschlüsselten Daten als ISO-8859-1-String darstellen string encryptedString = Encoding.GetEncoding( "ISO-8859-1").GetString(encryptedData);
1277
Sicherheitsgrundlagen
Decrypt entschlüsselt über den privaten Schlüssel
Zum Entschlüsseln von Daten müssen Sie über die FromXmlString das XML-Dokument zuweisen, das neben dem öffentlichen auch den privaten Schlüssel enthält. Entschlüsseln können Sie dann über die Decrypt-Methode. Dabei müssen Sie – neben dem korrekten privaten Schlüssel – dasselbe Padding verwenden. Listing 23.13: Asymmetrisches Entschlüsseln von Daten, die als String vorliegen // Die verschlüsselten Daten aus dem String ermitteln encryptedData = Encoding.GetEncoding( "ISO-8859-1").GetBytes(encryptedString); // RSA-Instanz erzeugen und mit dem XML-Schlüssel initialisieren RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); rsa.FromXmlString(File.ReadAllText(privateAndPublicKeyFileName)); // Entschlüsseln byte[] decryptedData = rsa.Decrypt(encryptedData, true);
Falls Sie versuchen, lediglich über den öffentlichen Schlüssel zu entschlüsseln, resultiert dies in einer CryptographicException.
Verschlüsseln mit dem Elliptic-Curve-Diffie-Hellman-Algorithmus ECDH ist der aktuelle Standard für den Austausch von Schlüsseln
Der Elliptic Curve Diffie-Hellman-Algorithmus (ECDH) ist sicherer als RSA, weil er ein sehr spezielles Verfahren einsetzt, das auf der Potenzierung der zu verschlüsselnden Daten mit großen Exponenten basiert. Eine solche Verschlüsselung ist nur sehr schwer zu knacken. ECDH ist zurzeit der aktuelle Standard für die Verschlüsselung von Schlüsseln und wird von der National Security Agency als der beste Weg beschrieben, eine private Kommunikation zu sichern (www.nsa.gov/ia/industry/ crypto_elliptic_curve.cfm). Die Grundidee von ECDH entspricht der von RSA: ECDH ist lediglich zum Austausch von Schlüsseln zwischen zwei Kommunikationspartnern vorgesehen, die für eine symmetrische Verschlüsselung von parallel (oder später) übertragenen Daten verwendet werden.
ECDH erzeugt einen privaten Schlüssel
ECDH arbeitet aber andersherum: ECDH verschlüsselt nicht einen vorhandenen Schlüssel für die symmetrische Verschlüsselung. ECDH erzeugt einen Schlüssel, der für die symmetrische Verschlüsselung eingesetzt wird. Zwischen den Kommunikationspartnern wird nur der öffentliche Schlüssel ausgetauscht. Kommunikationspartner A erzeugt eine Instanz der ECDiffieHellmanCng-Klasse. Er ruft die DeriveKeyMaterialMethode auf, der er den öffentlichen Schlüssel des Kommunikationspartners B übergibt. Diese Methode gibt einen privaten Schlüssel zurück, der für die symmetrische Verschlüsselung der eigentlichen Daten verwendet werden kann. B macht genau dasselbe wie A, nur eben mit dem öffentlichen Schlüssel von A.
ECDH muss auf beiden Seiten gleich eingestellt sein
Beide müssen sich zuvor noch auf eine Schlüssel-Extraktions-Funktion einigen. Diese wird in der Eigenschaften KeyDerivationFunction eingestellt. KeyDerivationFunction erwartet einen Wert der ECDiffieHellmanKeyDerivationFunction-Aufzählung. Die folgenden Werte sind definiert: ■
■
1278
Hash: Zur Generierung des Schlüssels wird ein Hash-Algorithmus verwendet. Die HashAlgorithm-Eigenschaft spezifiziert diesen über eine CngAlgorithmInstanz, die über statische Eigenschaften dieser Klasse erzeugt wird. Hmac: Zur Generierung des Schlüssels wird der HMAC-Algorithmus verwendet. Die Eigenschaft HmacKey gibt den zu verwendenden Schlüssel an. Alternativ kann UseSecretAgreementAsHmacKey auf true gesetzt werden.
Verschlüsseln und Entschlüsseln von Daten
■
Tls: Zum Generieren des Schlüssels wird das TLS (Transport Layer Security)-Protokoll verwendet. Dazu müssen Sie die Eigenschaften Seed und Label festlegen.
Die auf beiden Seiten erzeugten privaten Schlüssel sind dann gleich. Das folgende Beispiel demonstriert dies, indem es beide Seiten simuliert. Es nutzt die Tatsache, dass die Klasse ECDiffieHellmanCng einen öffentlichen Schlüssel per Zufall generiert, der aus der Eigenschaft PublicKey in Form einer ECDiffieHellmanPublicKey-Instanz ausgelesen werden kann. Diese ist serialisierbar und kann deswegen z. B. über WCF ausgetauscht werden.
12
Listing 23.14: Simulation der Erzeugung eines privaten Schlüssels auf zwei Seiten einer Kommunikation mit ECDH
13
// Kommunikationspartner A erzeugt einen öffentlichen Schlüssel ECDiffieHellmanCng ecDiffieHellmanA = new ECDiffieHellmanCng(); ECDiffieHellmanPublicKey publicKeyA = ecDiffieHellmanA.PublicKey;
14
// Kommunikationspartner B erzeugt ebenfalls einen öffentlichen Schlüssel ECDiffieHellmanCng ecDiffieHellmanB = new ECDiffieHellmanCng(); ECDiffieHellmanPublicKey publicKeyB = ecDiffieHellmanB.PublicKey;
15
// A sendet B seinen öffentlichen Schlüssel // B sendet A seinen öffentlichen Schlüssel // A erzeugt einen privaten Schlüssel mit dem öffentlichen // Schlüssel von B ecDiffieHellmanA.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash; ecDiffieHellmanA.HashAlgorithm = CngAlgorithm.Sha512; byte[] privateKeyA = ecDiffieHellmanA.DeriveKeyMaterial(publicKeyB);
16
17
// B erzeugt einen privaten Schlüssel mit dem öffentlichen // Schlüssel von A ecDiffieHellmanB.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash; ecDiffieHellmanB.HashAlgorithm = CngAlgorithm.Sha512; byte[] privateKeyB = ecDiffieHellmanB.DeriveKeyMaterial(publicKeyA);
18 Abbildung 23.2: Das Beispielprogramm zur ECDH-Verschlüsselung hat die beiden erzeugten privaten Schlüssel ausgegeben
19
20
23.2.7 Digitale Signaturen Das Signieren von Daten ist mit RSA (oder DSA) sehr einfach: Dazu verwenden Sie die SignData-Methode, die eine Signatur in Form eines Byte-Arrays zurückgibt. Diese Methode ist sehr effizient und kann auch über große Daten verwendet werden. Die erzeugte Signatur ist zurzeit 128 Byte groß.
SignData erzeugt eine Signatur
Auf der anderen Seite verwendet der Empfänger die VerifyData-Methode, um die Signatur zu überprüfen. Beiden Methoden übergeben Sie am ersten Argument einen Stream oder ein Byte-Array mit den Daten. Am Argument halg (steht wohl für »Hash Algorithmus«) müssen Sie Objekte übergeben, die einen Hashcode erzeugen. Leider ist das entsprechende Argument vom Typ Object. Zudem werden nicht alle HashcodeAlgorithmen akzeptiert (welche akzeptiert werden, ist natürlich nicht dokumentiert …).
VerifyData überprüft eine Signatur
21
22
23
1279
Sicherheitsgrundlagen
Das folgende Beispiel demonstriert dies für eine Kommunikation zwischen zwei Partnern (A und B). Das Beispiel wendet aber keine Verschlüsselung der versendeten Daten an. Diese muss in der Praxis separat erfolgen! Listing 23.15: Demo für das digitale Signieren von übertragenen Daten über RSA (ohne Verschlüsselung der Daten) // Kommunikationspartner A erzeugt eine RSA-Instanz // und ermittelt die Schlüssel RSACryptoServiceProvider rsaA = new RSACryptoServiceProvider(); string privateAndPublicKeyA = rsaA.ToXmlString(true); string publicKeyA = rsaA.ToXmlString(false); // A stellt Daten zusammen ... byte[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // ... und berechnet eine Signatur HashAlgorithm hashAlgorithm = SHA1.Create(); byte[] signature = rsaA.SignData(data, hashAlgorithm); // A sendet B seinen öffentlichen Schlüssel, die (in der Praxis // ebenfalls verschlüsselten) Daten und die Signatur // Ein "Man in the Middle" verändert die Daten data[0] = 42; // Kommunikationspartner B erzeugt eine RSA-Instanz // mit dem öffentlichen Schlüssel von A ... RSACryptoServiceProvider rsaB = new RSACryptoServiceProvider(); rsaA.FromXmlString(publicKeyA); // ... und überprüft die Daten // HashAlgorithm hashAlgorithm = SHA1.Create(); if (rsaA.VerifyData(data, hashAlgorithm, signature) == true) { Console.WriteLine("Die Daten sind OK"); } else { Console.WriteLine("Die Daten wurden verändert"); }
Das Beispiel demonstriert auch das Verändern der Daten über einen »Man in the Middle«, das dazu führt, dass die Überprüfung der Signatur false ergibt.
INFO
In der Praxis muss der Empfänger dem Sender natürlich vertrauen (also im Wesentlichen dessen öffentlichen Schlüssel). Die sicherste Variante dazu ist, dass der Schlüssel selbst zu einem Zertifikat von einer Vertrauensstelle gehört, das der Nachricht mitgeliefert wird. Im Namensraum System.Security.Cryptography.X509Certificates finden Sie Klassen zur Arbeit mit solchen Zertifikaten.
23.3
Das sichere Ende dieses Buchs
So. Das war es. Endlich ist das Buch zu Ende. Nicht, dass ich nicht gerne geschrieben habe. Das Schreiben hat viel Spaß gemacht (Ich hoffe, das Lesen Ihnen auch). Und ich freue mich auf Kommunikation mit meinen Lesern ☺. Aber nach etwa 1000 Stunden Arbeit an dem Buch brauche ich jetzt einmal etwas anderes. Urlaub. Oder windsurfen … Oder beides ☺. Viel Spaß noch mit .NET und C#.
1280
Teil 5 Anhang 1283
Glossar
A
1299
Die ersten 255 Zeichen des Unicode-Zeichensatzes
B
1301
Meine Namenskonvention für Steuerelemente
C
Inhalt
A
Glossar A
Algorithmus
And API
B
Ein Algorithmus ist eine Verarbeitungsvorschrift, die so präzise formuliert ist, dass sie von einem Computer durchgeführt werden kann. Da Computer nicht wie Menschen interpretieren oder assoziieren können, muss einem Computer bis auf das letzte Detail mitgeteilt werden, was er zu tun hat. Wenn Sie z. B. eine ´Methode schreiben wollen, die die Einkommensteuer aus einem gegebenen Gehalt berechnet, müssen Sie sich vorher Gedanken über den dazu notwendigen Algorithmus machen. ´Bitweises Und Ein Application Interface ist als eine Schnittstelle zu einer Anwendung zu verstehen. Viele Anwendungen bieten ihre Funktionalität auch für andere Anwendungen nach außen an. Bei älteren Anwendungen geschieht dies häufig in Form von klassischen ´DLL-Dateien oder in Form von ´COM-Komponenten. Die Microsoft-Office-Anwendungen installieren z. B. je eine COMKomponente, über die eine Anwendung z. B. relativ einfach ein Word-Dokument erzeugen und ausdrucken kann.
C
Neuere Anwendungen sollten ihre Funktionalität (zudem) über .NET-Assemblys veröffentlichen, was aber leider nur sehr selten geschieht.
Base64
1283
Index
ASCII
Aber nicht nur Anwendungen besitzen häufig ein API, sondern auch Betriebssysteme. Das für uns wichtigste ist das WindowsAPI, das aus einer Vielzahl von DLL-Dateien und einigen COMKomponenten besteht und das Anwendungen die Features des Betriebssystems anbietet. ASCII (American Standard Code for Information Interchange) ist eine sehr alte Zeichencodierung, die ein Zeichen in sieben Bit abbildet. Daher sind lediglich 128 Zeichen möglich. ASCII definiert nur die Basis-Zeichen und enthält z. B. keine deutschen Umlaute. Verschlüsselungsverfahren, bei dem Daten so codiert werden, dass diese dem 7-Bit-ASCII-Code entsprechen. Damit kann sichergestellt werden, dass Daten in Teilen des Internets, die noch den alten 7-Bit-Code verwenden, korrekt übertragen werden.
Anhang
Berichtsgenerator Ein Berichtsgenerator ermöglicht die Erzeugung von Berichten in verschiedenen Formen. Der Generator arbeitet dazu mit einer Beschreibung des Berichts und den darzustellenden Daten. Die Beschreibung des Berichts kann in verschiedenen Formen vorliegen, z. B. in XML. Die meisten Berichtssysteme integrieren neben dem Generator auch einen Designer, der die Erstellung von Berichten durch Anwender bzw. Programmierer ermöglicht. Bitfeld Ein Bitfeld ist eine zusammenhängende Liste von ´Flags. Dazu werden Aufzählungswerte (Flags-Aufzählungen) oder IntegerWerte verwendet. Jedes Bit der Integer-Werte (Aufzählungen stellen auch einen Integer-Wert dar) stellt dabei ein Flag dar. Der Wert 0 bedeutet, dass das Flag nicht gesetzt ist, bei 1 ist das Flag gesetzt. So können in einem Integer-Wert bis zu 64 (bei ulong) einzelne boolesche Informationen gespeichert werden. In einem Bitfeld besitzen die einzelnen Bits von rechts aus gesehen einen Wert, der die Potenz von 2 mit ihrem Positionsindex ergibt (der bei 0 beginnt). Das äußerst rechte Bit besitzt also den Wert 20 = 1, das zweite (von rechts) den Wert 21 = 2 und das dritte den Wert 22 = 4. Tipp: Wenn Sie das einmal ausprobieren wollen, konvertieren Sie mit Convert.ToByte(bitString, 2) einen String, der einen dualen Wert enthält (z. B. "00000101"), in einen Byte-Wert.
Bitweise Arithmetik Bitweises Exklusiv Oder
Bitweises Nicht Bitweises Oder
1284
Zum Lesen und Setzen der einzelnen Flags wird die bitweise Arithmetik verwendet (´Bitweises Und, ´Bitweises Nicht, ´Bitweises Oder, ´Bitweises Exklusiv Oder). Arithmetik, die sich auf die einzelnen Bits eines Integer-Werts bezieht. ´Bitfeld, ´Bitweises Und, ´Bitweises Nicht, ´Bitweises Oder, ´Bitweises Exklusiv Oder Beim bitweisen Exklusiv Oder (^ in C#) werden zwei ´Bitfelder so miteinander kombiniert, dass im Ergebnis die Flags gesetzt sind, die in der einen oder der anderen, aber nicht in beiden Listen gesetzt sind. 0101 ^ 0110 ergibt 0011. Über das logische Exklusiv Oder können Sie einzelne Flags in einem Bitfeld gezielt zurücksetzen, unabhängig davon, ob diese gesetzt sind oder nicht. Wollen Sie z. B. im Bitfeld x das dritte Flag (von rechts gesehen, das mit dem Wert 22 = 4) zurücksetzen, verwenden Sie die folgende Logik: x ^ 4. Das bitweise Nicht (! in C#) kippt alle ´Flags einer Flag-Liste um. !0101 ergibt 1010. Beim bitweisen Oder (| in C#) werden zwei ´Bitfelder so miteinander kombiniert, dass im Ergebnis die Flags gesetzt sind, die in der einen oder der anderen Liste gesetzt sind. 0101 | 0110 ergibt 0111. Über das logische Oder können Sie einzelne Flags in einer Flag-Liste gezielt setzen, unabhängig davon, ob diese bereits gesetzt sind oder nicht.
Glossar
Beim bitweisen Und (& in C#) werden zwei ´Bitfelder so miteinander kombiniert, dass im Ergebnis die Flags gesetzt sind, die in der einen und der anderen Liste gesetzt sind. 0101 & 0110 ergibt 0100. Über das logische Und können Sie herausfinden, ob ein Flag in einer Flag-Liste gesetzt ist. Blog Blog ist die Kurzform von »Weblog«. Ein Weblog ist eine Art Tagebuch im Internet. Viele Entwickler haben ein eigenes Blog, in dem sie ihre Erfahrungen verbreiten. Das Weblog von Scott Guthrie, einem Microsoft-Mitarbeiter, ist ein gutes Beispiel: weblogs.asp. net/scottgu. Business-Objekt- Ein Business-Objekt-Modell (BOM), oder auch Geschäftsmodell, Modelle bildet die Objekte einer Firma mit allen Beziehungen und Regeln als Objektmodell ab. Das BOM einer Firma kann z. B. aus den Klassen Customer, Product und Order bestehen, die über Referenzen miteinander in Beziehung stehen. Regeln definieren, wie der Status der Objekte geändert werden darf. Ein Kunde, der gesperrt ist, darf z. B. keiner Bestellung zugeordnet werden. CIL-Code CIL-Code (Intermediate Language Code) ist die Bezeichnung für ´Zwischencode in .NET. COM Vereinfacht ausgedrückt liefert das veraltete Component Object Model einen Standard, der ermöglicht, dass Anwendungen mit Objekten kommunizieren, die entweder in anderen Anwendungen oder in speziellen COM-DLL oder -EXE-Dateien gespeichert sind. Diese auch als Komponenten bezeichneten Dateien enthalten in der Regel ´Klassen, aus denen die benutzende Anwendung Objekte erzeugt. Ein Beispiel für eine solche COMKomponente ist ADO (ActiveX Data Objects), die Klassen zur Arbeit mit Datenbanken enthält. Jedes Programm, das COM-fähig ist, kann eine solche Komponente verwenden. COM-Komponenten sind mittlerweile durch die moderneren .NET-Komponenten ersetzt. Compiler Ein Compiler erzeugt aus Quellcode ausführbaren Code, der in Form einer Datei abgespeichert wird. Die ausführbare Datei wird dann später entweder direkt vom Betriebssystem ausgeführt (´Native Programme) oder, wenn es sich um ´Zwischencode handelt, über einen ´Just In Time Compiler. Der von einem Compiler ausgewertete Quellcode kann alles Mögliche sein. Ein C#-Compiler erzeugt z. B. aus C#-Code ´CIL-Code, ein C++Compiler kann aus C++-Quellcode ´Maschinencode erzeugen, ein XAML-Compiler erzeugt aus XAML-Dokumenten CIL-Code. Ein Compiler unterscheidet sich von einem ´Parser dadurch, dass ein Compiler Quellcode in ausführbare Dateien umsetzt. Ein Parser hingegen wertet Quellcode in der Laufzeit einer Anwendung aus. Component ´COM Object Model Contract First ´ Datenvertrag Design
Bitweises Und
A
1285
Anhang
Cross Site Scripting
Cross Site Scripting bezeichnet eine Hacker-Technik, die es ermöglicht, in einer Webanwendung aus einem Kontext heraus, der als nicht vertrauenswürdig eingestuft wird, Informationen in einen anderen Kontext einzufügen, der als vertrauenswürdig eingestuft wird. Nähere Informationen zu diesem komplexen Thema finden Sie bei Wikipedia: de.wikipedia.org/wiki/Cross-Site_Scripting. Datenvertrag Der etwas abstrakte Begriff »Datenvertrag« (Data contract) gehört zu dem Programmiermuster »Contract First Design«. Dieses Programmiermuster erleichtert den Datenaustausch zwischen zwei Systemen. Dabei wird vor der eigentlichen Programmierung ein »Vertrag« definiert, der bestimmt, wie die Daten ausgetauscht werden. An diesen Datenvertrag halten sich sowohl Sender als auch Empfänger. Der Vertrag kann in Form einer Schnittstelle definiert werden oder mit anderen Techniken wie der Beschreibung der Datenstruktur in XML-Form (z. B. als XML-Schema). Bei Webdiensten werden Datenverträge in einer besonderen Form, der WSDL (Web Service Description Language) definiert. Wenn ein Webserver einen Webdienst zu Verfügung stellt (wie z. B. bei Yahoo, wo mehrere Webdienste es ermöglichen, Yahoo-Inhalte abzufragen, siehe developer.yahoo.com), bietet er auch ein WSDLDokument mit der Beschreibung des Webdienstes. Über dieses können andere Systeme die Struktur der Daten ermitteln und bei der Kommunikation berücksichtigen. Wenn Sie z. B. mit Visual Studio einen Webdienst referenzieren (über eine Webdienst-Referenz), liest Visual Studio das zu dem Webdienst gehörende WSDLDokument aus und erzeugt daraus eine Proxy-Klasse, die die Regeln des Datenvertrags einhält. Dekorator-ProDas Dekorator-Programmiermuster ist eines der vielen ´Entgrammiermuster wurfsmuster, die in der Programmierung angewendet werden. Danach wird ein spezielles Objekt, der Dekorator, vor ein anderes Objekt geschaltet. Das Dekorator-Objekt enthält die gesamte Schnittstelle des dekorierten Objekts und kann deshalb von außen wie dieses verwendet werden. Weil ein Dekorator alle Methodenaufrufe vor und den Zugriff auf Eigenschaften abfängt, ist er in der Lage diese zu verändern. Mehrere Informationen zu diesem Programmiermuster finden Sie bei Wikipedia: de.wikipedia.org/wiki/Decorator. Deserialisieren Deserialisieren ist der umgekehrte Vorgang zum ´Serialisieren. Dabei werden serialisierte Objekte wieder in ihre Objektform gebracht. Dazu muss der Typ des Objekts bekannt sein. Detailtabelle Eine Detailtabelle ist eine Datenbanktabelle, die über ein Schlüsselfeld einen Datensatz in einer anderen, der Mastertabelle, referenziert. Ein Beispiel dafür ist eine Tabelle, die Artikeldaten verwaltet. Ein Feld (z. B. CategoryID) speichert die ID einer Artikel-Kategorie. Eine andere Tabelle verwaltet die Artikel-Kategorien. Ein Feld, das üblicherweise der ´Primärschlüssel ist, verwaltet die ID, die in der Detailtabelle referenziert wird. Um sicherzustellen, dass Detaildaten nur existierende Masterdaten referenzieren können, erlauben die meisten Datenbanksysteme ´Referentielle Integrität.
1286
Glossar
DLL-Datei
Eigenschaft
Entität
Entwurfsmuster
Enumerator
Als »DLL-Datei« wird eine klassische Datei mit der Endung .dll bezeichnet, die Funktionen in für ein Betriebssystem kompilierter Form enthält. Eine DLL-Datei ist damit fast so etwas wie eine Anwendung, nur dass eine Anwendung zusätzlich noch einen Einsprungpunkt definiert, der dem Betriebssystem mitteilt, womit die Anwendung startet. Klassische DLL-Dateien können von Anwendungen verwendet werden. Die Anwendung muss dazu aber die Signatur der in der DLL-Datei enthaltenen Funktionen kennen.
A
Dateien mit der Endung .dll können auch ´COM-Komponenten sein, die keine Funktionen, sondern Klassen enthalten. .NETAssemblys, die Klassenbibliotheken sind, besitzen ebenfalls die Endung .dll. Der Begriff »DLL-Datei« steht aber im Allgemeinen für eine klassische DLL-Datei. Eine Eigenschaft ist ein Teil einer ´Klasse, in der Daten gespeichert werden. Eigenschaften sind prinzipiell so etwas wie Variablen, gehören aber wie Methoden zu einer Klasse. In der objektorientierten Programmierung verwalten Objekte (Instanzen von Klassen) ihre Daten in Eigenschaften. Viele OOP-Sprachen bezeichnen lediglich öffentliche »Datenspeicher«, die beim Schreiben oder Lesen Programmcode ausführen können, der z. B. beim Schreiben den zu schreibenden Wert überprüft, als Eigenschaften. Einfache Datenspeicher, die keinen Programmcode ausführen, werden dann als »Felder« bezeichnet. Eine Entität (englisch: Entity) ist in der Informatik im Allgemeinen ein eindeutig identifizierbares Objekt, dem Informationen zugeordnet sind. In einer objektorientierten Anwendung ist jedes Objekt eine Entität. In einer relationalen Datenbank ist ein einzelner Datensatz einer Tabelle eine Entität. An diesem Beispiel wird auch deutlich, warum dieser Begriff überhaupt verwendet wird. Damit können Sie ein logisches Objekt bezeichnen, dessen Speicherform damit aber nicht festgelegt ist. Ob das Objekt als Objekt in einem Programm existiert, als Objekt serialisiert ist oder als Datensatz in einer Tabelle gespeichert ist, ist vollkommen unerheblich. Die Entität »Kunde« steht immer für einen Kunden, egal in welcher Form dieser verwaltet wird. Ein Entwurfsmuster, das im Englischen als »Design Pattern« bezeichnet wird, beschreibt ein in der Praxis erprobtes Vorgehen zur Lösung von typischen Programmier-Problemen. Das Singleton-Muster beschreibt z. B., wie das Problem gelöst werden kann, dass innerhalb einer Anwendung von einer Klasse nur eine Instanz existieren darf. Die (maßvolle) Anwendung von Programmiermustern macht eine Anwendung fehlerfreier und besser wartbar. Weitere Informationen zu Entwurfsmustern finden Sie an der Adresse de.wikipedia.org/wiki/Entwurfsmuster. Als Enumerator bezeichnet Microsoft einen ´Iterator, der ein Objekt ist, das die IEnumerator- oder IEnumerator-Schnittstelle implementiert.
1287
Anhang
FAQ
Flache Kopie
Flag
GarbageCollector
GUID Hash oder Hashcode
FAQ steht für Frequently Asked Questions, also für häufig gestellte Fragen. In einem FAQ werden diese Fragen und die Antworten darauf veröffentlicht. Eine flache Kopie (Shallow Copy) ist eine »normale« Kopie von Objekten. Referenztypen werden kopiert, indem die Referenz weitergegeben wird. Bei Werttypen wird deren Inhalt kopiert, also der Wert aller Felder. Sind Felder des Werttypen Referenztypen, werden wieder nur die Referenzen weitergegeben. Eine flache Kopie steht damit im Gegensatz zu einer ´tiefen Kopie. Ein Flag ist ein Hilfsmittel zur Kennzeichnung eines booleschen Zustands. Ein Flag kann den Zustand Wahr bzw. Ja oder Falsch bzw. Nein besitzen. Flags können in booleschen Variablen verwaltet werden. Werden mehrere zusammengehörige Flags verwaltet, geschieht dies meist als ´Bitfeld. Ein Garbage-Collector (»Müllsammler«) ist ein Prozess in einer Anwendung, der immer dann, wenn die Anwendung selbst gerade nicht beschäftigt ist, den Speicher nach unbenutzten Objekten (quasi nach »Müll«) durchsucht und diese dann freigibt. Einige Programmiersprachen wie Java und die Sprachen von Microsoft .NET integrieren einen Garbage-Collector in die mit diesen Sprachen erstellten Programme. Der Programmierer ist damit nicht mehr gezwungen – wie z. B. in C++ und Pascal – benutzte Objekte auch wieder freizugeben. Der Garbage-Collector kümmert sich automatisch um die »Entsorgung« dieser Objekte. ´UUID Ein Hash(code) ist ein Code, der Daten in kürzerer Form als die Daten selbst (möglichst) eindeutig identifiziert. In vielen Fällen besteht ein Hashcode aus einem Integer-Wert, der aus den Daten berechnet wird. Der Hashcode der Zeichenkette »Hallo« ist z. B. 7955905, der der Zeichenkette »Die Antwort auf die Frage aller Fragen ist 42« ist -791724380. .NET-Objekte besitzen zur Ermittlung eines Integer-Hashcode die von object geerbte Methode GetHashCode (diese muss allerdings in der jeweiligen Klasse korrekt überschrieben worden sein, damit der Hashcode wirklich eindeutig ist). Hashcodes werden immer dann verwendet, wenn Daten identifiziert werden müssen, für die ansonsten kein eindeutiger Identifizierer zur Verfügung steht. In .NET-Auflistungen vom Typ Dictionary wird z. B. der Hashcode des Schlüssels eines zu speichernden Werts gespeichert (und nicht der Schlüssel selbst). Ein anderer Einsatzbereich ist der Schutz vor ungewollter Veränderung von Daten. Dazu wird der Hashcode der Daten entweder getrennt von den Daten oder verschlüsselt mit den Daten übermittelt. Beim Lesen der Daten wird der Hashcode entschlüsselt und mit dem neu berechneten Hashcode verglichen. Sind beide gleich, wurden die Daten nicht verändert.
1288
Glossar
Heap
Der Heap ist ein Speicherbereich, der global für das gesamte Programm gilt und so lange besteht, wie das Programm läuft. Auf dem Heap werden üblicherweise programmglobale Daten, aber eben auch Referenztypen abgelegt. I/O I/O steht für »Input/Output« und bezeichnet im Allgemeinen Einund Ausgabe-Operationen, also z. B. das Lesen und Schreiben einer Datei. Im Deutschen wird I/O auch als E/A (Eingabe/Ausgabe) bezeichnet. Ini-Dateien Ini-Dateien sind mittlerweile veraltete Textdateien, die Initialisierungsdaten in einer speziellen Form verwalten. Die Endung einer solchen Datei ist .ini. Der Zugriff auf Ini-Dateien war im Vergleich zu der einfachen Konfiguration unter .NET sehr kompliziert. Iterator Ein Iterator ist in der allgemeinen Informatik eine Art Zeiger, mit dem durch die Elemente einer Liste iteriert werden kann. Er lässt im Gegensatz zu einem Index oder einem Schlüssel den direkten Zugriff auf die in der Liste gespeicherten Elemente zu. Microsoft bezeichnet Iteratoren verwirrenderweise als »Enumeratoren«. Ein Iterator ist für Microsoft ein Enumerator, der über die yieldAnweisung implementiert wird. JIT ´Just-In-Time-Compiler Just-In-TimeEin Just-In-Time-Compiler (JIT) wertet ein in einem ´ZwischenCompiler code vorliegendes Programm so aus, dass der gerade ausgeführte Programmteil »just in time« kompiliert, für spätere Ausführungen zwischengespeichert und dann ausgeführt wird. Ab dem zweiten Ausführen dieses Programmcodes wird dann der zwischengespeicherte ´native Programmcode verwendet. Klasse Eine Klasse wird in der objektorientierten Programmierung verwendet und beschreibt, welche ´Eigenschaften und ´Methoden (und Ereignisse) Objekte besitzen, die mit Hilfe dieser Klasse erzeugt werden. Klassenbibliothek Eine Klassenbibliothek ist, wie der Name schon sagt, eine Bibliothek, die Klassen enthält. Klassenbibliotheken sind in der Regel vorkompiliert und können in Programme eingebunden werden, damit der Programmierer aus den enthaltenen Klassen Objekte erzeugen und mit diesen Objekten arbeiten kann. Moderne Programmiersprachen liefern sehr viele Klassenbibliotheken mit, die dann aber manchmal etwas anders bezeichnet werden, Java verwendet z. B. den Begriff Package. Komponenten Komponenten sind im allgemeinen Sprachgebrauch Teile einer Anwendung, die als Ganzes auch in anderen Anwendungen verwendet werden können. In Windows.Forms sind Komponenten Klassen, die von Component abgeleitet sind und deswegen ähnlich Steuerelementen auf ein Windows.Forms-Formular gezogen werden können. Der Visual-Studio-Designer kümmert sich um den notwendigen Programmcode zur Erzeugung und Initialisierung der Komponenten. Der Vorteil einer Windows.Forms-Komponente gegenüber einer einfachen Klasse ist, dass Sie die Eigenschaften über das Visual-Studio-Eigenschaftenfenster einstellen und die Ereignisse ebenfalls über Visual Studio zuweisen können.
A
1289
Anhang
Konsole
Konsolenanwendung LambdaAusdruck
Logarithmus
Man in the Middle
Maschinencode
Mastertabelle MDA
1290
Eine Konsole ist eine einfache Umgebung für die Ausführung von Programmen, die keine eigene Oberfläche besitzen. Eine Konsole bietet dem Anwender eine Eingabeaufforderung (den Prompt), an der dieser einfache Text-Eingaben vornehmen kann. Konsoleprogramme laufen in dieser Konsole, nehmen die Eingaben des Benutzers entgegen und können Ergebnisse oder Meldungen in einfacher Textform ausgeben. Eine Konsolenanwendung verwendet die Konsole für Eingaben und Ausgaben (in Textform). Die Konsole ist vergleichbar mit der alten DOS-Eingabeaufforderung oder der Powershell von Vista. Der Begriff »Lambda-Ausdruck« entstammt dem Lambda-Kalkül, das in den 1930er Jahren entwickelt wurde. Im Wesentlichen geht es bei diesem Kalkül um die Konstruktion eines Ausdrucks, der sich wie ein zu lösendes Problem verhält. Damit können Problemlösungen als Ausdruck dargestellt werden. Näheres zu diesem Kalkül finden Sie bei Wikipedia: de.wikipedia.org/wiki/LambdaKalk%C3%BCl. Ein Logarithmus ist der Exponent zu einer Basis, der eine bestimmte Zahl ergibt. Wird z. B. der Logarithmus der Zahl 10 zur Basis 2 gesucht, bedeutet das: »Mit welcher Zahl muss 2 potenziert werden, sodass 16 herauskommt«. Das Ergebnis hier wäre 4 (24 = 16). Der so genannte natürliche Logarithmus verwendet als Basis die so genannte Eulersche Zahl (2,718281828459…). Als »Man in the Middle« wird ein Angreifer bezeichnet, der sich in die Datenkommunikation zwischen zwei Partnern geschleust hat. Der Angreifer kann den Datenverkehr komplett auslesen und manipulieren, in vielen Fällen ohne dass Sender und Empfänger davon etwas mitbekommen. Voraussetzung dafür ist natürlich, dass der Man in the Middle sich entweder physikalisch oder logisch zwischen die Kommunikationspartner einschleusen kann, z. B. über einen physischen Zugang zum Netzwerk oder über einen Router. Man-in-the-Middle-Angriffe können wirkungsvoll über eine Verschlüsselung der übertragenen Daten bekämpft werden. Maschinencode ist die Bezeichnung für Befehle, die direkt von einem Betriebssystem (bzw. von einer CPU) ausgeführt werden können. Viele C++ ´Compiler erzeugen Programme, die in reinem Maschinencode vorliegen und deshalb nicht von einem ´Just In Time Compiler ausgeführt werden müssen. ´ Detailtabelle Ein MDA (Mobile Digital Assistant) ist ein ´PDA, der mit einem Handy integriert ist. Ein MDA wird auch als »Smartphone« bezeichnet.
Glossar
MDI
Methode
MIME
Modal
Native Programme
Not Or Ordinaler Wert
MDI(Multiple-Document-Interface)-Anwendungen sind Anwendungen, die aus einem Haupt- und mehreren Unterfenstern bestehen. Das Hauptfenster bietet einen Rahmen für alle Unterfenster, unter anderem auch ein Menü und die Symbolleiste. MDI-Unterfenster können sich nur innerhalb des Hauptfensters bewegen und vom Hauptfenster aus in verschiedenen Formen angeordnet werden. Microsoft Word oder Excel sind typische Beispiele für diese Art von Anwendungen (allerdings nur in der Ansicht, die alle Dokumente in einer Instanz der Anwendung verwaltet). Eine Methode gehört zu einem Objekt. Methoden erledigen die Aufgabe, für die sie entwickelt wurden, oder berechnen ein Ergebnis. Bei einem Auto-Objekt in der realen Welt erledigt die Methode Starten das Starten des Motors und die Methode GaspedalBetätigen führt dazu, dass der Motor beschleunigt wird. MIME (Multipurpose Internet Mail Extensions) ist ein Standard, der die Struktur und den Aufbau von mehrteiligen E-Mails und anderer Internetnachrichten festlegt. Ein für Programmierer wichtiger Teil dieses Standards sind die MIME-Typen (oder Inhaltstypen), die die Art der (Teil-)Daten einer Nachricht definieren. Der MIME-Typ text/plain steht z. B. für reine Textdaten, text/xml für XML-Daten. Die MIME-Typen werden sehr übersichtlich in SELFHTML erläutert: de.selfhtml.org/diverses/mimetypen.htm. Ein modales Windows-Fenster wird (im Gegensatz zu einem ´unmodalen Fenster) im Vordergrund vor einem anderen Fenster angezeigt (zu dem es modal ist) und verhindert, dass das andere Fenster Benutzereingaben empfangen kann. Das modale Fenster muss erst geschlossen werden, damit der Benutzer mit dem anderen Fenster wieder arbeiten kann. Ein Beispiel für ein modales Fenster ist der Datei-öffnen-Dialog vieler Anwendungen, der erst geschlossen werden muss, bevor mit der Anwendung weitergearbeitet werden kann. Native Programme liegen in einem ´Maschinencode vor, der direkt von einem Betriebssystem ausgeführt werden kann und nicht erst über einen ´Just In Time Compiler interpretiert werden muss. ´Bitweises Nicht ´Bitweises Oder Ein ordinaler Wert ist ein Wert aus einer Reihe von Werten, bei denen alle Werte einen direkten Vorfahren haben (es sei denn, es handelt sich um den ersten Wert der Reihe) und einen direkten Nachfahren (es sei denn, es handelt sich um den letzten Wert der Reihe). Ganzzahlen zählen zu den ordinalen Werten, genau wie boolesche Werte, der Typ char oder Aufzählungen. Fließkommawerte und andere Dezimalzahlen gehören nicht zu den ordinalen Werten, da diesen keine direkten Vorfahren und Nachkommen zugeordnet werden können.
A
1291
Anhang
Parser
PDA
Prädikat
Primärschlüssel
Primzahl
1292
Ein Parser interpretiert einen Quellcode und erzeugt daraus in der Laufzeit der Anwendung entweder ´Zwischencode oder ´Maschinencode, der dann von einem ´Just In Time Compiler oder direkt vom Betriebssystem ausgeführt werden kann. Ein Parser unterscheidet sich von einem ´Compiler dadurch, dass ein Parser den Quellcode in der Laufzeit der Anwendung auswertet. Ein Compiler hingegen erzeugt ausführbare Dateien. Ein PDA (Personal Digital Assistant) ist ein kleiner, tragbarer Computer ohne Tastatur, der über ein berührungsempfindliches LCD-Display bedient wird. In der Logik ist ein Prädikat etwas, das über ein Subjekt ausgesagt wird. In der Grammatik ist ein Prädikat eine Satzaussage, die ein Subjekt näher bestimmt. Ein Prädikat bestimmt also Eigenschaften eines Subjekts und kann dazu verwendet werden, ein oder mehrere Subjekte (in der OOP sind das allerdings Objekte) zu lokalisieren, die dem Prädikat entsprechen. In LINQ ist ein Prädikat ein Lambda-Ausdruck oder eine Methode, die einen booleschen Wert zurückgibt. In den meisten Fällen entspricht ein Prädikat dem Delegaten Func und wird als Argument einer Methode übergeben. An einem solchen Argument können Sie einen Lambda-Ausdruck oder eine Methode übergeben, dem bzw. der am ersten Argument ein Objekt vom Typ T übergeben wird. Der Ausdruck bzw. die Methode gibt true zurück, wenn eine Bedingung erfüllt ist, und false, wenn diese Bedingung nicht erfüllt ist. Die aufrufende Methode (die das Prädikat aufruft) übernimmt das übergebene Element dann abhängig von der Rückgabe in ein Ergebnis. Ein Primärschlüssel ist in einer Datenbanktabelle ein Feld oder eine Kombination mehrerer Felder, dessen (kombinierter) Wert bezogen auf alle Datensätze eindeutig ist. Das Feld oder die Felder sind in der Datenbank als Primärschlüssel gekennzeichnet und eindeutig indiziert. Primärschlüssel werden zum einen verwendet, um Datensätze eindeutig identifizieren zu können. Zum anderen werden Primärschlüssel in ´Mastertabellen eingesetzt, um eine Beziehung zu ´Detailtabellen aufzubauen. Eine Primzahl ist eine Zahl größer als 1, die nur durch 1 und sich selbst ohne Rest teilbar ist. Primzahlen haben eine Bedeutung in der Verschlüsselungstechnik. Gute Informationen über Primzahlen finden Sie an der Adresse www.primzahlen.de.
Glossar
Refactoring
Referentielle Integrität
Referenz
Registrierdatenbank Registry Rendern
SDK
Refactoring bedeutet bei der Programmierung den Umbau eines Quellcodes, in der Regel um eine bessere Qualität des Quellcodes und des Programms zu erreichen, ohne die Funktionalität des Programms zu verändern. Dabei werden häufig die Namen von ´Klassen, Strukturen, anderen ´Typen und von ´Methoden, ´Eigenschaften und Variablen geändert, weil die Aussagekraft der alten Namen nicht besonders hoch ist (um es milde auszudrücken …). Refactoring bedeutet aber auch, den Quellcode daraufhin zu untersuchen, ob dieser (möglichst) optimal programmiert wurde, und diesen gegebenenfalls zu optimieren. In vielen Fällen bedeutet dies, dass redundanter Quellcode in Methoden ausgelagert wird, zu große Methoden in mehrere kleinere aufgetrennt werden, überflüssiger Quellcode entfernt und fehlerhaft oder zu kompliziert programmierter Quellcode ersetzt wird. Refactoring ist ein heikler Prozess, bei dem darauf geachtet werden muss, dass die Funktionalität der Anwendung bestehen bleibt und keine (neuen) Fehler eingebaut werden. Weil dies eine Menge Erfahrung erfordert, werden Refactoring-Jobs in der Regel sehr gut bezahlt. Referentielle Integrität bezeichnet eine Technik, die in einem Datenbanksystem dafür sorgt, dass Beziehungen zwischen ´Master- und ´Detailtabellen nicht ungültig werden dürfen. Referentielle Integrität wird immer zwischen dem ´Primärschlüssel der Mastertabelle und dem Fremdschlüssel der Detailtabelle hergestellt. Sie sorgt dafür, dass im Fremdschlüssel-Feld der Detailtabelle nur Werte angegeben werden können, die im ´Primärschlüssel-Feld der Mastertabelle auch vorkommen. Eine Sonderform der referentiellen Integrität ist die, die Nullwerte im Fremdschlüsselfeld zulässt. Damit ist es möglich, dass ein Detaildatensatz keinen Masterdatensatz referenziert. Eine Referenz ist im Prinzip ein ´Zeiger auf ein Objekt. Die Dereferenzierung einer Referenz ist aber wesentlich einfacher – über den Punkt-Operator – als bei Zeigern. Der Vorteil ist wie bei Zeigern, dass auch gleichzeitig mehrere Referenzen auf ein Objekt zeigen können. ´Registry
A
Die Registry ist eine Datenbank, in der Windows alle Einstellungen für das System und für die installierten Programme verwaltet. Der Begriff »Rendern« bezeichnet einen Vorgang, bei dem aus Rohdaten Mediendaten entstehen. In .NET wird dieser Begriff in ASP.NET und WPF verwendet. In beiden Umgebungen rendern Steuerelemente aus ihren Daten ihre visuelle Darstellung auf dem Bildschirm. Ein SDK (Software Development Kit) ist eine Sammlung aus Tools, Programmen und Dokumentationen, die bei der Entwicklung von Software hilfreich eingesetzt werden können. Viele Hersteller von Programmiersprachen (besonders Microsoft) bieten meist gleich mehrere SDKs für verschiedene Bereiche an.
1293
Anhang
Serialisieren
Signatur
Als Serialisieren wird ein Vorgang bezeichnet, bei dem ein Objekt in eine Form gebracht wird, die gespeichert oder über ein Netzwerk übertragen werden kann. In .NET können Sie Objekte z. B. nach XML serialisieren oder in ein binäres Format. Ein serialisiertes Objekt enthält nur die Daten des Objekts, je nach Serialisierungsform nur die öffentlichen oder auch die privaten. Die Methoden werden (natürlich) nicht serialisiert. Beim ´Deserialisieren muss der Typ des Objekts bekannt sein. Der Begriff »Signatur« steht bei Funktionen, Prozeduren und Methoden für die Argumentliste und den Rückgabedatentyp derselben. Die Übergabearten (»By Value« oder »By Reference«), die Datentypen der einzelnen Argumente und der Datentyp des Rückgabewerts (falls es sich um eine Funktion handelt) definieren zusammen also die Signatur. Ob der Rückgabewert zur Signatur einer Methode gehört oder nicht, ist übrigens umstritten. In der Java-Dokumentation wird der Rückgabetyp z. B. nicht in die Signatur aufgenommen. Beim Überschreiben und Neudefinieren von Methoden berücksichtigt der C#-Compiler den Rückgabewert ebenfalls nicht. In der Signatur eines Delegaten ist der Rückgabewert aber enthalten. Anderer Name für einen ´MDA. ´SDK
Smartphone Software Development Kit SQL Die Structured Query Language ist eine Sprache zur Abfrage und Bearbeitung von Datenbanken. Die meisten relationalen Datenbanksysteme bieten die Möglichkeit, über SQL Daten abzufragen, anzufügen, zu aktualisieren oder zu löschen (u. a.). Die verschiedenen Datenbanksysteme weichen dabei (leider) etwas vom Standard ab, der zurzeit (leider noch) SQL 99 ist. SQL Injection Der SQL Server hat ein »kleines« Problem: In einer ´SQL-Anweisung kann über zwei Striche ein Kommentar eingeleitet werden. Dies kann ein Angreifer in einer Anwendung ausnutzen, um seine eigenen SQL-Anweisungen auszuführen und damit entweder Schaden anzurichten oder geschützte Daten abzufragen. Erwartet eine Anwendung z. B. die Angabe eines Namens zur Suche in einer Kunden-Datenbank, kann der Angreifer über die folgende Eingabe ein Löschen der Kunden-Daten erreichen (vorausgesetzt er kennt den Namen der Kunden-Tabelle und die Datenbankverbindung verfügt über entsprechende Rechte): 1 = 2; DELETE FROM Customers --
Diese Eingabe führt u. U. zu der folgenden (gültigen) SQL-Anweisung: SELECT * FROM Customers WHERE 1 = 2; DELETE FROM Customers
1294
Glossar
Stack
Subklasse
Superklasse
Thread
Tiefe Kopie
UI-Thread
UNC-Pfad
Unicode
Unmodal
Der Stack ist ein spezieller Speicherbereich, den der Compiler für jede aufgerufene Methode neu reserviert. Alle lokalen Daten einer Methode werden, sofern es sich um Werttypen handelt, auf dem Stack abgelegt. Der Stack wird auch intern verwendet, um Argumente an eine Methode zu übergeben. Der aufrufende Programmteil legt die Argumente, die an die Methode übergeben werden sollen, auf dem Stack ab, die Methode liest diese Argumente dann aus dem Stack aus. Wird bei der Vererbung eine Klasse von einer anderen Klasse (einer ´Superklasse) abgeleitet, bezeichnet man diese als Subklasse. Als Superklasse wird in der OOP eine Klasse bezeichnet, von der eine so genannte ´Subklasse über die Vererbung abgeleitet wird. Die Subklasse erbt damit alle Eigenschaften, Methoden und Ereignisse der Superklasse. Eine Superklasse wird auch als Basisklasse bezeichnet. Ein Thread (Programmfaden) ist ein Programmteil, der parallel und quasi gleichzeitig zu anderen Programmteilen ausgeführt wird. Das Drucken im Hintergrund bei Microsoft Word ist ein gutes Beispiel für einen Thread, der eine Aufgabe im Hintergrund erledigt, während der Anwender im Vordergrund weiter arbeiten kann. Threads werden in Kapitel 20 behandelt. Bei einer tiefen Kopie (Deep Copy) werden (im Gegensatz zur ´flachen Kopie) Referenztypen beim Kopieren nicht einfach über die Referenz weitergegeben, sondern es werden neue Instanzen erzeugt, in die der Inhalt des kopierten Objekts kopiert wird. Sind Felder des Objekts auch Referenztypen, werden auch diese tief kopiert. Das gilt auch für das Kopieren von Werttypen, wenn deren Felder Referenztypen sind. Eine tiefe Kopie erreichen Sie am einfachsten über das ´Serialisieren und ´Deserialisieren. Der UI-Thread einer (Windows-)Anwendung ist der Haupt´Thread, in dem die Anwendung ausgeführt wird. Zusätzlich dazu besitzt eine .NET-Anwendung noch standardmäßig weitere Threads, in denen z. B. der ´Garbage Collector ausgeführt wird. UNC steht für »Uniform Naming Convention« oder »Universal Naming Convention«. UNC ist ein Standard zum Ansprechen von freigegebenen Ressourcen in einem lokalen Netzwerk. Das Format ist \\Servername\Freigabename\Pfad bzw. \\IP-Adresse\ Freigabename\Pfad. Unicode ist ein aktueller ´Zeichensatz, der die einzelnen Zeichen in bis zu vier Byte verwalten kann. Näheres dazu finden Sie in Kapitel 1. Ein unmodales Fenster verhindert nicht, dass Benutzereingaben zu einem anderen Fenster durchdringen. Es steht im Gegensatz zu einem ´modalen Fenster. Eine Anwendung kann gleichzeitig mehrere unmodale Fenster öffnen und dem Nutzer damit erlauben, zwischen den einzelnen Fenstern zu wechseln. Beispiele für unmodale Fenster sind die Dokument-Fenster von Microsoft Word oder Excel.
A
1295
Anhang
URI
Ein Uniform Resource Identifier (einheitlicher Bezeichner für Ressourcen) ist ein Bezeichner, der der Identifikation einer Ressource dient. Die Ressource muss dabei nicht wirklich vorhanden, sondern kann abstrakt sein. Eine E-Mail-Adresse ist z. B. ein URI, der die Ressource »E-Mail-Empfänger« bezeichnet, aber nicht festlegt, an welchem physikalischen Ort die Ressource gefunden wird (die Adresse »
[email protected]« legt zwar fest, dass der Mail-Empfangs-Server »juergen-bayer.net« heißt, aber nicht, wo genau das Postfach »kompendium« dort liegt). UTC-Zeit Die »Universal Time Coordinated« (Koordinierte Weltzeit) ist eine allgemeine Weltzeit, die aus der physikalischen Atomzeit und der astronomischen Zeit berechnet wird und die sich auf den 0-Meridian bezieht. Der 0-Meridian verläuft durch die englische Stadt Greenwich, weswegen UTC früher auch als Greenwich Mean Time (GMT) bezeichnet wurde. Eine Zeitangabe in UTC oder mit einem UTC-Offset kann an jedem Ort der Welt in die lokale Zeit umgerechnet werden. UUID Ein Universal Unique Identifier (Universal eindeutiger Identifizierer) wird auch als GUID bezeichnet (Global Unique Identifier). Eine weltweit eindeutige 128-Bit-Zahl, die über verschiedene aktuelle und zufällige Werte ermittelt wird. U. a. werden das bis auf die Millisekunde genaue Datum und aktuelle CPU-Registerinhalte für die Ermittlung der Zahl verwendet. Auch wenn gleichzeitig mehrere Tausend Benutzer auf der Welt eine UUID erzeugen, wird dabei keine doppelt vergeben. UUIDs eignen sich damit für die ein-eindeutige Identifizierung von Daten oder Ressourcen. Eine UUID wird üblicherweise hexadezimal mit Trennstrichen dargestellt. Eine typische UUID sieht folgendermaßen aus: 478B9B81-0A53-4137-B1CF-5FCC8F50B1F8. Verteilte Anwen- ´Client/Server-Anwendung dung Whitespace Ein »weißes Zeichen« ist ein im Druck oder auf dem Bildschirm (normalerweise) nicht sichtbares Zeichen. Typische WhitespaceZeichen sind das Leerzeichen, das Tabulatorzeichen und Zeilenumbrüche. Darüber hinaus gibt es aber auch weitere Whitespace-Zeichen wie z. B. das geschützte Leerzeichen (mit dem Wert 255).
1296
Glossar
XML-Namensraum
XML-Namensräume haben eine ähnliche Bedeutung wie die Namensräume in .NET: Sie trennen Elemente auf einer übergeordneten Ebene voneinander. Ein Element a, das dem Namensraum x zugeordnet ist, ist ein vollkommen anderes Element als ein Element a, das dem Namensraum y zugeordnet ist. Bedeutung haben XML-Namensräume beim Zusammenführen von verschiedenen XML-Dokumenten. Dabei kann es vorkommen, dass die zusammengeführten Dokumente auf derselben Ebene gleichnamige Elemente beinhalten. Gehören diese unterschiedlichen Namensräumen an, ist das aber kein Problem, da die Elemente über ihren Namensraum adressiert werden.
A
XML-Namensräume können alles Mögliche sein. In der Praxis werden häufig ´URIs und ´GUIDs verwendet. URIs müssen dabei nicht auf eine wirklich existierende Ressource im Internet verweisen, sondern können vollkommen fiktiv sein. Ein XML-Namensraum wird einem Element über das xmlns-Attribut zugeordnet. XOr Zeichensatz
Zwischencode
Weitere Informationen finden Sie in Kapitel 1. ´Bitweises Exklusiv Oder Ein Zeichensatz (auch: »Zeichenkodierung«) bestimmt die numerischen Werte von einzelnen darstellbaren Zeichen. Das große A besitzt z. B. in allen Standard-Zeichensätzen den Wert 65. Die verschiedenen Zeichensätze unterscheiden sich aber meist im Bereich der Zeichen ab dem Wert 128. Ältere Zeichensätze setzen 8 Bit für ein Zeichen ein, weswegen nur 265 Zeichen möglich sind. Der aktuelle ´Unicode-Zeichensatz verwendet hingegen bis zu vier Byte für ein Zeichen und ermöglicht damit nahezu alle Zeichen dieser Welt. Näheres zu Zeichensätzen finden Sie in Kapitel 1. Programm, das nicht in nativem Maschinencode vorliegt, sondern aus speziellen, CPU-unabhängigen Befehlen besteht. Zwischencode-Programme werden entweder von einem ´Interpreter oder einem ´Just-In-Time-Compiler ausgeführt.
1297
Inhalt
B
Die ersten 255 Zeichen des Unicode-Zeichensatzes A
Abbildung 1 zeigt die ersten 255 Zeichen des Unicode-Zeichensatzes, die mit den Zeichen des ISO-8859-1-Zeichensatzes identisch sind.
B
Die grau hinterlegten Zeichen sind Steuerzeichen. Die meisten dieser Zeichen, die den Anfängen der Nachrichtenübertragung entstammen, werden heute nicht mehr verwendet. Tabelle 1 beschreibt die in Abbildung 1 dargestellten, heute noch verwendeten Sonderzeichen.
C
1299
Index
Abbildung B.1: Die ersten 255 Zeichen der Unicode-Tabelle
Anhang
Tabelle B.1: Die heute noch verwendeten Sonderzeichen des Unicode- und ISO-8859-1Zeichensatzes
REF
1300
Sonderzeichen
Wert
Bedeutung
NUL
0
Nullzeichen. Markiert das Ende eines Strings in der Sprache C.
BS
8
Backspace (Rücktaste)
HT
9
Horizontaler Tabulator
LF
10
Line Feed (Zeilenvorschub)
FF
12
Form Feed (Seitenvorschub)
CR
13
Carriage Return (Wagenrücklauf bei der Schreibmaschine)
ESC
27
Escape-Zeichen
SPC
32
Space (Leerzeichen)
NBSP
160
Non Breaking Space (spezielles Leerzeichen, das in HTML bei mehrfachem Vorkommen nicht in ein Leerzeichen umgewandelt wird)
Informationen über alle Steuer- und sonstigen Sonderzeichen erhalten Sie bei Wikipedia an den Adressen de.wikipedia.org/wiki/Steuerzeichen, de.wikipedia.org/ wiki/Unicode-Block_Basis-Lateinisch und de.wikipedia.org/wiki/Unicode-Block_ Lateinisch-1%2C_Erg%C3%A4nzung.
Inhalt
C
Meine Namenskonvention für Steuerelemente A
Ich verwende zur Benennung von Steuerelementen eine Namenskonvention, bei der der Typ des Steuerelements mit einem zwei- bis dreistelligen, kleingeschriebenen Präfix vor den eigentlichen Namen gesetzt wird. Der Name btnOK benennt z. B. einen Button mit der Bedeutung, der OK-Schalter zu sein. Der Name txtPassword benennt eine TextBox, die ein Passwort aufnimmt.
B
C
Diese von Visual Basic stammende Namenskonvention ist in heutigen Zeiten zwar etwas archaisch, hat aber einen – in meinen Augen enormen – Vorteil: Wenn ich im Quellcode auf ein bestimmtes Steuerelement zugreifen will, muss ich lediglich den Typ des Steuerelements kennen. Ist dies z. B. eine TextBox, schreibe ich »txt« und betätige dann (STRG) + (____) um IntelliSense zu aktivieren. Die IntelliSense-Liste zeigt mir alle TextBox-Objekte untereinander an und ich kann die TextBox problemlos suchen, auf die ich zugreifen will. Außerdem erleichtert diese Konvention die Übersicht über den Quellcode. Einen Nachteil will ich aber nicht verschweigen: Mit der zunehmenden Anzahl an Steuerelementen wird es immer schwieriger, Präfixe für neue Steuerelemente zu finden. Tabelle 1 zeigt die Präfixe für die gängigen Steuerelemente Präfix
Button
btn
CheckBox
chk
CheckedListBox
chl
ColorDialog
cd
ComboBox
cbo
ContextMenu, ContextMenuStrip
ctx
DataGridView
dgv
DateTimePicker
dtp
FileSystemWatcher
fsw
FolderBrowserDialog
fbd
FontDialog
fd
Grid
grd
Tabelle C.1: Meine Namenskonvention für Steuerelemente und Komponenten von WPF und Windows.Forms
1301
Index
Steuerelement
Anhang
Tabelle C.1: Meine Namenskonvention für Steuerelemente und Komponenten von WPF und Windows.Forms (Forts.)
1302
Steuerelement
Präfix
GroupBox
gb
HScrollBar
hsb
Image
img
ImageList
il
Label
lbl
LinkLabel
ll
ListBox
lst
ListView
lv
Menu, MenuItem, MenuStrip, ToolStripMenuItem
mnu
MonthCalendar
mon
NotifyIcon
ni
NumericUpDown
num
OpenFileDialog
ofd
Panel
pnl
PasswordBox
pbx
PictureBox
pic
PrintDialog
pdl
ProgressBar
pbr
RadioButton
rb
RitchTextBox
rtb
SaveFileDialog
sfd
ScrollBar
sb
Slider
sli
StackPanel
sp
StatusBar
sbr
TabControl
tab
TextBlock
tb
TextBox
txt
Timer
tmr
ToolBar
tbr
Meine Namenskonvention für Steuerelemente
Steuerelement
Präfix
ToolTip
tt
TrackBar
tbr
TreeView
tv
VScrollBar
vsb
Tabelle C.1: Meine Namenskonvention für Steuerelemente und Komponenten von WPF und Windows.Forms (Forts.)
A
C
1303
Index
#endregion-Direktive 278 #error 228 #pragma 228 #region 228 #region-Direktive 278 #warning 228 .NET 68 CIL-Code 70 Programme 70 .NET Framework 68 Klassenbibliothek 72 Konfiguration 936 Quellcode evaluieren 560 .NET Framework SDK Tools 73 .NET Reflector 85 .resx-Ressourcen 948 .sln-Datei 91 .snk-Dateiendung 1233 .suo-Datei 91, 547 ??-Operator 213 @ 146 1:N-Beziehungen in LINQ to SQL 1103
A ■■■■■■■■■■ AbandonedMutexException 1194 Abbrechen von Threads 1164 Abfrageausdrücke 653 Abfrageeffizienz (LINQ to SQL) 1109 Abfragen LINQ to XML 1061 schachteln (LINQ) 705 siehe Verzweigungen XML mit Namensräumen 1065 Abhängigkeitseigenschaften (WPF) 768 Ablaufverfolgung siehe Protokollieren Ableiten siehe Vererbung Abort-Methode 1164
Abs-Methode 514 Absolutwert berechnen 514 Absteigend sortieren 405 Abstrakte Eigenschaften 300 Abstrakte Klassen 299 Abstrakte Methoden 300 AcceptsReturn-Eigenschaft (WPF) 852 Access-Einstellung (LINQ to SQL) 1092 Accessoren 253 Acos-Methode 514 Action-Delegat 323 Actions-Eigenschaft (WPF) 903 Activated-Ereignis (WPF) 787, 813 Activator-Klasse 1224, 1243 add-Accessor 333 AddAfter-Methode 428 AddAfterSelf-Methode 1057 AddBefore-Methode 428 AddBeforeSelf-Methode 1057 AddDays-Methode 509 AddedItems-Eigenschaft (WPF) 847 add-Element 940 AddFirst-Methode 1057 LinkedList 428 XElement-Klasse 1057 AddHours-Methode 509 Add-Methode DateTimeOffset-Struktur 509 Dictionary-Klasse 416 ICollection 383 IDictionary 385 TimeSpan-Struktur 512 XContainer-Klasse 1057 AddMilliseconds-Methode 509 AddMinutes-Methode 509 AddMonths-Methode 509 AddRange-Methode 396 AddSeconds-Methode 509 AddYears-Methode 509 AdoNetAppender-Klasse 591 AesCryptoServiceProvider-Klasse 1271 Aes-Klasse 1271 AesManaged-Klasse 1271 Aggregate-Methode (LINQ) 693
1305
Index
! ■■■■■■■■■■
Index
Aggregat-Methoden (LINQ) 690 Agile Softwareentwicklung 564 al.exe 74 Alias für Klassen 145 All-Methode (LINQ) 695 Alphawert 799 ALT-Taste abfragen 807 AncestorsAndSelf-Methode 1052 Ancestors-Methode 1051 Ändern von Daten (LINQ to SQL) 1114 Änderungsset durchgehen 1121 And-Operator 206 Anfügen von Daten (LINQ to SQL) 1114 Angefügte Eigenschaften (WPF) 776 Angefügte Ereignisse (WPF) 781 Anhalten eines Programms 545 Animationen (WPF) 933 Anonyme Methoden 322 Anonyme Typen 260 in LINQ 670 ANSI 62 Anweisungen 147 umschließen 131 unsichere 149 Anweisungsblöcke 148 Anwendungen Datenbankanwendungen 77 Grundaufbau 142 herunterfahren 788 konfigurieren 935 lokalisieren 953 Mehrfachstart verhindern 1195 normale 75 Ordner ermitteln 873 per Setup verteilen 972 starten 526 Webanwendungen 76 Anwendungsdomänen 1248 ausführbare Assemblys ausführen 1250 Datenaustausch 1253 entladen 1251 erzeugen 1250 Sinn 1249 Anwendungsmanifest 990 Anwendungsordner 614 Any-Methode (LINQ) 695 App.config 938 App.xaml 732, 786 AppDomainSetup-Klasse 1256 appender-ref-Element 596 AppendFormat-Methode 471 Append-Methode 471 application 876 Application-Klasse (WPF) 786
1306
appSettings-Element 940 Arbeitsthreads 1141 Arcuskosinus 514 Arcussinus 514 Arcustangens 514 AreEqual-Methode 568 AreNotEqual-Methode 568 AreNotSame-Methode 568 AreSame-Methode 568 Argumente an Threads übergeben 1160 Befehlszeile 527 out 245 ref 245 ref und out 244 Referenzparameter 245 Rückgabeparameter 245 Variable mit params 247 Wertparameter 244 ArgumentException 443 ArgumentNullException 443 ArgumentOutOfRangeException 443 ArithmeticException 443 Arithmetische Ausdrücke 202 ArrayExtension-Klasse (WPF) 759 Array-Klasse 394 ArrayList-Auflistung 438 Arrays 197, 389 aus Arrays 392 erzeugen und verwenden 389 implizit typisierte 391 initialisieren 391 mehrdimensionale 390 Verwendung 377 ASCII 62 AsEnumerable-Methode (LINQ) 699 Asin-Methode 514 as-Operator 215 bei Schnittstellen 309 aspnet_regiis.exe 74 aspnet_regsql.exe 74 AspNetTraceAppender-Klasse 591 AsQueryable-Methode (LINQ) 699 AsReadOnly-Methode 396 Assembly Linker 74 Assembly-Cache 81 AssemblyCompany-Attribut 367, 970 AssemblyCopyright-Attribut 367, 971 AssemblyDescription-Attribut 971 AssemblyFileVersion-Attribut 969 AssemblyInfo.cs 97, 368, 732 Assembly-Klasse 1238 AssemblyProduct-Attribut 970 AssemblyResolve-Ereignis 1256
Index
Aufgeschobene Ausführung (LINQ) 657 Auflistungen an WPF-Elemente binden 926 ArrayList 438 assoziative 413 BitArray 429 BitVector32 431 Collection 434 CollectionBase 438 Dictionary 413 DictionaryBase 438 eigene implementieren 422, 434, 435 füllen 399 HashSet 425 Hashtable 438 HybridDictionary 438 individualisierbare 434 Initialisierer 399 KeyedCollection 435 LinkedList 428 List 396 ListDictionary 438 mit BinarySearch durchsuchen 407 mit IndexOf durchsuchen 406 mit LINQ durchsuchen 410 mit Prädikaten durchsuchen 408 NameValueCollection 438 OrderedDictionary 438 Queue 423, 439 ReadOnlyCollection 422 ReadOnlyCollectionBase 439 SortedDictionary 413 SortedList 413, 439 sortieren 404 Speicher optimieren 412 Stack 424, 439 StringCollection 439 Auflistungsinitialisierer 399, 415 Dictionary-Klasse 415 Aufrufen von Konstruktoren 268 von Methoden 152 Aufrufliste 553 Aufteilen von Klassen, Strukturen und Schnittstellen 281 von Methoden 334 Aufzählungen 192 bitweise 193 Ausdrücke arithmetische 202 Lambda 356 Postfix-Notation 205 Präfix-Notation 205 Typ 201
1307
Index
Assemblys Begriffsdefinition 78 dynamisch erzeugen 1246 evaluieren 1240 GAC 81 in Unterordnern 1237 Modul 79 Name 1233 Namensräume 144 Referenzierung 83 Satelliten 956 signieren 1230 starker Name 80, 1231 Versionsverwaltung 84 verzögert signieren 1234 vorkompilieren 1235 AssemblyTitle-Attribut 367 AssemblyVersion-Attribut 969 Assert-Klasse 567 Assert-Methode 558 Assoziative Auflistungen 413 AsymmetricAlgorithm-Klasse 1274, 1275 Asymmetrische Verschlüsselung 1230, 1274 ECDH 1278 Grundlagen 1263 RSA 1276 AsyncCallback-Delegat 1151 Asynchrone Methoden 1181 Asynchrones Ausführen von Methoden 1148 AsyncOperation-Klasse 1180 Atan2-Methode 514 Atan-Methode 514 Atomare Anweisungen 1202 Attribute 366 AssemblyCompany 367 AssemblyCopyright 367 AssemblyTitle 367 AttributeUsage 370 auf Methoden 369 Conditional 367 DllImport 367 Flags 194, 367 Obsolete 367 Serializable 367 STAThread 144 WebMethod 367 XmlIgnore 367 Attribute-Methode 1052 Attributes-Eigenschaft 602, 609 Attributes-Methode 1052 AttributeTargets-Aufzählung 370 AttributeUsage-Attribut 370 Aufgabenkommentare 150 Aufgabenliste 107 Verknüpfungen 128
Index
Ausdrucksbäume 357 ausführen 364 Ausführen einer Projektmappe 110 von Ausdrucksbäumen 364 Ausnahmebehandlung 441 bei LINQ to SQL 1119 globale 458 in Threads 1176 schachteln 455 Ausnahmen beim Multithreading 1176 debuggen 123 direkt abfangen 448 finally-Blöcke 456 Grundlagen 442 ignorieren 456 innere auswerten 452 Nicht-CLS-Ausnahmen 444 Prinzip 444 Stack-Trace 454 testen 569 weiterwerfen 450 werfen 446 Auswerten innerer Ausnahmen 452 von Ausnahmen 448 von Befehlszeilenargumenten 527 Auto Generated Value-Einstellung (LINQ to SQL) 1093 Auto Sync-Einstellung (LINQ to SQL) 1093 Autofenster 553 Automatisch implementierte Eigenschaften 258 AutoResetEvent-Klasse 1197 AvailableFreeSpace-Eigenschaft 616 Average-Methode (LINQ) 690
B ■■■■■■■■■■ Background-Eigenschaft (WPF) 800, 811 BackgroundWorker-Klasse 1152 BAML 751 BAML Viewer 751 base-Schlüsselwort 289, 298 Basic Multilingual Plane 62 Baumstrukturen über yield durchlaufen 388 Bearbeiten und Fortfahren 124, 549 Bedingte Haltepunkte 554 Bedingte Kompilierung 228 Bedingungsoperator 212 Befehle im Direktfenster ausführen 553 WPF 887
1308
Befehlsaliase 553 Befehlsfenster 553 Befehlszeilenargumente auswerten 527 BeginInvoke-Methode 1150 Beispiele 52 Benannte Gruppen (reguläre Ausdrücke) 485 Benutzerdefinierte Aktionen (Setup) 979 Benutzerdefiniertes Tool 746 Bereichs-Steuerelemente 849 Bereitstellungsmanifest 990 Besitzer-Formular 523 Bezeichner 145 mit @ 146 Schlüsselwörter 146 umbenennen 130 BigMul-Methode 514 Binäre Dateien über BinaryReader lesen 633 über BinaryWriter schreiben 633 Binäre Serialisierung 1015 Binäres Lesen und Schreiben 630 BinaryFormatter-Klasse 1016 BinaryReader-Klasse 624, 633 BinarySearch-Methode Array-Klasse 395 List 397, 407 BinaryWriter-Klasse 624, 633 BindingExtension-Klasse (WPF) 759 BindingFlags-Aufzählung 1225, 1239 Binding-Markup-Erweiterung (WPF) 923 BindingOperations-Klasse (WPF) 923 BitArray-Auflistung 429 Verwendung 379 BitmapImage-Klasse (WPF) 881 BitVector32-Auflistung 431 Verwendung 379 Bitweise Aufzählungen 193 Bitweise Operatoren 205 Blockieren von Threads 1184 BMP 62 Bogenmaß 514 BOM 635 bool 172 BorderBrush-Eigenschaft (WPF) 804 BorderThickness-Eigenschaft (WPF) 805 Boxing 188 mit generischen Typen vermeiden 347 break 223 Break-Methode 556 Brush-Klasse (WPF) 800 BufferedStream-Klasse 624 Buildvorgang-Einstellung 869 ButtonBase-Klasse (WPF) 835 ButtonChrome-Dekorator (WPF) 904 Button-Steuerelement (WPF) 837
Index
C ■■■■■■■■■■ Cache (LINQ to SQL) 1111 CallingConvention-Eigenschaft 1207 Camel Casing 200 CancelAsync-Methode 1153 CancellationPending-Eigenschaft 1154 CanRead-Eigenschaft 622 CanSeek-Eigenschaft 622 CanTimeout-Eigenschaft 622 Canvas-Steuerelement (WPF) 859 CanWrite-Eigenschaft 622 Capacity-Eigenschaft 412 Carriage Return 184 CAS 85 CasPol.exe 74 Cast siehe Typumwandlungen Cast-Methode (LINQ) 698 catch-Block 448 CDATA-Element 531 Ceiling-Methode 514 c-Element 531 certmgr.exe 74 ChangeConflictException 1128 ChangeConflicts-Eigenschaft 1130 Changed-Ereignis 619 ChangeExtension-Methode 614 Changeset siehe Änderungsset char 172 CharSet-Eigenschaft 1207 CheckBox-Steuerelement (WPF) 839 checked-Block 179 Checked-Ereignis (WPF) 838, 855 CheckFileExists-Eigenschaft (WPF) 791 CheckPathExists-Eigenschaft (WPF) 791 Chiffrier-Modi 1270 CIL 70 ClassCleanup-Attribut 571 ClassInitialize-Attribut 571 ClearItems-Methode 434 Clear-Methode ICollection 383 Click-Ereignis (WPF) 837 ClickOnce 988
Anwendungen mit URL-Argumenten 1002 IIS-Konfiguration 999 Clipboard Ring 134 Clipboard-Klasse (WPF) 865 Clone-Methode Array-Klasse 394 Arrays 394 Closed-Ereignis (WPF) 813, 818 Close-Methode 622 Closing-Ereignis (WPF) 813, 818 CLR 71 Just-In-Time-Compiler 70 CngAlgorithm-Klasse 1278 Code Access Security 85 Code siehe Programmcode Codeausschnitte 129 in der Toolbox 130 CodeDomProvider-Klasse 1246 Code-Editor 104 Neues in Visual Studio 2008 138 code-Element 531 Codegruppen 1259 Code-Snippets 129 Codezugriffssicherheit 1258 Codierung 62 CoerceValueCallback 775 Collapsed-Ereignis (WPF) 842 Collection-Auflistung 434 Verwendung 377 CollectionBase-Klasse 438 Collections siehe Auflistungen 373 Collect-Methode (GC-Klasse) 540 ColorConvertedBitmapExtension-Klasse (WPF) 759 ColorDialog-Klasse (WPF) 790 ColoredConsoleAppender-Klasse 591 Colors-Klasse (WPF) 800 Color-Struktur (WPF) 799 Column-Eigenschaft (WPF) 862 ColumnSpan-Eigenschaft (WPF) 863 COM 1220 Combine-Methode 614 ComboBoxItem-Klasse (WPF) 848 ComboBox-Steuerelement (WPF) 847 TextChanged-Ereignis 848 COM-Komponenten frühe Bindung 1220 späte Bindung 1224 CommandBinding-Eigenschaft (WPF) 895 Command-Eigenschaft (WPF) 890 Commands (WPF) 887 CommandTarget-Eigenschaft (WPF) 893 Common Intermediate Language 70 Common Language Runtime 71
1309
Index
By Reference-Übergabe 245 By Value-Übergabe 244 byte 172 Byte Order Mark 635 Byte-Array in einen String konvertieren 632
Index
Common Type System 71 Compare-Methode 463 CompareOrdinal-Methode 464 CompareTo-Methode 209, 210, 318 Comparison-Delegat 405 CompileAssemblyFromSource-Methode 1247 CompilerParameters-Klasse 1247 CompileToAssembly-Methode 480 Component Object Model siehe COM ComponentResourceKey-Klasse (WPF) 884 ComputeHash-Methode 1269 Concat-Methode (LINQ) 700 Conditional-Attribut 367 Conditions-Eigenschaft (WPF) 902 ConfigurationFile-Eigenschaft 1256 ConfigurationManager-Klasse 941 ConflictMode-Aufzählung 1130 Connection-Einstellung (LINQ to SQL) 1092 connectionStrings-Element 946 Console 119 ConsoleAppender-Klasse 591 ContainsKey-Methode assoziative Auflistungen 414 Dictionary-Klasse 417 IDictionary 385 Contains-Methode ICollection 383 LINQ 694 String-Klasse 461 ContainsValue-Methode 414 ContentControl-Klasse (WPF) 835 Content-Eigenschaft (XAML) 754 ContentElement-Klasse (WPF) 784 ContentPresenter-Klasse (WPF) 916 Context Namespace-Einstellung (LINQ to SQL) 1092 ContextMenu-Eigenschaft (WPF) 805, 856 ContextMenuService-Klasse (WPF) 857 ContextMenu-Steuerelement (WPF) 856 ContinueOnConflict-Wert 1130 Control-Klasse (WPF) 784, 825 ConvertBack-Methode (WPF) 930 Converter-Eigenschaft (WPF) 923 ConverterParameter-Eigenschaft (WPF) 930 Convert-Klasse 190 Convert-Methode (WPF) 930 Copy-Methode 601 CopyTo-Methode Array-Klasse 394 FileInfo-Klasse 602 ICollection 383 List 397 Cosh-Methode 514 Cos-Methode 514 Count-Eigenschaft 383
1310
Count-Methode (LINQ) 692 CreateDatabase-Methode 1089 Created-Ereignis 619 CreateDirectory-Methode 607, 646 CreateEncryptor-Methode 1273 CreateInstance-Methode 1224, 1243 CreateMask-Methode 432 Create-Methode AsymmetricAlgorithm-Klasse 1274 DirectoryInfo-Klasse 608 HashAlgorithm-Klasse 1267 SymmetricAlgorithm-Klasse 1271 XmlReader 1069 XmlWriter 1073 CreateOperation-Methode 1180 CreateProvider-Methode 1247 CreateSection-Methode 432 CreateSubdirectory-Methode 608 CreationTime-Eigenschaft 603, 609 CreationTimeUtc-Eigenschaft 603, 609 Cross Joins 718 CrossAppDomainDelegate-Delegat 1252 CryptographicException 1276 CryptographicException-Klasse 1278 Cryptography Next Generation 1267 CryptoStream-Klasse 624 csc.exe 74 CSV-Dateien mit LINQ einlesen 719 CTRL-Taste abfragen 807 CultureInfo-Klasse 953 CurrentCulture-Eigenschaft 1165 Current-Eigenschaft IEnumerator 382 WPF 786 CurrentTestOutcome-Eigenschaft 572 CurrentThread-Eigenschaft 1157, 1165 CurrentUICulture-Eigenschaft 1165 Cursor-Eigenschaft (WPF) 805 CustomActionData-Eigenschaft 983
D ■■■■■■■■■■ DAPI 1265 Data contract 1019 Data Protection API 1265 DataContext-Eigenschaft (WPF) 928 DataContract-Attribut 1019 DataContractSerializer-Klasse 1019 Data-Eigenschaft 432 DataMember-Attribut 1020
Index
DateTime-Struktur 506 von DateTimeOffset konvertieren 510 Datumsfelder in LINQ to SQL 1117 Datumsformatierungen 497 Datumswerte 181 bearbeiten 506, 508 Differenz berechnen 512 formatieren 497 vergleichen 511 Day-Eigenschaft 509 DayOfWeek-Eigenschaft 509 DayOfYear-Eigenschaft 509 Days-Eigenschaft 512 DbConnection-Eigenschaft 572 Deactivated-Ereignis (WPF) 787, 813 Deadlocks 1191 DebugFormat-Methode 587 Debuggen 543 .NET-Framework-Quellcode 560 Anhalten des Programms 545 Aufrufliste 553 Ausnahmen 123 Autofenster 553 Bearbeiten und Fortfahren 124, 549 bedingte Haltepunkte 554 Befehlsfenster 553 Direktfenster 552 Endlosschleifen 546 Grundlagen 122 Haltepunkte 547 Haltepunkte-Fenster 553 Klassenbibliotheken 571 log4net 597 logische Fehler 125 Lokalfenster 553 Schnellüberwachungsfenster 550 Threads 1159 Timeout 563 Überwachungsfenster 551 von Unit-Tests 571 WPF-Anwendungen 741 Debugger-Klasse 556 Debuginformationen 543 Debug-Klasse 558 Debug-Methode 587 DEBUG-Symbol 228 decimal 172 bei Über- und Unterläufen 181 Declaration-Eigenschaft 1060 Decrypt-Methode 1265, 1278 DefaultExt-Eigenschaft (WPF) 791 DefaultIfEmpty-Methode (LINQ) 685, 698 default-Schlüsselwort 164, 353 DefaultTraceListener-Klasse 582
1311
Index
DataRow-Eigenschaft 573 DataSource-Attribut 574 DataTemplate-Klasse (WPF) 929 DataTrigger-Klasse (WPF) 903 Date-Eigenschaft 510 Dateiattribute lesen und setzen 604 Dateien Attribute 604 binär lesen und schreiben 630 CSV-Dateien mit LINQ einlesen 719 herunterladen 1167 Informationen auslesen 603 kopieren 605 löschen 607 suchen 611 Textdateien lesen und schreiben 634 überwachen 618 umbenennen 606 verschieben 606 verschlüsseln 1265 Dateisystem bearbeiten 600 überwachen 618 Dateisystem-Editor 976 Daten entschlüsseln 1270 in Klassen und Strukturen 235 komprimieren und dekomprimieren 636 verschlüsseln 1270 zwischen Anwendungsdomänen austauschen 1253 Datenbankanwendungen 77 Datenbankmodell LINQ to SQL 1087 Datenbindung (WPF) 922 Auflistungen 926 Datenkontext 928 Datenvorlagen 929 Fehler auswerten 924 Konvertieren der Werte 930 relative 924 Selektion 927 Datengetriebene Tests 573 Datenkontext LINQ to SQL 1096 WPF 928 Datentrigger (WPF) 903 Datentypen siehe Typen Datenvertrag 1019 Datenvertrag-Serialisierung 1019 Datenvorlagen (WPF) 929 DateTime-Eigenschaft 509 DateTimeOffset-Struktur 181, 506 Methoden und Eigenschaften 508 nach DateTime konvertieren 510 Problem beim XML-Serialisieren 1011
Index
Deferred execution 657 Deflate-Algorithmus 636 DeflateStream-Klasse 624, 637 Deklaration von abstrakten Klassen 299 von Delegaten 320 von Eigenschaften 251 von Ereignissen 330 von Feldern 235 von Klassen 233 von Konstanten 197 von Methoden 239 von Operatoren 312 von Schnittstellen 304 von Strukturen 233 von Variablen 196 Deklarative Sicherheit 1261 Dekomprimieren 636 Dekoratoren (WPF) 763, 904 Dekorator-Streams 624 Delay Loaded-Einstellung (LINQ to SQL) 1093 Delay-Eigenschaft (WPF) 837 Delegaten 320 generische 354 Kontravarianz 326 Kovarianz 326 Lambda-Ausdrücke 324 Multicast 327 vordefinierte 323 vs. Ereignisse und partielle Methoden 337 Delegates siehe Delegaten delegate-Schlüsselwort 322 DeleteAllOnSubmit-Methode 1114, 1118 Deleted-Ereignis 619 DeleteDirectory-Methode 647 DeleteFile-Methode 647 Delete-Methode 601, 602, 607, 609 DeleteOnSubmit-Methode 1114, 1118 DependencyObject-Klasse (WPF) 783 DependencyPropertyHelper-Klasse (WPF) 776 Deployment Manifest 990 Dequeue-Methode 423 DeriveKeyMaterialMethode-Methode 1278 DescendantNodesAndSelf-Methode 1052 DescendantNodes-Methode 1051 DescendantsAndSelf-Methode 1052 Descendants-Methode 1051 descending-Schlüsselwort (LINQ) 668 Description-Eigenschaft 794 DESCryptoServiceProvider-Klasse 1272 Deserialisieren binäre Serialisierung 1017 Datenvertrag-Serialisierung 1022 XML-Serialisierung 1012
1312
Deserialize-Methode 1017 Design Pattern 1287 DES-Klasse 1272 Destruktor 269 Dezimalzahlen 175 Dialoge 815 Datei öffnen 790 Datei speichern 790 Farbauswahl 795 Ordnerauswahl 793 WPF 819 DialogResult-Eigenschaft (WPF) 811, 819 DictionaryBase-Klasse 438 Dictionary-Klasse 413 Add-Methode 416 Durchgehen 418 Entfernen von Objekten 420 Hinzufügen von Objekten 416, 417 Indexer 417 Initialisierer 415 mit spez. Vergleichs-Objekten 421 Verwendung 377 Zugriff auf die Objekte 417 Digitale Signaturen 1263, 1264 Directory-Eigenschaft 603 Directory-Klasse 607 DirectoryName-Eigenschaft 603 Direktfenster 552 Disco.exe 74 Dispatcher-Eigenschaft 1148 DispatcherObject-Klasse (WPF) 783 DispatcherPriority-Aufzählung 1148 DispatcherUnhandledException-Ereignis 459, 787 beim Multithreading 1176 DisplayMemberPath-Eigenschaft (WPF) 843, 927 Dispose-Methode 622 Distinct-Methode (LINQ) 696 DivideByZeroException 443 Divisionen Ganzzahl 203 Restwert 204 DivRem-Methode 514 DllImport-Attribut 367, 1206 DllImportAttribute-Klasse 1206 DoCallBack-Methode 1251 Document Object Model 1031 DocumentType-Eigenschaft 1060 DoEvents-Methode 1144 Dokumentation der Programmierung 529 Dokumentationskommentare 151, 529 automatisch erstellen lassen 239 CDATA-Element 531 c-Element 531 code-Element 531
Index
E ■■■■■■■■■■ ECDH 1278 ECDiffieHellmanCng-Klasse 1275, 1278 ECDiffieHellmanKeyDerivationFunction-Aufzählung 1278 ECDiffieHellman-Klasse 1275 ECDsaCng-Klasse 1275 ECDsa-Klasse 1275 Edit and Continue 124 Effizienz SQL, bei LINQ to SQL 1109 Eigene Auflistungen 422, 434, 435
Eigenschaften Abhängigkeitseigenschaften 768 abstrakte 300 automatisch implementierte 258 Control (WPF) 804 deklarieren 251 Fenster (WPF) 810 Kapselung 250 Konstante 259 lesegeschützte 259 neu definieren 288 Referenzen anzeigen 131 schreibgeschützte 254 statische 273 unterschiedliche Gültigkeitsbereiche 256 virtuelle 292, 294 von Projekten 127 WPF-Standard-Eigenschaften 804 Eigenschaftenfenster 101 Eigenschaftensystemkoersion 775 Eigenschaftentrigger (WPF) 770, 901 Einfügen über den Zwischenablagering 134 von Strings 467 Eingabe-Bindungen (WPF) 898 Eingabe-Gesten (WPF) 898 Eingaben überprüfen 501 Eingebettete Ressource 948 Einschränken LINQ to SQL 1086 von Typparametern 349 Einschränkungen LINQ 667 Einsprungpunkt 142 ElapsedMilliseconds-Eigenschaft 526 ElapsedTicks-Eigenschaft 526 ElementAt-Methode (LINQ) 686 ElementAtOrDefault-Methode (LINQ) 686 Element-Methode 1051 ElementName-Eigenschaft (WPF) 923 ElementsAfterSelf-Methode 1050, 1051 Elements-Methode 1051 Elliptic Curve Diffie-Hellman-Algorithmus 1278 E-Mails über SMTP versenden 938 Empty-Feld 463 Empty-Methode (LINQ) 699, 712 Encoding-Klasse 634 Encrypt-Methode 1265 EndInvoke-Methode 1151 Endlos-Rekursion 268 Endlosschleifen debuggen 546 EndsWith-Methode 461 Enqueue-Methode 423
1313
Index
example-Element 530 include-Element 531 list-Element 531 para-Element 531 param-Element 530 paramref-Element 531 remarks-Element 530 returns-Element 530 seealso-Element 530 see-Element 531 summary-Element 530 typeparam-Element 530 typeparamref-Element 531 value-Element 530 DOM 1031 do-Schleife 224 Dotnet 68 Dotnet-Framework 68 double 172 Download von Dateien 1167 DoWork-Ereignis 1153 Doxygen 534 DPI 797 DrawingBrush-Klasse (WPF) 800 DriveFormat-Eigenschaft 616 DriveInfo-Klasse 616 DriveType-Eigenschaft 616 DSACryptoServiceProvider-Klasse 1275 DSA-Klasse 1275 Duration-Methode 512 DynamicResourceExtension-Klasse (WPF) 759 DynamicResource-Markup-Erweiterung 878 Dynamische Abfragen in LINQ to SQL 1100 LINQ 714 Dynamische Hilfe 107 Dynamischer Aufruf von Methoden 1245 Dynamisches Ausführen von Programmcode 1246 Dynamisches Instanzieren von Typen 1241
Index
EnsureCapacity-Methode 471 Entität 1080 Entitätsmenge 1080 Entity Namespace-Einstellung (LINQ to SQL) 1092 Entity-Referenzen (XML) 532 EntryPoint-Eigenschaft 1207 Entwurfsrichtlinien für Schnittstellen 304 enum 192 Enumerable-Klasse 650 Enumerationen 192 Enumeratoren 381 Environment-Klasse 528, 612 Equals-Methode 186 überschreiben 318 Ereignis-Accessoren 333 Ereignisbasiertes asynchrones Entwurfsmuster 1179 Ereignisorientierte Programmierung 60 Ereignisse Begriffsdefinition 60 Control (WPF) 806 Deklaration 330 Fenster (WPF) 813 Referenzen anzeigen 131 ThreadException 458 vs. Delegaten und partielle Methoden 337 Ereignistrigger (WPF) 903 Error-Ereignis 619 ErrorFormat-Methode 587 Error-Methode 587 Errors-Eigenschaft 1248 Ersetzen von Strings 490, 492 von Teilstrings 466 Erstellen einer Projektmappe 109 Erweiterungsmethoden 341 LINQ 663 und Polymorphismus 344 und Schnittstellen 344 Erzeugen eines Ordners 610 von Referenztypen 160 von Werttypen 160 von XML-Dokumenten mit Namensraum 1047 von XML-Dokumenten ohne Namensraum 1043 von XML-Dokumenten über LINQ 1065 XML-Dokumenten über einen XmlWriter 1073 Escape-Sequenzen 183 Reguläre Ausdrücke 475 Evaluieren .NET-Framework-Quellcode 560 von Assemblys 1240 von Typen 1238 EventArgs-Klasse 330
1314
EventHandler-Klasse 330 EventLogAppender-Klasse 591 EventLogTraceListener-Klasse 582 EventProviderTraceListener-Klasse 582 event-Schlüsselwort 330 EventSetter-Klasse (WPF) 912 EventTrigger-Klasse (WPF) 903 EventWaitHandle-Klasse 1196, 1201 ExactSpelling-Eigenschaft 1207 example-Element 530 Exception-Klasse 442, 448 Exceptions siehe Ausnahmen 441 Except-Methode (LINQ) 700 ExceptWith-Methode 426 ExecuteCommand-Methode 1135 Exists-Eigenschaft 603, 609 Exists-Methode Directory-Klasse 608 File-Klasse 601 List 397 Exit-Ereignis (WPF) 787 Exklusiv-Oder 206 ExpandDirection-Eigenschaft (WPF) 842 Expanded-Ereignis (WPF) 842 Expander-Steuerelement (WPF) 841 ExpectedException-Attribut 569 explicit-Modifizierer 316 Explizite Konvertierungen 189 für eigene Typen 316 Exp-Methode 514 Express-Editionen 46 Expression Trees 357 siehe Ausdrucksbäume Expression Trees siehe Ausdrucksbäume Extensible Application Markup Language siehe XAML Extensible Stylesheet Language 1040 Extension-Eigenschaft 603, 609 Extrahieren von Methoden 131 von Schnittstellen 131 Extreme Softwareentwicklung 564
F ■■■■■■■■■■ Fail-Methode 568 FailOnFirstConflict-Wert 1130 Farbangaben (WPF) 799 Farbauswahldialog 795 FatalFormat-Methode 587 Fatal-Methode 587 Faule Quantifizierer (Reguläre Ausdrücke) 488
Index
First In First Out 423 FirstAttribute-Eigenschaft 1051 First-Methode (LINQ) 687 FirstNode-Eigenschaft 1051 FirstOrDefault-Methode (LINQ) 687 FixedFrameHorizontalBorderHeight-Eigenschaft (WPF) 796 FixedFrameVerticalBorderWidth-Eigenschaft (WPF) 796 Flags-Attribut 194, 367 Fließkommatypen 175 float 172 Floor-Methode 515 Flush-Methode 622 Focusable-Eigenschaft (WPF) 805 Focus-Methode (WPF) 810 FolderBrowserDialog-Klasse (WPF) 790 FontDialog-Klasse (WPF) 790 FontFamily-Eigenschaft (WPF) 803 FontSize-Eigenschaft (WPF) 803 FontStretch-Eigenschaft (WPF) 803 FontStyle-Eigenschaft (WPF) 804 FontWeight-Eigenschaft (WPF) 804 ForEach-Methode 395 Array-Klasse 411 List 398, 411 foreach-Schleife 226, 382 Performance-Test 227 Foreground-Eigenschaft (WPF) 800, 811 FormatException 443 Formatieren 493 Datumswerte 497 kulturspezifisch 500 Zahlwerte 494 FormatMessage-Funktion 1218 Formular-Designer 100 for-Schleife 225 FrameworkContentElement-Klasse (WPF) 784 FrameworkElement-Klasse (WPF) 784, 825 Freezable-Klasse (WPF) 784 Freeze-Methode (WPF) 784 Freigeben von Objekten 170 Frequency-Eigenschaft 526 FromArgb-Methode (WPF) 800 FromDays-Methode 513 FromHours-Methode 513 from-Klausel (LINQ) 707 FromMillisecods-Methode 513 FromMinutes-Methode 513 FromRgb-Methode (WPF) 800 FromScRgb-Methode (WPF) 800 FromSeconds-Methode 513 FromTicks-Methode 513 FromXmlString-Methode 1276 FullName-Eigenschaft 603, 609 FullPath-Feld 602, 609
1315
Index
Fehler bei Datenbindung auswerten 924 suchen, die nur beim Anwender auftreten 454 Fehler suchen siehe Debuggen Fehlerbehandlung siehe Ausnahmebehandlung Fehlersuche siehe Debuggen 543 Felder 196 automatische Initialisierung 237 in Klassen und Strukturen 235 schreibgeschützte 254 statische 273 zu Eigenschaften hochstufen 131 Fenster (WPF) Dialoge 819 Eigenschaften 810 Ereignisse 813 immer im Vordergrund 812 initialisieren 822 öffnen 814, 815 schließen 818 Schließen abfangen 818 Tabulatorreihenfolge 814 Vorder- und Hintergrund 800 FieldInfo-Klasse 1239 FIFO-Liste 423 FileAccess-Aufzählung 631 FileAppender-Klasse 591 FileAttributes-Aufzählung 604 FileInfo-Klasse 602 File-Klasse 600, 1265 FileLoadException 1231 FileLogTraceListener-Klasse 582 FileMode-Aufzählung 630 FileName-Eigenschaft (WPF) 791 FileNames-Eigenschaft (WPF) 791 FileOptions-Aufzählung 631 FileShare-Aufzählung 631 FileStream-Klasse 630 FileSystemWatcher-Klasse 618 Filter-Eigenschaft (WPF) 791 filter-Element 583 FilterIndex-Eigenschaft (WPF) 791 Filtern (LINQ) 696 Finalisierer 269 Finalize-Methode 186 finally-Blöcke 456 für Dispose 272 in Threads 1177 FindAll-Methode 395, 397 FindIndex-Methode 397 FindLastIndex-Methode 397 FindLast-Methode 397 Find-Methode 395, 397 FindResource-Methode (WPF) 882
Index
Func-Delegat 323 Funktionale Konstruktion 1044 Funktionale Programmierung 320, 356, 652 Funktionen siehe Methoden 239 Fußgesteuerte Schleife 224
G ■■■■■■■■■■ GAC 81 gacutil.exe 74 Ganzzahldivision 203 Ganzzahlwert ermitteln 516 Garbage Collector 72, 170, 538 Arbeitsweise 538 GC-Klasse 540 Geheimer Schlüssel 1263 Gemischte Inhalte (XML) 1046 Genauigkeit (Fließkommatypen) 176 Generics siehe Generische Typen Generische Delegaten 354 Generische Klassen 345 Generische Methoden 348 Generische Schnittstellen 353 Generische Strukturen 345 Generische Typen 164, 345 einschränken 349 im Vergleich zu normalen 347 Geroutete Ereignisse (WPF) 777 Geschäftsregeln 257 Geschwindigkeit siehe Performance get-Accessor 253 GetAttribute-Methode 1071 GetAttributes-Methode 601 GetCommandLineArgs-Methode 528 GetCreationTime-Methode 601 GetCreationTimeUtc-Methode 601 GetCurrentProcess-Methode 1163 GetData-Methode 1204, 1253 GetDirectories-Methode 608, 609, 611 GetDirectoryName-Methode 614 GetDrives-Methode 618 GetEncoding-Methode 635 GetEnumerator-Methode 381 GetExtension-Methode 614 GetField 1225 GetField-Methode 1239 GetFields-Methode 1239 GetFileName-Methode 614 GetFileNames-Methode 646 GetFileNameWithoutExtension-Methode 614 GetFiles-Methode 608, 609, 611 GetFolderPath-Methode 612
1316
GetFullPath-Methode 614 GetHashCode-Methode 186 überschreiben 318 GetInterface-Methode 1242 GetInvalidFileNameChars-Methode 614 GetInvalidPathChars-Methode 615 GetKeyForItem-Methode 436 GetLastAccessTime-Methode 601 GetLastAccessTimeUtc-Methode 601 GetLastError-Funktion 1217 GetLastWriteTime-Methode 601 GetLastWriteTimeUtc-Methode 601 GetLength-Methode Array-Klasse 394 Arrays 390 GetLongLength-Methode 394 GetLowerBound-Methode 394 GetMachineStoreForApplication-Methode 644 GetMachineStoreForAssembly-Methode 644 GetMachineStoreForDomain-Methode 644 GetManifestResourceStream-Methode 948 GetMaxThreads-Methode 1174 GetMethod-Methode 1239 GetMethods-Methode 1239 GetModuleHandle-Funktion 1218 GetParent-Methode 608 GetPart-Methode 641 GetParts-Methode 641 GetPathRoot-Methode 615 GetProperties-Eigenschaft 1239 GetProperty 1225 GetProperty-Methode 1239 GetRandomFileName-Methode 615 GetRange-Methode 398 GetSection-Methode 947 GetTempFileName-Methode 615 GetTempPath-Methode 615 GetTypeFromCLSID-Methode 1224 GetTypeFromProgID-Methode 1224 GetType-Methode 186, 1238 GetUpperBound-Methode 394 GetUserStoreForApplication-Methode 644 GetUserStoreForAssembly-Methode 645 GetUserStoreForDomain-Methode 645 GetValue-Methode FieldInfo-Klasse 1244 WPF 770 GhostDoc 239 Global Assembly Cache 81 Globale Ausnahmebehandlung 458 Globale Daten 275 Globaler Namensraum 145 Globalisierung siehe Lokalisierung GMT 506
Index
H ■■■■■■■■■■ Haltepunkte bedingte 554 unbedingte 547 Haltepunkte-Fenster 553 Handle 1209 Handled-Eigenschaft (WPF) 778 HasAttributes-Eigenschaft 1051 HasElements-Eigenschaft 1051 HasExtension-Methode 615 HashAlgorithm-Eigenschaft 1278 Hashcodes berechnen 318, 1267 Grundlagen 1262 Hashing-Verfahren Grundlagen 1262 in .NET 1267 HashSet-Auflistung 425 Verwendung 379 Hashtable-Auflistung 438 HasItems-Eigenschaft (WPF) 844 HasValue 168 Header-Eigenschaft (WPF) 841 Heap 157 Height-Eigenschaft (WPF) 830
Herunterladen von Dateien 1167 Hexadezimale Schreibweise 174 Hide-Methode (WPF) 818 Hintergrund eines Fensters oder Steuerelements 800 Hintergrundthreads 1141 HmacKey-Eigenschaft 1278 HMAC-Klasse 1268 HMACMD5-Klasse 1268 HMACRIPEMD160-Klasse 1268 HMACSHA1-Klasse 1268 HMACSHA256-Klasse 1268 HMACSHA384-Klasse 1268 HMACSHA512-Klasse 1268 HorizontalAlignment-Eigenschaft (WPF) 831 HorizontalContentAlignment-Eigenschaft (WPF) 831 HorizontalOffset-Eigenschaft (WPF) 857 HorizontalScrollBarVisibility-Eigenschaft (WPF) 853, 863 Hostprozess 544 Hour-Eigenschaft 509 Hours-Eigenschaft 512 HybridDictionary-Auflistung 438 Hyperbolischer Kosinus 514 Hyperbolischer Sinus 514 Hyperbolischer Tangens 514
I ■■■■■■■■■■ IAsyncResult-Schnittstelle 1152 ICollection-Schnittstelle 382 ICommand-Schnittstelle (WPF) 887 IComparable-Schnittstelle 404 implementieren 318, 404 Icon-Eigenschaft (WPF) 811, 855 IconHeight-Eigenschaft (WPF) 796 IconWidth-Eigenschaft (WPF) 796 ICryptoTransform-Schnittstelle 1273 IDE 87 IDictionary-Schnittstelle 384 IDisposable-Schnittstelle 271 IEEERemainder-Methode 515 IEnumerable-Schnittstelle 381 IEnumerator-Schnittstelle 381 IEquatable-Schnittstelle 404 implementieren 318 IExtensibleDataObject-Schnittstelle 1028 if-Verzweigung 216 IgnoreCase 1225 Ignorieren von Ausnahmen 456 IIS Konfiguration für ClickOnce 999
1317
Index
GotFocus-Ereignis (WPF) 806 goto 223 Greenwich Mean Time 506 GridSplitter-Klasse (WPF) 863 Grid-Steuerelement (WPF) 861 Groß-/Kleinschreibung ignorieren 465 reguläre Ausdrücke 484 GroupBox-Steuerelement (WPF) 841 GroupBy-Methode (LINQ) 672 GroupJoin-Methode (LINQ) 683 group-Klausel (LINQ) 673 GroupName-Eigenschaft (WPF) 840 Gruppierungen LINQ 671 LINQ to SQL 1108 reguläre Ausdrücke 477 Gültigkeitsbereiche bei Klassen und Strukturen 259 bei Konstruktoren 269 bei Typ-Elementen 259 unterschiedliche bei Eigenschaften 256 GZIP-Algorithmus 636 GZipStream-Klasse 624, 637
Index
IisTraceListener-Klasse 582 ildasm.exe 74 IList-Schnittstelle 383 ILog-Schnittstelle 585 ImageBrush-Klasse (WPF) 800 immutable 469 Imperative Sicherheit 1261 Implementieren von Schnittstellen 306 implicit-Modifizierer 316 Implizit typisierte Arrays 391 Implizit typisierte Variablen 198 Implizite Konvertierungen 189 für eigene Typen 316 Importieren von Namensräumen 145 include-Element 531 Inconclusive-Methode 568 Indeterminate-Ereignis (WPF) 838 Indexer 263 BitArray 430 BitVector32 432 Dictionary-Klasse 417 IDictionary 385 IList 384 List 401 IndexOfAny-Methode 461 IndexOf-Methode Array-Klasse 395 IList 384 List 398 String-Klasse 461 IndexOutOfRangeException 443 Indizieren von Klassen 263 InfoFormat-Methode 587 Info-Methode 587 Inhalts-Eigenschaft (XAML) 754 Inhalts-Steuerelemente (WPF) 835 Inheritance Modifier-Einstellung (LINQ to SQL) 1092, 1093 InitialDirectory-Eigenschaft (WPF) 791 Initialisieren eines WPF-Fensters 822 von Arrays 391 von Feldern 237 von Objekten 161, 237 von Unit-Tests 571 XAML 752 Initialisierungsvektor 1270 InitializeComponent-Methode (WPF) 745 Fehler beim Kompilieren 742 Initialize-Methode 394 Inkrementelle Suche 133 Inlines-Eigenschaft (WPF) 852 Inner Join 679 Innere Ausnahmen 452
1318
Innere Verknüpfungen (LINQ) 679 InnerException-Eigenschaft 450, 451, 546 Input Bindings 898 Input Gestures 898 InputGesture-Klasse (WPF) 898 InputGestureText-Eigenschaft (WPF) 855 InsertAllOnSubmit-Methode 1114 InsertItem-Methode 434 Insert-Methode IList 383 String-Klasse 461 InsertOnSubmit-Methode 1114 InsertRange-Methode 398 Installation ClickOnce 988 Visual Studio 2008 47 Installationsanwendung 972 Instance 1225 Instanzeigenschaften 60 Instanzieren Typen, dynamisch 1241 von Referenztypen 160 von Werttypen 160 Instanzmethoden 60, 152 der Standardtypen 172 int 172 IntegerTypen 174 IntelliSense 105 interface 304 Interfaces siehe Schnittstellen internal-Konstruktor 269 internal-Modifizierer 259, 260 Internetrecherche 53 InteropServices 1206 Intersect-Methode (LINQ) 699 IntersectWith-Methode 426 Interval-Eigenschaft (WPF) 837 into-Schlüsselwort (LINQ) 674, 683, 703 InvalidCastException 443 InvalidOperationException 443 Invariante Kultur 954 InvokeMember-Methode 1225, 1245 InvokeMethod 1225 Invoke-Methode 1147, 1150, 1151 IOException 443 IQueryable-Schnittstelle 650 IsAlive-Eigenschaft 1165 IsBackground-Eigenschaft 1142 IsCancel-Eigenschaft (WPF) 819, 837 IsCheckable-Eigenschaft (WPF) 855 IsChecked-Eigenschaft (WPF) 837, 855 IsCompleted-Eigenschaft 1152 IsDefaulted-Eigenschaft (WPF) 837 IsDefault-Eigenschaft (WPF) 819, 837
Index
Iteratorblock 385 Iteratoren 381 IV-Eigenschaft 1272 IXmlSerializable-Schnittstelle 1008
J ■■■■■■■■■■ Jagged Arrays 392 JIT 70 join-Klausel (LINQ) 681 Join-Methode LINQ 679 String-Klasse 469 Thread-Klasse 1184 Joins siehe Verknüpfungen Just-In-Time-Compiler 70
K ■■■■■■■■■■ Kammerton 312 Kapselung 250, 275 Erweiterte 257 Kaufmännisches Runden 521 Keyboard-Klasse (WPF) 865 KeyDerivationFunction-Eigenschaft 1278 KeyDown-Ereignis (WPF) 807 KeyedCollection-Auflistung 435 Key-Eigenschaft 1272 KeyGesture-Klasse (WPF) 898 Keys-Eigenschaft Dictionary-Klasse 419 IDictionary 385 KeyUp-Ereignis (WPF) 807 KeyValuePair 418 Klassen abstrakte 299 aufteilen 281 automatische Initialisierung 161 deklarieren 233 erweitern 285, 341 Felder 235 Finalisierer 269 generische 345 Kapselung 250 Konstruktoren 266 Konvertierungsoperatoren 316 Operatoren 312 partielle 281 Schnittstellen 303
1319
Index
IsEditable-Eigenschaft (WPF) 847 IsEmpty-Eigenschaft 1051 IsEmptyElement-Eigenschaft 1072 IsEnabledChanged-Ereignis (WPF) 806 IsEnabled-Eigenschaft (WPF) 805, 811 IsExpanded-Eigenschaft (WPF) 841 IsFalse-Methode 568 IsFocused-Eigenschaft (WPF) 805 IsIndeterminate-Eigenschaft (WPF) 850 IsInstanceOfType-Methode 568 IsKeyDown-Methode (WPF) 865 IsMainMenu-Eigenschaft (WPF) 854 IsMatch-Methode (Regex-Klasse) 478 IsMouseOver-Eigenschaft (WPF) 805 IsMousePresent-Eigenschaft (WPF) 796 IsMouseWheelPresent-Eigenschaft (WPF) 796 IsNotInstanceOfType-Methode 568 IsNotNull-Methode 568 IsNull-Methode 568 IsNullOrEmpty-Methode 464 ISO 8859-1 62 ISO-8859-1-Zeichensatz 1299 IsolatedStorageFile-Klasse 643 IsolatedStorageScope-Aufzählung 644 Isolierter Speicher 642 is-Operator 214, 292 bei Schnittstellen 309 IsPathRooted-Methode 615 IsProperSubsetOf-Methode 426 IsProperSupersetOf-Methode 426 IsReadOnly-Eigenschaft FileInfo-Klasse 603 ICollection 383 WPF 847 IsReady-Eigenschaft 616 IsSelected-Eigenschaft (WPF) 845 IsSelectionActive-Eigenschaft (WPF) 845 IsSelectionRangeEnabled-Eigenschaft (WPF) 850 IsSubsetOf-Methode 426 IsSupersetOf-Methode 426 IsSynchronizedWithCurrentItem-Eigenschaft (WPF) 927 IsTabStop-Eigenschaft (WPF) 805 IsTextSearchEnabled-Eigenschaft (WPF) 848 IsThreadPoolThread-Eigenschaft 1165 IsThreeState-Eigenschaft (WPF) 837 IsTrue-Methode 568 IsVisibleChanged-Ereignis (WPF) 807 IsVisible-Eigenschaft (WPF) 805, 811 ItemHeight-Eigenschaft (WPF) 860 ItemsControl-Klasse (WPF) 842 Items-Eigenschaft (WPF) 843 ItemsSource-Eigenschaft (WPF) 844 ItemWidth-Eigenschaft (WPF) 860
Index
statische 275 statische Klassenelemente 273 statische Methoden 276 Unterschiede zu Strukturen 231 versiegelte 303 Klassenansicht 98 Klassenbibliotheken 338 .NETFramework 72 debuggen 571 Klasseneigenschaften 60 Klassenmethoden 60, 152, 276 Standardtypen 173 Klonen Objekt 186 tiefes 1008 Knowledge-Base-Recherche 55 KnownType-Attribut 1025 Kommandozeilencompiler 135 Kommaseparierte Dateien mit LINQ einlesen 719 Kommentare 150 Aufgabenkommentare 150 Dokumentationskommentare 151, 529 einfache 150 Kompilieren 109 bedingt 228 ohne Visual Studio 135 von regulären Ausdrücken 479 Kompilierfehler beseitigen 123 Komplementär-Operator 206 Komponententests siehe Unit-Tests Komprimieren 636 Konfiguration .NET Framework 936 Anwendung 935 log4net 589 Konfliktlösung (LINQ to SQL) 1128 Konkurrenz optimistische 1128 pessimistische 1128 Konsolenanwendungen 119, 142 testen 122 Konstante Eigenschaften 259 Konstanten 195 Deklaration 197 Konstruktoren 266 Aufrufen anderer 268 Aufrufen geerbter 286, 298 geschützte 269 interne 269 private 269 Standardkonstruktor 266 statische 277 vs. Objektinitialisierer 267
1320
Kontextmenüs (WPF) 856 Kontravarianz 326 Konventionen 44 Konvertieren bei der Datenbindung 930 Byte-Array in String 632 Convert-Klasse 190 DateTimeOffset nach DateTime 510 eigene Typen 316 explizit 189 implizit 189 Sequenzen (LINQ) 698 String in Byte-Array 632 über ToString 191 von Typen 188 Kopfgesteuerte Schleife 224 Kopieren von Dateien 605 Kosinus 514 Kovarianz 326 Kreuzprodukt-Verknüpfungen 718 Kulturen 953 invariante 954 neutrale 954 spezifische 955 Kurzschluss logischer 212
L ■■■■■■■■■■ Label-Steuerelement (WPF) 840 Lambda-Ausdrücke 356 für Delegaten 324 Lambda-Kalkül 324 Lambda-Operator 324 Language Integrated Query siehe LINQ Language-Eigenschaft 959 Last In, First Out 424 LastAccessTime-Eigenschaft 603, 609 LastAccessTimeUtc-Eigenschaft 603, 609 LastAttribute-Eigenschaft 1051 LastIndexOfAny-Methode 461 LastIndexOf-Methode List 398 String-Klasse 461 Last-Methode (LINQ) 687 LastNode-Eigenschaft 1051 LastOrDefault-Methode (LINQ) 687 LastWriteTime-Eigenschaft 603, 609 LastWriteTimeUtc-Eigenschaft 603, 609 Latin-1 62 Layout (WPF) 858
Index
Ungleichheits-Verknüpfungen 716 Unterabfragen 712 Verknüpfen von Sequenzen 679 LINQ to SQL 1:N-Beziehungen 1103 Abfrage-Effizienz 1109 Ändern 1114 Anfügen 1114 Ausnahmebehandlung 1119 Beziehungen auflösen 1103 Cache 1111 Datenbankmodell 1087 Datumsfelder 1117 dynamische Abfragen 1100 einfache Abfragen 1099 Einschränkungen 1086 Gruppierungen 1108 Konfliktlösung 1128 LIKE-Abfragen 1100 Löschen 1118 N:M-Beziehungen 1104 progressive Abfragen 1102 Protokoll 1099 Transaktionen 1126 Verknüpfungen 1106 verzögertes Laden 1109 LINQ to XML 1061 Abfragen auf Elemente, die leer sein können 1063 einfache Abfragen 1061 Elemente auslesen 1064 mit Namensräumen abfragen 1065 XML-Dokumente erzeugen 1065 XML-Dokumente transformieren 1067 List-Auflistung 396 durchgehen 401 Entfernen von Objekten 402 Objekte lesen 401 Verwendung 377 ListBoxItem-Klasse (WPF) 845 ListBox-Steuerelement (WPF) 845 Inhalt umbrechen 861 ListDictionary-Auflistung 438 list-Element 531 Listen siehe Auflistungen Listener (TraceSource) 582 listeners-Element 580 Listen-Steuerelemente (WPF) 842 Literale für Strings 183 für Zahlen 174 für Zeichen 182 wortwörtliche Strings 184 Loaded-Ereignis (WPF) 813 LoaderOptimization-Attribut 1255
1321
Index
Layout-Container (WPF) 858 Lebensdauer von Variablen 195 Left Outer Join 684 Left-Eigenschaft (WPF) 811 legacyUnhandledExceptionPolicy 1178 LegalBlockSizes-Eigenschaft 1272 LegalKeySizes-Eigenschaft 1272 Length-Eigenschaft Array-Klasse 394 FileInfo-Klasse 603 Stream-Klasse 622 StringBuilder-Klasse 471 String-Klasse 461 Lesegeschützte Eigenschaften 259 Lesen von Dateien 630 von Textdateien 634 XML-Dokumente über einen XmlReader 1069 XML-Dokumente über X-DOM 1054 Lesezeichen 128 in Lesezeichen-Ordnern 128 let-Schlüsselwort (LINQ) 705 LIFO-Liste 424 LIKE-Operator (SQL) 1100 LinearGradientBrush-Klasse (WPF) 800 LinkedList-Auflistung 428 Verwendung 379 Linksgerichtete Verknüpfung (LINQ) 684 Linksverschiebung 207 LINQ 649 Abfrageausdrücke 653 Abfragen schachteln 705 Aggregat-Methoden 690 aufgeschobene Ausführung 657 dynamische Abfragen 714 Einschränken mit Where 667 einzelne Elemente ermitteln 686 Erweiterungsmethoden 663 Erzeugungsmethoden 699 Gruppierungen 671 into-Schlüsselwort 703 Konvertier-Methoden 698 Kreuzprodukt-Verknüpfungen 718 let-Schlüsselwort 705 mehrere from-Klauseln 707 Mengen-Methoden 699 Methoden zur Suche 694 Performance 662 Performance-Test 662 Performance-Test (let, into) 706 progressive Abfragen 714 Projektionen 669 sortieren 668 spezielle Filter-Methoden 696
Index
Load-Methode 1042 LoadOptions-Eigenschaft 1104 LocalFileSettingsProvider-Klasse 944 Localization.Attributes-Eigenschaft (WPF) 966 Localization.Comments-Eigenschaft (WPF) 966 LocBaml 964 lock-Schlüsselwort 1188 Log10-Methode 515 log4net 585 Konfiguration debuggen 597 konfigurieren 589 Log4Net Viewer 591 Logarithmus berechnen 515 Log-Eigenschaft 1099 LogicalTreeHelper-Klasse (WPF) 767 Logische Bäume (WPF) 765 Logische Fehler debuggen 125 Logische Operatoren 211 Logischer Kurzschluss 212 Log-Methode 515 Lokalfenster 553 Lokalisierung Grundlagen 953 Windows.Forms 959 WPF 960 long 172 LongCount-Methode (LINQ) 691 Lookahead-Gruppierungen (Reguläre Ausdrücke) 486 Lookbehind-Gruppierungen (Reguläre Ausdrücke) 486 Loose XAML 730 Löschen eines Ordners 611 LINQ to SQL 1118 von Dateien 607 von Methoden-Parametern 131 von Strings 467 von unbenutzten using-Direktiven 132 LostFocus-Ereignis (WPF) 806
M ■■■■■■■■■■ machine.config 936 MACTripleDES-Klasse 1268 Mage.exe 74, 996, 998 MageUI.exe 74, 996, 998 Magic Numbers 197 Main-Methode 142 MainWindow-Eigenschaft (WPF) 786 Makecert.exe 74, 994 Man in the Middle 1264 Managed Code 70 ManagedThreadId-Eigenschaft 1165 Manifest 79
1322
ManualResetEvent-Klasse 1197 Margin-Eigenschaft (WPF) 806, 830 Markup-Erweiterungen (XAML) 756 MarshalAs-Attribut 1209 Marshalling 1208 Matches-Methode (Regex-Klasse) 482 MatchEvaluator-Delegat 492 Match-Klasse 482 Match-Methode (Regex-Klasse) 482 Mathematisches Runden 521 Math-Klasse 513 MaxCapacity-Eigenschaft 471 MaxHeight-Eigenschaft (WPF) 811, 830 Maximum-Eigenschaft (WPF) 849 Max-Methode LINQ 690 Math-Klasse 515 MaxValue-Eigenschaft DateTimeOffset-Struktur 508 Standardtypen 173 TimeSpan-Struktur 513 MaxWidth-Eigenschaft (WPF) 811, 830 MD5 1268 MD5CryptoServiceProvider-Klasse 1268 MD5-Klasse 1268 Mehrdimensionale Arrays 390 Mehrfachstart verhindern 1195 Mehrfachzuweisungen 208 Mehrzeilige Strings (Reguläre Ausdrücke) 483 MemberChangeConflict-Klasse 1131 MemberwiseClone-Methode 186 MemoryStream-Klasse 623 Mengen-Methoden (LINQ) 699 MenuItem-Eigenschaft (WPF) 855 MenuItem-Klasse (WPF) 854 Menüs (WPF) 854 Menu-Steuerelement (WPF) 854 MergedDictionaries-Eigenschaft (WPF) 883 Mergemodul 972 Message Digest 1268 MessageBox-Klasse 521 Message-Eigenschaft 450 Metadaten 79 Metainformationen 366 Methoden abstrakte 300 anonyme 322 asynchron ausführen 1148 asynchrone 1181 aufrufen 152 aufteilen 334 Control (WPF) 809 Deklaration 239 Delegaten 320 der Standardtypen 173
Index
static 239, 273 virtual 294 Modifizier-Tasten 807 Modul 79 Modulo-Operator 204 Mole 742 Monitor-Klasse 1191 Month-Eigenschaft 509 MouseDoubleClick-Ereignis (WPF) 807 MouseDown-Ereignis (WPF) 807 MouseEnter-Ereignis (WPF) 807 MouseGesture-Klasse (WPF) 898 MouseLeave-Ereignis (WPF) 807 MouseLeftButtonDown-Ereignis (WPF) 807 MouseLeftButtonUp-Ereignis (WPF) 807 MouseMove-Ereignis (WPF) 807 MouseRightButtonDown-Ereignis (WPF) 807 MouseRightButtonUp-Ereignis (WPF) 807 MouseUp-Ereignis (WPF) 807 MouseWheel-Ereignis (WPF) 807 Move-Methode 601, 602, 608 MoveNext-Methode 382 MoveTo-Methode 609 mscorcfg.msc 75, 937, 1259 mscorlib.dll 72, 144 MSDN-Recherche 54 Multicast-Delegaten 327 MultiDataTrigger-Klasse (WPF) 903 Multiline-Option (Reguläre Ausdrücke) 484 Multiselect-Eigenschaft (WPF) 791 Multitargeting 139 Multithreading 1139 Argumente übergeben 1160 atomare Anweisungen 1202 Ausnahmen in Threads 1176 Deadlocks 1191 Debuggen 1159 Einführung 1140 Ende signalisieren 1160 ereignisbasierte asynchrone Entwurfsmuster 1178 finally-Blöcke 1177 Hintergrundthreads 1141 Informationen ermitteln 1165 Interlocked-Klasse 1202 mögliche Probleme 1142 Mutex 1193 Priorität 1163 Prozessorlast 1152 Race Conditions 1191 Sichere Punkte 1164 Signalisierungs-Konstrukte 1195 Synchronisation 1183 ThreadPool-Klasse 1173 Threads abbrechen 1164
1323
Index
dynamisch aufrufen 320, 1245 Erweiterungsmethoden 341 extrahieren 131 generische 348 in einem Thread ausführen 1156 Instanzmethoden 152 Klassenmethoden 152 mit Attributen versehen 369 neu definieren 288 Parameter löschen 131 Parameter umsortieren 131 partielle 334 partielle vs. Delegaten und Ereignisse 337 ref- und out-Argumente 244 Referenzen anzeigen 131 rekursive 248 Simulation globaler 276 Standardtypen 172 statische 152, 276 überladen 242 Variablen zu Parametern heraufstufen 131 verbergen 291 versiegeln 303 virtuelle 292, 294 Methodenzeiger siehe Delegaten Millisecond-Eigenschaft 509 Milliseconds-Eigenschaft (TimeSpan-Struktur) 512 MinHeight-Eigenschaft (WPF) 811, 830 Minimum-Eigenschaft (WPF) 849 Min-Methode (LINQ) 690 Minute-Eigenschaft 509 Minutes-Eigenschaft 512 MinValue-Eigenschaft DateTimeOffset-Struktur 508 Standardtypen 173 TimeSpan-Struktur 513 MinWidth-Eigenschaft (WPF) 811, 830 Missing.Value-Eigenschaft 1223 Modales Öffnen 815 Mode-Eigenschaft (WPF) 923 Modifiers-Eigenschaft (WPF) 865 Modifizierer explicit 316 Gültigkeitsbereich 259 implicit 316 internal 259, 260 new 291 override 294 partial 281, 335 private 259 protected 259, 286 public 259, 260 readonly 255 sealed 303
Index
Threads blockieren 1184 Threads starten 1157 Threadsicherheit 1142 Timer 1203 UI-Threads 1140 Vordergrundthreads 1141 Warteschlangen 1198 Zugriff auf Daten sperren 1186 Zugriff auf die Benutzeroberfläche 1147 Multi-Trigger (WPF) 902 MultiTrigger-Klasse (WPF) 902 Muster (Reguläre Ausdrücke) 473 Mutex-Klasse 1193
N ■■■■■■■■■■ N:M-Beziehungen in LINQ to SQL 1104 Naked Constraints 352 Name-Eigenschaft DirectoryInfo-Klasse 609 DriveInfo-Klasse 616 FileInfo-Klasse 603 WPF 806 XElement-Klasse 1052 Namen siehe Bezeichner Namen von Assemblys 1233 Namenskonvention 1301 Namensräume Begriffsdefinition 78 globale 145 importieren 145 in Assemblys 144 XAML 749 XML 65 Namensrichtlinien 200 Namespaces siehe Namensräume 78 NameValueCollection-Auflistung 438 NaN 180 Navigieren in XML-Dokumenten 1050 Negate-Methode 513 Nested Diagnostic Context 593 NetDataContractSerializer-Klasse 1019 NetSendAppender-Klasse 591 Neutrale Kulturen 954 new-Modifizierer 291 Newsgroup-Recherche 53 NextMatch-Methode 482 NextNode-Eigenschaft 1050 ngen.exe 75 Nicht aufzeichnende Gruppen 486
1324
NodesAfterSelf-Methode 1050 NodesBeforeSelf-Methode 1050 Nodes-Methode 1051 Non-Equi Join 716 NonSerialized-Attribut 1017 NotImplementedException 443 Now-Eigenschaft 508 null 160 für Werttypen 167 umwandeln 213 Nullable-Einstellung (LINQ to SQL) 1093 Nullables 167 NullExtension-Klasse (WPF) 759 NullReferenceException 444 Nullwerte bei Referenztypen 160 bei Werttypen 167 in XML 1045
O ■■■■■■■■■■ O/R-Mapper 1085 Object 155 ObjectChangeConflict-Klasse 1130 ObjectDisposedException 444 Object-Klasse 185 Werttypen 159 ObjectTrackingEnabled-Eigenschaft 1097, 1113 Objektbrowser 108 Objekte freigeben 170 initialisieren 161 klonen 186 serialisieren 1007 Objekt-Graph 1007 Objektinitialisierer 161, 237 vs. Konstruktoren 267 Obsolete-Attribut 367 Offene Einschränkungen 352 Öffentlicher Schlüssel 1263 Öffnen Fenster (WPF) 814 Offset-Eigenschaft 509 OfType-Methode (LINQ) 698 Oktave 313 OnCreated-Methode 1122 OnDeserialized-Attribut 1017 OnDeserializing-Attribut 1017 OnSerialized-Attribut 1017 OnSerializing-Attribut 1017 OnValidate-Methode 1125
Index
OverflowException 181 OverflowMode-Eigenschaft (WPF) 858 Overlaps-Methode 426 override-Modifizierer 294 OverwritePrompt-Eigenschaft (WPF) 791
P ■■■■■■■■■■ Padding 1271 Padding-Eigenschaft (WPF) 806, 831 PadLeft-Methode 461 PadRight-Methode 462 Paket-URIs 876 para-Element 531 param-Element 530 Parameter siehe Argumente ParameterizedThreadStart-Delegat 1157 paramref-Element 531 params 247 Parent-Eigenschaft 610, 1050 ParseExact-Methode DateTimeOffset-Struktur 508 ParseExcact-Methode Datumstypen 505 Parse-Methode 173, 503, 1042 DateTimeOffset-Struktur 508 TimeSpan-Struktur 513 Parsen spezifischer Datumsformate 505 von Strings 503 PartExists-Methode 641 partial-Modifizierer 281, 335 Partielle Klassen 281 Partielle Methoden 334 vs. Delegaten und Ereignisse 337 Partielle Schnittstellen 281 Partielle Strukturen 281 Pascal Casing 200 PasswordBox-Steuerelement (WPF) 853 PasswordChar-Eigenschaft (WPF) 853 Passwörter verschlüsseln 1267, 1270 Path-Eigenschaft (WPF) 923 Path-Klasse 614 Pattern (Reguläre Ausdrücke) 473 Peek-Methode Queue 423 Stack 424 Performance bei Auflistungen 376 beim Suchen in Auflistungen 410 LINQ 662
1325
Index
OpenExeConfiguration-Methode 947 OpenFileDialog-Klasse (WPF) 790 OpenMachineConfiguration-Methode 947 Operatoren -- 204 ++ 204 ?? 213 And 206 as 215 Bedingungsoperator 212 bitweise 205 für Vergleiche 208 für Zuweisungen 207 is 214 Komplementär 206 Konvertierungen für eigene Typen 316 Lambda-Operator 324 Linksverschiebung 207 logische 211 Or 205 Rechtsverschiebung 207 sizeof 215 typeof 214 überladen 312 unäre und binäre für eigene Klassen 314 XOr 206 Optimieren Speicherverbrauch 540 von Auflistungen 412 Optimistische Konkurrenz 1128 OptionalField-Attribut 1017 Optionen von Projekten 127 von Visual Studio 126 orderby-Klausel (LINQ) 668 OrderBy-Methode (LINQ) 668 OrderedDictionary-Auflistung 438 Ordinaler Wert 1291 Ordner der Anwendung 614 der Anwendung ermitteln 873 erzeugen 610 für Programmdaten 612 für temporäre Dateien 613 löschen 611 suchen 611 Systemordner 612 verschieben 610 Ordnerauswahl-Dialog 793 Orientation-Eigenschaft (WPF) 850, 860 OriginalPath-Feld 602, 609 OriginalSource-Eigenschaft (WPF) 778 Or-Operator 205 OutOfMemoryException 444
Index
Performancemessungen 526 Performance-Test foreach-Schleife 227 LINQ 662 LINQ (let, into) 706 Performance-Text Reguläre Ausdrücke 481 Peripheriegeräte BitVector32 zur Datenübergabe 432 permission-Element 531 Pfade bearbeiten 614 PIA 1222 PI-Eigenschaft 515 PInvoke 1206 Placement-Eigenschaft (WPF) 857 Plugin-Systeme 1241 Polymorphismus 292 bei der Datenvertrag-Serialisierung 1024 Erweiterungsmethoden 344 wichtige Punkte 295 Pop-Methode Stack 424 Position-Eigenschaft 622 Positionierung von Steuerelementen 830 Postfix-Notation 204, 205 Potenz einer Zahl berechnen 515 Potenzierungen 204 Pow-Methode 204, 515 Prädikat 409 Prädikatbasierte Suche 408 Präfix-Notation 204, 205 Präprozessor-Direktiven 228 Predicate-Delegat 409 PredictFocus-Methode 810 PresentationTraceSources-Klasse (WPF) 744 PreserveSig-Eigenschaft 1208 PreviewKeyDown-Ereignis (WPF) 807 PreviewKeyUp-Ereignis (WPF) 807 PreviewMouseDoubleClick-Ereignis (WPF) 807 PreviewMouseDown-Ereignis (WPF) 807 PreviewMouseLeftButtonDown-Ereignis (WPF) 807 PreviewMouseLeftButtonUp-Ereignis (WPF) 807 PreviewMouseMove-Ereignis (WPF) 807 PreviewMouseRightButtonDown-Ereignis (WPF) 807 PreviewMouseRightButtonUp-Ereignis (WPF) 807 PreviewMouseUp-Ereignis (WPF) 807 PreviewMouseWheel-Ereignis (WPF) 807 PreviousNode-Eigenschaft 1050 Primary Interop Assembly 1222 Primary Key-Einstellung (LINQ to SQL) 1094 PrimaryScreenHeight-Eigenschaft (WPF) 796 PrimaryScreenWidth-Eigenschaft (WPF) 796 Primzahlenberechnung 387 PrintDialog-Klasse (WPF) 790
1326
Print-Methode 558 Priorität Abhängigkeitseigenschafts-Werte 775 von Threads 1163 PriorityClass-Eigenschaft 1163 PrivateBinPath-Eigenschaft 1256 PrivateBinPathProbe-Eigenschaft 1256 private-Konstruktor 269 private-Modifizierer 259 Privater Schlüssel 1263 Process-Klasse 526, 1163 ProcessPriorityClass-Aufzählung 1163 Programmcode dynamisch ausführen 1246 Programmdaten-Ordner 612 Programmierung funktionale 320, 356 ProgressBar-Steuerelement (WPF) 850 ProgressChanged-Ereignis 1153 Progressive Abfragen LINQ 714 LINQ to SQL 1102 Projekt 91 Projekteigenschaften 127 Projektionen (LINQ) 669 Projektmappen 91 ausführen 110 erstellen 109 Konfiguration 111 Projektmappen-Explorer 96 Projektmappenkonfiguration 111 Properties-Eigenschaft (WPF) 786 PropertyInfo-Klasse 1239 ProtectedData-Klasse 1265 protected-Konstruktor 269 protected-Modifizierer 259, 286 Protect-Methode 1265 Protokollieren 575 mit log4net 585 mit TraceSource 575 Providers-Eigenschaft 944 Prozeduren siehe Methoden 239 Prozessorlast beim Multithreading 1152 Prozess-Priorität 1163 Public 1225 Public-Key-Verfahren 1263 public-Modifizierer 259, 260 PublicResXFileCodeGenerator 962 PulseAll-Methode 1201 Pulse-Methode 1201 Push-Methode 424 pvk2pfx.exe 994
Index
Quadratwurzel berechnen 516 Quantifizierer 477 auf Gruppen 486 faule 488 Quellcode-Editor 103 Quellcode-Verknüpfungen 128 Queryable-Klasse 650 Queue-Auflistung 423, 439 Verwendung 378 QueueUserWorkItem-Methode 1173
R ■■■■■■■■■■ Race Conditions 1191 RadialGradientBrush-Klasse (WPF) 800 RadioButton-Steuerelement (WPF) 839 Range-Methode (LINQ) 699 Rank-Eigenschaft 394 RC2CryptoServiceProvider-Klasse 1272 RC2-Klasse 1272 Read Only-Einstellung (LINQ to SQL) 1094 ReadBlock-Methode 635 ReadByte-Methode 622 ReadElementContentAs-Methode 1071 ReadEndElement-Methode 1071 ReaderWriterLockSlim-Klasse 1204 ReadLine-Methode 635 Read-Methode Stream-Klasse 621 StreamWriter-Klasse 635 XmlReader-Klasse 1069, 1071 ReadObject-Methode 1022 ReadOnlyCollection-Auflistung 422 Verwendung 377 ReadOnlyCollectionBase-Klasse 439 readonly-Modifizierer 255 ReadStartElement-Methode 1071 ReadTimeout-Eigenschaft 622 ReadToEnd-Methode 635 Realzeit-Anwendungen 1164 Recherche Internet 53 Knowledge Base 55 MSDN 54 Newsgroups 53 Rechtsverschiebung 207 Refactoring 130 Anweisungen umschließen 131 Bezeichner umbenennen 130
Felder zu Eigenschaften heraufstufen 131 Methoden extrahieren 131 Parameter löschen 131 Parameter umsortieren 131 Referenzliste anzeigen 131 Schnittstellen extrahieren 131 usings organisieren 131 Variablen zu Parametern heraufstufen 131 ref-Argumente 245 ReferenceEquals 209 ReferenceEquals-Methode 186 Referenz 1293 von Elementen anzeigen 131 Referenzierung von Assemblys 83 Referenzliste 132 Referenztypen 156 erzeugen 160 vergleichen 209 Zuweisungen 162 Referenzübergabe 245 Reflector (Lutz Roeder) 85 Reflektion 214, 1237 Refresh-Methode 602 RefreshMode-Aufzählung 1132 Regasm.exe 75 Regex-Klasse 478 RegexOptions.Compiled 481 Regionen 278 RegisterWaitForSingleObject-Methode 1175 Registry 1293 Reguläre Ausdrücke 472 benannte Gruppen 485 Escape-Sequenzen 475 Fundstellen auswerten 482 Groß-/Kleinschreibung ignorieren 484 Gruppierungen 477 Lookahead-Gruppierungen 486 Lookbehind-Gruppierungen 486 mehrzeilig suchen 483 Musterzeichen 474 nicht aufzeichnende Gruppen 486 Oder-Spezifizierer 478 Performance-Vergleich 481 Quantifizierer 477 Quantifizierer auf Gruppen 486 Rückreferenzen 489 String-Anfang und -Ende 483 Strings ersetzen 490, 492 Zeichenklassen 476 Reihenfolge von Unit-Tests 572 Rekursionen 248 Rekursive Methoden 248 RelativeSource-Eigenschaft (WPF) 923
1327
Index
Q ■■■■■■■■■■
Index
RelativeSource-Klasse (WPF) 759 ReleaseMutex-Methode 1194 remarks-Element 530 remove-Accessor 333 RemoveAll-Methode List 398 List-Auflistung 402 XElement-Klasse 1057 RemoveAt-Methode IList 383 List-Auflistung 402 SortedList 414, 420 RemoveAttributes-Methode 1057 RemovedItems-Eigenschaft (WPF) 847 RemoveItem-Methode 434 Remove-Methode Dictionary-Klasse 420 ICollection 383 IDictionary 385 LinkedList 428 List-Auflistung 402 StringBuilder-Klasse 471 String-Klasse 462 XElement-Klasse 1057 RemoveNodes-Methode 1058 RemoveRange-Methode 398 Renamed-Ereignis 619 RenderTransform-Eigenschaft (WPF) 932 RepeatButton-Steuerelement (WPF) 837 Repeat-Methode (LINQ) 699 ReplaceAll-Methode 1058 ReplaceAttributes-Methode 1058 Replace-Methode StringBuilder-Klasse 472 String-Klasse 462, 466 ReplaceNodes-Methode 1058 ReplaceNullChars-Methode 568 ReplaceWith-Methode 1058 ReportProgress-Methode 1153 Reset-Methode 382, 1196 ResizeFrameHorizontalBorderHeight-Eigenschaft (WPF) 796 ResizeFrameVerticalBorderWidth-Eigenschaft (WPF) 796 ResizeMode-Eigenschaft (WPF) 811 ResolveAll-Methode 1132 Resolve-Methode 1132 ResourceId-Eigenschaft (WPF) 884 ResourceReferenceKeyNotFoundException 880 Resources.resx 97, 732, 952 Resources-Eigenschaft (WPF) 786, 877 Ressourcen allgemeine 947 Angabe in WPF 871 eingebettete 948 in separaten Assemblys referenzieren 874
1328
in WPF 868 resx 948 Wörterbuchressourcen 877 Restwertdivision 204 Resume-Methode 1165 ResXFileCodeGenerator 961 RESX-Ressourcen 948 return 223, 239 returns-Element 530 Reverse-Methode 398 Reverse-Methode (LINQ) 700 Rijndael-Klasse 1272 RijndaelManaged-Klasse 1272 RIPEMD160-Klasse 1268 roaming user 612 RollingFileAppender-Klasse 591 RootDirectory-Eigenschaft 616 Root-Eigenschaft 610, 1060 RootFolder-Eigenschaft 794 RotateTransform-Klasse (WPF) 932 Round-Methode 515, 520 Routed events (WPF) 777 RoutedCommand-Klasse (WPF) 892 RoutedEventArgs-Klasse (WPF) 778 RoutedEvent-Eigenschaft (WPF) 778, 903 RoutedUICommand-Klasse (WPF) 892 Routingereignisse (WPF) 777 Row-Eigenschaft (WPF) 862 RowSpan-Eigenschaft (WPF) 863 RSACryptoServiceProvider-Klasse 1275 RSA-Klasse 1275 RSA-Verschlüsselung 1276 Rückgabeparameter 245 Rückgabetyp 239 Rückreferenzen (Reguläre Ausdrücke) 489 Runden 520 RuntimeWrappedException 444 RunWorkerAsync-Methode 1153 RunWorkerCompleted-Ereignis 1153
S ■■■■■■■■■■ Safe Point 1164 Satelliten-Assemblys 956 Satz des Pythagoras 517 SaveFileDialog-Klasse (WPF) 790 Save-Methode 1047 sbyte 172 ScaleTransform-Klasse (WPF) 932 Schleifen do 224 for 225
Index
SelectedItem-Eigenschaft (WPF) 845 SelectedItems-Eigenschaft (WPF) 845 SelectedPath-Eigenschaft 794 SelectedValue-Eigenschaft (WPF) 845 SelectedValuePath-Eigenschaft (WPF) 845 SelectionChanged-Ereignis (WPF) 848 SelectionEnd-Eigenschaft (WPF) 850 SelectionMode-Eigenschaft (WPF) 845 SelectionStart-Eigenschaft (WPF) 850 select-Klausel (LINQ) 671 SelectMany-Methode 1107 SelectMany-Methode (LINQ) 711 Select-Methode (LINQ) 670 Selector-Klasse (WPF) 844 Semaphore-Klasse 1201 SendOrPostCallback-Delegat 1180 Separator-Klasse (WPF) 854 SequenceEqual-Methode (LINQ) 702 Sequenz 652 Serialisierung 1007 binäre 1015 Datenvertragserialisierung 1019 Grundlagen 1007 SOAP-Serialisierung 1015 XML-Serialisierung 1009 Serializable-Attribut 367, 1015 Serialization Mode-Einstellung (LINQ to SQL) 1092 Serialize-Methode 1016 Server Data Type-Einstellung (LINQ to SQL) 1094 Server-Explorer 132 SessionEnding-Ereignis (WPF) 787 set-Accessor 253 SetAttributes-Methode 601 SetAttributeValue-Methode 1058 SetBinding-Methode (WPF) 923 SetData-Methode 1204, 1253 SetElementValue-Methode 1058 SetEquals-Methode 426 SetField 1225 SetItem-Methode 435 SetLastError-Eigenschaft 1208 SetLastError-Funktion 1217 SetMaxThreads-Methode 1174 SetProperty 1225 SetResourceReference-Methode (WPF) 882 Settings.settings 97, 732 Settings-Klasse 942 Setup 972 Benutzerdefinierte Aktionen 979 Setup-Assistent 972 SetValue-Methode FieldInfo-Klasse 1244 WPF 770 XElement-Klasse 1058
1329
Index
foreach 226, 382 fußgesteuerte 224 kopfgesteuerte 224 while 223 Schließen eines Fensters 818 Schlüssel bei der Verschlüsselung 1262 geheimer 1263 öffentlicher 1263 privater 1263 Schlüsselwörter C# 146 XAML 760 Schnellansicht 548 Schnellüberwachungsfenster 550 Schnellzugriffstasten 836 Schnittmenge 426 Schnittstellen 303 aufteilen 281 aus Klasse extrahieren 131 Entwurfsrichtlinien 304 Erweiterungsmethoden 344 generische 353 ICollection 382 IComparable 318, 404 IDictionary 384 IDisposable 271 IEnumerable 381 IEnumerator 381 IEquatable 318, 404 IList 383 implementieren 306 partielle 281 überschreiben 310 Schreiben von Dateien 630 von Textdateien 634 Schreibgeschützte Eigenschaften 254 Schreibgeschützte Felder 254 Schriftarten (WPF) 803 ScRGB-Farbraum 799 Scrollen (WPF) 863 ScrollViewer-Steuerelement (WPF) 863 sealed-Modifizierer 303, 1016 Second-Eigenschaft 509 Seconds-Eigenschaft (TimeSpan-Struktur) 512 Secure Hash Algorithm 1268 SecurityException 444 secutil.exe 75 seealso-Element 530 see-Element 531 Seek-Methode 622 Segmentierung (BitVector32) 432 SelectedIndex-Eigenschaft (WPF) 844
Index
SHA1Cng-Klasse 1268 SHA1-Klasse 1268 SHA1Managed-Klasse 1268 SHA256Cng-Klasse 1268 SHA256-Klasse 1268 SHA256Managed-Klasse 1268 SHA384Cng-Klasse 1268 SHA384-Klasse 1268 SHA384Managed-Klasse 1268 SHA512Cng-Klasse 1268 SHA512-Klasse 1268 SHA512Managed-Klasse 1268 sharedListeners-Element 584 SharedSizeGroup-Eigenschaft (WPF) 863 SharpDevelop 136 short 172 ShowDialog-Methode Datei-Dialoge 792 WPF 819 ShowGridLines-Eigenschaft (WPF) 863 ShowInTaskbar-Eigenschaft (WPF) 812 Show-Methode (WPF) 815 ShowModal-Methode (WPF) 815 ShowNewFolderButton-Eigenschaft 794 ShowReadOnly-Eigenschaft (WPF) 791 Shutdown-Methode (WPF) 787 ShutdownMode-Eigenschaft (WPF) 786 Sicherer Punkt (Threads) 1164 Sicherheit 1257 Daten verschlüsseln 1270 deklarative 1261 Hashing-Verfahren 1262, 1267 imperative 1261 Sichtbarkeit siehe Gültigkeitsbereich Sieb des Eratosthenes 387 SignalAndWait-Methode 1201 Signalisierungskonstrukte 1195 Signatur 1294 bei Methoden 242 digitale 1263, 1264 signcode.exe 994 SignData-Methode 1279 Signieren von Assemblys 1230 Sign-Methode 515 SignTool.exe 75 Silverlight 728, 730 Single-Methode (LINQ) 688 SingleOrDefault-Methode (LINQ) 688 Singleton-Entwurfsmuster 889 Sinh-Methode 514 Sin-Methode 514 Sinus 514 siteOfOrigin 876
1330
SizeChanged-Ereignis (WPF) 813 sizeof-Operator 215 SizeToContent-Eigenschaft (WPF) 960 Skins (WPF) 764, 919 Skip-Methode (LINQ) 696 SkipWhile-Methode (LINQ) 696 Slider-Steuerelement (WPF) 850 SmallChange-Eigenschaft (WPF) 849 SmtpAppender-Klasse 591 SmtpPickupDirAppender-Klasse 591 sn.exe 1234 Snoop 743 SOAP Serialisierung 1015 SoapFormatter-Klasse 1016 SolidColorBrush-Klasse (WPF) 800 Solution 91 SortDescriptions-Eigenschaft (WPF) 847 SortedDictionary-Auflistung 413 mit speziellen Vergleichsobjekten 421 Verwendung 378 SortedList-Auflistung 439 SortedList-Auflistung 413 mit speziellen Vergleichsobjekten 421 Verwendung 378 Sortieren Auflistungen, absteigend 405 LINQ 668 von Auflistungen 404 von Methoden-Parametern 131 von using-Direktiven 132 Sort-Methode Array-Klasse 396 List 398 Source-Eigenschaft 450 Source-Eigenschaft (WPF) 778, 923 Source-Einstellung (LINQ to SQL) 1094 source-Element 580 sources-Element 580 Späte Bindung (COM) 1224 SpecialFolder-Aufzählung 612 Speicherverbrauch von Auflistungen 412 Spezialformate parsen (Datumswerte) 505 Spezifische Kulturen 955 Spezifische-Version-Option 98, 1232 Split-Methode Regex-Klasse 492 String-Klasse 468 SQL Injection 1294 SQL Server Management Studio Express 1082 SqlDateTime-Überlauf 1117 SqlMetal.exe 75, 1087 SqlMethods-Klasse 1100
Index
Stopwatch-Klasse 526 Storeadm.exe 75 Stream-Adapter 624 StreamReader-Klasse 624, 634 Streams 620 StreamWriter-Klasse 624, 634 String 171, 172 StringBuilder-Klasse 469 StringCollection-Auflistung 439 StringDictionary-Auflistung 439 Stringliterale 183 wortwörtliche 184 StringReader-Klasse 628 Strings 182, 185 an API-Funktionen übergeben 1211 bearbeiten 460 einfügen 467 entschlüsseln 1270 ersetzen 466 ersetzen (Reguläre Ausdrücke) 490, 492 Escape-Sequenzen 183 formatieren 493 in Byte-Array konvertieren 632 in Groß-/Kleinschreibung umwandeln 467 kürzen und extrahieren 466 löschen 467 parsen 503 splitten 492 Teilstrings extrahieren 466 über StringBuilder manipulieren 469 vergleichen 209, 210, 463, 464 Verkettungen 204 verschlüsseln 1270 wortwörtliche 184 Zeilenumbrüche 184 Stringvergleiche Groß-/Kleinschreibung ignorieren 465 String-Verkettungen 204 StringWriter-Klasse 628 Strong Name 80 Strong name key file 1233 struct 232 StructLayout-Attribut 1215 Strukturen aufteilen 281 automatische Initialisierung 160 deklarieren 233 erweitern 341 Felder 235 generische 345 Konstruktoren 266 Konvertierungs-Operatoren 316 partielle 281 Unterschiede zu Klassen 231 Styles siehe Stile (WPF)
1331
Index
Sqrt-Methode 516 sRGB-Farbraum 799 Stack 156 Stack-Auflistung 424, 439 Verwendung 379 StackOverflowException 249, 269, 444 StackPanel-Steuerelement (WPF) 860 Stack-Trace 454 StackTrace-Eigenschaft 450 Standarddialoge Datei öffnen 790 Datei speichern 790 Farbauswahl 795 Ordnerauswahl 793 Standardkonstruktor 266 Standardtypen 171 Instanzmethoden 172 Klassenmethoden 173 Starker Name 80, 1231 Starten eines Threads 1157 von Visual Studio 91 Start-Methode 526 StartsWith-Methode 462 Startup-Ereignis (WPF) 787 StartupUri-Attribut (WPF) 731 STAThread-Attribut 144 Static 1225 StaticExtension-Klasse (WPF) 759 static-Modifizierer 239, 273 StaticResourceExtension-Klasse (WPF) 759 StaticResource-Markuperweiterung 878 Statische Eigenschaften 273 Statische Felder 273 Statische Klassen 275 Statische Klassenelemente 273 Statische Konstruktoren 277 Statische Methoden 152, 276 Statuszeilen (WPF) 858 Steuerelemente (WPF) Bereichssteuerelemente 849 Inhalt 827, 835 Layout 858 Listen 842 Positionierung 830 Schnellzugriffstasten 836 Text 851 verankern 102 Vorder- und Hintergrund 800 Stile (WPF) 762, 905 benannte, typisierte 908 benannte, untypisierte 909 typisierte 907 vererben 911 Stoppuhr 526
Index
Subklasse 285, 1295 SubmitChanges-Methode 1114 Substring-Methode 462 Subtract-Methode 513 Success-Eigenschaft 482 Suchen Auflistungs-Performancetest 410 in Auflistungen mit BinarySearch 407 in Auflistungen mit IndexOf 406 in Auflistungen mit LINQ 410 in Auflistungen mit Prädikaten 408 in XML-Dokumenten 1050, 1061 inkrementell 133 LINQ 694 mit Ergebnisliste 133 von Dateien 611 von Fehlern 454 von Ordnern 611 Suchen von Informationen siehe Recherche Suffixe Dezimalzahlen 175 Ganzzahlen 175 summary-Element 530 Sum-Methode (LINQ) 690 Superklasse 285, 1295 Suspend-Methode 1165 switch-Attribut 580 switches-Element 580 switchType-Attribut 580 switch-Verzweigung 218 mit Variablen 221 Symbolleisten (WPF) 857 SymmetricAlgorithm-Klasse 1271 SymmetricExceptWith-Methode 426 Symmetrische Differenz 426 Symmetrische Verschlüsselung 1270 Grundlagen 1263 Synchronisieren von Threads 1183 Synchronized-Eigenschaft 1204 SynchronizingObject-Eigenschaft 1203 Syntaxhilfe 106 System.Core.dll 72 System.Data.dll 72 System.Data.Linq.dll 73 System.Data.OracleClient.dll 73 System.dll 72 System.Drawing.dll 73 System.Net.dll 73 System.Security.dll 73 System.Web.dll 73 System.Web.Services.dll 73 System.Windows.Forms.dll 73 System.Xml.dll 73
1332
System.Xml.Linq.dll 73 SystemColors-Klasse (WPF) 800 SystemDirectory-Eigenschaft 612 Systemordner ermitteln 612
T ■■■■■■■■■■ TabIndex-Eigenschaft (WPF) 805, 814 Table-Klasse 1096 Tabulatorreihenfolge 805, 814 Tag-Eigenschaft (WPF) 805 Take-Methode (LINQ) 696 TakeWhile-Methode (LINQ) 696 Tangens 514 Tanh-Methode 514 Tan-Methode 514 Target-Eigenschaft (WPF) 840 TargetName-Eigenschaft (WPF) 918 Tastatur abfragen 807, 865 Tausendertrennzeichen-Problem 502 Teilen siehe Divisionen Teilstrings extrahieren 466 TemplateBindingExtension-Klasse (WPF) 759 TemplateBinding-Markuperweiterung 915 Temporäre Dateien Name erzeugen 615 Ordner 613 Test Driven Development 564 Testansichtfenster 569 TestClass-Attribut 566 TestCleanup-Attribut 572 TestContext-Eigenschaft 572 TestContext-Klasse 572 Testen auf erwartete Ausnahmen 569 mit Unit-Tests 564 Testgetriebene Entwicklung 564 TestInitialize-Attribut 571 TestLogsDir-Eigenschaft 573 TestMethod-Attribut 566 TestName-Eigenschaft 573 Testreihen 572 TextBox-Steuerelement (WPF) 852 TextChanged-Ereignis (WPF) 853 Textdateien lesen und schreiben 634 Text-Steuerelemente (WPF) 851 TextWrapping-Eigenschaft (WPF) 851 TextWriterTraceListener-Klasse 582 ThemeDictionaryExtension-Klasse (WPF) 760 ThemeInfo-Attribut 884
Index
TotalHours-Eigenschaft 512 TotalMilliseconds-Eigenschaft 512 TotalMinutes-Eigenschaft 512 TotalSeconds-Eigenschaft 512 TotalSize-Eigenschaft 616 ToUniversalTime-Methode 509 ToUpper-Methode 463 TraceAppender-Klasse 591 TraceData-Methode 577 TraceEvent-Methode 577 TraceEventType-Aufzählung 577 TraceInformation-Methode 577 TraceLevel-Aufzählung 580 TraceSource-Klasse 575 TraceTransfer-Methode 577 TransactionAbortedException 1128 TransactionScope-Klasse 1127 Transaktionen 1126 Timeouts 1128 TransactionAbortedException 1128 Transformationen (WPF) 932 TransformGroup-Klasse (WPF) 932 Transformieren mit LINQ to XML 1067 TranslateTransform-Klasse (WPF) 932 Trigger (WPF) 763, 899 TriggerAction-Klasse (WPF) 903 Trigger-Klasse (WPF) 901 Triggers-Auflistung (WPF) 917 Trigonometrie 516 TrimEnd-Methode 463, 466 TrimExcess-Methode 413 Trim-Methode 463, 466 TrimStart-Methode 463, 466 TripleDESCryptoServiceProvider-Klasse 1272 TripleDES-Klasse 1272 TrueForAll-Methode 398 Truncate-Methode 516 try-Block 272, 448 TryEnter-Methode 1193 TryFindResource-Methode (WPF) 882 TryGetValue-Methode 385 TryParseExact-Methode 508 TryParseExcact-Methode 505 TryParse-Methode 173, 501, 502 DateTimeOffset-Struktur 508 TimeSpan-Struktur 513 Typ 77 Type cast siehe Typumwandlungen Type inference 199, 349 TypeExtension-Klasse (WPF) 760 TypeInTargetAssembly-Eigenschaft (WPF) 884 Type-Klasse 1238
1333
Index
Themen (WPF) 764 Themes siehe Themen (WPF) ThenBy-Methode (LINQ) 669 this-Schlüsselwort 240 ThreadAbortException 1164, 1177 ThreadException-Ereignis 458 beim Multithreading 1176 Threading siehe Multithreading Thread-Klasse 1158 ThreadPool-Klasse 1173 ThreadPriority-Aufzählung 1163 Threads siehe Multithreading Threadsicherheit 1142 ThreadStart-Delegat 1157 ThreadState-Eigenschaft 1165 Tick-Ereignis 1203 TickFrequency-Eigenschaft (WPF) 850 TickPlacement-Eigenschaft (WPF) 850 Ticks-Eigenschaft DateTime und DateTimeOffset 506 DateTimeOffset-Struktur 509 TimeSpan-Struktur 512 Tiefes Klonen 1008 Time Stamp-Einstellung (LINQ to SQL) 1094 TimeOfDay-Eigenschaft 510 Timeouts bei Transaktionen 1128 beim Debuggen 563 Timer-Klasse 1203 TimeSpan-Struktur 512 Title-Eigenschaft (WPF) 791, 811 ToArray-Methode LINQ 698 List 398 ToCharArray-Methode 462 ToDictionary-Methode (LINQ) 698 ToggleButton-Steuerelement (WPF) 837 ToList-Methode (LINQ) 698 ToLocalTime-Methode 509 ToLookup-Methode (LINQ) 698 ToLower-Methode 463 Tonleiter 313 ToOffset-Methode 509 Toolbox 99 Codeausschnitte 130 Tools des .NET-Framework SDK 73 ToolTip-Eigenschaft (WPF) 806 Tooltipp (WPF) 806 Topmost-Eigenschaft (WPF) 812 ToString-Methode 186, 191 zum Formatieren 494 TotalDays-Eigenschaft 512 TotalFreeSpace-Eigenschaft 616
Index
Typen 155 anonyme 260 anonyme in LINQ 670 Datumswerte 181 dynamisch erzeugen 1246 dynamisch instanzieren 1241 erweitern 341 evaluieren 1238 Fließkomma 175 generische 164, 345 Integer 174 konvertieren 188 Object 185 Referenztypen erzeugen 160 Rückschluss 199, 349 Standardtypen 171 String 171 Typumwandlung 189 ValueType 159 verschachtelte 283 von Ausdrücken 201 Wert- und Referenztypen 156 Werttypen erzeugen 160 typeof-Operator 214, 1238 typeparam-Element 530 typeparamref-Element 531 Typkonverter (WPF) 753 Typparameter einschränken 349 Typ-Rückschluss 199, 349 Typsicherheit 155 mit generischen Typen 347 Typumwandlungen 189
U ■■■■■■■■■■ Überladen von Methoden 242 Überlauf 177, 202 Übermenge 426 Überprüfen von Eingaben 501 Überschreiben von Eigenschaften und Methoden 294 von Equals 318 von GetHashCode 318 Überwachen des Dateisystems 618 Überwachungsfenster 551 UCS-2 63 UCS-4 63 UdpAppender-Klasse 591 Uhrzeit 518 UIElement-Klasse (WPF) 784 uint 172
1334
UI-Threads 1140 ulong 172 Umbenennen von Bezeichnern 130 von Dateien 606 Umgestaltung siehe Refactoring Umwandeln von null 213 von Typen 189 Unboxing 188 mit generischen Typen vermeiden 347 unchecked-Bock 179 Unchecked-Ereignis (WPF) 838, 855 Ungenauigkeit bei Fließkommatypen 176 Ungleichheitsverknüpfungen 716 Unicode-Blockbereich 476 Unicode-Gruppe 476 Unicode-Zeichenklasse 476 Unicode-Zeichensatz 1299 Uniform Resource Identifier 871 Union-Methode (LINQ) 700 UnionWith-Methode 427 Unit-Tests 564 datengetriebene 573 debuggen 571 in Visual Studio 565 initialisieren 571 Reihenfolge 572 Testreihen 572 Universal Character Set 2 63 Universal Time Coordinated 506 Unloaded-Ereignis (WPF) 813 Unload-Methode 1251 UnmanagedType-Aufzählung 1212 Unmodales Öffnen 815 Unprotect-Methode 1265 unsafe 149 UnsafeQueueUserWorkItem-Methode 1175 UnsafeRegisterWaitForSingleObject-Methode 1175 Unsichere Anweisungsblöcke 149 Unterabfragen (LINQ) 712 Unterlauf 178, 202 Untermenge 426 Update Check-Einstellung (LINQ to SQL) 1094 Update-Patch 987 Upgrade-Methode 945 URI 871, 1296 Uri-Klasse (WPF) 871 ushort 172 using-Anweisung 272 using-Direktive 145 sortieren 132 unbenutzte löschen 132 UtcDateTime-Eigenschaft 510
Index
V ■■■■■■■■■■ ValidateValueCallback 775 value-Argument 253 Value-Eigenschaft Nullables 168 WPF 849 XElement-Klasse 1055 value-Element 530 Values-Eigenschaft Dictionary-Klasse 419 IDictionary 385 ValueType-Klasse 159 Variable Argumente 247 Variablen 195 Deklaration 196 implizit typisierte 198 in case-Blöcken 221 Lebensdauer 195 Referenzen anzeigen 131 zu Parametern heraufstufen 131 Variant-Typ 1222 var-Schlüsselwort 198 Verankerung von Steuerelementen 102 Verbergen von Methoden 291 Vereinigungsmenge 427 Vererbung 284 Erweitern von Klassen 285 Klassen ableiten 285 von WPF-Stilen 911 Vergleiche 208 von Datumswerten 511 von Referenztypen 209 von Strings 209, 210, 464 Vergleichsausdrücke verknüpfen 211 VerifyData-Methode 1279 Verkettete Listen 428 Verknüpfungen Code-Editor 128 in LINQ 679 in LINQ to SQL 1106 innere 679 linksgerichtete 684 Ungleichheitsverknüpfungen 716 Verschachtelte Typen 283
Verschieben eines Ordners 610 von Dateien 606 Verschlüsselung Asymmetrische 1274 asymmetrische 1263 Hashcodes 1262, 1267 RSA 1276 Schlüssel 1262 symmetrische 1263, 1270 von Dateien 1265 Versiegelte Klassen 303 Versionierung bei den Datenvertrag-Serialisierern 1027 bei der binären Serialisierung 1018 Versionsnummer 969 Versionsverwaltung bei XML-Dokumenten 1068 VerticalAlignment-Eigenschaft (WPF) 831 VerticalContentAlignment-Eigenschaft (WPF) 831 VerticalOffset-Eigenschaft (WPF) 857 VerticalScrollBarVisibility-Eigenschaft (WPF) 853, 863 Verweise 97, 732 verwalten 98 Verzögerte Signierung 1234 Verzögertes Laden LINQ to SQL 1093, 1109 Verzweigungen if 216 switch 218 ViewBox-Dekorator (WPF) 904 Virtual Method Table 295 virtual-Modifizierer 294 Virtuelle Eigenschaften 292, 294 Virtuelle Methoden 292, 294 Visibility-Eigenschaft (WPF) 806 Visual Studio 87 Ausführen einer Projektmappe 110 Befehle ausführen 553 Codeausschnitte 129 Code-Editor 103 dynamische Hilfe 107 Eigene Vorlagen 134 Eigenschaftenfenster 101 Express 46 Fenster positionieren 96 Formular-Designer 100 Hostprozess 544 IDE-Einstellungen 95 IntelliSense 105 Klassenansicht 98 Kompilieren 109 Konsolenanwendungen 119
1335
Index
UtcNow-Eigenschaft 508 UTC-Zeit 506 UTF-32 63 UTF-8 63 UUID 1296
Index
Lesezeichen 128 Neues in Visual Studio 2008 137 Optionen 126 Professional 46 Projektmappenkonfiguration 111 Projekttypen 88 Refactoring 130 Server-Explorer 132 sonstiges Neues 139 Standard 46 starten 91 Team Suite Edition 46 Toolbox 99 Unit-Tests 565 Verweise verwalten 98 VisualBrush-Klasse (WPF) 800 Visualisierer 548 Mole (WPF) 742 Visual-Klasse (WPF) 784 Visual-Studio-Befehle 553 VisualTreeHelper-Klasse (WPF) 767 Visuelle Bäume (WPF) 765 VMT 295 void 239 volatile-Schlüsselwort 1204 VolumeLabel-Eigenschaft 616 Vordergrund eines Fensters oder Steuerelements 800 Vordergrundfenster 812 Vordergrundthreads 1141 Vorkompilieren von Assemblys 1235 Vorlagen (WPF) 763, 913 VTable 295
W ■■■■■■■■■■ W3C 1031 Währungsberechnungen 177 WaitAll-Methode 1201 WaitAny-Methode 1201 WaitForExit-Methode 526 WaitForPendingFinalizers-Methode 540 WaitHandle-Klasse 1195 Wait-Methode 1201 WaitOne-Methode 1193 Wandernder Benutzer 612 WarnFormat-Methode 587 Warn-Methode 587 Warnungen deaktivieren 228 Warteschlangen-Muster 1198 Webanwendungen 76
1336
WebMethod-Attribut 367 WebPageTraceListener-Klasse 582 Websites zu C# und .NET 57 Weiterwerfen einer Ausnahme 450 Werfen einer Ausnahme 446 Werttypen 156 erzeugen 160 Hintergründe 159 ohne Wert 167 Zuweisungen 162 Wert-Übergabe 244 WheelScrollLines-Eigenschaft (WPF) 796 where-Klausel (LINQ) 667 Where-Methode (LINQ) 666, 667 where-Schlüsselwort 349 while-Schleife 223 Width-Eigenschaft (WPF) 812, 830 Wiederverwendbarer Code 285, 345 wincv.exe 75 Window-Klasse (WPF) 810 Windows Installer 972 Windows Presentation Foundation siehe WPF Windows.Forms 39 globale Ausnahmebehandlung 458 Lokalisierung 959 Unterschiede zu WPF 726 Windows-Eigenschaft (WPF) 786 WindowStartupLocation-Eigenschaft (WPF) 812 WindowState-Eigenschaft (WPF) 812 WindowStyle-Eigenschaft (WPF) 812 WiX 973 WorkArea-Eigenschaft (WPF) 796 WorkerReportsProgress-Eigenschaft 1154 WorkerSupportsCancellation-Eigenschaft 1154 World Wide Web Consortium 1031 Wortwörtliche Stringliterale 184 WPF Abhängigkeitseigenschaften 768 Angefügte Eigenschaften 776 Angefügte Ereignisse 781 Animationen 933 Anwendung 786 Anwendungen debuggen 741 Auflistungen binden 926 Befehle 887 Bereichs-Steuerelemente 849 Control-Eigenschaften 804 Datenbindung 922 Datenkontext 928 Datentrigger 903 Datenvorlagen 929 Dekoratoren 763, 904 Dialoge 819 Eingabe-Bindungen 898
Index
WriteLine-Eigenschaft 573 WriteLine-Methode 635 Write-Methode 622, 635 WriteObject-Methode 1021 WriteStartDocument-Methode 1074 WriteStartElement-Methode 1074 WriteTimeout-Eigenschaft 622 WriteValue-Methode 1074 WSDL 1019 Wurzel berechnen 516 Wurzelknoten ermitteln 1054
X ■■■■■■■■■■ x:ClassModifier-Schlüsselwort (XAML) 760 x:Class-Schlüsselwort (XAML) 760 x:Code-Schlüsselwort (WPF) 760 x:FieldModifier-Schlüsselwort (WPF) 760 x:Key-Markuperweiterung (WPF) 877 x:Key-Schlüsselwort (WPF) 760 x:Name-Schlüsselwort (WPF) 760 x:Shared-Markuperweiterung (WPF) 881 x:Shared-Schlüsselwort (WPF) 760 x:Subclass-Schlüsselwort (WPF) 761 x:TypeArguments-Schlüsselwort (WPF) 761 x:Uid-Schlüsselwort (WPF) 761, 963 x:XData-Schlüsselwort (WPF) 761 XAML 725, 748 Content-Eigenschaft 754 Grundaufbau 748 Inhaltseigenschaft 754 Markup-Erweiterungen 756 Namensräume 749 Objektinitialisierung 752 Positions- und Größenangaben 798 Schlüsselwörter 760 Typkonverter 753 XamlPad 748 XamlParseException 741 XAttribute-Klasse 1034 XBAP 729 XContainer-Klasse 1034 Member zur Navigation 1051 XCopy-Verteilung 972 XDeclaration-Klasse 1060 XDocument-Klasse 1060 XDocumentType-Eigenschaft 1060 X-DOM 1032 XElement-Klasse 1034 Member zur Navigation 1051
1337
Index
Eingabe-Gesten 898 Ereignistrigger 903 Farbangaben 799 Fenster 810 geroutete Ereignisse 777 Inhalts-Steuerelemente 835 Kontextmenüs 856 Layout-Container 858 Listen-Steuerelemente 842 logische Bäume 765 Lokalisierung 960 Menüs 854 Möglichkeiten 729 Multi-Trigger 902 Positionierung 830 Positions- und Größenangaben 797 Ressourcen 868 Ressourcenangaben 871 Schnellzugriffstasten 836 Schriftangaben 803 Silverlight 728 Skins 919 Standarddialoge 790 Standardeigenschaften 804 Standardereignisse 806 Standardmethoden 809 Statuszeilen 858 Steuerelemente 824 Stile 762, 905 Stile vererben 911 Symbolleisten 857 Tabulatorreihenfolge 814 Text-Steuerelemente 851 Themen 880, 921 Tooltipps 806 Transformationen 932 Trigger 763, 899 Typkonverter 753 Unterschiede zu Windows.Forms 726 Visualisierer 742 visuelle Bäume 765 Vorlagen 763, 913 Window-Eigenschaften 810 Window-Ereignisse 813 WPF-Anwendungen 725 WPF-Anwendungen 725 globale Ausnahmebehandlung 459 WrapPanel-Steuerelement (WPF) 860 Write 119 WriteAttributeString-Methode 1074 WriteByte-Methode 622 WriteElementString-Methode 1074 WriteLine 119
Index
XML 64, 1031 ändern 1057 Document Object Model 1031 Entity-Referenzen 532 erzeugen 1043, 1047 gemischte Inhalte 1046 lesen und schreiben 1041, 1069 LINQ to XML 1061 Namensräume 65 Navigation 1050, 1054 serialisieren 1047 transformieren 1067 XML-Schema (XSD) 1035 XSL 1040 XML Paper Specification 876 XML Schema Definition siehe XSD XmlArray-Attribut 1014 XmlArrayElement-Attribut 1014 XmlAttribute-Attribut 1013 XmlDictionaryReader-Klasse 1023 XmlDictionaryWriter-Klasse 1023 XML-Dokumentation 151 XML-Dokumente ändern 1057 mit LINQ erzeugen 1065 mit LINQ to XML abfragen 1061 mit LINQ to XML transformieren 1067 mit Namensräumen abfragen 1065 navigieren und durchsuchen 1050 speichern 1047 über das X-DOM erzeugen 1043, 1044 über das X-DOM laden und parsen 1042 über einen XmlReader lesen 1069 über einen XmlWriter erzeugen 1073 validierend lesen 1075 Wurzelknoten ermitteln 1054 XmlElement-Attribut 1013 XmlIgnore-Attribut 367, 1014 XmlInclude-Attribut 1014 XML-Namensräume 65 beim Abfragen 1065 beim Erzeugen 1047 beim Navigieren 1054 xmlns 66 XmlReader-Klasse 624 XmlRoot-Attribut 1013 XML-Serialisierung 1009 XmlSerializer-Klasse 1010
1338
XmlWriter-Klasse 624, 1073 XNamespace-Klasse 1048 XNode-Klasse 1034 Member zur Navigation 1050 XObject-Klasse 1033 XOr-Operator 206 XPath 1039 XPath-Eigenschaft (WPF) 923 XProcessingInstruction-Klasse 1060 XPS 876 XSD 1035 Xsd.exe 75 XSL 1040 XText-Klasse 1046
Y ■■■■■■■■■■ Year-Eigenschaft 509 yield break 388 yield return 385 yield-Anweisung 385
Z ■■■■■■■■■■ Zahlen formatieren 494 runden 520 Zahlformatierungen 494 Zeichen 182 Zeichenketten siehe Strings Zeichenklassen (reguläre Ausdrücke) 476 Zeichenkodierung 62 Zeichensätze 62 Zeilenumbrüche 184 Zeitspannen 512 Zero-Eigenschaft 513 Zertifikat 992 ZIP-Algorithmus 637 ZipPackage-Klasse 637 Zuweisungen 162, 207 Zwischenablage 856, 865 Zwischenablagering 134
"MMFT8JDIUJHF[VNOFVFO#FUSJFCTTZTUFN
-ÚTVOHTCF[PHFOFSIBMUFO4JFIJFSBMMF*OGPSNBUJPOFOGàSEFOPQUJNBMFO&JOTBU[WPO 8JOEPXT7JTUBTPXJFOàU[MJDIF5JQQTVOE5SJDLT/FCFO5IFNFOXJF*OTUBMMBUJPO EFSOFVFO"FSP0CFSGMÊDIF 7JTUBJN/FU[XFSL 7JTUBJN*OUFSOFUTUFIFOGPMHFOEF 'SBHTUFMMVOHFOJN.JUUFMQVOLUEFT#VDIFT8BTJTUOFV 8FMDIF7FSTJPOFOHJCUFT 8BT MFJTUFUEJF)PNF&EJUJPO XBTOJDIU 8JFGVOLUJPOJFSUEBT6QEBUFWPO91 ;VS)PNF #BTJD )PNF1SFNJVNVOE6MUJNBUF&EJUJPO 5IPNBT+PPT *4#/ &63
4JFTVDIFOFJOQSPGFTTJPOFMMFT)BOECVDI[VXJDIUJHFO1SPHSBNNFO PEFS4QSBDIFO %BT,PNQFOEJVNJTU&JOGàISVOH "SCFJUTCVDIVOE/BDI TDIMBHFXFSLJOFJOFN"VTGàISMJDIVOEQSBYJTPSJFOUJFSU
.FISBVGXXXNVUEF
(FIÚSUJOKFEFT#àSPSFHBM
&SMFCFO4JF&YDFMQVSWPOFJOFN&YDFM1SPGJEFSFSTUFO4UVOEF4FMCTUVNGBOHSFJDIF &YDFMBOXFOEVOHFOWFSMJFSFOKFU[UJISFO4DISFDLFO*HOBU[4DIFMTGàISU4JFEVSDIEBT VNGBOHSFJDIF1SPHSBNNVOEFSMÊVUFSUBVDILPNQMFYF;VTBNNFOIÊOHF.JU[BIMSFJDIFO 4DISJUUGàS4DISJUU"OMFJUVOHFO[FJHUFS*IOFOQSBYJTOBIXPFTMBOHHFIU *HOBU[4DIFMT *4#/ &63
4JFTVDIFOFJOQSPGFTTJPOFMMFT)BOECVDI[VXJDIUJHFO1SPHSBNNFO PEFS4QSBDIFO %BT,PNQFOEJVNJTU&JOGàISVOH "SCFJUTCVDIVOE/BDI TDIMBHFXFSLJOFJOFN"VTGàISMJDIVOEQSBYJTPSJFOUJFSU
.FISBVGXXXNVUEF
4PHFXJOOFO4JF;FJU
.JUEJFTFN#VDIHFMJOHUFT*IOFO *ISF"SCFJUNJU0VUMPPLJOEFS1SBYJTTPFGGFLUJWXJF NÚHMJDI[VHFTUBMUFO-FSOFO4JF EFO6NHBOHNJU&.BJMT "VGHBCFOVOE5FSNJOFO JO0VUMPPL[VPQUJNJFSFO0MBGWPO)PGGIJMGU*IOFO OFVFVOETUSFTTSFTJTUFOUF-PHJTUJL ,PO[FQUFVOE4USBUFHJFOGàSEBT.FEJVN&.BJM[VFOUXJDLFMO [FJHU*IOFOMFJDIU OBDIWPMM[JFICBSF8FHF[VS&OUXJDLMVOHFJOFT0VUMPPL1SPKFLUNBOBHFNFOUTVOE HJCU*IOFO5JQQTGàSFJOFCFTTFSFVOEàCFSTJDIUMJDIFSF0SEOFS4USVLUVS &JO[JHBSUJH%FS#VTJOFTT$POUBDU.BOBHFSJN%FUBJM 0MBGWPO)PGG *4#/ &63
.FISBVGXXXNVUEF
4UBOEBSEXFSL
&SGPMHTBVUPS%JSL-PVJTMJFGFSU*IOFOFJOFVNGBTTFOEF&JOGàISVOHJOEJF1SPHSBNNJFSVOH NJU7JTVBM$ *NFSTUFO5FJMXFSEFOEJFXJDIUJHTUFO4QSBDIHSVOEMBHFO CFIBOEFMU8FJUFSHFIUFTJN[XFJUFO5FJM[VEFOOPUXFOEJHFO&SXFJUFSVOHFOEFS 4QSBDIF6OEEBOOTUFJHFO4JFJOEJF1SPHSBNNJFSVOHWPOHSBGJTDIFO0CFSGMÊDIFO FJO FSTUFMMFO4UFVFSFMFNFOUFVOE.FOàMFJTUFO%B[VGJOEFO4JFNFISFSF "OXFOEVOHTCFJTQJFMF EJF4JFJOFJHFOFO1SPKFLUFOFJOTFU[FOLÚOOFO"VFSEFN FSGBISFO4JF XJF4JFFJHFOF%BSTUFMMVOHFO[FJDIOFOLÚOOFO"VGEFS$%GJOEFO4JFBMMF #FJTQJFMFBVTEFN#VDITPXJFEJF7JTVBM$ &YQSFTT&EJUJPO %JSL-PVJT *4#/ &63
4JFTVDIFOFJOQSPGFTTJPOFMMFT)BOECVDI[VXJDIUJHFO1SPHSBNNFO PEFS4QSBDIFO %BT,PNQFOEJVNJTU&JOGàISVOH "SCFJUTCVDIVOE/BDI TDIMBHFXFSLJOFJOFN"VTGàISMJDIVOEQSBYJTPSJFOUJFSU
.FISBVGXXXNVUEF
*EFBMGàS&JOTUFJHFS
%JSL-PVJTFSLMÊSU*IOFOJOEJFTFN#VDIWPO(SVOEBVGBMMFT XBT$ 1SPHSBNNJFSFS CFJN6NHBOHNJU7JTVBM$ CFSàDLTJDIUJHFONàTTFO"MTFSGPMHSFJDIFS"VUPS JOTCFTPOEFSF[VN5IFNB7JTVBM$ LFOOUFSEJF'SBHFO NJUEFOFOTJDI1SPHSBNNJFSFS C[X-FTFSCFTDIÊGUJHFO%JF"OUXPSUFOEBSBVGXFJFSàCFSBVTMPDLFS[VWFSNJUUFMO%JF #FJTQJFMFTJOEJNNFSBVTEFN-FCFOHFHSJGGFOVOEÊVFSTUMFJDIUWFSTUÊOEMJDIFSMÊVUFSU %JSL-PVJT *4#/ &63
4UBSUPIOF7PSXJTTFO.JUEFO#àDIFSOBVTEFS3FJIFv+FU[UMFSOFJDIi CFLPNNFO4JFFJOFOQSBLUJTDIFOVOEWFSTUÊOEMJDIFO&JOTUJFHJOQSPGFT TJPOFMMF$PNQVUFSUIFNFO
.FISBVGXXXNVUEF
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