Das Access 2003 Entwicklerbuch
programmer’s
choice
Die Wahl für professionelle Programmierer und Softwareentwickler. Anerkannte Experten wie z.B. Bjarne Stroustrup, der Erfinder von C++, liefern umfassendes Fachwissen zu allen wichtigen Programmiersprachen und den neuesten Technologien, aber auch Tipps aus der Praxis. Die Reihe von Profis für Profis!
Hier eine Auswahl:
Die C++ Programmiersprache
Die C# Programmiersprache
Bjarne Stroustrup 1084 Seiten € 49,95 (D), € 51,40 (A) ISBN 3-8273-1660-X
Anders Hejlsberg, Scott Wiltamuth, Peter Golde 696 Seiten € 49,95 (D), € 51,40 (A) ISBN 3-8273-2156-5
Das Buch, geschrieben vom Erfinder der Sprache, ist das umfassendste Werk zu C++. Es basiert auf dem ANSI/ ISO-C++-Standard und vermittelt aktuelle und verständliche Informationen zur Sprache, zur Standard Library und zu Design-Techniken. Die 4. Auflage des Bestsellers hat zwei neue Anhänge über Locales und Exception Safety.
C# ist eine moderne, objektorientierte und typsichere Programmiersprache, die die Produktivität eines RAD-Tool mit der Leistungsfähigkeit von Sprachen wie C oder C++ verbindet. Mit diesem Buch erhalten Sie die komplette Sprachreferenz. Geschrieben vom Erfinder selbst und Mitgliedern des Design-Teams.
André Minhorst
Das Access 2003 Entwicklerbuch
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über
abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ® Symbol in diesem Buch nicht verwendet. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
10
9
07
06
8
7
6
5 4
3
2
1
05
ISBN 3-8273-2265-0
© 2005 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Marco Lindenbeck, webwo GmbH ([email protected]) Titelbild: © Karl Blossfeldt Archiv, Ann und Jürgen Wilde, Zülpich/VG Bild-Kunst Bonn, 2005 Lektorat: Sylvia Hasselbach, [email protected] Korrektorat: Petra Kienle, München Herstellung: Elisabeth Prümm, [email protected] Satz: reemers publishing services gmbh, Krefeld, www.reemers.de Druck und Verarbeitung: Bercker Graph. Betrieb, Kevelaer Printed in Germany
Für Anja, Maja und Lena, die ungefähr auf Seite 312 zu uns stieß.
Inhalt
Vorwort
19
1
Datenbankanwendungen planen
23
1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11
Aller Anfang ist schwer Architektur Grobe Architektur: Systemaufbau und Schnittstellen Ein wenig feiner: Die Rolle der Datenbankobjekte Datenmodell Benutzungsoberfläche und Anwendungslogik Einzelheiten der Architektur Ergonomie Das Auge isst mit Gewohntes unterstützen Ungewohntes verständlich machen Größe und Position der Steuerelemente Schriften Menüs bereitstellen Tastenkombinationen bereitstellen Abläufe unterstützen Eins nach dem anderen Helfen Sie! Ist meine Anwendung ergonomisch?
23 25 26 27 28 28 34 35 35 36 36 36 38 38 39 39 40 41 41
2
Tabellen und Datenmodellierung
43
2.1 2.1.1 2.1.2 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1
Namenskonventionen für Tabellen und Felder Tabellennamen Feldnamen Normalisierung Die erste Normalform Die zweite Normalform Die dritte Normalform Weitere Normalformen Das richtige Maß treffen Integritätsregeln Integrität der Werte (Wertbereichsintegrität)
44 45 47 49 51 56 59 60 60 61 61
8
Inhalt
2.3.2 2.3.3 2.3.4 2.3.5 2.4 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.5 2.6 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.6.6 2.6.7 2.6.8 2.6.9 2.6.10 2.6.11 2.6.12 2.6.13 2.7 2.7.1 2.7.2 2.7.3
Format der Werte (semantische Integrität) Abhängigkeit von Feldinhalten (Attributintegrität) Eindeutige Datensätze (Entitätsintegrität) Referentielle Integrität Beziehungen Benennen von Primär- und Fremdschlüsselfeldern Halbautomatisches Festlegen von Beziehungen Festlegen referentieller Integrität 1:n-Beziehungen n:1-Beziehungen oder Lookup-Beziehungen m:n-Beziehungen 1:1-Beziehungen Reflexive Beziehungen Autowerte als Long oder GUID? Datenmodell-Muster Adressen-/Kundenverwaltung Rezepteverwaltung Artikelverwaltung CD-Verwaltung Projektverwaltung Mitarbeiterverwaltung Literaturverwaltung Mitgliederverwaltung Urlaubsverwaltung Aufgabenverwaltung Projektzeitverwaltung Kunden und Weihnachtsgeschenke Fahrtenbuch Bilder und Dateien in Tabellen speichern Bilder im OLE-Feld speichern Dateien nicht in der Datenbank speichern Dateien als Binärstrom in der Datenbank speichern
3
Abfragen
3.1 3.1.1 3.2 3.3 3.3.1 3.3.2 3.3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft Probleme mit Kriterienausdrücken bei SQL-Ausdrücken in VBA Aktualisierbarkeit von Abfragen UNION-Abfragen UNION-Abfragen zur Optimierung von Kombinationsfeldern Eindeutige Schlüssel mit UNION-Abfragen INSERT INTO mit UNION-Abfragen Suchen in m:n-Beziehungen Handhabung von 1:1-Beziehungen Extremwerte per Abfrage ermitteln Datensätze mehrfach anzeigen Nummerierung von Datensätzen Reflexive 1:n-Beziehungen Reflexive m:n-Beziehungen
62 63 64 64 66 67 68 69 71 72 75 78 83 85 86 86 89 91 92 94 95 95 97 99 99 100 102 104 105 106 107 114
127 128 136 140 142 142 143 145 145 149 154 157 160 162 163
Inhalt
4
9
Formulare
4.1 4.2 4.2.1 4.2.2
Formulare öffnen Ereignisse in Formularen und Steuerelementen Ereignisse in Formularen Abfolge und Bedeutung der Ereignisse beim Öffnen und Schließen eines Formulars 4.2.3 Abfolge und Bedeutung der Ereignisse beim Bearbeiten von Datensätzen 4.3 Ereignisse von Steuerelementen 4.4 Abbildung verschiedener Beziehungsarten 4.4.1 Einfache Daten in der Detailansicht 4.4.2 Einfache Daten in der Übersicht mit Endlosformularen 4.4.3 Einfache Daten in der Übersicht als Datenblatt 4.4.4 Daten in der Übersicht als Listenfeld 4.4.5 1:1-Beziehungen 4.4.6 n:1-Beziehungen 4.4.7 1:n-Beziehungen 4.4.8 1:n-Beziehung per Unterformular und Datenblattansicht 4.4.9 1:n-Beziehung per Listenfeld 4.4.10 m:n-Beziehungen in Haupt- und Unterformular 4.4.11 m:n-Beziehungen per Listenfeld 4.4.12 Reflexive Beziehungen 4.5 Von Formular zu Formular 4.6 Besonderheiten von Unterformularen 4.6.1 Eingabe von Daten ohne Detaildatensatz 4.6.2 Undo in Haupt- und Unterformular 4.7 Eingabevalidierung 4.7.1 Validieren direkt bei der Eingabe 4.7.2 Validieren vor dem Speichern 4.7.3 Sonderfälle beim Validieren 4.8 Suchen in Formularen 4.8.1 Schnellauswahl per Kombinationsfeld 4.8.2 Schnelles Filtern von Listenfeldern
5
Berichte
5.1 5.2 5.3 5.3.1 5.3.2 5.3.3 5.4 5.4.1 5.4.2
Berichte anzeigen Filtern und sortieren Berichtsbereiche und Ereignisse Berichtsbereiche Ereignisse in Berichten Zugriff auf die Berichtsbereiche Beispiele für den Einsatz der Berichts- und Bereichsereignisse Beim Öffnen: Auswertung von Öffnungsargumenten Bei Aktivierung und Bei Deaktivierung: Berichtsabhängige Funktionen ein- und ausschalten Bei Ohne Daten: Öffnen leerer Berichte vermeiden Bei Fehler: Fehler abfangen Bei Seite: Seiten verschönern
5.4.3 5.4.4 5.4.5
165 165 166 166 168 170 173 175 175 180 184 188 190 191 192 193 198 199 205 214 218 222 222 223 238 238 239 241 243 243 246
249 249 250 252 252 253 255 256 256 260 260 261 261
10
Inhalt
5.4.6 5.4.7 5.5 5.6 5.6.1 5.6.2 5.6.3 5.7 5.7.1 5.7.2 5.8 5.8.1 5.8.2 5.8.3 5.8.4 5.8.5 5.8.6 5.8.7 5.8.8
Beim Formatieren: Layout anpassen Beim Drucken Wichtige Eigenschaften von Berichten und Berichtsbereichen Darstellung von Daten Einzelne Tabellen 1:n-Beziehungen m:n-Beziehungen Berichte mit Unterberichten Unterberichte Unterberichte über mehrere Seiten Rechnungserstellung mit Berichten Konzept für die Erstellung des Berichts Erstellen des Gruppenkopfs Anlegen des Detailbereichs Berechnungen in Berichten oder Berechnungen in Formularen Summenbildung im Fußbereich der Gruppierung Feinheiten: Zwischensumme und Übertrag Überschriften für Folgeseiten und Rechnungsübertrag Rechnungsentwurf im Zusammenhang und Restarbeiten
6
VBA
6.1 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.4 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.6.6 6.6.7 6.7
Namenskonventionen in VBA Layout von Code Funktionalität vor Schönheit? Code einrücken zur Verdeutlichung der logischen Struktur Leerzeilen für bessere Lesbarkeit Zeilenumbrüche Anweisungen zusammenfassen Kommentare Konstanten Variablen Variablennamen Spezielle Variablennamen Aufzählungstypen Arrays Benutzerdefinierte Typen Alle Variablen verwenden Globale Variablen Kontrollstrukturen If Then-Anweisung Select Case For Next-Schleifen For Each-Schleifen Do While…Loop-Schleifen und Varianten Exit Die GoTo-Anweisung und Sprungmarken Routinen
262 263 266 270 271 276 280 280 281 284 284 287 287 288 289 289 290 290 291
297 297 298 299 299 301 302 303 304 305 307 307 308 309 310 311 312 312 312 313 315 316 317 317 319 320 320
Inhalt
11
6.7.1 6.7.2 6.7.3 6.7.4 6.7.5 6.7.6 6.7.7 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5
Routinenarten Routinennamen Starker Zusammenhalt von Routinen Lose Kopplung zwischen Routinen Parameter und Rückgabewerte einer Routine Gleichzeitige Rückgabe von Statuswert und Ergebnissen Alle Routinen verwenden Zugriff auf andere Bibliotheken und Objekte Type Libraries: Zugriff per Bibliothek Der Objektkatalog Zugriff per Early Binding Zugriff per Late Binding Weitere Informationen zur Verwendung der Office-Anwendungen per VBA
7
Access-SQL
7.1 7.2 7.2.1 7.2.2 7.2.3 7.2.4 7.2.5 7.2.6 7.2.7 7.2.8 7.2.9 7.2.10 7.3 7.3.1 7.3.2 7.3.3 7.3.4 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5
SQL und Access Daten auswählen Festlegen der anzuzeigenden Felder Festlegen der enthaltenen Tabellen Festlegen von Bedingungen Vergleichsausdrücke Sortieren von Daten Aggregatfunktionen Gruppieren von Daten WHERE, GROUP BY, HAVING und ORDER BY im Überblick Verknüpfen von Tabellen in Abfragen Zugriff auf externe Datenquellen Daten manipulieren Daten aktualisieren Daten löschen Daten an bestehende Tabelle anfügen Neue Tabelle mit Daten erstellen Datenmodell erstellen und manipulieren Tabellen erstellen Primärschlüssel, Indizes und Einschränkungen mit CONSTRAINT Tabelle ändern Tabelle löschen Index löschen
8
DAO
8.1 8.2 8.2.1 8.2.2 8.2.3 8.2.4 8.3
DAO und ADO im Einsatz Das DAO-Objektmodell Zugriff auf die Elemente des Objektmodells Deklarieren und Instanzieren Auf Auflistungen zugreifen Punkte und Ausrufezeichen DBEngine
321 321 322 322 323 326 327 327 327 328 329 330 331
335 336 339 340 341 343 344 346 347 348 351 352 362 363 363 363 364 366 366 367 370 375 377 377
379 380 381 382 383 385 386 386
12 8.4 8.4.1 8.4.2 8.4.3 8.5 8.5.1 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.1 8.7.2 8.7.3 8.7.4 8.7.5 8.8 8.8.1 8.8.2 8.8.3 8.9 8.9.1 8.9.2 8.9.3 8.10 8.11
Inhalt Workspace – Arbeitsbereich oder Sitzung? Auflistungen des Workspace-Objekts Aufgaben des Workspace-Objekts Datenbanken erzeugen und öffnen Aktuelle Datenbank referenzieren Users und Groups Das Database-Objekt Manipulation des Datenmodells Zugriff auf Auflistungen und Elemente Datensatzgruppen erstellen mit OpenRecordset Ausführen von Aktionsabfragen Daten bearbeiten mit dem Recordset-Objekt Methoden und Eigenschaften des Recordset-Objekts Datensätze durchlaufen Daten aus Datensätzen ausgeben Datensätze suchen Lesezeichen Sortieren und Filtern von Datensätzen Sortieren mit der Sort-Eigenschaft Sortieren mit der Index-Eigenschaft Filtern mit der Filter-Eigenschaft Daten bearbeiten Anlegen eines Datensatzes Bearbeiten eines Datensatzes Löschen eines Datensatzes QueryDefs – Auswahl oder Aktion nach Wahl Transaktionen
9
ADO
9.1 9.1.1 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.2.6 9.2.7 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.3.7
Zugriff auf eine Datenquelle herstellen Connection und ConnectionString Manipulation des Datenmodells Anlegen einer Tabelle Autowert anlegen Löschen einer Tabelle Erstellen eines Index Löschen eines Index Erstellen einer Beziehung Löschen einer Beziehung Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten Ausgeben aller Tabellen Prüfen, ob eine Tabelle vorhanden ist Datensatzgruppe auf Basis einer Tabelle öffnen Cursor-Typen Sperrung von Daten Datensätze eines Recordsets durchlaufen Anzahl der Datensätze in einer Datensatzgruppe ermitteln
387 388 388 388 388 390 390 390 398 400 405 406 406 406 411 412 416 416 416 417 418 419 420 420 421 421 422
427 428 428 430 430 432 433 433 434 435 436 437 437 437 438 439 439 440 440
Inhalt
13
9.3.8 9.3.9 9.3.10 9.3.11 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.4.6 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.6 9.7 9.7.1 9.7.2 9.7.3 9.7.4
Prüfen, ob eine Datensatzgruppe leer ist Ausgabe des Inhalts eines Recordsets Speichern der Daten in einem Array Abfragen mit Parametern verwenden Datensätze suchen Gesuchte Datensätze per Source-Eigenschaft des Recordsets ermitteln Seek Find Filtern Sortieren Lesezeichen Datensätze bearbeiten Datensatz anlegen Datensatz bearbeiten Datensatz löschen Aktionsabfragen ausführen Transaktionen Besonderheiten von ADO gegenüber DAO Datensatzgruppe speichern Datensatzgruppe laden Ungebundene Recordsets verwenden Ereignisse von Datensatzgruppen
10
Menüs
10.1 10.2 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.4.3 10.4.4 10.4.5 10.4.6 10.4.7 10.5 10.6 10.6.1 10.6.2 10.7 10.8 10.8.1 10.8.2 10.8.3 10.8.4
Grundlagen zu Menüs Beispielmenü VBA: Objektmodell für den Zugriff auf Menüs Zugriff auf die Menüstruktur Besonderheiten bei der Verwendung des Objektmodells für Menüs Hinzufügen eines Menüs Hinzufügen eines Menüs per Benutzungsoberfläche Hinzufügen eines Menüs per VBA Hinzufügen von Untermenüs Hinzufügen von Schaltflächen Schaltflächen dynamisch aktivieren und deaktivieren Eigene Symbole verwenden Hinzufügen von Kombinationsfeldern Hinzufügen von Symbolleisten Hinzufügen von Kontextmenüs Hinzufügen eines Kontextmenüs per Benutzungsoberfläche Hinzufügen eines Kontextmenüs per VBA Menüsteuerelemente referenzieren Feinschliff Menüleiste beim Anwendungsstart ersetzen Eingebaute Symbolleisten deaktivieren Menüs positionieren Eigenschaften von Menüs in der Registry
441 442 442 443 444 444 445 447 448 449 450 450 450 451 452 452 453 453 453 453 454 455
457 459 461 462 463 465 465 465 467 468 471 478 479 481 485 486 486 489 490 491 491 492 492 495
14
Inhalt
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
11.1 11.1.1 11.1.2 11.1.3 11.2 11.2.1 11.2.2 11.2.3 11.2.4 11.2.5 11.2.6 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2
Fehlerarten Syntaxfehler Laufzeitfehler Logische Fehler Debugging in der VBA-Entwicklungsumgebung Die Debuggen-Symbolleiste Das Direktfenster Haltepunkte Die Aufrufliste Ausdrücke überwachen Das Lokal-Fenster Fehlerbehandlung in VBA Elemente der Fehlerbehandlung Benutzerdefinierte Fehlerbehandlung temporär ausschalten Funktionale Fehlerbehandlung Benutzerdefinierte Fehler Fehlerdokumentation und -übermittlung Wichtige Fehlerinformationen Einsatz der accessVBATools Fehlerbehandlung in Formularen Behandlung von Formularfehlern Formularfehler dokumentieren
12
Performance
12.1 12.1.1 12.1.2 12.1.3 12.2 12.2.1 12.2.2 12.2.3 12.2.4 12.3 12.3.1 12.3.2 12.3.3 12.3.4 12.4 12.4.1 12.4.2 12.4.3 12.5 12.5.1 12.5.2 12.5.3
Tabellen Normalisieren des Datenmodells Indizes Datentypen Abfragen Abfragen und die Jet-Engine Datenbank mit kompilierten Abfragen ausliefern Gespeicherte Abfragen versus Ad-hoc-Abfragen Abfragen auf Performance trimmen Formulare Formulare offen halten oder schließen? Daten des Formulars Steuerelemente VBA in Formularen Berichte Datenherkunft unsortiert übergeben Keine Funktionen und Ausdrücke in Sortierungen und Gruppierungen Bericht nur öffnen, wenn er Daten enthält VBA Performance von VBA-Code optimieren Punkt oder Ausrufezeichen Datenzugriff optimieren
497 497 497 500 500 501 501 502 503 504 505 506 507 508 511 512 515 516 516 520 523 524 525
527 527 527 529 531 532 532 539 540 541 543 543 543 544 547 548 549 549 549 550 550 560 560
Inhalt
15
12.6 12.6.1 12.6.2 12.6.3 12.6.4 12.6.5 12.6.6 12.6.7 12.6.8 12.7 12.7.1
Sonstige Performance-Tipps Verwendung als .mde-Datei Verwendung als .mdb-Datei Arbeitsgruppen-Informationsdatei auf aktueller Access-Version halten Exklusiver Zugriff bei Einzelplatzanwendungen Komprimieren der Datenbank Objektnamen-Autokorrektur abschalten Unterdatenblätter abschalten Rechtschreibprüfung ausschalten Performance-Unterschiede messen Werkzeug für Performance-Tests selbst gebaut
13
Objektorientierte Programmierung
13.1 13.2 13.2.1 13.2.2 13.2.3 13.2.4 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.4.5 13.4.6 13.5 13.6 13.7 13.7.1 13.7.2 13.8 13.8.1 13.8.2 13.8.3 13.8.4 13.9 13.9.1 13.9.2 13.9.3 13.9.4
Abstrakte Datentypen, Klassen und Objekte Objekte Eingebaute Objekte Erzeugen eines Objekts Zugriff auf die Methoden, Eigenschaften und Ereignisse eines Objekts Lebensdauer eines Objekts Klassenmodule Anlegen eines Klassenmoduls Benennen des Klassenmoduls Eigenschaften einer Klasse Öffentliche und nicht öffentliche Eigenschaften Zugriff auf die Eigenschaften einer Klasse kontrollieren Property Let: Setzen von skalaren Variablen Property Set: Setzen von Objektvariablen Property Get: Lesen von skalaren Variablen und Objektvariablen Vertrauen ist gut, Kontrolle ist besser Methoden einer Klasse Standardereignisse in Klassen Benutzerdefinierte Ereignisse Ereignisse abfangen Eigene Ereignisse anlegen Benutzerdefinierte Auflistungen mit dem Collection-Objekt Auflistungen selbst gemacht Benutzerdefinierte Auflistungsklassen Nachbildung relationaler Beziehungen per Auflistungsklasse »Echtes« Objekt mit Auflistung Schnittstellen und Vererbung Beispiel für den Einsatz der Schnittstellenvererbung Vereinheitlichen per Schnittstellenvererbung Realisierung der Schnittstellenvererbung Was vom Beispiel übrig bleibt …
562 562 562 563 563 563 563 564 565 565 565
573 576 577 577 581 581 583 583 583 583 584 585 586 588 589 589 590 591 593 593 593 596 601 602 605 607 611 615 615 617 618 620
16
Inhalt
14
Objektorientierung im Praxiseinsatz
14.1 14.1.1 14.1.2 14.1.3 14.1.4 14.2 14.2.1 14.2.2 14.2.3 14.3 14.3.1 14.3.2 14.3.3 14.3.4 14.3.5 14.3.6 14.3.7 14.3.8 14.3.9 14.3.10 14.3.11 14.3.12
Standardfunktionen von Formularen auslagern Codeauslagerung am Beispiel der OK-Schaltfläche Auslagern weiterer Ereignisprozeduren Einstellen des Kombinationsfeldes für die Schnellauswahl Weitere Möglichkeiten Mehrere Formularinstanzen anzeigen Beispielformulare Erzeugen einer neuen Instanz Öffnen mehrerer Instanzen eines Formulars Mehrschichtige Anwendungen Beispiel Die GUI-Schicht Die Business-Schicht Die Datenzugriffsschicht Die Datenschicht Zusammenhänge der Objekte und Schichten Initialisieren des Formulars Auswählen und Anzeigen eines Datensatzes Neuer Datensatz Speichern eines Datensatzes Löschen eines Datensatzes Businesslogik und mehr
15
Anpassen der Entwicklungsumgebung
15.1 15.2 15.3 15.4 15.4.1 15.4.2 15.4.3 15.5 15.5.1 15.5.2 15.5.3 15.5.4 15.6 15.6.1 15.6.2 15.6.3 15.6.4 15.6.5 15.6.6 15.6.7 15.6.8 15.6.9
Gründe für die Erweiterung der Entwicklungsumgebung Programmieren der Entwicklungsumgebung Das Objektmodell der VBA-Entwicklungsumgebung Mit Modulen arbeiten Auflisten aller enthaltenen Module Anlegen eines neuen Moduls Entfernen eines Moduls Mit Prozeduren arbeiten Lesender Zugriff auf den Quellcode Beispielanwendung: Codeviewer Manipulieren des Quellcodes Beispielanwendung: Nummerieren von Codezeilen in einem Modul Toolwindows Benutzerdefiniertes Toolwindow = COM-Add-In Anlegen eines leeren Toolwindows Anlegen eines neuen Projekts Der COM-Add-In-Designer Das Userdocument als Toolwindow Ereignisprozeduren des COM-Add-Ins mit Leben füllen Anpassen der Eigenschaften des COM-Add-Ins Anzeige des Toolwindows beim Starten der VBA-Entwicklungsumgebung Testen des neuen Toolwindows
623 623 625 630 633 638 638 638 639 641 650 652 653 653 654 655 655 656 661 663 664 667 668
673 676 678 680 682 683 684 685 685 685 694 702 705 706 707 709 709 711 712 713 718 719 719
Inhalt
17
15.6.10 15.7 15.7.1 15.7.2
Das Toolwindow füllen COM-Add-Ins per Menübefehl aufrufen Vorbereitungen Hinzufügen der Funktionen und Menüs
16
Sicherheit von Access-Datenbanken
16.1 16.2 16.3 16.4 16.5 16.5.1 16.5.2 16.5.3 16.5.4 16.6
Code schützen per .mde-Datenbank Code schützen per Kennwort Einfacher Kennwortschutz Verschlüsseln einer Datenbank Das Sicherheitssystem von Access Leistungen des Sicherheitssystems Die Arbeitsgruppen-Informationsdatei Aktivieren des Sicherheitssystems Rekonstruieren einer .mdw-Datei Sichern von Access-Datenbanken per Assistent
17
Installation, Betrieb und Wartung
17.1 17.2 17.2.1 17.2.2 17.3 17.3.1 17.3.2 17.3.3 17.4 17.5 17.5.1 17.5.2
Verschiedene Access-Versionen auf demselben Rechner Weitergabe von Access-Datenbanken Weitergabe mit Runtime Weitergabe ohne Runtime Aktionen beim Starten oder Beenden der Datenbank durchführen Code beim Starten einer Datenbank ausführen Formular beim Starten einer Datenbank anzeigen Aktion beim Schließen einer Datenbank ausführen Datenbanken komprimieren und reparieren Mehrbenutzerbetrieb mit Access-Datenbanken Aufteilen einer Access-Datenbank Erneutes Einbinden der Tabellen nach Umbenennen oder Verschieben des Backends Replikation von Datenbanken Funktionsweise der Replikation Erzeugen weiterer Replikate Replikation und Synchronisation im Einsatz Weitere Informationen Wann sollten Sie Replikation verwenden? Sichern von Access-Datenbanken Voraussetzungen und Vorbereitungen Sichern des Datenbank-Backends Sicherungsstrategie Sicheres Ausführen von Access-Anwendungen Schutz vor bösartigem Code Schutz vor bösartigen SQL-Statements Deaktivieren der Sicherheitswarnungen Digitale Signaturen
17.6 17.6.1 17.6.2 17.6.3 17.6.4 17.6.5 17.7 17.7.1 17.7.2 17.7.3 17.8 17.8.1 17.8.2 17.8.3 17.8.4
721 722 722 726
731 731 733 733 734 735 735 736 739 745 745
753 753 755 755 757 758 758 759 760 761 762 763 764 768 769 770 770 774 774 775 775 778 779 783 783 784 785 787
18 17.8.5 17.8.6 17.8.7 17.9 17.9.1 17.9.2 17.9.3 17.9.4 17.10
Inhalt Sicherheitseinstellungen per Registry vornehmen Makro-Sicherheitsstufe einstellen Sandbox-Modus einstellen Datenbank reparieren Symptome Sicherung geht vor Allgemeine Reparaturversuche Weitere Informationen Verweise und Probleme mit Verweisen
Anhang Hilfsmittel für Access-Entwickler Internetangebote zu Access und verwandten Themen
Index
792 792 792 793 794 794 795 795 796
803 803 805
807
Vorwort Als ich mich entschloss, dieses Access-Entwicklerbuch zu schreiben, war mir bewusst, dass ich mich damit zugleich auf eine schwierige und zeitintensive Tätigkeit einlasse, die viel Fantasie erfordert – das Schreiben eines Vorworts. Ich müsste den Lesern, die dieses Buch ganz oder auch nur teilweise lesen möchten, ein wenig Appetit machen, die interessanten Stellen des Buchs hervorheben und vor allem darstellen, warum die Access-Welt noch weitere fünf Zentimeter Platz im Bücherregal für dieses Buch freimachen sollte. Wenn Sie sich Ihre Access-Bücher einmal vornehmen – ich gehe davon aus, dass Sie in Besitz von mindestens ein oder zwei dieser Sorte sind – oder gar einmal die Titel in der nächsten größeren Buchhandlung überfliegen, wird Ihnen auffallen, dass es nur wenige unterschiedliche Kategorien von Access-Büchern gibt. Bei den meisten handelt es sich um »Grundlagen« oder »Handbücher«, die die Verwendung von Access und der enthaltenen Objekte erläutern, einige liefern einen tieferen Einblick in die Möglichkeiten von Access und VBA und schließlich gibt es noch die »kleineren« Bücher mit wenigen hundert Seiten, welche »Schnelleinstiege« in den einen oder anderen genannten Themenkomplex bieten. Jeder Verlag hat mindestens ein Buch jeder Kategorie im Portfolio. Hinzu kommt hier und da noch ein Ausreißer wie eine Code- oder Tippsund-Tricks-Sammlung. Die meisten Bücher haben den Anspruch, den Leser über alle Bereiche zu informieren, die beim Umgang mit Access und/oder VBA wichtig sind. Das funktioniert mal gut, ein anderes Mal fehlt einfach der Platz, um weit genug in die Tiefe zu gehen. Auf jeden Fall dreht sich alles um die Technik und die sollte der Leser nach dem Studium eines solchen Buches auch beherrschen. Sie haben schon ein derartiges Buch durchgeackert und fühlen sich mit der Oberfläche von Access vertraut? Und außerdem haben Sie bereits die eine oder andere Prozedur auf dem Buckel? Gut! Dann sind Sie hier genau richtig, denn das vorliegende Buch holt Sie genau dort ab, wo viele andere Bücher Sie absetzen. Dabei versucht es, sich ein wenig mehr auf die eigentliche Anwendungsentwicklung zu konzentrieren. Vielleicht können die folgenden Beispiele dies verdeutlichen.
20
Vorwort
Architektur und Datenmodellierung Die Entwicklung einer Anwendung ohne gründliche Planung läuft meist schief. In Kapitel 1 finden Sie Informationen, welche Gedanken Sie sich vor dem Eintippen der ersten Codezeile machen sollten. Dabei spielt auch der Begriff »Architektur« eine wichtige Rolle – versuchen Sie sich doch einfach einmal als Architekt. Und vergessen Sie nicht die Ergonomie – die aufwändigste Anwendung taugt nichts, wenn der Anwender sie nach kurzer Zeit entnervt wegwirft. Datemodellierung ist einer der wichtigsten Aspekte bei der Datenbankentwicklung. Wer hier Fehler macht, muss später kräftig bluten, denn alle anderen Datenbankobjekte basieren auf den zugrunde liegenden Daten – somit pflanzen sich Fehler durch Abfragen, Formulare und Berichte fort. Deshalb erfahren Sie in Kapitel 2, wie Sie aus einem Berg Daten unter Verwendung der Normalisierungsregeln ein relationales Datenmodell ableiten, welche Beziehungsarten es gibt und wie Sie diese einsetzen. Und falls noch Fragen offen bleiben, finden Sie eine Menge Beispieldatenmodelle.
Die übliche Verdächtigen – Abfragen, Formulare und Berichte Mit Abfragen extrahieren Sie aus den Tabellen die später benötigten Daten, indem Sie die Tabellen zusammenführen, Felder auswählen und die Daten durch passende Kriterien einschränken. Anhand einiger Beispiele erfahren Sie in Kapitel 3, wie Sie nicht-triviale Probleme mit Abfragen lösen – wie etwa die Suche von Daten in m:nBeziehungen, Auffinden von Extremwerten in gruppierten Abfragen oder Nummerieren von Datensätzen. Nun brauchen Sie die in den Tabellen und Abfragen enthaltenen Daten nur noch in Formularen abzubilden. Deshalb erfahren Sie, wie Sie die Daten in den Formularen und Steuerelementen anzeigen – und zwar für alle denkbaren Beziehungsarten wie 1:1-, 1:n-, n:1-, m:n- und reflexive Beziehungen. Außerdem finden Sie im vorliegenden Buch Informationen über den richtigen Einsatz der Formularereignisse, die Besonderheiten von Unterformularen und das Validieren von Daten und lernen Möglichkeiten zur Suche in Formularen kennen (Kapitel 4). Das Gleiche gilt für Berichte: Auch hier lernen Sie, wie Sie unterschiedlich aufgebaute Daten darstellen und wozu Berichtsereignisse gut sind und wie Sie diese einsetzen. Am Beispiel des Rechnungsdrucks erfahren Sie, wie man einen komplexen Bericht anlegen kann (Kapitel 5).
VBA und der Datenzugriff Die Grundlagen von VBA haben Sie sich schnell angeeignet und die Objekte, Eigenschaften und Methoden können Sie nachschlagen. Aber was ist notwendig, um statt endlosen Spaghetti-Codes knackige, kurze, leicht verständliche und gut wartbare Routinen zu zaubern? Lesen Sie nach in Kapitel 6.
21
Ein ganz wichtiges Thema in Access ist natürlich der Datenzugriff. Diesem widmen sich gleich drei Kapitel: Dort erfahren Sie alles über den Datenzugriff mit SQL, DAO und ADO. Sie erfahren dort, wann Sie welche Technik bevorzugen sollten und welche Besonderheiten die einzelnen Techniken aufweisen (Kapitel 7 bis Kapitel 9).
Finishing Touch: Menüs, Fehlerbehandlung und Speed Menüleisten sind das Salz in der Suppe der Benutzungsoberfläche. Wer will schon den Benutzern seiner Anwendung das Datenbankfenster zum Öffnen der Formulare anbieten? Alles zum Menüleistenbau finden Sie in Kapitel 10. Stabil und schnell soll die Anwendung natürlich auch laufen. Für eine stabile Anwendung brauchen Sie eigentlich nur »keine Fehler« einzubauen. Da das aber relativ schwierig wird, sollten Sie zumindest für eine vernünftige Fehlerbehandlung und eine automatische Fehlerdokumentation sorgen – oder möchten Sie den Benutzer am Telefon bitten, dass er Ihnen mal eben einen Screenshot der Fehlermeldung zusendet? Mehr dazu in Kapitel 11. Vielleicht erstellen Sie auch eine so schnelle Anwendung, dass der Benutzer auftretende Fehler gar nicht bemerkt? An welchen Schräubchen Sie da drehen können, erfahren Sie in Kapitel 12.
Ab in die Zukunft mit Objektorientierung und einem besseren VBA-Editor Kapitel 13 und 14 widmen sich ganz der objektorientierten Programmierung mit VBA und Access. Erfahren Sie, wozu Sie Klassen und Collections einsetzen können. Außerdem: Irgendwann werden sich die objektorientierten Sprachen von .NET auch in Access breit machen. Ein bisschen Vorbereitung kann da nicht schaden. Und wenn Sie gerade dabei sind: Moderne Entwicklungsumgebungen wie Eclipse und Visual Studio .NET bieten Funktionen, von denen VBA-Entwickler nur träumen können. Das muss nicht sein: Die VBA-Entwicklungsumgebung lässt sich (fast) beliebig erweitern. Sie benötigen nur ein wenig Fantasie und das Know-how aus Kapitel 15.
Alles für die Datenbank Den Abschluss bilden Kapitel 16 und 17: Hier erfahren Sie, wie Sie Ihre Anwendung auf den Einsatz in der realen Welt vorbereiten: Mit einem ordentlichen Sicherheitskonzept und allem, was sonst noch zur Pflege der Datenbankanwendung notwendig ist. Im Anhang finden Sie schließlich noch Links zum Download einiger Tools und weitere Links zum Thema Access.
Was fehlt? Einige Bereiche von Access werden in diesem Buch nicht behandelt – aus verschiedenen Gründen, wie Sie nachfolgend lesen können:
22
Vorwort
Makros sind obsolet, wenn Sie mit VBA arbeiten, was wesentlich flexibler ist. Eine Ausnahme bilden die beiden Makros AutoExec und AutoKeys mit Kurzauftritten in diesem Buch. Die so genannten »Seiten«, die immerhin eine ganze Registerseite im Datenbankfenster spendiert bekommen haben, scheinen sich nie durchgesetzt zu haben – und wenn, dann nur unter Ausschluss der Öffentlichkeit. Deshalb finden Sie auch in diesem Buch nichts zu diesem Thema. Einige neue Techniken wie PivotChart- und PivotTable-Ansichten bleiben ebenfalls außen vor. Client/Server-Techniken zur Verwendung von SQL-Server-Datenbanken hätten ebenso wie detaillierte Informationen zu Themen Internet, Webservices oder XML dazu geführt, dass die anderen Themen nicht in der gewünschten Ausführlichkeit behandelt worden wären. Daher gilt hier: Vielleicht in einem anderen Buch oder in einer anderen Auflage.
Dankeschön Mein bester Dank geht an Rita Klingenstein (sprachliches Lektorat), mit der ich seit Jahren erfolgreich zusammenarbeite und die immer noch Fehler in meinen Texten findet, an den außerordentlich kompetenten Fachlektor Alexander »Sascha« Trowitzsch, der nicht nur Fehler ausgebügelt, sondern auch noch die eine oder andere Anregung beigesteuert hat, und an meine Lektoren bei Addison-Wesley: Frank Eller, der dieses Buch »eingestielt« hat, bevor er sich beruflich umorientierte, und Sylvia Hasselbach, die seine Aufgabe übernommen hat und jederzeit mit Rat und Tat zur Seite stand.
Viel Spaß! Ich hoffe, dass Sie eine Menge Freude mit diesem Buch haben werden und das eine oder andere lernen und für Ihre Zwecke einsetzen können. Fragen, Anregungen oder Hinweise auf eventuelle Fehler senden Sie bitte per E-Mail an [email protected]. Aktuelle Informationen zu diesem Buch finden Sie unter http:// www.access-entwicklerbuch.de.
Duisburg, den 17. August 2005
André Minhorst
1 Datenbankanwendungen planen Die Entwicklung von Software im Allgemeinen und speziell von Datenbankanwendungen erfordert eine sorgfältige Planung. Diese fällt umso leichter, je mehr Erfahrung – auch schlechte – Sie in Ihrem Entwicklerleben gesammelt haben und je besser die verwendeten Werkzeuge zum Entwickeln geeignet sind. Wie Sie von Anfang an gute Voraussetzungen für die Entwicklung einer Datenbankanwendung schaffen, erfahren Sie in den folgenden Abschnitten.
1.1 Aller Anfang ist schwer In diesem ersten Kapitel erfahren Sie, wie Sie eine Datenbankanwendung von vornherein so planen, dass diese Ihnen während und nach der Fertigstellung keine Kopfschmerzen bereitet. Dazu sind im Wesentlichen die folgenden Punkte maßgebend: Sie haben bereits vor Beginn der Programmierung möglichst viele notwendige Informationen über die zu erstellende Anwendung gesammelt, sodass sich keine unnötig langen Leerlaufphasen ergeben. Sie gliedern die zu entwickelnde Anwendung in leicht verdauliche Häppchen und arbeiten diese nacheinander ab. Zu viele offene oder zu große Baustellen wirken eher lähmend – sicher kennen Sie die Situation, dass Sie vor lauter ungelösten Aufgaben nicht mehr wissen, wo Sie als Nächstes ansetzen sollen. Legen Sie bei umfangreicher Software mit dem Auftraggeber zusammen fest, welche Teile zuerst fertig gestellt werden sollen, und planen Sie entsprechende Meilensteine ein. Oft lässt sich so frühzeitig feststellen, ob noch in die richtige Richtung programmiert wird oder eine Kurskorrektur notwendig ist. Stellen Sie sich darauf ein, dass sich während der Entwicklung – vor allem dann, wenn Sie dem Kunden erste Ergebnisse zeigen – die Anforderungen ändern, unabhängig davon, ob Sie mit einem Pflichtenheft arbeiten oder nicht (Informationen zum Thema Pflichtenheft erhalten Sie beispielsweise unter http://www.pflichtenheft.de). Viele Auftraggeber entdecken beispielsweise erst während der Programmierung, welche zusätzlichen Möglichkeiten sich durch die neue Software
24
1
Datenbankanwendungen planen
ergeben. Ein wahres Ideenfeuerwerk ist die Folge. Hier sollten Sie mehr oder weniger schonend darauf hinweisen, dass durch Änderungen und zusätzliche Wünsche gegebenenfalls auch weitere Kosten entstehen. Wenn Sie nicht nur das vereinbarte Honorar kassieren und dann die Software vergessen möchten, sollten Sie nicht den Auftraggeber allein in die Planung und Durchführung des Projekts einbeziehen, sondern auch die künftigen Benutzer der Anwendung – es sei denn, der Auftraggeber ist selbst der einzige Nutzer. Nehmen Sie unbedingt die Wünsche der Benutzer wahr. Diese werden die Anwendung nur akzeptieren, wenn sie die damit zu erledigenden Aufgaben besser als vorher durchführen können. Wenn Sie also beim nächsten Besuch im Unternehmen des Auftraggebers fröhlich mit Ihrer Anwendung arbeitende Mitarbeiter vorfinden möchten und keine mies gelaunten Nervenbündel, die vielleicht ihre Arbeit wieder auf die gleiche Weise erledigen wie vor dem Einsatz Ihrer Anwendung, beherzigen Sie die Wünsche der Anwender. Bauen Sie die Software so auf, dass Sie Änderungen und Erweiterungen leicht vornehmen können. Das bedingt einen möglichst modularen Aufbau mit weitgehend voneinander unabhängigen Komponenten. Behalten Sie immer die Ergonomie der Anwendung im Auge, damit die Software von den Benutzern akzeptiert wird – mehr dazu in Abschnitt 1.3, »Ergonomie«. Wenn die Benutzer Ihre Software nicht akzeptieren, weil sie den Zweck nicht erfüllt, ergonomische Schwächen aufweist oder fehlerhaft arbeitet, brauchen Sie sich um den vorletzten Punkt keine Sorgen zu machen – Änderungen und Erweiterungen wird es in diesem Fall nicht geben, geschweige denn Folgeaufträge …
Die einzelnen Stationen der Softwareentwicklung Theoretisch durchläuft eine Software während der Entwicklung verschiedene Stationen: Der Auftraggeber legt die Anforderungen fest, der Architekt erstellt ein Pflichtenheft mit Informationen wie Datenmodell, Architektur, Systemvoraussetzungen und weiteren Bedingungen und der Programmierer setzt die im Pflichtenheft festgelegten Spezifikationen in eine funktionierende Datenbankanwendung um. Die Realität sieht aber meist ganz anders aus: Die Software wird von einem Ein-Mann-»Team« entwickelt. Sobald Sie mehr als nur eine einzige Programmier-Station bei der Softwareentwicklung übernehmen, ist es besonders wichtig, sorgfältig zu planen und sich nicht nur auf die vermeintlich interessanteste Arbeit – das Programmieren – zu konzentrieren.
Je früher Sie Fehler entdecken, desto besser Je früher Sie feststellen, dass eine Forderung nicht umsetzbar ist oder die Anwendung einen Fehler enthält, desto besser. Wenn Ihnen beispielsweise bei der Datenmodellie-
Architektur
25
rung ein Fehler unterläuft, kann er sich unter Umständen über die Erstellung der Abfragen, Formulare und Berichte bis in VBA-Routinen hinein fortsetzen. Eine nachträgliche Änderung des Datenmodells wirkt sich auf alle Datenbankobjekte aus, in denen Sie die entsprechenden Tabellen oder darauf aufbauende Abfragen verwenden.
Richtige Wahl der Anwendung Wenn ein Auftraggeber auf Sie zukommt und Ihnen mit einem Auftrag zur Erstellung einer Access-Datenbank »droht«, ist das natürlich zunächst ein gutes Zeichen: Der Auftraggeber hat sicher gute Gründe, Access als Entwicklungssystem für die geplante Anwendung zu verwenden, und sicher auch gute Gründe, Sie mit der Entwicklung zu betrauen. Vermutlich hat er Access als Entwicklungswerkzeug für die Datenbankapplikation auswählt, weil er die technischen Möglichkeiten schätzt und Access für die richtige Wahl hält. Es kann aber auch der Fall eintreten, dass er gar keine näheren Kenntnisse besitzt und sich für Access nur deshalb entschieden hat, weil in seinem Unternehmen bereits alle Arbeitsplätze mit Access ausgestattet sind und neben den reinen Entwicklungskosten keine weiteren Kosten entstehen sollen. Hier sollten Sie sehr vorsichtig sein und selbst genau prüfen, ob Access die passende Anwendung für die geplante Aufgabe ist. Dabei spielt natürlich vor allem auch die Menge der zu verwaltenden Daten, die Anzahl der Benutzer und der gleichzeitigen Zugriffe eine Rolle. Aber mit Access lassen sich durchaus auch Anwendungen entwickeln, die auf anderen Backends (den Datenspeichern) als der Jet-Engine aufsetzen – etwa der kostenlosen MSDE, dem Microsoft SQL Server oder auch SQL-Servern von Drittherstellern wie MySQL oder Oracle. Vielleicht ist Access aber selbst als Frontend (der Oberfläche und Schnittstelle der Anwendung) nicht die richtige Wahl? Möglicherweise möchte der Auftraggeber eine Access-Anwendung dazu missbrauchen, eine unternehmensweite Adressenliste bereitzustellen, und wäre mit einer entsprechenden Outlook-Lösung oder gar einer Intranet-Anwendung besser bedient. Zusammengefasst heißt das: Lassen Sie sich nicht auf einen Auftrag über die Erstellung einer Access-Datenbank ein, bevor Sie nicht geprüft haben, ob Access überhaupt die richtige Anwendung für den geplanten Zweck ist – auch wenn der Auftraggeber diese Entwicklungsumgebung zunächst für die richtige hält.
1.2 Architektur »Architektur? Für eine Access-Anwendung? Ja, klar, Client-Server-Architektur kenne ich…« – das könnte eine mögliche Antwort auf die Frage nach der Architektur in Zusammenhang mit der Entwicklung von Datenbankanwendungen auf der Basis von Microsoft Access sein. Natürlich wird der Begriff »Architektur« in Bezug auf objektorientierte Programmiersprachen viel größer geschrieben als bei der Datenbankent-
26
1
Datenbankanwendungen planen
wicklung mit Access. Dort gibt es nicht ein solch einengendes Framework wie eine Access-Datenbank, die praktisch alle durch eine Architektur zu beschreibenden Elemente bereits in sich trägt (mit Ausnahme eines externen Backends natürlich) – dort ist von mehrschichtigen Anwendungen, Komponenten, Microkernel, serviceorientierter Architektur, Patterns, verteilten Anwendungen und noch wilderen Begriffen die Rede, mit denen Access-Entwickler in Ruhe gelassen werden wollen. Access-Datenbanken sind der Fels in der Technologie-Brandung. Hier herrschen eiserne Gesetze, und neumodischer Schnickschnack hat hier nichts zu suchen. Oder steckt doch ein klein wenig Architektur in jeder Access-Anwendung? Vielleicht versteckt? Nun, Sie sollten diesen Begriff zunächst ein wenig genauer unter die Lupe nehmen. Eine von vielen Definitionen für Architektur lautet folgendermaßen: »Softwarearchitektur ist die Struktur eines Systems, welche aus Softwarekomponenten, den nach außen sichtbaren Eigenschaften dieser Komponenten und den Beziehungen zwischen ihnen besteht.« (Bass, L., Clements, P., Kazman, R.: Software Architecture in Practice. 2. Aufl., Addison-Wesley Professional. München: Addison-Wesley Verlag 2003.) Das hört sich zunächst ganz schlüssig an. Es fehlt allerdings noch die Information, was denn nun unter einer Komponente zu verstehen ist. Natürlich gibt es auch hier mindestens eine Definition: »Softwarekomponenten sind aktive Einheiten einer Architektur, die Aufgaben durch interne Berechnungen und externe Kommunikation mit anderen Komponenten des Systems bewerkstelligen.« (Bosch, J.: Design and Use of Software Architectures. München: Addison-Wesley Verlag 2002.) Wie lässt sich dies nun auf die Entwicklung von Datenbankanwendungen mit Access übertragen? Worüber sich beide Definitionen ausschweigen, ist die Dimension der beschriebenen Begriffe – bezieht man den Begriff Komponente nun ganz grob auf Frontend und Backend oder etwas feiner auf die einzelnen »Schichten« innerhalb der Datenbankanwendung wie Benutzungsoberfläche, Businesslogik und Datenzugriff (wobei diese Schichten sich unter Access teilweise überschneiden)? Betrachtet man damit einzelne Objekte wie Tabellen, Abfragen, Formulare, Berichte und Klassen oder geht man bis auf Prozedurebene hinunter?
1.2.1 Grobe Architektur: Systemaufbau und Schnittstellen Der beste Ansatz ist vermutlich, ganz grob anzufangen und sich je nach Stadium in immer tiefere, feinkörnigere Schichten vorzuarbeiten. Für den Anfang reicht es aus, das zu erstellende System zu skizzieren: Wie ist das System aufgebaut – ist es eine Einzelplatz- oder eine Mehrbenutzeranwendung? Mit welcher Technologie werden Frontend und Backend realisiert? Wo liegen die Daten und von wo aus greifen wie viele Benutzer auf diese Daten zu? Welche
Architektur
27
Schnittstellen nach außen gibt es – werden Daten aus anderen Systemen bezogen oder an andere Systeme geliefert und wie sehen die Details aus? Wer steuert den Ein- und Ausgang der Daten? Vielleicht werden für die Anwendung auch externe Komponenten benötigt – etwa weitere Software wie Word zur Ausgabe von Serienbriefen oder Outlook zum Versenden von E-Mails? Beispiel: Sie sollen eine Projektzeiterfassung erstellen. Darin sollen die Zeiten, die einzelne Mitarbeiter eines Unternehmens mit den unterschiedlichen Projekten verbringen, minutengenau erfasst werden, damit eine exakte Analyse der Projekte und der dafür aufgebrachten Ressourcen erstellt werden kann. Es gibt somit zwei Benutzergruppen: Die erste besteht aus den Mitarbeitern, die Projekte bearbeiten, die zweite aus Mitarbeitern, die die Projekte analysieren. Dabei sind Letztere genau genommen eine Teilmenge der Erstgenannten, da für eine konsistente Erfassung eigentlich jede Tätigkeit einem Projekt zugeordnet werden sollte. Wenn Sie die wichtigsten Elemente der Anwendung erfasst haben, skizzieren Sie diese. Abbildung 1.1 zeigt, wie dies aussehen kann. Die Skizze berücksichtigt, worin die genannten Anforderungen bestehen: Sie benötigen separate Benutzungsoberflächen für die »normalen« Mitarbeiter und für diejenigen, die mit der Auswertung der gewonnenen Daten betraut sind. Hier stellt sich bereits die Frage, ob Sie die Benutzungsoberfläche für beide Anwendergruppen in ein Frontend packen oder ob Sie diese getrennt erstellen. Die Antwort lautet: Natürlich benötigen Sie zwei Frontends. Nicht jeder Mitarbeiter muss sehen, wie lange sich andere Mitarbeiter mit den jeweiligen Projekten beschäftigen. Sie könnten zwar auch alle Elemente der Benutzungsoberfläche in einem einzigen Frontend unterbringen, aber wenn Sie die einzelnen Elemente je nach Benutzergruppe freigeben möchten, ist dies mit zusätzlichem Aufwand verbunden.
1.2.2 Ein wenig feiner: Die Rolle der Datenbankobjekte Als Nächstes steht die Frage an, wie die Anwendung aufgebaut sein soll. Typische Access-Anwendungen bestehen aus einem Durcheinander von Tabellen, Abfragen, Formularen und Berichten, wobei Formulare und Berichte direkt an die Tabellen oder Abfragen gebunden, die Validierung und die Businesslogik auf Tabellen, Formulare und VBA-Module verteilt und alle Objekte eher mehr als weniger voneinander abhängig sind. Das ist nun kein Vorwurf, denn Access bietet aufgrund seiner Struktur nur wenig alternative Möglichkeiten. Kaum jemand wird einsehen, warum er eine extra Datenzugriffsschicht mit mindestens einer zusätzlichen Klasse pro Tabelle programmieren soll, wenn er mit dem Zuweisen einer einzigen Eigenschaft und dem Ziehen der Feldsteuerelemente in ein Formular binnen Sekunden ein an eine Tabelle gebundenes Formular erzeugen kann (weitere Informationen zu mehrschichtigen Access-Anwendungen finden Sie übrigens in Kapitel 14, »Objektorientierte Techniken in der Praxis«).
28
1
Controlling
Mitarbeiter
Auswertungs- und Administrationsfrontend
Mitarbeiter
Datenbankanwendungen planen
Mitarbeiter
Datenerfassung (Projektzeiten)
Datenbank
Abbildung 1.1: Grobe Skizzierung der Elemente einer Anwendung einschließlich der Benutzer
Aber auch, wenn Sie zunächst keine mehrschichtige Anwendung mit Access erzeugen möchten, gibt es noch genügend Ansätze, sich vor dem Zusammenklicken von Tabellen, Abfragen und Formularen einige Gedanken über den grundsätzlichen Aufbau zu machen.
1.2.3 Datenmodell Grundlegend und daher vom Aufbau der darauf zugreifenden Elemente unabhängig ist sicher das Datenmodell. Das Aussehen der einzelnen Tabellen mit den enthaltenen Feldern und die Beziehungen zwischen den Tabellen sind sichere Kandidaten für die Aufnahme in die Beschreibung der Anwendungsarchitektur. Eine Menge Informationen zur Datenmodellierung finden Sie in Kapitel 2, »Tabellen und Datenmodellierung«.
1.2.4 Benutzungsoberfläche und Anwendungslogik Nur schwer zu trennen sind unter Access klassischerweise die Benutzungsoberfläche in Form von Formularen und die Anwendungslogik. Letztere befindet sich normalerweise mehr oder weniger komplett in den Klassenmodulen der Formulare. Das ist insofern schade, als dass deren Wiederverwendbarkeit damit gegen Null geht, und lässt sich auf konsequente Art und Weise nur durch die Aufteilung in mehrere Schichten verhindern – was aber mit einer Menge Aufwand verbunden ist. Aber die Realisierung einer mehrschichtigen Anwendung auf Basis von Microsoft Access macht umso mehr Sinn, je umfangreicher die Anwendung geplant ist.
Architektur
29
Aber auch im klassischen »Schichtsalat« von Access-Anwendungen lässt sich mit ein paar Vorüberlegungen zur Architektur eine Menge Vorarbeit leisten. Ausschlaggebend für diese Überlegungen ist die eingehende Analyse der künftigen Benutzer, die mit dem System arbeiten werden – wenn es überhaupt mehrere Benutzer gibt –, und der Aufgaben, die diese mit der geplanten Anwendung erledigen sollen. Im obigen Beispiel der Projektzeiterfassung sind zunächst einmal zwei verschiedene Frontend-Anwendungen notwendig – eine für den Controller, der die Projekte und die darauf verwendete Zeit auswertet, und eine für die Mitarbeiter, damit diese die Zeiten eingeben können, die sie mit den verschiedenen Projekten verbracht haben. Im Folgenden soll beispielhaft das Frontend zur Projektzeiteingabe beleuchtet werden. Die Benutzung dieses Frontends soll so einfach wie möglich gehalten werden, damit die Benutzer keine unnötige Zeit verschwenden und das Tool eine hohe Akzeptanz erfährt. Falls die vorherige Methode zur Ermittlung der Projektzeiten umständlich und zeitraubend war, werden die Benutzer das neue Tool vermutlich gerne akzeptieren. Analysieren Sie, welche Aufgaben die Benutzer mit dem Tool durchführen sollen, und notieren Sie diese wie in Abbildung 1.2. Das hier gezeigte Use-Case-Diagramm ist eine der Möglichkeiten, mit der UML (Unified Modeling Language) Anwendungsfälle grafisch darzustellen. Eine Beschreibung der UML und ihrer Möglichkeiten würde den Rahmen dieses Kapitels bei weitem sprengen. Daher werden hier nur Diagrammtypen verwendet, die keiner weiteren Erläuterung bedürfen. Es reicht an dieser Stelle, wenn Sie wissen, dass die UML eine weitgehend standardisierte Möglichkeit zum Entwurf von Software bietet. Dieses Diagramm zeigt, dass der Controller auch Mitarbeiter ist und ebenso wie dieser seine Projektzeiten in das geplante Tool eintragen soll. Die damit verbundenen Anwendungsfälle sind im Use-Case-Diagramm enthalten. Der Pfeil vom Anwendungsfall Neues Projekt auswählen zum Anwendungsfall Projektzeit anlegen bedeutet, dass der erste den zweiten Use-Case enthält. Use-Case-Diagramme bieten immer nur einen Überblick über die geplanten Anwendungsfälle. Zusätzlich gibt es zu jedem Use-Case einen Text, der den Use-Case beschreibt und/oder ein Aktivitätsdiagramm.
Beschreibung eines Use-Case in Textform Dieser Text sollte nach folgendem Schema aufgebaut sein (nach Cockburn, A.: Use-Cases effektiv erstellen. Bonn: mitp-Verlag 2003): Name des Use-Case Ziel des Use-Case Kategorie: primär, sekundär oder optional
30
1
Datenbankanwendungen planen
Neues Projekt auswählen Mitarbeiter Tätigkeitsbericht anlegen
Anzeige der Projekte bearbeiten Controller
Abbildung 1.2: Use-Case-Diagramm des Mitarbeiter-Frontends
Vorbedingung: Zustand, bevor der Use-Case beginnt Nachbedingung: Zustand, nachdem der Use-Case abgearbeitet ist Nachbedingung Fehlschlag: Zustand, wenn der Use-Case nicht wie vorgesehen abgearbeitet werden konnte Akteure Auslösendes Ereignis Beschreibung Erweiterungen Alternativen Nicht alle Eigenschaften sind für jeden Use-Case erforderlich beziehungsweise sinnvoll. Für das vorliegende Beispiel sieht das folgendermaßen aus: Name des Use-Case: Neues Projekt auswählen Ziel des Use-Case: Änderung des aktuellen Projekts des Mitarbeiters Kategorie: primär Akteure: Mitarbeiter Auslösendes Ereignis: Der Mitarbeiter beendet die Bearbeitung eines Projekts und beginnt die Bearbeitung eines anderen Objekts.
Architektur
31
Beschreibung: 1. Neues Projekt auswählen 2. Use-Case Projektzeit anlegen wird ausgelöst Der Use-Case Projektzeit anlegen sieht so aus: Name des Use-Case: Tätigkeitsbericht anlegen Ziel des Use-Case: Speichern der Eigenschaften der letzten Tätigkeit des Mitarbeiters Kategorie: primär Akteure: Mitarbeiter Auslösendes Ereignis: Der Mitarbeiter hat das aktuell bearbeitete Projekt gewechselt. Beschreibung: 1. Eingabe einer Tätigkeitsbeschreibung 2. Auswahl einer Tätigkeitskategorie
Use-Cases mit Aktivitätsdiagrammen beschreiben Eine andere Möglichkeit zur Beschreibung der Details eines Use-Case ist die Verwendung einer weiteren Diagrammart der UML. Dabei handelt es um das Aktivitätsdiagramm. Die einzelnen Schritte werden in diesem Diagramm als einzelne Elemente dargestellt und der Abfolge entsprechend mit Pfeilen verbunden. Die Aktivität ist – wie auch bei der schriftlichen Darstellung – in einem eigenen Aktivitätsdiagramm zu beschreiben.
Neues Projekt auswählen
Tätigkeitsbericht anlegen
Abbildung 1.3: Aktivitätsdiagramm für das Auswählen eines neuen Projekts
32
1
Datenbankanwendungen planen
Neben dieser sehr einfachen Darstellung können Sie ein Aktivitätsdiagramm natürlich auch für die Darstellung von Vorgängen über mehrere Use-Cases beziehungsweise mehrere Objekte wie beispielsweise Formulare verwenden. Abbildung 1.4 zeigt etwa die Tätigkeit für alle Use-Cases aus Abbildung 1.2 gleichzeitig an. Die gestrichelten Kästchen deuten bereits auf eine erste Design-Entscheidung hin: Jedes Kästchen steht für ein einzelnes Formular. Nach diesem Diagramm können Sie nach dem Öffnen der Projektübersicht jederzeit den Dialog mit den Projektlisten-Eigenschaften anzeigen, um die Reihenfolge einzustellen oder Projekte ein- und auszublenden. Sie können auch einfach nur die Projektübersicht betrachten und den Dialog wieder verlassen. Die eigentliche Aufgabe des Dialogs ist aber der Wechsel des Projekts. Dabei wird unweigerlich das Formular zum Eingeben eines Berichts zur Tätigkeit mit dem vorherigen Projekt geöffnet.
Projektübersicht
Projektliste-Eigenschaften
[Ansicht ändern]
Anzeige der Projekte bearbeiten
[Projekt wechseln]
Neues Projekt auswählen
Tätigkeits-Details
Tätigkeitsbericht anlegen
Abbildung 1.4: Aktivitätsdiagramm mit mehreren Use-Cases
Architektur
33
Folgerungen aus den Use-Cases Aus diesen Informationen lässt sich bereits eine Möglichkeit für den Aufbau der Benutzungsoberfläche ableiten. Der erste auslösende Use-Case resultiert in einem Formular, das alle Projekte, an denen ein Mitarbeiter beteiligt ist, anzeigt. Beim Klick auf ein anderes als das aktuelle, farbig markierte Projekt wird das hiermit neu ausgewählte Projekt zum aktuellen Projekt. Zuvor muss der Mitarbeiter allerdings noch die Tätigkeit an dem vorherigen Projekt dokumentieren, was am besten in einem eigenen Formular geschieht. Wie diese Formulare aussehen können, zeigt Abbildung 1.5. Die Möglichkeit, die Reihenfolge der angezeigten Projekte zu ändern oder Projekte einoder auszublenden, ist hier noch nicht vorgesehen.
Abbildung 1.5: Entwurf zweier Formulare zum Auswählen des folgenden Projekts und zum Eingeben der vorhergehenden Tätigkeit
Wohin nun mit diesen Eingabeformularen? Der Mitarbeiter soll nicht damit aufgehalten werden, manuell die Datenbank zu öffnen und sie nach dem Eingeben der Projektzeit wieder zu schließen, weil ihn zusätzliche Einträge in der Taskleiste ablenken würden. Access selbst soll am besten gar nicht erscheinen, sondern nur die Projektliste – das ist technisch möglich. Wohin aber damit, wenn der Mitarbeiter sich wieder seinen Projekten widmen möchte? Ganz einfach: Lassen Sie die Anwendung im Systray verschwinden. Dort stört sie nicht und kann blitzschnell wieder hervorgeholt werden. Den übrigen Use-Case für das Mitarbeiter-Frontend formulieren Sie genau wie die anderen Use-Cases aus und leiten daraus ein entsprechendes Element der Benutzungsoberfläche ab. Das Gleiche geschieht mit dem Frontend für den Controller – dies dürfte
34
1
Datenbankanwendungen planen
jedoch etwas umfangreicher ausfallen, da Sie neben Mitarbeitern, Projekten und Projektzeiten auch noch Kunden verwalten sollten – irgendeiner soll die berechneten Stunden ja bezahlen. Sie brauchen natürlich nicht so weit zu gehen und wie in Abbildung 1.5 komplette Formulare zu erstellen – genau genommen schießt man damit ein wenig über das Ziel hinaus. Sie nehmen einfach Papier und Bleistift oder auch eine Anwendung wie Microsoft Visio und zeichnen die einzelnen Formulare in Form von Rechtecken und ihre Funktionen auf. Sie brauchen Steuerelemente und Ähnliches gar nicht einzusetzen – tragen Sie nur die Funktionen in die Rechtecke ein. Als Nächstes kennzeichnen Sie mit entsprechenden Pfeilen, welches Formular ein anderes Formular aufruft. Die Elemente des Mitarbeiter-Frontends sehen nun etwa wie in Abbildung 1.6 aus. frmProjektzeiten Eingabe der Tätigkeitsbeschreibung frmProjektliste
Projektwechsel
Liste der Projekte Anzeige des aktuellen Projekts
Projektansicht bearbeiten
frmProjektlisteEigenschaften Anpassen der Reihenfolge der Projekte Ein-/Ausblenden von Projekten
Abbildung 1.6: Elemente eines Teils einer Anwendung
1.2.5 Einzelheiten der Architektur Mit dem aktuellen Stand haben Sie bereits eine Grundlage, um zu prüfen, ob Ihre Vorstellungen von der zu entwickelnden Anwendung mit denen des Auftraggebers übereinstimmen: Sie haben ein Datenmodell und Sie haben eine Übersicht der Komponenten der Benutzungsoberfläche. Gerade Letzteres ist sehr wichtig, denn der Auftraggeber will schließlich wissen, wie seine Vorstellungen und Wünsche in etwa umgesetzt werden. Das Datenmodell sollten Sie – wenn möglich – ebenfalls mit dem Auftraggeber abstimmen, denn gerade hier nisten sich gerne Fehler ein, die aus Missverständnissen bei der Aufnahme der zu erfassenden Daten herrühren. Das gilt insbesondere für Anwendungen, die bestehende Daten weiterverwenden sollen.
Ergonomie
35
Stimmen Datenmodell und Entwurf der Use-Cases und der Elemente der Benutzungsoberfläche, liegt das Weitere bei Ihnen: Bei der Umsetzung der Vorgaben in eine fertige Anwendung wird der Auftraggeber vermutlich nicht mehr dazwischenfunken (wollen oder können). Auch hier sollten Sie sich nun nicht mit Tastatur und Maus bewaffnet sofort auf die Entwicklungsumgebung stürzen. Planen Sie vorher sorgfältig, welche Elemente der Anwendung für die Erledigung der unterschiedlichen Aufgaben zuständig sein sollen. Beispiel Validierung: Soll die Validierung auf Tabellenebene erfolgen – also mit Gültigkeitsregeln für Felder und Tabellen? Oder soll die Validierung direkt im Formular durchgeführt werden – in der Ereignisprozedur Vor Aktualisierung einzelner Textfelder beziehungsweise des Formulars? Vielleicht programmieren Sie auch eine mehrschichtige Anwendung, deren Business-Schicht für die Durchsetzung der Geschäftsregeln und damit auch für die Validierung sorgt? Daneben gibt es noch viele weitere Entscheidungen, die zu fällen sind. Erst wenn Sie wissen, welche Funktionalität in welches Objekt der Anwendung integriert werden soll, krempeln Sie die Ärmel hoch und legen los.
1.3 Ergonomie Ein wichtiger Punkt bei der Entwicklung von Datenbankanwendungen und Software im Allgemeinen ist die Bereitstellung einer ergonomischen Benutzungsoberfläche. Den Benutzer Ihrer Anwendung interessiert es nicht, ob Sie ein ausgefeiltes Datenmodell verwenden und welche VBA-Tricks Ihnen eingefallen sind, damit dies oder jenes wie gewünscht funktioniert. Stattdessen möchte dieser eine einfach und intuitiv zu bedienende Anwendung. Die folgenden Abschnitte enthalten einige Hinweise, wie Sie diesem Wunsch nachkommen können.
1.3.1 Das Auge isst mit Unabhängig davon, ob Sie persönlich das aktuelle Windows-Design für attraktiv halten oder nicht, müssen Sie davon ausgehen, dass der Großteil der Benutzer dieses Design bevorzugt. Microsoft hat vermutlich den einen oder anderen Gedanken an die richtige Farbgestaltung verschwendet. Sie können also grundsätzlich davon ausgehen, dass Sie den Anwender nicht negativ überraschen, wenn Sie das Look & Feel der gängigen Windows-Anwendungen beibehalten. Sie meinen, das sei selbstverständlich? Dann ist Ihnen noch keine Anwendung untergekommen, bei der der Entwickler (oder Designer?) ein wenig zu stark in den »Farbtopf« gegriffen hat – und das kommt nicht
36
1
Datenbankanwendungen planen
selten vor. Sollte der Anwender eine individuelle Farbgebung bevorzugen, dann kann er dies im Notfall auch über die Systemeinstellungen von Windows nachholen. Es gibt natürlich eine Ausnahme: Wenn der Auftraggeber ein Corporate Design oder eine andere Gestaltungsvorschrift vorgibt, können Sie davon nicht abweichen. Auf Gefahr des Auftraggebers, versteht sich … Eine weitere Ausnahme ist gegeben, wenn die jeweilige Funktionalität eine individuelle Farbgebung erforderlich macht. Wenn also beispielsweise Daten von gelben Papierformularen immer in bestimmte Formulare eingegeben werden müssen und das Gleiche entsprechend für hellblaue Papierformulare gilt, liegt es nahe, das Layout farblich der jeweiligen Aufgabe anzupassen. Farbe ist allerdings noch lange nicht alles. Neben einigen Regeln zum Design, die weiter unten folgen, sollten Sie natürlich dafür sorgen, dass Sie die standardmäßig verwendeten Steuerelemente einsetzen, diese mit gewohnten oder zumindest ähnlichen Schriften versehen und auch beim übrigen Layout von Steuerelementen nicht allzu sehr vom Standard abweichen, der – wie dem Tenor der vorangehenden Ausführungen bereits zu entnehmen war – durch die gängigen Windows-Anwendungen vorgegeben ist.
1.3.2 Gewohntes unterstützen Die neue Benutzungsoberfläche sollte nach den gleichen Prinzipien gestaltet werden wie die Benutzungsoberfläche anderer Anwendungen, mit denen der Benutzer täglich arbeitet. Das bedeutet insbesondere, dass diese keine Elemente enthält, die der Benutzer nicht kennt. Im Allgemeinen ist damit nicht zu rechnen – zumindest nicht, wenn Sie bei der Entwicklung mit den standardmäßig zur Verfügung stehenden Steuerelementen auskommen. Sollten Sie jedoch einmal ein außergewöhnliches Steuerelement verwenden, müssen Sie schon gute Gründe dafür vorweisen.
1.3.3 Ungewohntes verständlich machen Gelegentlich lassen sich Steuerelemente nur auf eine Art verwenden, die Sie nicht bei allen Anwendern als bekannt voraussetzen können. Wenn Sie beispielsweise ein Kombinationsfeld nicht nur zur Auswahl von Daten, sondern auch zur Eingabe neuer Datensätze in die dem Kombinationsfeld zugrunde liegende Datensatzherkunft verwenden, müssen Sie den Anwender darauf aufmerksam machen – beispielsweise durch einen entsprechenden SteuerelementTip-Text.
1.3.4 Größe und Position der Steuerelemente Steuerelemente sollten bezüglich der Tab-Reihenfolge und damit in der Reihenfolge der Abarbeitung immer von links oben nach rechts unten angeordnet sein. Ob Sie dabei erst von links nach rechts oder von oben nach unten vorgehen, ist sicher situa-
Ergonomie
37
tionsabhängig. Ausnahmen gibt es, wenn Sie etwa drei untereinander liegende Bereiche verwenden, die durch einen Rahmen als solche gekennzeichnet sind – hier werden zunächst alle Elemente eines Bereichs abgearbeitet, bevor der nächste Bereich an der Reihe ist. Die Höhe gleichartiger Steuerelemente sollte in der ganzen Anwendung gleich sein – bezogen auf die Anzahl Zeilen des darin anzuzeigenden Textes und die Textgröße. Die Breite orientiert sich natürlich an den anzuzeigenden Inhalten – hier ist es wichtig, möglichst realitätsnahe Testdaten zu verwenden, damit alle Inhalte komplett zu sehen sind. Schaltflächen sollten innerhalb eines Formulars prinzipiell die gleiche Größe haben – zumindest die Standardschaltflächen wie OK oder Abbrechen. Wenn wirklich einmal eine Schaltfläche dabei ist, deren Beschriftung diesen Rahmen sprengt, sollten Sie zunächst über einen alternativen Text und erst dann über ein Abweichen von der Schaltflächenbreite nachdenken. Auch hier gelten Ausnahmen. Manchmal gibt es Schaltflächen, die zu einem Steuerelement gehören und beispielsweise weitere Details zu dessen Inhalt anzeigen. Deren Höhe passen Sie natürlich der Höhe des anhängenden Steuerelements an. Planen Sie bei der Auslegung von Formularen einen virtuellen inneren Rahmen ein, der beispielsweise fünf Pixel Abstand zwischen den Steuerelementen und jedem Seitenrand gewährleistet. Tipp: Ordnen Sie zunächst alle Steuerelemente vom linken und vom oberen Rand ausgehend an. Schieben Sie den rechten und den unteren Rand ganz an die enthaltenen Steuerelemente heran. Markieren Sie dann alle Steuerelemente, verschieben Sie diese um zehn Pixel nach rechts und nach unten und dann wieder um fünf Pixel nach links und nach oben – fertig ist der fünf Pixel breite Rahmen. Diesen Abstand sollten Sie im Übrigen auch innerhalb von Unterformularen, Rahmen von Optionsgruppen und dergleichen einhalten. Natürlich müssen es nicht unbedingt fünf Pixel sein, genauso wenig wie beim vertikalen Abstand zwischen zwei Steuerelementen. Manch einer legt Textfelder direkt untereinander an, sodass man nur noch den Rand zwischen den beiden Textfeldern sieht, ein anderer lässt zwischen zwei Textfeldern eine bestimmte Anzahl Pixel Platz. Für die Festlegung des horizontalen Abstands ist es zunächst wichtig, Steuerelemente untereinander mit dem gleichen Abstand zum linken Rand des Formulars anzuordnen. Das geschieht meist in mindestens zwei Spalten – eine für die Beschriftungen und eine für das Steuerelement selbst. Gegebenenfalls verteilen Sie die Steuerelemente wiederum auf zwei Spalten – das wären dann insgesamt vier Spalten, die jeweils bündig angeordnet werden müssen.
38
1
Datenbankanwendungen planen
Optimal ist es natürlich, wenn Sie die Breite des Formulars durch die maximale Summe der Breiten von Beschriftungsfeld und Steuerelement teilen und die Steuerelemente gleichmäßig über die Breite verteilen. Manchmal sind Steuerelemente allerdings so breit, dass Sie diese direkt über zwei oder mehr Spalten der übrigen Steuerelemente verteilen müssen. Aber es ist nicht nur wichtig, die Steuerelemente – wie zuvor beschrieben – in ein bestimmtes Raster zu stecken. Natürlich müssen Sie die Steuerelemente auch nach dem Inhalt gruppieren. Gegebenenfalls sollten Sie diese Gruppierung durch einen Rahmen verdeutlichen, den Sie mit einer passenden Überschrift versehen.
1.3.5 Schriften Setzen Sie gut lesbare Schriftgrößen ein. Microsoft verwendet beispielsweise in Access durchgängig die Schriftgröße 8. Für viele ist das in Ordnung, aber Sie sollten sich nicht darauf verlassen. Stimmen Sie die Schriftgrößen mit dem Auftraggeber ab, denn das nachträgliche Anpassen aller Formulare und Berichte bedeutet viel Aufwand. Verwenden Sie fette oder größere Schriftarten, um Texte besonders hervorzuheben. Sollten Sie das Gefühl haben, ein Formular mit einer Überschrift versehen zu müssen, gehört diese in die Titelleiste des Formulars. In Berichten ist das natürlich anders: Schließlich sollen diese in der Regel ausgedruckt werden, daher gehört die Überschrift auf den Bericht selbst.
1.3.6 Menüs bereitstellen Alle Funktionen, die der Benutzer benötigt, lassen sich über die Menüleiste, die Symbolleisten und über Kontextmenüs aufrufen. Dabei sollten Menüpunkte explizit niemals ein- oder ausgeblendet werden, nur weil sie besonders oft oder besonders selten verwendet werden. Diese von Microsoft Office verwendete Technik scheint gerade unbedarfte Benutzer zu verwirren (»Wo ist denn dieser Menüpunkt? Zuhause finde ich ihn doch immer ...«). Wenn ein Menüpunkt in bestimmten Zusammenhängen nicht verfügbar sein soll, deaktivieren Sie ihn am besten – so sieht der Benutzer, dass der Menüpunkt nicht verschwunden, sondern lediglich deaktiviert ist. Wenn Sie Kontextmenüs zur Verfügung stellen, ist das in den meisten Fällen eine tolle Sache für den, der die Arbeit mit Kontextmenüs gewohnt ist und es an der entsprechenden Stelle erwartet. Setzen Sie aber nicht voraus, dass der Benutzer die Dokumentation Ihrer Anwendung liest, um herauszufinden, dass der Befehl xyz mit dem Kontextmenü aufgerufen werden muss. Kontextmenüs sollten immer nur als ergänzende Möglichkeit zum Aufruf einer Funktion dienen.
Ergonomie
39
Viele Entwickler setzen auf so genannte Switchboards – das sind Übersichtsformulare, die Steuerelemente zum Aufrufen der Funktionen der Anwendung enthalten. Der Nachteil dieser Switchboards gegenüber Menüleisten ist: Menüleisten sind quasi immer sichtbar, Switchboards können als herkömmliche Formulare auch mal hinter anderen Formularen verschwinden. Alle Funktionen, die wichtig für den reibungslosen Ablauf der Anwendung sind und nicht unmittelbar von bestimmten Dialogen abhängen, sollten Sie über die Menüleiste verfügbar machen. Ordnen Sie die Menüs und ihre Einträge so an, dass sie nach Funktionen gruppiert und nach Priorität sortiert sind.
1.3.7 Tastenkombinationen bereitstellen Viele Anwender erledigen ihre Arbeit schneller als andere, weil sie die zur Verfügung stehenden Tastenkombinationen verwenden. In der Tat helfen Tastenkombinationen eine Menge Zeit sparen. Tastenkombinationen können Sie zunächst einmal überall dort unterbringen, wo sich auch ein Menüpunkt oder ein Steuerelement mit Beschriftung befindet. Fügen Sie der Beschriftung des Menüpunkts oder des Steuerelements einfach das Und-Zeichen (&) vor dem Buchstaben hinzu, der in Kombination mit der (Alt)-Taste die entsprechende Funktion ausführen soll. So können Sie beispielsweise einen Menübefehl des Datei-Menüs bei gedrückter (Alt)Taste mit zwei Tastenanschlägen ausführen. Natürlich geht es auch noch schneller. Einige Funktionen besitzen eine universelle Tastenkombination, die sich unabhängig von Menü- oder Steuerelementbeschriftungen ausführen lässt. Die bekanntesten Beispiele sind vermutlich (Strg) + (C) und (Strg) + (V) zum Kopieren und Einfügen und (Strg) + (S) zum Speichern von Dateien. Wenn Sie eigene globale Tastenkombinationen festlegen möchten, verwenden Sie das Makro Autokeys (weitere Informationen in der Online-Hilfe von Access unter diesem Stichwort).
1.3.8 Abläufe unterstützen Ein wichtiger Punkt ist es, die voraussichtlichen Abläufe zu erfassen und die Steuerelemente so auszulegen, dass sie die am meisten vorkommenden Abläufe auch am besten unterstützen. Dafür gibt es mehrere Beispiele: Anordnung von Eingabesteuerelementen: Wenn Sie etwa übliche Daten wie Adressinformationen abfragen, ordnen Sie Textfelder in der richtigen Reihenfolge an –
40
1
Datenbankanwendungen planen
also beispielsweise »Anrede«, »Vorname«, »Nachname«, »Straße«, »PLZ«, »Ort«. Benutzer sind an gewisse Reihenfolgen gewöhnt – Abweichungen werden als Stolperstein empfunden. Belegen Sie die am wahrscheinlichsten benutzte Schaltfläche als Standard vor, sodass der Benutzer diese nach dem Eingeben der gewünschten Daten oder per Tabulator-Taste leicht erreichen kann. Stellen Sie die Eigenschaft Abbrechen der gleichnamigen Schaltfläche – soweit vorhanden – auf Ja ein. Beim Betätigen der Escape-Taste wird dann die Ereigniseigenschaft Beim Klicken dieser Schaltfläche ausgelöst. Stichwort Tabulator-Taste: Nicht jeder Benutzer bearbeitet Eingabeformulare, indem er die Einfügemarke mit der Maus in das jeweilige Feld setzt. Gerade Standardeingaben wie Adressinformationen sind dazu prädestiniert, per TabulatorTaste durchlaufen zu werden. Achten Sie unbedingt auf die Tabulator-Reihenfolge – und wenn es die letzte Optimierung vor Aushändigung der Software sein sollte. Einige Anwender verwenden die Eingabetaste, um von einem Eingabefeld zum nächsten zu gelangen. Wenn dies der Fall ist, können Sie die OK-Schaltfläche nicht gleichzeitig mit der Eingabetaste vorbelegen. Klären Sie diese Frage am besten mit den künftigen Anwendern ab. Ein wichtiges Kriterium für die Unterstützung wahrscheinlicher Abläufe ist ihr Start. Setzen Sie den Fokus auf das Steuerelement, das vermutlich als Erstes verwendet wird. In den meisten Fällen wird dies die OK-Schaltfläche sein. Wenn Sie allerdings in einem Dialog Daten zu einem neuen Datensatz abfragen, gehört der Fokus natürlich auf das erste Eingabesteuerelement (etwa »Anrede« bei der Eingabe von Adressdaten). Wenn der Benutzer der Anwendung die Sitzung standardmäßig mit einem bestimmten Formular beginnen wird, können Sie dieses beim Starten der Anwendung direkt zur Verfügung stellen. In den meisten Fällen werden Sie jedoch ein leeres AccessFenster bereitstellen, mit dessen Menüleiste der Benutzer die gewünschten Dialoge öffnen kann.
1.3.9 Eins nach dem anderen Sorgen Sie dafür, dass der Benutzer nicht allzu viele Dialoge öffnen kann und dadurch den Überblick verliert. Meist geschieht dies unbedacht im Verlauf der Arbeit, weil der Benutzer etwa in einem Formular die Details zu einem im aufrufenden Formular ausgewählten Datensatz anschaut oder bearbeitet. Solche Formulare werden oft als modale Dialoge geöffnet, das heißt, dass diese Formulare erst wieder geschlossen werden müssen, bevor mit der Arbeit in anderen Formularen der Anwendung fortgefahren werden kann.
Ergonomie
41
Mit dieser Vorgehensweise tun Sie auch sich selbst einen Gefallen, denn oftmals enthalten Formulare voneinander abhängige Daten. Wenn Sie keinen programmiertechnischen Overhead möchten, sollten Sie dafür sorgen, dass Formulare, die Daten bereits geöffneter Formulare anzeigen, auch nur durch diese geöffnet werden – und zwar als modaler Dialog. Anderenfalls müssen Sie jederzeit auf Veränderungen in allen angezeigten Versionen der jeweiligen Daten lauschen und etwaige Änderungen der Konsistenz halber auf alle anderen Formulare gleichen Inhalts übertragen. Also: Wenn Sie per Menüleiste ein Formular mit einer Übersicht über alle Mitarbeiter öffnen und in einem weiteren Formular Detailinformationen zu einem ausgewählten Mitarbeiter anzeigen möchten, öffnen Sie dieses als modalen Dialog. Änderungen an den Daten des Detailformulars übertragen Sie nach dessen Schließen auf die im aufrufenden Formular angezeigten Daten. Davon abgesehen, dass Ihnen dies unnötige Arbeit erspart, hilft es auch dem Benutzer, indem Sie ihn auf sanfte Weise davon abhalten, allzu viele Formulare gleichzeitig zu öffnen.
1.3.10 Helfen Sie! Stellen Sie dem Anwender Hilfe und Informationen zur Verfügung, wo es nur geht. Sie sollen natürlich nicht beim Überfahren eines Steuerelements mit der Maus gleich ein Meldungsfenster aufpoppen lassen, aber die anderen Möglichkeiten sollten Sie ruhig nutzen. Wenn beispielsweise nicht unmittelbar erkennbar ist, welchen Inhalt ein bestimmtes Textfeld erwartet, sorgen Sie für die Einblendung eines für die Eigenschaft SteuerelementTip-Text angegebenen Hilfetextes und zeigen Sie die gleiche Meldung auch noch in der Statusleiste an – hierfür ist die Eigenschaft Statusleistentext verantwortlich. Wenn Sie der Anwendung eine Online-Hilfe spendiert haben, können Sie diese natürlich auch für die Anzeige einer kontextabhängigen Hilfeseite verwenden. Falls das Kind doch einmal in den Brunnen fällt und der Benutzer ein Steuerelement auf die falsche Art verwendet, indem er etwa einen Text in einem falschen Format in ein Textfeld eingibt, dann stellen Sie ihm eine aussagekräftige Meldung zur Verfügung und lassen Sie ihn nicht mit der entsprechenden Standardmeldung von Access allein.
1.3.11 Ist meine Anwendung ergonomisch? Ob Sie nicht nur eine funktionierende Anwendung entwickelt haben, sondern auch eine, die aufgrund ihrer ergonomischen Eigenschaften von den Anwendern angenommen wird (allerdings werden Sie sicher nie ein Lob für die gute Ergonomie einer Anwendung erhalten), erfahren Sie vielleicht erst, wenn Sie Anfragen nach Erweiterung der Anwendung erhalten.
42
1
Datenbankanwendungen planen
Um dieses Ergebnis zu erreichen, testen Sie die Anwendung so gründlich wie möglich selbst, soweit dies möglich ist – die während der Entwicklung entstandene Betriebsblindheit wird allerdings zuverlässige Resultate verhindern. Lassen Sie die Anwendung dann von ein, zwei Entwicklerkollegen durchklicken – diese werden mögliche Ungereimtheiten und Stolpersteine vermutlich besser entdecken als Sie. Verlassen Sie sich auf keinen Fall darauf, dass die Anwender an Sie herantreten und Ihnen Vorschläge zur Verbesserung der Ergonomie unterbreiten.
2 Tabellen und Datenmodellierung Dieses Kapitel setzt Kenntnisse im grundlegenden Umgang mit Tabellen voraus. Darauf aufbauend erfahren Sie hier, wie Sie die Anforderungen an die geplante Anwendung in ein adäquates Datenmodell umsetzen. Dabei ist es wichtig, dass die Daten den Objekten der realen Welt entsprechend abgebildet und dazu in Relation gesetzt werden. Dabei helfen die Normalisierungsregeln, die manch einer vielleicht schon intuitiv einsetzt, und eine konsistente Benennung von Tabellen und Tabellenfeldern. Damit Sie nicht nur mit trockener Theorie abgespeist werden, finden Sie im mittleren Teil des Kapitels eine Menge Beispiele für Datenmodelle. Diese können Sie als Basis für eigene Anwendungen oder auch nur als Anregung verwenden. Hintergrund dieses Kapitels ist die Tatsache, dass viele (angehende) Entwickler ins kalte Access-Wasser geworfen werden und keine oder wenig Erfahrung in der Datenmodellierung haben. Zudem findet sich in den meisten Grundlagen-Büchern zu Access meist nur eine Beispielanwendung mit Datenmodell – und das hält dann im ganzen restlichen Teil des Buches als Grundlage für Abfrage-, Formular-, Berichts- und VBA-Beispiele her. Dieses Kapitel hat verschiedene Ziele: Einführung einer Konvention für die Namen von Tabellen und Feldern Normalformen: Wozu dienen sie und wie normalisiert man ein Datenmodell? Begriffsklärung (Beziehungsarten, relationale Integrität, Primärschlüssel, Fremdschlüssel) Erläuterung der Beziehungsarten anhand praktischer Beispiele Vermittlung eines Gefühls für die jeweils richtige Beziehungsart anhand einiger Datenmodelle verschiedener Anwendungen Außerdem finden Sie am Ende dieses Kapitels ausführliche Informationen zum Speichern von Dateien und insbesondere von Bildern in Access-Datenbanken.
44
2
Tabellen und Datenmodellierung
2.1 Namenskonventionen für Tabellen und Felder Die Namenskonventionen für Tabellen enthalten Vorschläge für die Benennung von Tabellen und der enthaltenen Felder. Die nachfolgenden Abschnitte fassen dabei Elemente der Ungarischen Notation (http://www.xoc.net/standards/rvbanc.asp) und einige weitere Regeln zusammen, die sich bewährt haben und von vielen Entwicklern so oder ähnlich berücksichtigt werden. Details zur Ungarischen Notation finden Sie unter dem oben genannten Link; ein Abdruck der recht umfangreichen Tabellen ist aus Platzgründen leider nicht möglich. Grundsätzlich gelten für die Vergabe von Namen an Access-Objekte, Feldnamen und Steuerelemente folgende Regeln: Der Name darf aus maximal 64 Zeichen bestehen. Der Name kann aus beliebigen Zeichen mit Ausnahme des Punktes (.), Ausrufezeichens (!), Gravis-Akzents (`), einfachen (') und doppelten Anführungszeichens (") und der eckigen Klammern ([]) bestehen. Leerzeichen dürfen nicht am Anfang oder am Ende des Namens stehen. Zu empfehlen ist darüber hinaus, dass Namen von Access-Objekten, Feldern und Steuerelementen gar keine Sonderzeichen enthalten, da Namen mit Sonderzeichen an manchen Stellen eine Spezialbehandlung erfordern – beispielsweise müssen Sie diese in SQL-Ausdrücken und in VBA in eckige Klammern einfassen. Verwenden Sie außerdem besser keine Namen, die bereits für ein reserviertes Wort innerhalb von Access, VBA oder referenzierten Objektbibliotheken benutzt werden. Abschreckendes Beispiel ist die Verwendung von »Name« als Feldname in Tabellen mit Kontaktdaten – diese Bezeichnung ist bereits als Eigenschaft diverser Objekte vergeben. In Zweifelsfällen konsultieren Sie einfach den Objektkatalog und suchen nach dem fraglichen Namen – wenn Sie ihn dort nicht finden, können Sie ihn einsetzen. Natürlich können Sie auch Sonderzeichen, Leerzeichen oder bereits als Schlüsselwort verwendete Namen aufgreifen. Sie müssen dann allerdings einige Regeln beachten – etwa, dass Sie die Bezeichnungen von Feldern in SQL-Ausdrücken und in VBA in eckige Klammern setzen, wenn diese nicht ohnehin in Anführungszeichen stehen.
Warum soll man eine Namenskonvention verwenden? Grundsätzlich können Sie Tabellen und die enthaltenen Felder nennen, wie Sie möchten. Abhängig davon, ob Sie eine Anwendung nur für das stille Kämmerlein oder für jemand anderen entwickeln, sollten Sie jedoch folgende Punkte beachten: Eine feste Systematik bei der Vergabe von Namen für Tabellen und Felder erleichtert Ihnen beim Programmieren das Leben. Wenn Ihre Tabellennamen etwa grund-
Namenskonventionen für Tabellen und Felder
45
sätzlich mit »tbl« beginnen und der Plural der darin beschriebenen Objekte angefügt ist, müssen Sie sich beim Referenzieren dieser Tabelle aus Formularen, Berichten oder aus VBA-Modulen niemals Gedanken machen, wie Sie die Tabelle genannt haben, wenn Sie nur wissen, welches Objekt darin beschrieben wird. Präfixe bei Objektnamen erlauben Ihnen auf einfache Weise, ein Objekt am Namen zu erkennen und gleiche Basisnamen für mehrere unterschiedliche Objekttypen zu verwenden. Wenn Sie eine Tabelle beispielsweise nur Bestellungen nennen, können Sie schon keine Abfrage gleichen Namens mehr verwenden. Daher verwenden Sie für die Tabelle den Namen tblBestellungen und für die Abfrage den Namen qryBestellungen. Wenn mehrere Entwickler die gleiche Konvention verwenden, erleichtert dies anderen Entwicklern das Analysieren und Bearbeiten der Anwendung und des enthaltenen Codes. Sie würden wahrscheinlich verrückt werden, wenn Sie eine Anwendung weiterentwickeln müssten, die eine völlig andere Konvention als die Ihnen vertraute verwendet.
Verwenden alle Access-Entwickler die gleiche Konvention? Wenn Sie eine Anwendung oder eine Beispieldatenbank eines halbwegs professionellen Access-Entwicklers in die Finger bekommen, werden Sie feststellen, dass sich dort die Ungarische Notation durchgesetzt hat. Natürlich gibt es hier und da Abweichungen, die persönlichen Vorlieben oder einfach der Macht der Gewohnheit unterliegen, aber mit diesen kann man in der Regel gut leben. Sicher kommt ein Entwickler damit klar, wenn ein Bericht nicht das Präfix »rpt« hat, sondern mit »rep« beginnt. Auch nennen manche Entwickler eine Datensatzgruppe »rs…« und nicht »rst…«, aber es versteht trotzdem jeder, was gemeint ist.
2.1.1 Tabellennamen Tabellen haben genau wie alle anderen Objekte unter Access ein bestimmtes Präfix, damit man sie von anderen Objekten unterscheiden kann. Dieses Präfix lautet tbl. Der Rest des Tabellennamens soll möglichst gut beschreiben, was die Tabelle enthält. Dabei wählt man für diesen Teil des Tabellennamens in der Regel die Pluralform. Der Grund dafür ist, dass die meisten Tabellen auch mehrere Datensätze und damit Informationen über das betroffene Objekt enthalten. Man kann nicht oft genug erwähnen, das jede »normale« Tabelle Informationen über ein reales Objekt enthalten sollte, wie beispielsweise Personen, Fahrzeuge, Projekte, Termine, Gebäude oder Artikel.
46
2
Tabellen und Datenmodellierung
Tabellen mit realen Objekten Tabellen, die reale Objekte beschreiben, heißen beispielsweise folgendermaßen: tblArtikel tblProjekte tblPersonen tblFahrzeuge
Verknüpfungstabellen Wenn schon von »normalen« Tabellen die Rede ist, sollen auch die anderen Typen vorgestellt werden: m:n-Beziehungen erfordern die Verwendung von Verknüpfungstabellen, die mindestens jeweils das Primärschlüsselfeld der zu verknüpfenden Tabellen enthalten. In manchen Fällen lässt sich für solche Tabellen ein erstklassiger Name finden, doch eine Kombination der Namen der beteiligten Tabellen macht meist deutlich, was die Tabelle für Informationen enthält. Beispiele: tblFahrzeugeSonderausstattungen tblBestellungenArtikel (besser: tblBestelldetails) tblArtikelFirmen (besser: tblLieferanten)
Detailtabellen in 1:1-Beziehungen Und da wären noch die Tabellen, die zusätzliche Daten zu anderen Tabellen enthalten und per 1:1-Beziehung mit diesen verknüpft sind. Mit ein wenig objektorientierter Sichtweise bilden die »Basistabelle« und die »Erweiterungstabelle« eine neue Tabelle, die quasi von der »Basistabelle« erbt. Wenn Sie beispielsweise eine Tabelle tblPersonen haben, die Kunden und Mitarbeiter zusammenfassen soll, können Sie die spezifischen Daten der beiden Personenarten in weiteren Tabellen speichern, die Sie 1:1 mit der »Basistabelle« verknüpfen. Diese Tabellen könnten Sie tblKunden oder tblMitarbeiter nennen, aber daraus würde nicht deutlich, dass diese Tabellen lediglich Detaildaten zu einer weiteren Tabelle enthalten. Besser wären Bezeichnungen wie tblPersonenKunden und tblPersonenMitarbeiter.
Lookup-Tabellen Die letzte Gruppe Tabellen, die nicht reale Objekte aus dem wirklichen Leben nachbilden, sind so genannte »Lookup-Tabellen«. Diese Tabellen enthalten Informationen, die eigentlich zu den »normalen« Tabellen gehören, aber im Zuge der Normalisierung in eigene Tabellen ausgegrenzt wurden. Beispiele dafür sind Anrede, Geschlecht oder Kontaktart.
Namenskonventionen für Tabellen und Felder
47
Für diese Tabellen gelten die gleichen Regeln wie für »normale« Tabellen, also beispielsweise folgende: tblAnreden tblGeschlechter (der Plural liest sich merkwürdig, ist aber hier konsequent) tblKontaktarten
Temporäre Tabellen Gelegentlich benötigen Sie eine Tabelle nur kurze Zeit und löschen diese nach der Verwendung wieder. Um diese Tabellen von anderen unterscheiden zu können, vor allem aber, damit Sie einen Überblick behalten, welche Tabellen nur vorübergehend benötigt werden, können Sie diese Tabellen kennzeichnen, indem Sie ihnen das Suffix Tmp oder Temp anhängen, etwa tblImportTemp. Eine Einsatzmöglichkeit für solche Tabellen ist etwa das Speichern des Ergebnisses von aufwändigen Abfragen (gegebenenfalls mit eingebauten und benutzerdefinierten Funktionen). Wenn die Ermittlung des Abfrageergebnisses lange dauert und das Ergebnis beziehungsweise die zugrunde liegenden Daten sich selten ändern, macht das Speichern des Ergebnisses in einer temporären Tabelle sehr viel Sinn.
2.1.2 Feldnamen Entgegen der bei Variablen üblichen Verwendung von Präfixen, anhand derer sich eine Aussage über den Datentyp der Variablen treffen lässt, verwendet man für Feldnamen im Allgemeinen kein Präfix. Das ist natürlich Geschmackssache; man findet jedoch kaum Datenbanken, in denen auch die Feldnamen mit einem entsprechenden Präfix versehen sind.
Primärschlüsselfelder Der Name des Primärschlüsselfeldes setzt sich aus der Bezeichnung des durch die Tabelle abgebildeten Objekts im Singular und dem Suffix ID zusammen. Beispiele: tblArtikel: ArtikelID tblPersonen: PersonID In Verknüpfungstabellen kommt es darauf an, ob die Tabelle einen eigenen Primärschlüssel hat oder ob sie aus den Fremdschlüsselfeldern zur Verknüpfung mit den jeweiligen Tabellen zusammengesetzt wird. Im ersteren Fall gibt es vermutlich auch einen sinnvollen Tabellennamen, der nicht aus den Namen der beiden verknüpften Tabellen besteht, wie etwa tblBestelldetails. Hier würde der Primärschlüssel (soweit vorhanden) BestelldetailID heißen.
48
2
Tabellen und Datenmodellierung
In Tabellen, die zusätzliche Daten zu einer anderen Tabelle enthalten und per 1:1Beziehung mit dieser verknüpft sind, verwendet man normalerweise den gleichen Namen für das Primärschlüsselfeld wie in der Basistabelle. Gegebenenfalls ist das Verknüpfungsfeld der Erweiterungstabelle nicht das Primärschlüsselfeld der Erweiterungstabelle, sondern lediglich ein eindeutiges Feld. In diesem Fall greift wieder die erstgenannte Regel: Primärschlüsselname = enthaltenes Objekt im Singular + ID.
Fremdschlüsselfelder Für die Benennung von Fremdschlüsselfeldern gibt es in der Praxis zwei Ansätze: Der einfachere verwendet einfach den Namen des Primärschlüsselfeldes der verknüpften Tabelle. Der zweite Ansatz macht noch ein wenig deutlicher, dass es sich bei diesem Feld definitiv um ein Fremdschlüsselfeld handelt, indem er dem Namen dieses Feldes noch das Präfix ref verpasst. Die zweite Variante hat Vorteile, wenn es um m:n-Beziehungen und 1:1-Beziehungen geht: Eine Tabelle mit zwei Feldern, die beide das Präfix ref enthalten, lässt sich zweifelsfrei als Verknüpfungstabelle identifizieren; und auch eine Tabelle, deren Primärschlüsselfeld das Präfix ref enthält, ist schnell als Erweiterungsteil einer 1:1-Beziehung enttarnt.
Allgemein Sowohl für Feldnamen von Primärschlüsselfeldern, Fremdschlüsselfeldern als auch für alle anderen Felder gilt, dass der Name des Feldes sorgfältig gewählt sein will. Am besten ist es, wenn er grobe Informationen über den Datentyp enthält: Text: Vorname, Nachname Datum: Geburtsdatum, Einstellungsdatum, Erscheinungstermin Zahlen: Anzahl, Meldebestand, Lagerbestand Währung: Einzelpreis Boolean: MehrwertsteuerAusweisbar, Aktiv, InReihenfolge
Unterstrich – ja oder nein? Ob Sie in Tabellen- und Feldnamen den Unterstrich als Trennung zwischen einzelnen Wörtern verwenden oder die einzelnen Wörter einfach groß beginnen, ist reine Geschmacksache (in diesem Buch finden Sie übrigens ausschließlich letztere Variante). Wichtig ist, dass Sie es überhaupt hervorheben, wenn ein Tabellen- oder Variablenname aus mehr als einem Wort besteht.
Normalisierung
49
Beispiele: Anzahl_Datensaetze oder AnzahlDatensaetze Fahrzeug_ID oder FahrzeugID Man sollte es aber nicht übertreiben: Dem Autor sind schon Varianten untergekommen, in denen nicht nur jedes einzelne Wort, sondern fast jede Silbe groß geschrieben wurde (etwa MehrwertSteuersatz) – wenn ein Wort im Deutschen zusammengeschrieben wird, sind auch keine Großbuchstaben erforderlich. Einen deutlichen Vorteil hat die Verwendung von Großbuchstaben zur Unterteilung mehrerer Wörter: Sie müssen sich nicht merken, was Sie groß und was Sie klein geschrieben haben. Access ist äußerst nachsichtig, was die Groß-/Kleinschreibung von Objektnamen angeht. Einen Unterstrich zu viel oder zu wenig wird Access Ihnen hingegen nicht verzeihen.
Lang oder kurz – mit oder ohne Abkürzung? Die Zeiten, in denen die beschränkte Länge von Variablennamen die Fantasie der Programmierer beflügelte, sind zum Glück vorbei – und Gleiches gilt für Tabellen- und Feldnamen. Die in der Zwischenüberschrift gestellte Frage nach der Länge von Feldnamen ist leicht zu beantworten: So lang wie nötig und so kurz wie möglich. Der Feldname sollte nicht in die Irre führen, nur um ein paar Zeichen zu sparen, andererseits lassen sich zu lange Feldnamen nicht gut merken. Wenn Sie für die Zusammenstellung jeder einzelnen SQL-Anweisung erst das Beziehungsfenster öffnen müssen, wissen Sie, dass Sie an den Tabellen- und Feldnamen arbeiten müssen.
2.2 Normalisierung Unter Normalisierung versteht man die Überführung des Datenmodells in einen bestimmten Zustand. Dieser Zustand wird durch die Nummer der Normalform unterschieden. Meistens genügt das Erreichen der dritten Normalform, um Redundanzen und Inkonsistenzen vorzubeugen und dadurch die Wartung der enthaltenen Daten zu vereinfachen. Die Möglichkeit, Redundanzen und Inkonsistenzen zu vermeiden, ist eine der Haupteigenschaften von relationalen Datenbanksystemen. Jeder, der schon einmal eine Datenbank für einen Kunden entwickeln sollte, der die betroffenen Daten zuvor mit Excel verwaltet hat, und das Vergnügen hatte, auch den Import dieser Daten vorzunehmen, weiß, was Redundanzen und Inkonsistenzen sind (das soll keine Verunglimpfung der Möglichkeiten von Excel sein – aber dessen Stärken liegen eher woanders).
50
2
Tabellen und Datenmodellierung
Ein gern gesehenes Beispiel ist die Verwaltung von Rechnungen in einer einzigen Tabelle. Dort finden sich unter Umständen alle Rechnungs- und Kundendaten zu einer Rechnung in einer Zeile. Sobald zwei Rechnungen für den gleichen Kunden gespeichert werden, gibt es auch zwei Kopien der Kundendaten in der Tabelle. Ändern sich die Kundendaten, werden diese Änderungen möglicherweise nur in einer neuen Zeile vorgenommen. Sobald ein anderer Mitarbeiter eine Rechnung für diesen Kunden stellen soll, steht er vor mindestens zwei verschiedenen Kunden-Datensätzen und das Unheil nimmt seinen Lauf. Um solches Ungemach zu verhindern, gibt es relationale Datenbanken, die Normalformen und die referentielle Integrität.
Halbautomatisches Normalisieren In den folgenden Abschnitten lernen Sie die unterschiedlichen Normalformen kennen und finden Beispiele für das Umwandeln nicht normalisierter Tabellen in die jeweilige Normalform. Die Zwischenüberschrift bezieht sich darauf, dass jeder Normalisierungsschritt nicht vollautomatisch abläuft. Genau genommen besteht jeder Schritt aus drei Teilen: Der erste passt das Datenmodell an und der zweite sorgt für die Umorganisierung der vorhandenen Daten. Der dritte Schritt räumt auf und löscht eventuell nicht mehr benötigte Felder. Das Anpassen des Datenmodells und damit des Tabellenentwurfs erfolgt manuell. Das Umorganisieren der Daten kann – bei kleinen Datenmengen – auch manuell erfolgen, aber das ist sicher keine Arbeit für einen Entwickler: Der baut sich eine kleine Routine, die diesen Vorgang mit ein paar Aktionsabfragen oder im schlimmsten Fall ein paar ADO- oder DAO-Anweisungen automatisch durchführt.
Warum nicht direkt normalisieren? Der Grund für den Einsatz der Normalisierung liegt meist in Altlasten bezogen auf die Organisation der Daten vor der Erstellung einer Datenbankanwendung. Viele Datenbanken werden neu erstellt, weil bestehende Daten auf die bisherige Art und Weise nicht mehr verwaltet werden können – entweder es sind keine Erweiterungen mehr möglich, es wurden immer wieder neue Tabellen und Felder an das bestehende Datenmodell angestückelt und die Anwendung läuft nicht mehr schnell genug oder die Daten liegen in einem nicht für diesen Zweck geeigneten Format vor – etwa in Form von Excel-Tabellen. In diesen Fällen müssen Sie ein bestehendes Datenmodell normalisieren. Das bedeutet nicht, dass Sie mit spitzen Fingern an der Originaldatenbank herumschrauben müssen – meist werden Sie eher eine neue Datenbank erstellen und ein neues Datenmodell aufsetzen, das alle in dem vorhandenen Datenmodell enthaltenen Informationen beinhaltet. Anschließend werden Sie die Daten in die neue Datenbank importieren – natürlich entsprechend umorganisiert.
Normalisierung
51
Es kann natürlich auch sein, dass an einer bestehenden Datenbank hier und da ein kleines Problem besteht – in dem Fall werden Sie kein komplett neues Datenmodell ersinnen, sondern punktuelle Änderungen vornehmen. Auch wenn Sie die folgenden Regeln nicht unter dem Schlagwort Normalisierung kennen, werden Sie die eine oder andere vermutlich bereits anwenden – allein, weil sie einfach logisch erscheint und weil die meisten Entwickler, die Beispiele für Datenmodelle und damit Vorlagen für Gleichgesinnte veröffentlichen, wissen, wo es hapert. Damit Sie die Normalisierungsregeln gut verinnerlichen, finden Sie einige Beispiele für Datenmodelle, die diesen Regeln widersprechen; außerdem werden praktische Wege aufgezeigt, um solche Daten zu normalisieren.
2.2.1 Die erste Normalform Die erste Normalform fordert, dass jede in einem Feld gespeicherte Information atomar ist und nicht mehr in weitere Informationen unterteilt werden kann. Dadurch erreichen Sie, dass Sie die enthaltenen Werte einfach abfragen oder sortieren können. Beispiele für nicht atomare Informationen sind folgende in einem einzigen Feld gespeicherte Informationen: Vorname und Nachname Straße, Hausnummer, PLZ und Ort Wenn eine Tabelle ein Feld mit der Bezeichnung Name enthält und dieses sowohl den Vor- und den Nachnamen (in dieser Reihenfolge) wiedergibt, können Sie beispielsweise nur schwer nach dem Nachnamen sortieren. Vor- und der Nachname sind daher unbedingt in zwei Feldern zu speichern. Bei der Adresse bietet sich ein ähnliches Bild: Nach Datensätzen mit einem bestimmten Ort oder einer bestimmten PLZ wird oft gesucht. Diese Informationen sollten Sie daher in separaten Feldern speichern (siehe Abbildung 2.1 und Abbildung 2.2). Etwas anders sieht es bei der Straße und der Hausnummer aus: Die Angabe einer Straße hat ohne die Hausnummer zwar noch einen gewissen Informationsgehalt (wenn Sie zum nächsten IKEA fahren möchten, kommen Sie wahrscheinlich ohne die Angabe der Hausnummer aus), aber andersherum lässt sich mit einer Hausnummer allein wenig anfangen. Und das Sortieren danach macht in den meisten Fällen auch keinen Sinn. Fazit: Straße und Hausnummer sind quasi atomar und gehören deshalb normalerweise in ein Feld. Und dort, wo diese Regel durch eine Ausnahme bestätigt wird, werden die Anforderungen schon durchblicken lassen, dass die Informationen besser in getrennte Felder gehören.
52
2
Tabellen und Datenmodellierung
Abbildung 2.1: Tabelle mit Personendaten vor ...
Abbildung 2.2: ... und nach der Atomisierung
Um den Inhalt des Feldes Name aus Abbildung 2.1 in die beiden Felder Vorname und Nachname aus Abbildung 2.2 zu überführen, ist zumindest eine regelmäßige Anordnung des zusammengesetzten Feldes erforderlich – also entweder immer oder , . Dann lassen sich die Daten leicht von der einen in die andere Tabelle überführen, etwa mit folgender Prozedur: Public Sub NameAufteilen() Dim Dim Dim Dim Dim Dim Dim Dim
db As DAO.Database rst As DAO.Recordset longPos As Long strName As String strVorname As String strNachname As String strSQL As String lngLastSpace As Long
Set db = CurrentDb Set rst = db.OpenRecordset("tblPersonenNichtNormalisiert", dbOpenDynaset) Do While Not rst.EOF strName = rst!Name lngLastSpace = InStrRev(strName, " ") strVorname = Mid(strName, 1, lngLastSpace)
Normalisierung
53
strNachname = Mid(strName, lngLastSpace + 1) strSQL = "INSERT INTO tblPersonenNormalisiert(Vorname, Nachname) " _ & " VALUES('" & strVorname & "','" & strNachname & "')" db.Execute strSQL rst.MoveNext Loop End Sub Listing 2.1: Extrahieren der Bestandteile des Feldes Name in Vor- und Nachname
Die Prozedur geht davon aus, dass die Reihenfolge ist und dass der Nachname keine Leerzeichen enthält. Sie schreibt den Teil des Namens hinter dem letzten Leerzeichen in das Feld Nachname und den Rest in das Feld Vorname. Wenn der Name auch mal in der anderen Reihenfolge und durch Komma getrennt angegeben ist, können Sie dies durch eine entsprechende Verfeinerung der Prozedur abfangen. Aber Unarten, wie zuerst den Nachnamen und dann den Vornamen zu schreiben, ohne dazwischen ein Komma einzufügen, sind leider ebenfalls gängig; hier hilft wohl nur manuelles Nacharbeiten. Ein anderes Beispiel, das nach der ersten Normalform schreit, sind mehrere gleichartige Informationen in Listenform in einem einzigen Feld wie in Abbildung 2.3.
Abbildung 2.3: Beispiel für nicht atomare Informationen
Ein erster naiver Ansatz, die im Feld Lieferanten enthaltenen Daten in atomare Feldinhalte umzuwandeln, sieht wie in Abbildung 2.4 aus. Das ist eine oft zu beobachtende Variante, um gleichartige Informationen zu einem Datensatz zu speichern. Leider birgt diese Variante mindestens drei Schwächen: 1. Früher oder später gibt es einen Artikel, der mehr Lieferanten als dafür vorgesehene Felder hat. Dann heißt es: Felder anfügen, Abfragen anpassen, Formulare anpassen, Code anpassen.
54
2
Tabellen und Datenmodellierung
2. Wenn ein Artikel weniger Lieferanten als dafür vorgesehene Felder hat, bleiben diese leer und verschwenden unnötig Platz. 3. Wenn man nach Artikeln mit einem bestimmten Lieferanten sucht, muss man alle dafür vorgesehenen Felder durchgehen.
Abbildung 2.4: Atomar, aber nicht optimal: Felder mit gleichartigen Informationen
In diesem Fall gibt es nur eine Lösung: Da theoretisch jeder Artikel von jedem Lieferanten geliefert werden kann, muss eine m:n-Beziehung her. Das bedeutet, dass die Lieferanten in einer eigenen Tabelle gespeichert werden. Welcher Lieferant welchen Artikel liefert, speichern Sie in einer Verknüpfungstabelle, die jeweils die Nummer des Artikels und des Lieferanten aufnimmt. Das aus der Tabelle in Abbildung 2.4 entstehende Datenmodell sieht wie in Abbildung 2.5 aus.
Abbildung 2.5: Manchmal führt Atomatisieren zu m:n-Beziehungen.
Um Daten wie aus der Tabelle tblArtikel_1 in Abbildung 2.4 in die Tabellen einer solchen m:n-Beziehung zu überführen, verwenden Sie etwa den Code aus folgendem Listing. Public Sub AtomizeIntoMNRelation() Dim Dim Dim Dim Dim
db As DAO.Database rstArtikel As DAO.Recordset rstLieferanten As DAO.Recordset strLieferant As String fld As DAO.Field
Normalisierung
55
Dim lngLieferantID As Long Set db = CurrentDb Set rstArtikel = db.OpenRecordset("tblArtikel_1", dbOpenDynaset) 'Alle Datensätze der Datensatzgruppe rstArtikel durchlaufen Do While Not rstArtikel.EOF 'Für alle Felder der Tabelle... For Each fld In rstArtikel.Fields '...kontrolliere, ob der Name mit 'Lieferant' beginnt If Left(fld.Name, 9) = "Lieferant" _ And Not IsNull(fld.Value) Then 'Prüfen, ob schon ein Lieferant mit diesem Namen 'vorhanden ist strLieferant = fld.Value Set rstLieferanten = db.OpenRecordset _ ("SELECT * FROM tblLieferanten WHERE Lieferant = '" _ & DoubleQuotes(strLieferant) & "'", dbOpenDynaset) 'Wenn noch nicht vorhanden, Lieferant 'in tblLieferanten anlegen If rstLieferanten.RecordCount = 0 Then rstLieferanten.AddNew rstLieferanten!Lieferant = strLieferant 'LieferantID merken lngLieferantID = rstLieferanten!LieferantID rstLieferanten.Update Else lngLieferantID = rstLieferanten!LieferantID End If End If 'Neuen Datensatz in Verknüpfungstabelle anlegen db.Execute "INSERT INTO tblArtikelLieferanten(ArtikelID, " _ & "LieferantID) VALUES(" & rstArtikel!ArtikelID _ & ", " & lngLieferantID & ")" Next fld rstArtikel.MoveNext Loop End Sub Listing 2.2: Aufbrechen nicht atomarer Informationen in eine m:n-Beziehung
56
2
Tabellen und Datenmodellierung
Das Resultat dieser Prozedur für die Daten aus der in Abbildung 2.4 gezeigten Tabelle finden Sie in Abbildung 2.6. Die Prozedur durchläuft alle Datensätze der ursprünglichen Tabelle und unterzieht die Inhalte aller Felder, deren Feldname mit Lieferant beginnt (also Lieferant1, Lieferant2, …) und deren Feldinhalt nicht leer ist, einer gesonderten Behandlung: Zunächst wird überprüft, ob die Tabelle tblLieferanten bereits einen Lieferanten mit dem angegebenen Namen enthält. Falls nein, wird ein entsprechender Datensatz in dieser Tabelle angelegt. Der Wert des Feldes LieferantID wird in jedem Fall festgehalten, um in einer abschließenden Aktionsabfrage einen neuen Datensatz in der Verknüpfungstabelle tblArtikelLieferanten anzulegen. Fast für jede Vorgehensweise gibt es Alternativen. Ein passendes Beispiel für das Anfügen verknüpfter Daten finden Sie in Abschnitt 11.3.3 des Kapitels 11, »Fehlerbehandlung«.
Abbildung 2.6: Diese Daten entsprechen der ersten Normalform.
2.2.2 Die zweite Normalform Die zweite Normalform besagt, dass alle Felder einer Tabelle vom Primärschlüssel beziehungsweise vom ganzen Primärschlüssel abhängig sein müssen. »Ganzer« Primärschlüssel bezieht sich auf Tabellen mit mehreren Primärschlüsseln – das hört sich im ersten Augenblick unlogisch an, aber folgendes Beispiel wird verdeutlichen, wie das gemeint ist. Im Beispiel aus Abbildung 2.7 verwaltet jemand Kunden und Projekte in einer ExcelTabelle. Jeder Datensatz dieser Tabelle ist durch die Kombination der Felder KundeID und ProjektID eindeutig identifizierbar.
Normalisierung
57
Wenn man die Felder der Tabelle auf ihre Abhängigkeit vom Primärschlüssel untersucht, stellt man schnell fest, dass nicht alle vom »ganzen«, also zusammengesetzten Primärschlüssel, abhängen. Die Kundendaten sind zwar vom Primärschlüsselfeld KundeID abhängig, aber nicht vom Feld ProjektID. Dadurch kann derselbe Kunde mehrmals in der Tabelle auftreten. Der in Abbildung 2.7 dargestellte Zustand heißt Redundanz. Unter diesen Bedingungen ist die Konsistenz der Daten gefährdet. Sobald man Informationen zu einem Kunden nur in einem der Datensätze ändert, ist die Konsistenz dahin und die Integrität der Daten verloren.
Abbildung 2.7: Tabelle mit zwei Primärschlüsseln
Wie leicht aus einer Tabelle mit redundanten Daten Inkonsistenzen entstehen können, zeigt Abbildung 2.8. Hier wurde in einem neuen Datensatz der Name des Kunden falsch geschrieben. Die Inkonsistenz würde sich hier bemerkbar machen, wenn man alle Projekte nach dem Kunden »Addison-Wesley« filtert. Der letzte Eintrag mit dem Kundennamen ohne Bindestrich würde nicht angezeigt.
Abbildung 2.8: Tabelle mit inkonsistenten Daten
58
2
Tabellen und Datenmodellierung
Überführung der Daten in die zweite Normalform Die Gefahr von Redundanzen und Inkonsistenzen lässt sich beheben, indem Sie die Daten so auf mehrere Tabellen aufteilen, dass alle Felder der Tabellen vom jeweiligen Primärschlüsselfeld beziehungsweise vom aus mehreren Feldern zusammengesetzten Primärschlüssel abhängen. Im obigen Beispiel gibt es zwei Primärschlüssel – KundeID und ProjektID –, die beide abhängige Felder aufweisen. Die Notwendigkeit einer zweiten Tabelle ist offensichtlich. Die beiden Tabellen sehen wie in Abbildung 2.9 aus.
Abbildung 2.9: Tabellen mit ausschließlich vom Primärschlüssel abhängigen Feldern
Jetzt fehlt allerdings die Information, welches Projekt zu welchem Kunden gehört. Da in der Regel jedes Projekt nur für einen Kunden durchgeführt wird, halten Sie diese Information in der Tabelle tblProjekte fest, indem Sie per Fremdschlüsselfeld auf den jeweiligen Primärschlüssel der Tabelle tblKunden verweisen. Abbildung 2.10 zeigt die Tabelle tblProjekte mit dem neuen Fremdschlüsselfeld, das mit dem Primärschlüsselfeld der Tabelle tblKunden verknüpft ist.
Abbildung 2.10: Beziehung zwischen den Tabellen tblKunden und tblProjekte
Normalisierung
59
2.2.3 Die dritte Normalform Die dritte Normalform sorgt dafür, dass es keine transitiven Abhängigkeiten innerhalb einer Tabelle gibt. Alle Nicht-Schlüssel-Felder müssen direkt vom Primärschlüssel der Tabelle abhängig sein. Oder andersherum: Es darf kein Feld geben, das Detailinformationen über ein anderes Feld enthält. Um sicherzugehen, dass eine Tabelle der dritten Normalform entspricht, prüfen Sie, ob Sie die Daten aller Felder mit Ausnahme des Primärschlüssels einzeln ändern können, ohne dass ein weiteres Feld in dieser Tabelle davon betroffen ist. Beispiel: Die Tabelle aus Abbildung 2.11 enthält neben dem Primärschlüsselfeld einige Felder, die von diesem abhängig sind. Im Falle des Feldes Verkaufsleiter besteht allerdings nur eine indirekte Abhängigkeit, da der Verkaufsleiter zunächst vom Feld Hersteller abhängt.
Abbildung 2.11: Diese Tabelle beruht nicht auf der dritten Normalform.
Lösung: Der Hersteller samt Verkaufsleiter wird in eine eigene Tabelle ausquartiert. Das Ergebnis sieht wie im Datenmodell aus Abbildung 2.12 aus.
Abbildung 2.12: Die Tabelle aus Abbildung 2.11 in der dritten Normalform
60
2
Tabellen und Datenmodellierung
Felder mit berechneten Werten widersprechen der dritten Normalform Eine schlechte Angewohnheit und nur in ganz wenigen Fällen sinnvoll ist das Speichern von berechneten Werten in einer Tabelle wie in folgendem Beispiel (siehe Abbildung 2.13). Hier ist der Bruttopreis das Produkt aus Einzelpreis und Mehrwertsteuer. Solch ein Tabellenentwurf ist sehr anfällig für Inkonsistenzen. Wenn Sie nur einen der drei Werte ändern, ohne die Abhängigkeit zu berücksichtigen, stimmt die Berechnung nicht mehr.
Abbildung 2.13: Der Bruttopreis berechnet sich aus dem Einzelpreis und der Mehrwertsteuer.
Die Änderung des Datenmodells fällt hier vergleichsweise einfach aus: Entfernen Sie das Feld Bruttopreis und verwenden Sie zu dessen Ermittlung eine Abfrage wie die folgende (siehe Abbildung 2.14). Das aus der Tabelle entfernte Feld wird durch ein in der Abfrage berechnetes Feld ersetzt. Sie brauchen lediglich das Format des berechneten Feldes auf Währung einzustellen, um ein mit der Ursprungstabelle identisches Ergebnis zu erhalten.
2.2.4 Weitere Normalformen Die übrigen Normalformen sind eher akademischer Natur und werden deshalb hier nicht behandelt.
2.2.5 Das richtige Maß treffen Wenn man die Normalisierung aus Sicht der Performance betrachtet, verhält es sich so wie mit der Verwendung von Indizes auf Tabellenfeldern (mehr dazu in Kapitel 12, »Performance«) – manchmal ist weniger mehr. Je mehr Tabellen und Beziehungen in einer Abfrage referenziert werden, desto langsamer wird die Abfrage. Patentrezepte für das richtige Maß an Normalisierung gibt es nicht. Es gibt aber für die meisten Fälle bereits in der Praxis erprobte Datenmodelle, nach denen Sie sich bei der Modellierung der eigenen Datenbank richten können.
Integritätsregeln
61
Abbildung 2.14: Realisieren eines berechneten Feldes per Abfrage
2.3 Integritätsregeln Mit Integritätsregeln sorgen Sie dafür, dass die Tabellen einer Datenbank nur die für den jeweiligen Anwendungszweck gültigen Werte enthalten. Es gibt eine Menge unterschiedliche Arten von Integritätsregeln, die entsprechend auf unterschiedlichste Weise umgesetzt werden.
2.3.1 Integrität der Werte (Wertbereichsintegrität) Mit dem Datentyp eines Feldes schränken Sie die Menge der möglichen Eingaben schon relativ weit ein. Weitere Möglichkeiten bestehen in der Verwendung von Feldeigenschaften wie beispielsweise Gültigkeitsregel. Damit geben Sie eine Regel an, mit der die Gültigkeit der vorhandenen Daten geprüft wird. Dabei lassen sich durchaus flexible Ausdrücke eingeben. Beispiel: Es sollen keine Personen aufgenommen werden, deren Alter unter 18 Jahren liegt. Dazu stellen Sie die Eigenschaft Gültigkeitsprüfung auf den folgenden Ausdruck ein: <=DatAdd("jjjj";-18;Datum())
62
2
Tabellen und Datenmodellierung
Verwenden Sie außerdem die Eigenschaft Gültigkeitsmeldung, damit Access bei Nichteinhalten der Regel eine entsprechende Meldung ausgibt (siehe Abbildung 2.15).
Abbildung 2.15: Anwendung einer Gültigkeitsregel
Auch die beiden Eigenschaften Eingabe erforderlich und Leere Zeichenfolge sorgen für die Integrität der eingegebenen Werte. Die beiden Werte kombiniert liefern unterschiedliche Anforderungen an die Werte für ein Feld. Interessant zum Einschränken des Wertebereichs sind die Kombinationen aus Tabelle 2.1:. Eingabe erforderlich
Leere Zeichenfolge
Resultat
Ja
Nein
Wert darf nicht NULL sein.
Ja
Ja
Wert kann eine leere Zeichenfolge sein.
Tabelle 2.1: Relevante Kombinationen der Eigenschaften »Eingabe erforderlich« und »Leere Zeichenfolge«
2.3.2 Format der Werte (semantische Integrität) Im Tabellenentwurf können Sie für jedes Feld ein Format vorgeben, das bei der Eingabe eingehalten werden muss. Dies ist hilfreich, wenn die Werte ein bestimmtes Format haben müssen – etwa wenn eine Postleitzahl mit führender Länderkennung und mit Bindestrich angegeben werden muss (beispielsweise D-47137). In diesem Fall verwenden Sie folgenden Ausdruck für die Eigenschaft Eingabeformat: >L\-00009
Integritätsregeln
63
Das Größer-Zeichen (>) sorgt dafür, dass alle enthaltenen Buchstaben automatisch groß gesetzt werden. Der Backslash signalisiert ein nachfolgendes Literal, die vier Nullen sind Pflichtzahlen und die Neun kennzeichnet eine freiwillige Zahl. Alle möglichen Zeichen und ihre Beschreibung finden Sie in Tabelle 2.2 (Quelle: Microsoft Access 2003, Online-Hilfe). Zeichen
Beschreibung
0
Ziffer (0 bis 9, Eingabe erforderlich, Plus- [+] und Minuszeichen [–] sind nicht erlaubt).
9
Ziffer oder Leerzeichen (Eingabe nicht erforderlich, Plus- und Minuszeichen sind nicht erlaubt).
#
Ziffer oder Leerzeichen (Eingabe nicht erforderlich, Leerzeichen werden als Leerzeichen im Bearbeitungsmodus angezeigt, aber beim Speichern der Daten entfernt, Plus- und Minuszeichen sind erlaubt).
L
Buchstabe (A bis Z, Eingabe erforderlich).
?
Buchstabe (A bis Z, Eingabe optional).
A
Buchstabe oder Ziffer (Eingabe erforderlich).
a
Buchstabe oder Ziffer (Eingabe nicht erforderlich).
&
Ein beliebiges Zeichen oder ein Leerzeichen (Eingabe erforderlich).
C
Ein beliebiges Zeichen oder ein Leerzeichen (Eingabe nicht erforderlich).
.,:;–/
Platzhalter für Dezimaltrennzeichen sowie Tausender-, Datums- und Zeit-Trennzeichen (das tatsächlich verwendete Zeichen hängt von den Einstellungen im Dialogfeld Eigenschaften von Ländereinstellungen in der Systemsteuerung von Windows ab).
<
Alle Buchstaben werden in Kleinbuchstaben umgewandelt.
>
Alle Buchstaben werden in Großbuchstaben umgewandelt.
!
Bewirkt, dass die Anzeige im Eingabeformat von rechts nach links anstatt von links nach rechts erfolgt. Eingegebene Zeichen füllen das Eingabeformat immer von links nach rechts aus. Sie können das Ausrufezeichen-Symbol an jeder beliebigen Stelle im Eingabeformat einfügen.
\
Das folgende Zeichen wird als Literal angezeigt, die Wirkung als Sonderzeichen wird ggf. dadurch aufgehoben (zum Beispiel wird \A als A angezeigt).
Tabelle 2.2: Beschreibung der Zeichen für Eingabeformate
2.3.3 Abhängigkeit von Feldinhalten (Attributintegrität) Sie können bei der Datendefinition auch festlegen, dass bestimmte Abhängigkeiten zwischen den Inhalten verschiedener Felder einzuhalten sind. Wenn Sie etwa sicherstellen möchten, dass der maximale Rabatt eines Artikels vom Preis abhängt, verwenden Sie die Eigenschaften Gültigkeitsregel und Gültigkeitsmeldung der Tabelle selbst.
64
2
Tabellen und Datenmodellierung
Das Beispiel aus Abbildung 2.16 sorgt dafür, dass für Preise kleiner als EUR 1.000,– nicht mehr als fünf Prozent Rabatt gewährt werden dürfen.
Abbildung 2.16: Gültigkeitsregel für abhängige Feldinhalte
2.3.4 Eindeutige Datensätze (Entitätsintegrität) Weiter oben haben Sie bereits erfahren, dass jeder Datensatz einer Tabelle eindeutig sein sollte – dafür sorgen Sie unter Access im einfachsten Fall durch die Verwendung eines Primärschlüsselfeldes. Damit Sie sich keine Sorgen um die Auswahl eindeutiger Werte machen müssen, gibt es in Access den Datentyp Autowert. Neue Werte lassen sich entweder inkrementell oder per Zufall generieren – für die entsprechende Einstellung ist die Eigenschaft Neue Werte zuständig (siehe Abbildung 2.17). Üblich ist bei einem Autowert die Verwendung inkrementeller neuer Werte.
2.3.5 Referentielle Integrität Referentielle Integrität sorgt für die Integrität der Beziehungen zwischen den Datensätzen verknüpfter Tabellen. Damit sorgen Sie beispielsweise dafür, dass der Benutzer kein Projekt anlegen kann, ohne einen Kunden ausgewählt zu haben.
Integritätsregeln
65
Abbildung 2.17: Inkrementelle oder zufällige neue Werte?
Die Erstellung einer Beziehung mit referentieller Integrität erfordert, dass eines der beteiligten Felder das Primärschlüsselfeld seiner Tabelle oder zumindest ein eindeutiger Index ist, dass beide Felder kompatible Datentypen aufweisen und dass die beiden beteiligten Tabellen sich in der gleichen Datenbank befinden. Durch die Definition referentieller Integrität stellen Sie sicher, dass die in den Tabellen enthaltenen Daten in folgenden Punkten konsistent sind: Es gibt zu jedem Datensatz der Detailtabelle einen passenden Datensatz in der Mastertabelle. Datensätze der Mastertabelle, die mit mindestens einem Datensatz der Detailtabelle verknüpft sind, können standardmäßig nicht gelöscht werden. Access sorgt dafür, dass diese Grundsätze eingehalten werden, und gibt bei Verletzung dieser Regeln eine entsprechende Meldung aus. Optional lassen sich unter Access noch zwei weitere Automatismen einrichten: Löschweitergabe: Wenn ein Datensatz der Mastertabelle gelöscht wird, werden automatisch alle verknüpften Datensätze der Detailtabelle gelöscht. Aktualisierungsweitergabe: Wenn das Verknüpfungsfeld der Mastertabelle geändert wird, ändert Access automatisch das Verknüpfungsfeld aller Datensätze der Detailtabelle, die mit diesem Datensatz verknüpft sind.
66
2
Tabellen und Datenmodellierung
Referentielle Integrität legen Sie im Beziehungen-Fenster der Datenbank-Anwendung fest. Das Fenster zeigt die benötigten Tabellen und eventuell bereits bestehende Beziehungen an. Um für eine Beziehung referentielle Integrität zu definieren, klicken Sie auf den Beziehungspfeil zwischen den beteiligten Tabellen und wählen per Kontextmenü den Eintrag Beziehung bearbeiten… aus. Dort finden Sie die an der Beziehung beteiligten Felder, können referentielle Integrität definieren und Aktualisierungs- und Löschweitergabe festlegen. Außerdem finden Sie hier die Angabe des Beziehungstyps, der sich aus der Art der verknüpften Felder ergibt, und die Möglichkeit, den Verknüpfungstyp anzupassen (siehe Abbildung 2.18). Zu all diesen Optionen erfahren Sie weiter unten mehr.
Abbildung 2.18: Bearbeiten einer Beziehung zwischen zwei Tabellen
2.4 Beziehungen Mit Beziehungen legen Sie die Verknüpfungen zwischen den Tabellen einer Datenbank fest. Sie sind das A und O bei relationalen Datenbanken, denn Sie können damit nicht nur festlegen, welche Felder der einen Tabelle mit den entsprechenden Feldern der anderen Tabelle verknüpft sind. Access bietet die Möglichkeit, etwa referentielle Integrität zu definieren und dabei unterschiedliche Eigenschaften für die Beziehung festzulegen.
Beziehungen
67
Voraussetzung für das Erstellen einer Beziehung ist, dass mindestens eines der Felder das Primärschlüsselfeld seiner Tabelle ist. Die Tabelle mit dem Primärschlüsselfeld spielt die Rolle der Mastertabelle (auch Parent-Tabelle genannt) der Beziehung. Das Verknüpfungsfeld der anderen Tabelle heißt Fremdschlüsselfeld, die Tabelle mit dem Fremdschlüsselfeld ist die Detailtabelle (auch Child-Tabelle genannt). Eigentlich sollte man meinen, dass die Tabelle mit dem Fremdschlüsselfeld die Mastertabelle sei und über die per Fremdschlüsselfeld referenzierte Tabelle Details enthielte und dementsprechend Detailtabelle hieße. Das erscheint zumindest bei jenen verknüpften Tabellen sinnvoll, die im Rahmen der Normalisierung ausgegliederte Informationen enthalten – also beispielsweise Anreden, Geschlecht oder Titel. Tatsächlich ist es aber umgekehrt – die Tabelle mit dem Primärschlüsselfeld in der Beziehung heißt Master- und die mit dem Fremdschlüsselfeld Detailtabelle. Beziehungen werden im Beziehungsfenster abgebildet und können dort auch erzeugt und bearbeitet werden. Es gibt aber auch die Möglichkeit, Beziehungen durch einen Assistenten erstellen zu lassen.
2.4.1 Benennen von Primär- und Fremdschlüsselfeldern Einen Vorschlag für die Namen von Primärschlüsselfeldern haben Sie bereits weiter oben kennen gelernt – demnach soll das Primärschlüsselfeld aus dem Singular der Bezeichnung des mit den Tabellenfeldern beschriebenen Objekts plus der angehängten Zeichenkette »ID« bestehen – also etwa »ProjektID«, »MitarbeiterID«, »KundeID« oder »ArtikelID«. Das Fremdschlüsselfeld einer Tabelle enthält einen Wert, der dem Wert des Primärschlüsselfeldes der zu verknüpfenden Tabelle entspricht – deshalb sollten Sie es auch genauso nennen. Ein klassisches Beispiel sind Projekte und Kunden (siehe Abbildung 2.19). Die Projekte-Tabelle enthält hier ein Fremdschlüsselfeld, für das man den Wert des Primärschlüsselfeldes eines in der Tabelle tblKunden enthaltenen Datensatzes eintragen kann.
Abbildung 2.19: Beziehung zwischen Kunden und Projekten
68
2
Tabellen und Datenmodellierung
Natürlich gibt es auch hier Ausnahmen – beispielsweise kommt es vor, dass eine Detailtabelle zwei Fremdschlüsselfelder enthält, die auf das gleiche Feld der Mastertabelle verweisen. Ein gutes Beispiel ist die Beziehung zwischen Artikeln und Firmen: Dort kann der eine Eintrag der Firmentabelle als Lieferant herhalten, während die andere Firma der Hersteller des Artikels ist (siehe Abbildung 2.20). HerstellerID und LieferantID scheinen hier adäquate Bezeichnungen zu sein, genauer wären allerdings HerstellerFirmaID und LieferantFirmaID – auf diese Weise ließe sich deutlicher erkennen, wohin die Verknüpfung geht.
Abbildung 2.20: Zwei Beziehungen zu der gleichen Tabelle erfordern unterschiedliche Fremdschlüsselnamen
Die Beziehung aus Abbildung 2.20 zeigt das Beziehungen-Fenster übrigens nicht automatisch an; nachdem Sie die Artikel-Tabelle einmal und die Firmen-Tabelle zweimal in diese Ansicht eingefügt haben, wird lediglich eine der beiden Beziehungen angezeigt. Die zweite fügen Sie zu Fuß hinzu, indem Sie das Feld LieferantID auf das Feld FirmaID der Tabelle tblFirmen_1 ziehen. Die zweite Inkarnation der Firmen-Tabelle wird im Beziehungen-Fenster nur mit anderem Namen angezeigt, um Verwechslungen zu vermeiden – tatsächlich existiert nur eine Tabelle namens tblFirmen in der Datenbank.
2.4.2 Halbautomatisches Festlegen von Beziehungen Es gibt eine Möglichkeit, Access dazu zu bringen, automatisch eine Beziehung zwischen zwei Tabellen festzulegen. Das ist immer dann der Fall, wenn Sie mit dem Nachschlage-Assistenten eine Verknüpfung zwischen zwei Tabellen erstellen. Wenn Sie beispielsweise eine Kunden- und eine Projekte-Tabelle miteinander verknüpfen und dabei das Feld KundeID für die Auswahl des dem Projekt zugeordneten Kunden verwendet werden soll, gehen Sie folgendermaßen vor: 1. Öffnen Sie die Tabelle tblKunden in der Entwurfsansicht. 2. Wählen Sie für das Feld, über das die Beziehung hergestellt werden soll, den Datentyp Nachschlage-Assistent aus. Damit öffnen Sie den Nachschlage-Assistenten.
Beziehungen
69
3. Zum Herstellen der Beziehung zu einer anderen Tabelle wählen Sie im ersten Schritt die erste Option aus: Das Nachschlagefeld soll die Werte einer Tabelle oder Abfrage entnehmen. 4. Wählen Sie im nächsten Schritt die Tabelle aus, mit der Sie das Feld verknüpfen möchten – in diesem Fall die Tabelle tblKunden. 5. Im folgenden Schritt legen Sie fest, welche Felder der verknüpften Tabelle im Nachschlagefeld angezeigt werden sollen. Normalerweise wählt man dort den Primärindex der Tabelle sowie ein Feld, dessen Inhalt den enthaltenen Datensatz am besten charakterisiert. Hier ist das der Name des Kunden, der im Feld Kunde gespeichert wird. Diese Einstellung können Sie später problemlos ändern. 6. In den letzten drei Schritten können Sie noch eine Sortierung festlegen, angeben, ob die Spalte mit dem Primärschlüsselwert ausgeblendet werden soll, und einen Namen für das Fremdschlüsselfeld eingeben. Nach der Eingabe der benötigten Informationen nimmt der Assistent folgende Änderungen am Fremdschlüsselfeld der Tabelle vor (siehe Abbildung 2.21): Ändern der Eigenschaft Steuerelement anzeigen auf den Eintrag Kombinationsfeld Einstellen einer Datensatzherkunft für dieses Steuerelement, hier: SELECT tblKunden.KundeID, tblKunden.Kunde FROM tblKunden;
Einstellen der Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 2 und 0cm;2,54cm. Dadurch wird nur die zweite Spalte der Datensatzherkunft angezeigt, während das in der Eigenschaft Gebundene Spalte angegebene erste Feld unsichtbar bleibt. Außerdem fügt der Assistent die Beziehung zwischen dem Fremdschlüsselfeld der bearbeiteten Tabelle und dem Primärschlüsselfeld der verknüpften Tabelle hinzu (siehe Abbildung 2.22).
2.4.3 Festlegen referentieller Integrität Referentielle Integrität für die Beziehung zwischen zwei Tabellen können Sie ebenfalls im Beziehungen-Fenster festlegen. Dazu markieren Sie die Verbindungslinie zwischen den beteiligten Tabellen und wählen aus dem Kontextmenü den Eintrag Beziehung bearbeiten… aus. Alternativ können Sie auch einfach doppelt auf den Beziehungspfeil klicken. Im nun erscheinenden Dialog Beziehungen bearbeiten aktivieren Sie mindestens die Option Mit referentieller Integrität.
70
2
Tabellen und Datenmodellierung
Abbildung 2.21: Der Nachschlage-Assistent ändert einige Eigenschaften eines Tabellenfeldes und der Tabelle.
Abbildung 2.22: Diese Verknüpfung wurde durch den Nachschlage-Assistenten erstellt.
Falls Tabellen, für deren Verknüpfung referentielle Integrität definiert werden soll, bereits Datensätze enthalten, kann es an dieser Stelle zu einer Fehlermeldung kommen, falls die Daten nicht der referentiellen Integrität entsprechen. Entweder Sie leben damit und passen anschließend eventuell zu Null geänderte Inhalte von Fremdschlüsselfeldern an oder kümmern sich vorher um die Integrität der Daten. Die Option Aktualisierungsweitergabe an verwandte Felder sorgt dafür, dass Änderungen am Primärschlüssel der Mastertabelle der Beziehung auf das Fremdschlüsselfeld der Detailtabelle übertragen werden.
Beziehungen
71
Mit der Option Löschweitergabe an verwandte Datensätze legen Sie fest, dass beim Löschen eines Datensatzes der Mastertabelle auch alle Datensätze der Detailtabelle gelöscht werden. Im Beispiel Kunden und Projekte bedeutet das Folgendes: Wenn Sie einen Kunden löschen, entfernt Access auch alle dazugehörenden Projekte.
Abbildung 2.23: Festlegen referentieller Integrität per Dialog
2.4.4 1:n-Beziehungen Die 1:n-Beziehung ist die Mutter aller Beziehungen. Alle weiteren Beziehungstypen sind Sonderfälle der 1:n-Beziehung, wie Sie weiter unten erfahren werden. Deshalb ist es auch kein Zufall, dass gerade dieser Beziehungstyp für die einführenden Abschnitte zum Thema Beziehungen ausgewählt wurde. Da Sie dort bereits die wichtigsten Grundlagen zu diesem Thema kennen gelernt haben, werden in diesem Abschnitt lediglich einige Beispiele für den Einsatz von 1:n-Beziehungen vorgestellt. In anderen Büchern, Fachbeiträgen oder im Internet stoßen Sie vielleicht auch auf die Bezeichnung n:1-Beziehung. Prinzipiell ist daran nichts auszusetzen, da hier lediglich die Reihenfolge vertauscht wurde. In der in diesem Buch verwendeten Terminologie macht das allerdings sehr wohl einen Unterschied: Hier ist eine 1:nBeziehung die Beziehung zwischen zwei Tabellen, die Eigenschaften eines Objekts enthalten – also beispielsweise Kunden, Projekte, Artikel, Produkte, Unternehmen oder Fahrzeuge. Wenn Sie eine Beziehung zwischen zwei solchen Tabellen herstellen, heißt sie in diesem Buch 1:n-Beziehung. Die nachfolgend vorgestellten n:1Beziehungen verknüpfen eine der eben genannten Tabellen mit ausgegliederten Daten wie Anrede, Geschlecht, Titel bei Adressen, Kategorie bei Artikeln oder Fahrzeugart bei Fahrzeugen. Eine andere nachfolgend verwendete Bezeichnung für diese Tabellen ist Lookup-Tabelle.
72
2
Tabellen und Datenmodellierung
Beispiele für 1:n-Beziehungen Im Folgenden einige Beispiele für 1:n-Beziehungen: Unternehmen und Personen als Ansprechpartner Projekte und Kunden Projekte und Mitarbeiter als Projektleiter Artikel und Firmen als Lieferanten Artikel und Firmen als Hersteller Diese Liste ließe sich beliebig fortsetzen. Weitere Beispiele finden Sie weiter unten in Abschnitt 2.6, »Datenmodell-Muster«.
2.4.5 n:1-Beziehungen oder Lookup-Beziehungen n:1-Beziehungen verknüpfen Tabellen, deren Eigenschaften Objekte beschreiben, mit jenen, die ausgegliederte Eigenschaften dieses Objekts enthalten. Wenn Sie sich etwa eine Adresstabelle vorstellen, die ein Textfeld zur Angabe der Anrede enthält, würden Sie vermutlich ungern zu jedem Eintrag »Herr« oder »Frau« manuell hinzufügen wollen. Statt dessen finden Sie in der Regel ein Kombinationsfeld vor, mit dem sich der gewünschte Eintrag auswählen lässt. Das hat zwei entscheidende Vorteile: Erstens sparen Sie Tipparbeit bei der Eingabe dieses Feldes und zweitens sorgen Sie so dafür, dass nur die für das Feld vorgesehenen Einträge eingegeben werden können. Gleiches gilt für die Angabe des Geschlechts: In jeden Datensatz »männlich« oder »weiblich« einzutragen, ist eine sehr mühselige Arbeit, die Auswahl der Werte aber durchaus zumutbar. Bei der manuellen Eingabe dürfte sich außerdem früher oder später ein Tippfehler einschleichen, den Sie mit der Auswahlmöglichkeit ausschließen. Und diese Tippfehler können sich durchaus auswirken: Wenn Sie beispielsweise alle Datensätze der Tabelle ausgeben möchten, in denen das Feld Geschlecht den Wert »weiblich« hat, oder sich nur die Anzahl dieser Felder anzeigen lassen, sorgt eine einzige unrichtige Schreibweise für ein falsches Ergebnis. Daher sollten Sie immer, wenn ein Feld häufig den gleichen Wert annimmt, ein Kombinationsfeld zur Eingabe der Daten in Erwägung ziehen, das aus den Daten einer verknüpften Tabelle gefüttert wird. Es gibt auch die Möglichkeit, in der Felddefinition eine Wertliste als Datensatzherkunft für ein Kombinationsfeld anzugeben. Dazu stellen Sie die in Abbildung 2.24 abgebildeten Eigenschaften ein.
Beziehungen
73
Abbildung 2.24: Kombinationsfeldeinträge per Wertliste
Ausgliedern eines Feldes in eine separate Tabelle Folgendes Beispiel zeigt, wie Sie ein Feld aus einer Tabelle in eine neue Tabelle ausgliedern. Ausgangspunkt ist die Tabelle aus Abbildung 2.25, die das Feld Anrede für die manuelle Eingabe bereitstellt.
Abbildung 2.25: Adressentabelle mit »hart codierten« Anreden
Um das Feld auszugliedern, legen Sie zunächst eine neue Tabelle namens tblAnreden mit den beiden Feldern AnredeID und Anrede an. Legen Sie außerdem in der Tabelle tblAdressen ein neues Feld namens AnredeID mit dem Datentyp Zahl zum Herstellen der Verknüpfung an. Nun brauchen Sie nur noch die verschiedenen Einträge des Feldes Anrede der Tabelle tblAdressen in die Tabelle tblAnreden zu übertragen und die dort verwendeten Werte für das Feld AnredeID in das gleichnamige neue Feld der Tabelle tblAdressen einzutragen.
74
2
Tabellen und Datenmodellierung
Keine Frage, dass Sie das für wenige Datensätze von Hand erledigen können, aber wenn die Adressen-Tabelle mehrere hundert Datensätze enthält, verwenden Sie vielleicht besser zwei Aktionsabfragen oder die folgende VBA-Prozedur. Der Aufruf für den hier vorliegenden Fall lautet folgendermaßen: FeldAusgliedern "tblAdressen", "tblAnreden", "AnredeID", "Anrede"
Die Prozedur erwartet den Namen der Ausgangstabelle und der Zieltabelle sowie den Namen des Primärschlüsselfeldes der Zieltabelle und des Zielfeldes der auszugliedernden Daten. Diese beiden Felder heißen hier AnredeID und Anrede und müssen in der Ziel- und in der Ausgangstabelle gleich benannt sein: Public Sub FeldAusgliedern(strAusgangstabelle As String, strZieltabelle _ As String, strSchluesselfeld As String, strFeldname As String) Dim db As DAO.Database Dim rst As DAO.Recordset Dim lngSchluesselfeld As String Set db = CurrentDb Set rst = db.OpenRecordset(strAusgangstabelle, dbOpenDynaset) Do While Not rst.EOF 'Ermitteln, ob Eintrag schon in Lookuptabelle vorhanden ist lngSchluesselfeld = Nz(DLookup(strSchluesselfeld, strZieltabelle, _ strFeldname & " = '" & rst(strFeldname) & "'"), 0) 'Falls nicht, diesen Eintrag hinzufügen... If lngSchluesselfeld = 0 Then db.Execute "INSERT INTO " & strZieltabelle & "(" & strFeldname _ & ") VALUES('" & rst(strFeldname) & "')" '...und Primärschlüssel ermitteln lngSchluesselfeld = Nz(DLookup(strSchluesselfeld, _ strZieltabelle, strFeldname & " = '" & rst(strFeldname) _ & "'"), 0) End If 'Verweisfeld auf Lookuptabelle mit Wert füllen rst.Edit rst(strSchluesselfeld) = lngSchluesselfeld rst.Update rst.MoveNext Loop
Beziehungen
75
Set rst = Nothing Set db = Nothing End Sub Listing 2.3: Ausgliedern von Daten in eine Lookup-Tabelle
Abbildung 2.26 zeigt das Ergebnis der Ausgliederung. Die im Feld Anrede vorhandenen Werte wurden in die Tabelle tblAnreden eingetragen und die Werte des dortigen Primärschlüsselfeldes in das neue Fremdschlüsselfeld AnredeID der Tabelle tblAdressen. Zur Kontrolle ist das alte Feld Anrede noch in der Ausgangstabelle vorhanden, dieses können Sie aber ohne Bedenken löschen.
Abbildung 2.26: Ergebnis der Ausgliederung eines Feldes in eine zusätzliche Tabelle
Damit Sie die Werte auch per Kombinationsfeld aus der Lookup-Tabelle auswählen können, legen Sie mit dem Nachschlage-Assistenten eine Beziehung zwischen den beiden Tabellen an. Wie das funktioniert, haben Sie bereits in Abschnitt 2.4.2, »Halbautomatisches Festlegen von Beziehungen« erfahren.
2.4.6 m:n-Beziehungen m:n-Beziehungen sind nichts weiter als zwei 1:n-Beziehungen, die zwei Tabellen über eine Hilfstabelle miteinander verknüpfen. Im Gegensatz zu einer einzelnen 1:n-Beziehung, mit der sich beliebig viele Datensätze der einen Mastertabelle mit einem Datensatz der Detailtabelle verknüpfen lassen, ist das Ziel der m:n-Beziehung, dass sich jeder Datensatz der ersten Tabelle mit beliebig vielen Datensätzen der zweiten Tabelle verknüpfen lässt und umgekehrt.
76
2
Tabellen und Datenmodellierung
Beispiele für solche Beziehungen gibt es viele. Das bekannteste ist wohl die Verknüpfung der Bestellungen-Tabelle mit der Artikel-Tabelle über eine Bestelldetails-Tabelle wie in der Nordwind-Datenbank (siehe Abbildung 2.27).
Abbildung 2.27: Klassisches Beispiel einer m:n-Beziehung: Bestellungen und Artikel in der Nordwind-Datenbank
Diese Variante ist zugleich eine kompliziertere Form der m:n-Beziehung, die in der Verknüpfungstabelle zusätzliche Daten speichert.
m:n-Beziehung am Beispiel von Fahrzeugen und Sonderausstattungen Ein einfacheres Beispiel sind Ausstattungsmerkmale von Fahrzeugen. Fahrzeuge haben einige unveränderliche Eigenschaften wie Marke, Modell, Leistung und so weiter. Außerdem besitzt jedes Fahrzeug verschiedene Ausstattungsmerkmale, die aber bei dem einen vorhanden und bei dem anderen nicht vorhanden sind. Ein nicht normalisiertes Datenmodell würde aus einer einzigen Tabelle mit einigen Dutzend Ja/Nein-Feldern für die einzelnen Ausstattungsmerkmale bestehen. Das ist legitim, kann aber zu Problemen führen: Zwar ändern sich die gängigen Ausstattungsmerkmale nur alle Jubeljahre, aber sie ändern sich, und damit müssten Sie auch die komplette Datenbank von der Tabelle bis zu den Formularen, Berichten und VBA-Modulen anpassen. Außerdem wird für jede Ausstattung, die nicht vorhanden ist, Speicherplatz verschwendet. Also legen Sie eine Tabelle mit sämtlichen Ausstattungsmerkmalen an und sorgen mit einer Verknüpfungstabelle dafür, dass Sie alle Fahrzeuge mit allen Ausstattungsmerkmalen kombinieren können (siehe Abbildung 2.28). Die Verknüpfungstabelle ist dabei nichts anderes als eine Tabelle mit zwei Fremdschlüsselfeldern, die die beiden Primärschlüsselfelder der zu verknüpfenden Tabellen referenzieren. In der Entwurfsansicht sieht die Verknüpfungstabelle wie in Abbildung 2.29 aus. Dort wird Ihnen vermutlich zuerst auffallen, dass es dort zwei als Primärschlüssel gekennzeichnete Felder gibt. Genau genommen ist dies ein zusammengesetzter Primärschlüssel, der verhindert, dass eine Kombination der beiden Felder zweimal eingegeben wird. Schließlich soll jedes Ausstattungsmerkmal jedem Fahrzeug nur einmal zugewiesen werden.
Beziehungen
77
Abbildung 2.28: m:n-Beziehung am Beispiel von Fahrzeugen und ihrer Ausstattung
Abbildung 2.29: Entwurfsansicht einer m:n-Verknüpfungstabelle
Verknüpfungstabellen mit zusätzlichen Daten: Bestellungen und Artikel Nun können Sie sich der Bestelldetails-Tabelle der Nordwind-Datenbank zuwenden, die bereits weiter oben kurz vorgestellt wurde. Dort befinden sich neben den beiden Fremdschlüsselfeldern zum Herstellen der Beziehung noch weitere Felder zum Speichern von Einzelpreis, Anzahl und Rabatt des jeweiligen Artikels. Dabei handelt es sich um individuelle Daten für jede Kombination aus Bestellung und Artikel. Dass die Anzahl flexibel sein muss, ist klar, aber warum Einzelpreis und Rabatt? Der Einzelpreis ist zwar bereits in der Tabelle Artikel festgelegt, es aber kann durchaus sein, dass der Preis sich einmal ändert. Und wenn Sie diesen dann nur in der Artikel-Tabelle gespeichert haben und ihn dort aktualisieren, dann wirkt sich das auch auf die Rechnungsbeträge aller bisherigen Bestellungen aus. Daher muss der Preis unbedingt in Zusammenhang mit der Kombina-
78
2
Tabellen und Datenmodellierung
tion aus Bestellung und Artikel gespeichert werden. Und der Rabatt ist ohnehin eine Größe, die je nach Kunde oder je nach Angebot flexibel gestaltet wird – daher macht auch die Aufnahme dieses Feldes in die Verknüpfungstabelle Sinn.
Weitere Beispiele für m:n-Beziehungen m:n-Beziehungen treten an vielen Stellen auf. Hier finden Sie zwei weitere Beispiele: Verteiler: Jeder Verteiler basiert auf einer m:n-Beziehung. Die beiden zu verknüpfenden Tabellen enthalten die Publikation auf der einen und die Adressaten auf der anderen Seite. Die Verknüpfungstabelle ist einfach, es sind außer den beiden Fremdschlüsselfeldern keine weiteren Felder notwendig. Projektteams: Jedes Projektteam besteht aus einem oder mehreren Mitarbeitern, und jeder Mitarbeiter gehört zu einem oder mehreren Projektteams. Der Verknüpfungstabelle könnte man eine dritte Verknüpfung hinzufügen, um die Funktion des jeweiligen Teammitglieds festzulegen. Abbildung 2.30 zeigt das Datenmodell dieser Verknüpfung, bei der es sich eigentlich sogar um eine m:n:o-Verknüpfung handelt.
Abbildung 2.30: m:n-Verknüpfung mit dritter Verknüpfung
2.4.7 1:1-Beziehungen 1:1-Beziehungen trifft man relativ selten an, obwohl sie sehr hilfreich sein können. Eine 1:1-Beziehung verknüpft jeden Datensatz mit nur einem Datensatz der verknüpften Tabelle und umgekehrt. Wozu soll das nun hilfreich sein? Solche Daten kann man doch auch in eine Tabelle schreiben? Diese Fragen sind durchaus berechtigt. Deshalb lernen Sie nun die Gründe und Einsatzmöglichkeiten für 1:1-Beziehungen kennen.
Beziehungen
79
Es gibt häufig Fälle, in denen Tabellen aus endlos vielen Feldern bestehen. Das liegt oft daran, dass die Tabellen recht verschiedenartige Daten enthalten sollen, von denen jede Art eigene Eigenschaften besitzt und damit neue Felder erzeugt.
Beispiel: Unterschiedliche Mitarbeiterarten Ein Unternehmen sammelt die Daten aller Mitarbeiter in einer Tabelle und unterscheidet dabei zunächst nicht zwischen fest angestellten und freien Mitarbeitern. Die Tabelle sieht wie in Abbildung 2.31 aus.
Abbildung 2.31: Entwurf einer Tabelle mit teilweise nicht benötigten Daten
Enthält die Tabelle einen Angestellten, wird das Feld Stundensatz nicht benötigt, weil der Angestellte ein Gehalt bekommt. Handelt es sich um einen freien Mitarbeiter, sind die Felder Personalnummer, Gehalt und AbteilungID überflüssig. Eine reale Mitarbeitertabelle enthält sicher noch viele weitere nützliche Felder für den Angestellten und den freien Mitarbeiter, die aber im jeweils anderen Fall nicht benötigt werden. Oder konkret ausgedrückt: Hier wird einiges an Speicherplatz verschenkt.
Tabellen aufteilen und wieder verknüpfen Um dies zu verhindern, trennt man einfach die nur für den Angestellten oder den freien Mitarbeiter vorgesehenen Felder aus der Tabelle heraus und fügt diese in zwei weitere Tabellen namens tblAngestellte und tblFreieMitarbeiter ein. Die Tabellen sehen nun so wie in Abbildung 2.32 aus. Die Tabelle tblMitarbeiter enthält nur noch die Daten, die für beide Mitarbeiterarten gelten. Die Tabelle tblFreieMitarbeiter enthält ein eigenes Primärschlüsselfeld, ein eindeutiges Feld namens PersonID zur Herstellung der 1:1-Beziehung und ein Feld mit den speziellen Informationen zu freien Mitarbeitern – hier den Stundensatz. Die Tabelle tblAngestellte ist genauso aufgebaut, enthält aber die angestellten-spezifischen Informationen.
80
2
Tabellen und Datenmodellierung
Abbildung 2.32: Aufteilung einer Tabelle in eine Haupt- und zwei Untertabellen
Voraussetzung: Eindeutige Schlüsselfelder auf beiden Seiten der Beziehung 1:1-Beziehungen verbinden zwei Tabellen über eindeutige Felder. Primärindexfelder sind eindeutig und auch andere Felder können Sie als eindeutig festlegen. Dazu erstellen Sie einfach einen entsprechenden Index über den Indizes-Dialog. Diesen zeigen Sie an, indem Sie die gewünschte Tabelle in der Entwurfsansicht öffnen und den Menüeintrag Ansicht/Indizes auswählen. Nach dem Öffnen dieses Dialogs werden Sie vermutlich verwundert sein, dass Access nicht nur für das Primärschlüsselfeld, sondern auch noch für einige andere Felder scheinbar willkürlich Indizes angelegt hat (siehe Abbildung 2.33). Die Willkür hält sich aber in Grenzen: Access legt standardmäßig für alle Felder, deren Name eine der Zeichenketten »ID«, »Schlüssel«, »Code« oder »Nummer« enthält, einen Index an. Diese Einstellung können Sie auf der Registerseite Tabellen/Abfragen des Optionen-Dialogs mit der Eigenschaft Autoindex beim Importieren/Erstellen anpassen (siehe Abbildung 2.34). Wenn Sie in beiden Tabellen einen eindeutigen Index für das Feld PersonID angelegt haben, können Sie zum Verknüpfen schreiten. Dazu zeigen Sie wie gewohnt die zu verknüpfenden Tabellen im Beziehungen-Fenster an. Nun kommt der wichtigste Schritt und hier müssen Sie besonders auf die Reihenfolge achten: Ziehen Sie das Feld PersonID der Ausgangstabelle, also der Tabelle tblPersonen, in das Feld PersonID der Tabelle tblAngestellte. Definieren Sie referentielle Integrität und aktivieren Sie die Option Löschweitergabe an verwandte Datensätze. Hierfür ist die Reihenfolge wichtig: Der Dialog Beziehungen bearbeiten zeigt unter Tabelle/Abfrage die Ausgangstabelle und unter Verwandte Tabelle/Abfrage die Zieltabelle einer Löschweitergabe an. Die Löschweitergabe soll von der Tabelle tblPersonen ausgehen und nicht umgekehrt, daher muss auch der Beziehungspfeil von dieser Tabelle ausgehen (siehe Abbildung 2.35).
Beziehungen
Abbildung 2.33: Einstellen eines eindeutigen Index
Abbildung 2.34: Anpassen der Feldnamen, die das automatische Anlegen eines Index forcieren
81
82
2
Tabellen und Datenmodellierung
Übrigens: Sollte der Dialog Beziehungen bearbeiten im unteren Bereich nicht 1:1 als Beziehungstyp anzeigen, müssen Sie nochmals die Eindeutigkeit der beteiligten Tabellen prüfen.
Abbildung 2.35: 1:1-Beziehung zwischen zwei Tabellen
Nach dem Anlegen der beiden 1:1-Beziehungen sieht das Ergebnis wie in Abbildung 2.36 aus.
Abbildung 2.36: Tabelle mit zwei 1:1-Beziehungen
Beziehungen
83
Wie arbeitet man mit per 1:1-Beziehung verknüpften Tabellen? Eine solche Beziehung können Sie prinzipiell wie eine ganz normale Tabelle behandeln – Sie müssen nur eine geeignete Abfrage anlegen, um die Daten wieder zusammenzuführen. Wie dies funktioniert und wie Sie Formulare nutzen, um solche Daten zu bearbeiten, erfahren Sie in Abschnitt 3.5 des Kapitels 3, »Abfragen«, und in Abschnitt 4.4.5 des Kapitels 4, »Formulare«.
Wo kommen 1:1-Beziehungen sonst noch zum Einsatz? Neben der »Spezialisierung« von Tabellen durch Anhängen von Tabellen mit weiteren Informationen gibt es noch weitere Gründe für den Einsatz von 1:1-Beziehungen: Eine Tabelle hat mehr als 255 Felder. Sicher gibt es Objekte, die so viele Eigenschaften mitbringen. In der Regel sollte man aber das Datenmodell einer genaueren Prüfung unterziehen, wenn eine Tabelle derart viele Felder besitzt. Eine Tabelle enthält eine Menge Felder, die aber nur selten benötigt werden. Diese gliedert man wie in obigem Beispiel in eine weitere Tabelle aus und gibt dort bei Bedarf Daten ein. Beispiel: Ein Ja/Nein-Feld, das Datensätze beispielsweise zum Drucken festlegt. Dieses Feld benötigt man nie, außer wenn man zu druckende Datensätze festlegen oder die ausgewählten Datensätze drucken möchte. Also erstellen Sie einfach eine eigene Tabelle und verknüpfen diese mit der Zieltabelle.
2.4.8 Reflexive Beziehungen Reflexive Beziehungen sind Beziehungen, die Datensätze einer Tabelle mit Datensätzen der gleichen Tabelle verknüpfen. Dabei enthalten die verknüpften Datensätze meist unterschiedliche Rollen, etwa Vorgesetzter und Untergebener. Dabei handelt es sich prinzipiell um eine klassische 1:n-Beziehung – es befindet sich lediglich die gleiche Tabelle auf beiden Seiten. Reflexive Beziehungen (manchmal auch rekursive Beziehungen genannt) können auch über eine Zwischentabelle hergestellt werden, um eine reflexive m:n-Beziehung zu realisieren.
Reflexive 1:n-Beziehungen Die Tabelle in Abbildung 2.37 greift das oben genannte Beispiel der Beziehung zwischen Mitarbeitern und Vorgesetzten auf. Dazu enthält die Tabelle tblMitarbeiterMitVorgesetzten ein Feld namens VorgesetzterID mit dem Datentyp Zahl. Wer nun versucht, den Nachschlage-Assistenten zum Erstellen der gewünschten Beziehung zu bewegen, wird feststellen, dass dieser nur andere Tabellen zum Erstellen von Beziehungen anbietet, aber nicht die, für die eine Beziehung erstellt werden soll.
84
2
Tabellen und Datenmodellierung
Abbildung 2.37: Entwurf einer Tabelle mit reflexiver Beziehung
Hier ist also Handarbeit angesagt: Öffnen Sie also den Beziehungen-Dialog und fügen Sie die Tabelle tblMitarbeiterMitVorgesetzten hinzu. Da sich auch dieser Dialog etwas bockig anstellt, wenn man die Tabelle mit sich selbst verknüpfen möchte, hilft nur noch ein Trick: Fügen Sie eine zweite Instanz der Tabelle hinzu, indem Sie die Tabelle tblMitarbeiterMitVorgesetzten noch einmal einfügen. Anschließend können Sie die Beziehung wie bei einer normalen 1:n-Beziehung hinzusetzen; auch referentielle Integrität lässt sich problemlos festlegen (siehe Abbildung 2.43). Das Aktivieren der Löschweitergabe ist übrigens nicht zu empfehlen – wenn beim Löschen des Chefs auch gleich alle Untergebenen aus der Datenbank verschwinden, wird der neue Chef nicht besonders glücklich sein …
Abbildung 2.38: Rekursive Beziehungen lassen sich im Beziehungen-Fenster nur über mehrere Instanzen derselben Tabelle anlegen.
Informationen über die Anzeige von Datensätzen, die in reflexiver Beziehung zueinander stehen, finden Sie in Abschnitt 4.4.12 , Kapitel 4, »Formulare«.
Reflexive m:n-Beziehungen Weniger bekannt, da sehr selten verwendet, sind reflexive m:n-Beziehungen. Eines der rar gesäten Beispiele sind die Teile einer Produktdatenbank. Ein Produkt besteht aus mehreren Teilen, die wiederum aus anderen Teilen zusammengesetzt sind. Hier wer-
Autowerte als Long oder GUID?
85
den also nicht nur Endprodukte verwaltet, die aus hunderten von Einzelteilen bestehen, sondern auch Baugruppen, die Bestandteil anderer Baugruppen sind und wiederum weitere Baugruppen enthalten können. Warum reicht hier eine reflexive 1:n-Beziehung nicht aus? Ganz einfach: Weil ein Teil oder eine Baugruppe nicht einer anderen Baugruppe, sondern mehreren Baugruppen als Bestandteil zur Verfügung stehen soll. Und wie realisieren Sie eine reflexive m:n-Beziehung? Wie eine ganz normale m:nBeziehung! Der einzige Unterschied ist, dass Sie zwei Instanzen der Tabelle tblProdukte statt zwei unterschiedliche Tabellen verwenden (siehe Abbildung 2.39).
Abbildung 2.39: Reflexive m:n-Beziehung
Die Verknüpfungstabelle enthält – da die Namen der Primärschlüsselfelder beider verknüpften Tabellen gleich sind – Fremdschlüsselfelder mit Namen, die den Datensätzen der verknüpften Tabellen gleichzeitig die Rolle in der Verknüpfung zuweisen. In diesem Fall ist das übergeordnete Element die Baugruppe und das untergeordnete Element ein Teil. Zusätzlich enthält die Verknüpfungstabelle ein Feld namens Anzahl, damit man festlegen kann, wie viele Teile einer Sorte die Baugruppe enthält.
2.5 Autowerte als Long oder GUID? In Access ist die Verwendung von Autowerten als Primärschlüssel praktisch als Standard anzusehen. In manchen Fällen leisten GUIDs allerdings wertvolle Dienste, die Autowerte nicht leisten können. So macht es beispielsweise sehr viel Sinn, Tabellen mit einer GUID als Autowert zu versehen, deren Daten gelegentlich archiviert und dazu in eine andere Tabelle übertragen und aus der ursprünglichen Tabelle gelöscht werden – gegebenenfalls befindet sich die Archivtabelle sogar in einer anderen Datenbank. Sollten Sie diese Daten noch einmal in der Originaltabelle benötigen, müssen Sie sicherstellen, dass die Daten unter dem alten Primärschlüssel eingetragen werden können. Das ist mit herkömmlichen Autowerten nicht möglich. Wenn Sie etwa den neuesten
86
2
Tabellen und Datenmodellierung
Datensatz einer Tabelle in die Archivdatenbank übertragen und die Originaldatenbank komprimieren, wird der Autowertzähler so eingestellt, dass er als Nächstes die Zahl verwendet, die um eins größer als die bisher größte verwendete Zahl ist. Das heißt, dass unter Umständen der Primärschlüsselwert des archivierten Datensatzes bereits vergeben ist. Bei der Verwendung von GUIDs als Primärschlüssel können Sie archivierte Datensätze problemlos wieder einfügen. Das liegt daran, dass ein Wert des Typs GUID weltweit einzigartig ist. Wenn Sie sich ein Beispiel für einen solchen Wert ansehen, verstehen Sie, warum das tatsächlich wahr sein kann: {B9FF24C2-C32D-4053-B5FB-FCAF8AC8C7FC}
Ein GUID besteht aus 32 Zeichen, von denen jedes Zeichen mit einer Zahl von 0 bis 9 oder einem Buchstaben von A bis F gefüllt werden kann – also mit einer hexadezimalen Zahl. Das ergibt insgesamt 3,4 x 1038 Möglichkeiten. Ein weiterer Anwendungszweck für GUIDs ist die Replikation und Synchronisation. Bei der Replikation kann man eine oder mehrere Kopien einer Datenbank erstellen, die dann unabhängig voneinander geändert und anschließend synchronisiert werden können. Dazu gehört auch, dass man in den unterschiedlichen Replikationen neue Datensätze anlegt. Auch dort werden GUIDs zur eindeutigen Kennzeichnung der Datensätze verwendet. Weitere Informationen zur Replikation erhalten Sie in Abschnitt 17.6 des Kapitels 17, »Installation, Betrieb und Wartung«.
2.6 Datenmodell-Muster Auch die Kenntniss der einzelnen Beziehungstypen und der Normalisierung garantiert noch lange kein perfektes Datenmodell. Dazu gehört auch eine Menge Erfahrung oder eine Vorlage, von der man weiß, dass sie bereits erfolgreich in der Praxis eingesetzt wurde. Die gute Nachricht ist, dass es bei den meisten Anwendungen nur einen Weg gibt, um das Datenmodell umzusetzen – natürlich unter Berücksichtigung der Normalisierung. Den muss man allerdings erst einmal finden – und dabei soll die nachfolgende Sammlung von Datenmodell-Mustern helfen. Es handelt sich dabei um grundlegende Datenmodelle für verschiedene Anwendungsfälle – wobei nicht nur geschäftliche Themen betrachtet werden, sondern auch die eine oder andere Heimanwendung unter die Lupe genommen wird.
2.6.1 Adressen-/Kundenverwaltung Wer mit Access arbeitet, hat in den meisten Fällen auch schon einmal eine Adressverwaltung programmiert, wenn er sich nicht sogar am Beispiel einer Adressverwaltung in Access einarbeiten durfte. So trivial wie diese Anwendung zunächst scheint, so
Datenmodell-Muster
87
kompliziert kann sie in bestimmten Fällen werden. Das gilt insbesondere dann, wenn nicht nur Adressen mit den üblichen Daten wie Name, Straße, PLZ, Ort und den Kontaktdaten wie Telefon- oder E-Mail gefragt sind, sondern auch die Unternehmen der jeweiligen Personen ins Spiel kommen. All diese Daten lassen sich leicht in einer Tabelle unterbringen, die beispielsweise tblPersonen heißt. Unter Umständen hängt an dieser Tabelle noch eine Lookup-Tabelle mit den Anreden (siehe Abbildung 2.40).
Abbildung 2.40: Datenmodell einer einfachen Adressverwaltung
Was auffällt, sind die vielen Kontaktmöglichkeiten via Telefon oder E-Mail. Diese lassen sich leicht in eine weitere Tabelle ausgliedern, die per 1:n-Beziehung mit der Tabelle tblPersonen_1 verknüpft wird (siehe Abbildung 2.41). Wenn Sie aber mehr aus den Adressen machen möchten – etwa um sich in Richtung Customer Relation Management zu bewegen – wird es komplizierter. Der erste Schritt in diese Richtung ist, dass Sie die im Feld Firma gespeicherten Unternehmen in einer eigenen Tabelle speichern und von der Tabelle tblPersonen auf diese Tabelle verweisen. Dadurch können Sie alle Unternehmensdaten in einer einzigen Tabelle unterbringen und diese konsistent halten. Abbildung 2.42 zeigt, wie das Datenmodell nach dieser weiteren Änderung aussieht. Die Unternehmensdaten sind komplett in der Tabelle tblUnternehmen_2 untergebracht, auf die nun von der Tabelle tblPersonen_2 verwiesen wird. Dies ist ein Zustand, auf dem man eine CRM-Anwendung aufbauen kann – Unternehmen und Personen befinden sich in einzelnen, miteinander verknüpften Tabellen.
88
2
Tabellen und Datenmodellierung
Abbildung 2.41: Adressentabelle mit ausgegliederten Kontaktmöglichkeiten
Damit ist sichergestellt, dass nicht zwei Datensätze der Personen-Tabelle unterschiedliche Firmendaten enthalten, wie das noch in der Fassung in Abbildung 2.41 möglich war. Außerdem können Sie jedem Unternehmen beliebig viele Personen zuordnen.
Abbildung 2.42: Personen und Unternehmen
Datenmodell-Muster
89
Das Ende der Fahnenstange ist damit allerdings noch lange nicht erreicht. Die Unternehmen lassen sich noch zu Konzernen zusammenfassen, was eine weitere Tabelle erfordern würde. Auch über die Beziehung zwischen Personen und Unternehmen ist noch nicht das letzte Wort gesprochen: Was ist beispielsweise mit freien Mitarbeitern, die Sie schließlich auch unter einer beruflichen Telefonnummer erreichen möchten? Freie Mitarbeiter sind ja gerade deshalb »frei«, weil sie nicht nur für ein Unternehmen arbeiten. Theoretisch müssten Sie also zwischen Personen und Unternehmen eine m:nBeziehung erstellen. Und wie gehen Sie vor, wenn Sie eine Liste nicht nur aller Unternehmen oder aller Personen, sondern etwa eine Gesamtliste von Unternehmen und Personen ausgeben möchten? Gegebenenfalls könnten Sie die gewünschten Daten per UNION-Abfrage zusammenfassen (was eine UNION-Abfrage ist, erfahren Sie in Abschnitt 3.3 des Kapitels 3, »Abfragen«). Wie Sie sehen, ist die Verwendung der Adressdaten von Unternehmen und Personen keine triviale Angelegenheit. Die genannten Konfigurationen sind Beispiele, die Sie beim Erstellen einer Adressenverwaltung nach Sichtung der individuellen Gegebenheiten berücksichtigen können. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Adressverwaltung.mdb.
2.6.2 Rezepteverwaltung Um zu ermitteln, welche Daten eine Rezepteverwaltung enthalten muss, schlagen Sie einfach ein beliebiges Kochbuch auf und schauen sich an, welche Informationen dort pro Rezept enthalten sind. Jedes Rezept enthält die Angabe der benötigten Zutaten mit Menge und Einheit, die Beschreibung, die Zubereitungsdauer, die Anzahl der Personen, die sich an dem Ergebnis laben kann, und vielleicht noch ein Foto. Da ein Kochbuch meist von einem einzigen Autor stammt, wird dieser nicht explizit für jedes Rezept angegeben – das wäre das einzige Feature, das man seinem Datenmodell noch zusätzlich gönnen sollte. Vermutlich möchten Sie Rezepte verschiedener Autoren in einer Datenbank sammeln. Diese Informationen werden allerdings bereits im ersten Ansatz auf insgesamt sieben Tabellen aufgeteilt (siehe Abbildung 2.43) – und hier sind noch Verfeinerungen möglich. Doch zunächst zu dieser Version des Datenmodells: Informationen wie die Beschreibung, die Dauer der Zubereitung, die Anzahl Portionen, der Autor und der Pfad zu einer Abbildung befinden sich in der Haupttabelle tblRezepte. Da mehrere Rezepte vom gleichen Autor stammen können, wird dessen Name noch in eine LookupTabelle namens tblAutoren ausgegliedert.
90
2
Tabellen und Datenmodellierung
Eine nützliche Geschichte für eine Rezeptesammlung ist die Angabe einer oder mehrerer Kategorien wie Fleischgerichte, vegetarische Gerichte, Salate, Nudelgerichte oder Suppen. Für ausreichend Flexibilität – etwa, wenn sich einmal ein Gericht nicht eindeutig zuordnen lässt – verknüpften Sie die Tabelle tblKategorien nicht direkt mit der Tabelle tblRezepte, sondern über eine Zwischentabelle namens tblRezepteKategorien. Mit dieser m:n-Beziehung lassen sich jedem Rezept mehrere Kategorien zuordnen. Das Wichtigste sind natürlich die Zutaten: Diese werden zunächst in einer eigenen Tabelle namens tblZutaten erfasst. Über die Zwischentabelle tblRezepteZutaten werden nicht nur die Zutaten zu einem Rezept, sondern auch noch Menge und Einheit festgelegt. Die Einheiten sollten – wie der Name schon sagt – immer einheitlich gewählt werden, weshalb Sie diese in die Tabelle tblEinheiten auslagern und mit der Tabelle tblRezepteZutaten verknüpfen. Dieses Datenmodell ermöglicht nicht nur die Ausgabe von Rezepten mit den angegebenen Informationen, sondern auch noch die Berechnung von Rezepten für eine andere Anzahl hungriger Esser als im Feld AnzahlPortionen angegeben. Um solch ein alternatives Rezept zu berechnen, müssen Sie lediglich die in der Tabelle tblRezepteZutaten angegebene Menge je Zutat durch die Anzahl zu verköstigenden Personen teilen und mit der Zahl der gewünschten Mahlzeiten multiplizieren.
Abbildung 2.43: Datenmodell einer Rezepteverwaltung
Datenmodell-Muster
91
Erweiterungsmöglichkeiten Das Datenmodell bietet noch die Erweiterungsmöglichkeit, mehrere Bilder zu einem Rezept zu speichern oder die einzelnen Schritte der Rezeptbeschreibung in einer eigenen Tabelle zu speichern. Wie grob man dies vornimmt, bleibt jedem selbst überlassen – sinnvoll könnte aber auf jeden Fall die Aufteilung in einzelne Elemente wie »Fleischzubereitung«, »Beilagen« und »Sauce« sein. Das führt aber spätestens bei Gerichten, in denen zur Optimierung der Zubereitungszeit mehrere Elemente gleichzeitig zubereitet werden, zu Problemen – das Speichern der Beschreibung in einem einzigen Feld scheint also sinnvoller zu sein. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Rezeptverwaltung.mdb.
2.6.3 Artikelverwaltung Mit der Nordwind-Datenbank als Bestandteil von Access bietet Microsoft ein Beispiel für eine Artikelverwaltung, die sich von Version zu Version jeglicher Namenskonvention widersetzt. Immerhin liefert das Datenmodell Beispiele für fast alle gebräuchlichen Beziehungstypen mit Ausnahme der 1:1-Beziehung. Nicht auf den ersten Blick zu erkennen ist die reflexive Beziehung in der Personal-Tabelle. Dort lässt sich als Wert des Feldes Vorgesetzte(r) ein Eintrag der gleichen Tabelle auswählen (siehe Abbildung 2.44). Das Datenmodell ist für eine Beispieldatenbank durchaus in Ordnung, der tägliche Einsatz dürfte jedoch noch einige Erweiterungen verlangen. So wäre es beispielsweise praktisch, wenn man die bestellten Artikel direkt in die Bestandsverwaltung einbeziehen könnte. Diese würde also nicht nur die Ausgänge der bestellten und anschließend verkauften Artikel verwalten, sondern auch den Wareneingang, und gegebenenfalls notwendige Umbuchungen erfassen, etwa zurückgelieferte Ware oder bei Inventuren festgestellte Fehlmengen. Natürlich könnten Sie dies mit zwei weiteren Tabellen erledigen, die ähnlich wie die Tabelle Bestelldetails aufgebaut sind, und für Bestandserfassungen die bewegten Waren per Union-Abfrage zusammenfassen. Eine andere Möglichkeit ist die aus Abbildung 2.45. Mit diesem Datenmodell gehen Sie das Problem von einer anderen Warte an: Hier werden alle Bewegungen in einer einzigen Tabelle namens tblBestandsaenderungen erfasst. Die Tabelle ist per 1:1-Beziehung etwa mit einer Tabelle namens tblPositionen verknüpft. Zusammen enthalten diese beiden Tabellen genau die gleichen Daten wie die Tabelle Bestelldetails in der Nordwind-Datenbank. Arbeiten können Sie mit diesen Daten – wie bereits weiter oben erwähnt – indem Sie diese einfach per Abfrage zusammenfassen.
92
2
Tabellen und Datenmodellierung
Abbildung 2.44: Das Datenmodell der Nordwind-Datenbank
Der Vorteil dieser Vorgehensweise ist, dass sich die für die Erfassung der Wareneingänge und der Umbuchungen benötigten Informationen in Form der Zusatztabellen tblWareneingang und tblUmbuchungen ebenfalls per 1:1-Beziehungen an die Tabelle tblBestandsaenderungen anfügen lassen. Die Bestandsänderungen sind dennoch alle in einer einzigen Tabelle verfügbar. Unterschiede zwischen Ein- und Ausgängen markiert das Feld Vorzeichen, das bei Ausgängen den Wert –1 enthält und bei Eingängen den Wert 1.
2.6.4 CD-Verwaltung »In welchem Fach von welchem Schrank ist noch mal die Depeche Mode-CD mit dem Song Photographic versteckt?« – so oder ähnlich fragen begeisterte CD-Sammler. Und wer seine Sammlung sicher verwalten und jederzeit die gewünschten Titel finden möchte, baut natürlich eine eigene CD-Verwaltung auf Access-Basis auf. Und dabei muss man noch nicht einmal alle CDs und Tracks selbst erfassen, denn im Internet finden sich Datenserver, die zu einer eindeutigen ID einer CD online alle Daten zur Verfügung stellen. Ein Beispiel für diesen Service, der praktischerweise noch ein .ocx-Steuerelement für die Verwendung unter VBA mitliefert, findet sich unter der Internetadresse http://freedb.org. Diesem System lehnt sich auch das folgende Datenmodell an: Es enthält lediglich drei Tabellen, wobei die CDs und die Tracks in je einer Tabelle gespeichert werden und beide die Interpreten aus einer Lookup-Tabelle beziehen (siehe Abbildung 2.46).
Datenmodell-Muster
93
Abbildung 2.45: Erweiterung der Bestellverwaltung um Wareneingang und Umbuchungen
Abbildung 2.46: Datenmodell einer CD-Verwaltung
Zwischen CDs und Tracks ergibt sich eine klassische 1:n-Beziehung, da jeder Track sich genau einer CD zuordnen lässt, jede CD aber aus mehreren Tracks besteht. Aber ist das wirklich so? Kann nicht ein Track auf einem Album, auf einer Maxi-CD und vielleicht noch auf verschiedenen Samplern vorhanden sein? Natürlich ist das möglich, aber da sich hier meist auch noch die Spieldauer und die Version unterscheiden, scheint eine m:n-Beziehung hier doch unangemessen. Die Felder sind im Gegensatz zu den sonstigen Gepflogenheiten dieses Buchs nicht mit deutschen Namen versehen. Namensgeber war in diesem Fall das Objektmodell der uFreeDB-Bibliothek.
94
2
Tabellen und Datenmodellierung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/CD-Verwaltung.mdb.
2.6.5 Projektverwaltung Wer keine professionelle Software für die Verwaltung von Projekten verwenden möchte, kann sich mit einer passenden Access-Anwendung helfen. Abbildung 2.47 zeigt ein rudimentäres Datenmodell für eine solche Projektverwaltung. Es basiert darauf, dass jedes Projekt von einem Kunden in Auftrag gegeben wird, wobei für interne Projekte einfach das eigene Unternehmen als Kunde eingetragen werden kann. Jedem Kunden können Sie mehrere Projekte zuweisen, daher sind die Tabellen tblKunden und tblProjekte mit einer 1:n-Beziehung verknüpft. Projekte haben einen Namen, eine Dauer, eine Leitung und vor allem einzelne Projektphasen (mindestens aber eine). Die Projektphasen haben wiederum eine Bezeichnung, eine Leitung sowie ein Start- und ein Enddatum. Konkreter wird es auf der nächsten Ebene: Projektphasen gliedern sich in Tätigkeiten, die in der Tabelle tblTaetigkeiten gespeichert werden. Diese sind immer auf einen Tag begrenzt, daher enthält die Tabelle ein Datumsfeld sowie zwei weitere Felder zur Angabe der Start- und der End-Uhrzeit. Außerdem werden hier eine Tätigkeitsbeschreibung und der Tätigkeitstyp eingegeben.
Abbildung 2.47: Datenmodell einer Projektverwaltung
Datenmodell-Muster
95
2.6.6 Mitarbeiterverwaltung Das Datenmodell aus Abbildung 2.48 zeigt ein rudimentäres Abbild dessen, was Sie bei einer Mitbearbeiterverwaltung berücksichtigen müssen. Wichtig ist hier vor allem die Aufteilung der Informationen über einen Mitarbeiter auf die Mitarbeiterdaten und auf die Beschäftigungsdaten. Die Daten in der Tabelle tblMitarbeiter können sich ändern, ohne dass sich dies auf das Beschäftigungsverhältnis auswirkt – der Mitarbeiter kann seinen Wohnsitz, seine Bankverbindung, seine Telefonnummern wechseln und es werden einfach die aktuellen Daten weiter verwendet. Für die Personalabteilung ist es viel interessanter, wann der Mitarbeiter in welcher Position und in welcher Abteilung gearbeitet hat. Deshalb finden Sie im Datenmodell eine Tabelle namens tblBeschaeftigungen, die alle Informationen über die einzelnen Beschäftigungsverhältnisse enthält. Neben Abteilung und Position finden sich auch Details darüber, in welchen Räumlichkeiten der Mitarbeiter sein Unwesen treibt und wer sein Vorgesetzter ist. Das Feld VorgesetzterID ist übrigens auch mit der Tabelle tblMitarbeiter verknüpft; aus Platzgründen wurde diese Tabelle jedoch nicht noch einmal zusätzlich abgebildet. In die beiden Felder Eintrittsdatum und Austrittsdatum tragen Sie ein, wie lange die jeweiligen Beschäftigungsverhältnisse gedauert haben. Erweiterungsmöglichkeiten bieten sich hier in ausreichender Menge – so können Sie Daten zu Lohn/Gehalt ebenfalls in der Tabelle tblBeschaeftigungen unterbringen oder auch Verweise auf die Standorte der jeweiligen Arbeitsverträge hinterlegen. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Mitarbeiterverwaltung.mdb.
2.6.7 Literaturverwaltung Wenn ein Unternehmen Fachbücher, Magazine und sonstige Literatur zentral einkauft und lagert, macht eine Literaturverwaltung Sinn. Im Mittelpunkt des Datenmodells einer solchen Verwaltung steht die Tabelle tblLiteratur. Sie enthält die wichtigsten Informationen über die einzelnen Werke wie Titel, Erscheinungsjahr, Schlagwörter oder einen Abstract. Weitere Informationen wie Verlag und Dokumenttyp befinden sich in Lookup-Tabellen (siehe Abbildung 2.49).
96
2
Tabellen und Datenmodellierung
Abbildung 2.48: Datenmodell einer Mitarbeiterverwaltung
Für die Angabe der Autoren ist hingegen eine m:n-Beziehung erforderlich, denn erstens kann es mehrere Autoren je Werk geben und zweitens schreiben Autoren unter Umständen für mehr als eine Publikation. Zusätzlich liefert das Datenmodell den Komfort, dass man der Kombination aus Veröffentlichung und Autor noch die Funktion des Autors hinzufügen kann – unter Umständen gibt es Haupt- und Co-Autoren, die Sie ebenfalls in der Datenbank speichern möchten. Wichtig ist vor allem in größeren Unternehmen die Verwaltung der Standorte: Wenn Sie schon den Literaturbestand in einer Datenbank verwalten, möchten Sie vielleicht auch wissen, wo sich die einzelnen Werke zu einem bestimmten Zeitpunkt befinden. Dazu legen Sie in der Tabelle tblStandorte alle vorhandenen Standorte fest und verknüpfen diese wiederum mit einer m:n-Beziehung mit der Tabelle tblLiteratur. Warum nun mit einer m:n-Beziehung – eine Veröffentlichung hat doch in der Regel auch nur einen Standort? Das ist richtig, aber wer zum Beispiel Zeitschriften in der Literaturverwaltung hütet, möchte vielleicht auch deren Rundlauf durch die Abteilungen verfolgen, bevor diese ihren endgültigen Platz finden. Wer es den Benutzern ganz besonders angenehm machen möchte, kann auch noch die in einem Buch enthaltenen Verweise auswerten und eine reflexive Verknüpfung erstellen. In diesem Fall wird dort noch die Tabelle tblVerweise zwischengeschaltet, um gegebenenfalls Bemerkungen unterzubringen (zum Beispiel »Das Buch … liefert weiterführende Informationen zum Thema …«).
Datenmodell-Muster
97
Abbildung 2.49: Datenmodell einer Literaturverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Literaturverwaltung.mdb.
2.6.8 Mitgliederverwaltung Wegen der hohen Anzahl Vereine in Deutschland sollte man meinen, dass Mitgliederverwaltungen eine der meistgebrauchten Anwendungen überhaupt sind. Mit einem gewissen Grundstock können Sie Mitgliederverwaltungen für alle möglichen Vereine erstellen. Abbildung 2.50 liefert eine solche Grundausstattung: Im Mittelpunkt steht hier die Tabelle tblMitglieder, in der prinzipiell alle Daten erfasst werden. Neben dieser Tabelle gibt es nur einige Lookup-Tabellen zur Auswahl von Daten. Wichtig ist: Mitglieder – vor allem von Sportvereinen – wollen sich untereinander immer und überall erreichen können, um Wettkämpfe und/oder gesellschaftliche Anlässe zu verabreden. Im Gegensatz zur Kontaktverwaltung in Unternehmen sollen hier möglichst die private, die geschäftliche und die mobile Telefonnummer gepflegt
98
2
Tabellen und Datenmodellierung
werden. Da hierfür eine ganze Menge Felder draufgehen können, sind diese Daten in zwei weitere Tabellen ausgelagert. Dabei dient die Tabelle tblTelefonnummern als Verknüpfungstabelle zwischen den Tabellen tblMitglieder und tblTelefonnummerarten. Letztere enthält Einträge wie Privat (Festnetz), Privat (Mobil), Privat (Fax), Geschäftlich (Festnetz), Geschäftlich (Mobil) oder Geschäftlich (Fax). Die Tabelle tblTelefonnummern speichert das Mitglied und die Telefonnummerart sowie die eigentliche Telefonnummer. Die Lookup-Tabellen sind weitgehend selbsterklärend. Die Tabelle tblZahlungsarten enthält Informationen wie Bankeinzug oder Überweisung, die Tabelle tblFunktionen die Aufgabe innerhalb des Vereins (erster Vorsitzender, Schriftführer, Jugendwart etc.), die Tabelle tblBeitragsklassen Einträge wie Jugendlicher oder Erwachsener und die Tabelle tblMitgliedsarten gibt an, ob es sich um ein aktives oder passives Mitglied handelt.
Abbildung 2.50: Datenmodell einer Mitgliederverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Vereinsverwaltung.mdb.
Datenmodell-Muster
99
2.6.9 Urlaubsverwaltung Die Verwaltung von Urlaub ist gerade in der Sommerzeit eine knifflige Angelegenheit. Mit einer passenden Access-Anwendung haben Sie nicht nur die verbleibenden Urlaubstage im Griff, sondern können sich auch per Bericht ausgeben lassen, wo es besonders eng wird. Eine weitere mögliche Funktion ist das Festlegen von Stellvertretern für die Zeit des Urlaubs eines Mitarbeiters. Das Datenmodell aus Abbildung 2.51 sorgt hier für die Grundlage. Im Mittelpunkt stehen die Mitarbeiter, die in der Tabelle tblMitarbeiter gespeichert werden. Dazu gehören Informationen wie Name, Position, Abteilung, Kontakt- und Firmenzugehörigkeitsdaten. Mit den beiden Tabellen tblAbwesenheiten und tblAbwesenheitsarten pflegen Sie nicht nur die Urlaubstage, sondern auch übrige Abwesenheitszeiten durch Krankheit oder Fortbildungen – weiteren Variationen öffnen Erweiterungen der Tabelle tblAbwesenheitsarten Tür und Tor. Durch eine zusätzliche Verknüpfung von der Tabelle tblAbwesenheiten zur Tabelle tblMitarbeiter – in der Abbildung durch die Tabelle tblMitarbeiter_1 repräsentiert – ermöglichen Sie das Zuweisen eines Stellvertreters für die Zeit der Abwesenheit. Und damit niemand mehr Urlaub nimmt, als er darf, speichert die Tabelle tblUrlaubsanspruch den individuellen Anspruch pro Mitarbeiter und pro Jahr. Das Feld JahrID dieser Tabelle und die Verknüpfung zur Tabelle tblJahre sorgen dafür, dass Sie den Mitarbeitern über die Jahre eine unterschiedliche (in der Regel steigende) Anzahl Urlaubstage zuweisen können. So lassen sich auch im Nachhinein die verfallenen oder ins Folgejahr übertragenen Urlaubstage genau nachhalten. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Urlaubsverwaltung.mdb.
2.6.10 Aufgabenverwaltung Für Entwickler – egal, ob als Einzelgänger oder im Rudel – kann eine Aufgabenverwaltung eine sehr sinnvolle Sache sein. Damit lassen sich Aufgaben in eine Datenbank eintragen und mit wichtigen Informationen versehen: eine Priorität für das Abarbeiten in der richtigen Reihenfolge, ein Status für den Projektmanager, Informationen über den Urheber und den Ausführenden der Aufgabe, das geplante Enddatum für die Fertigstellung und mehr. All diese Informationen werden in der Tabelle tblAufgaben gespeichert (siehe Abbildung 2.52).
100
2
Tabellen und Datenmodellierung
Abbildung 2.51: Datenmodell einer Urlaubsverwaltung
Von dort aus gibt es zwei Verknüpfungen zur Tabelle tblBenutzer: eine, um den Benutzer festzulegen, der die Aufgabe erstellt hat, und eine, um den Ausführenden zu kennzeichnen. Jede Aufgabe lässt sich noch in einzelne Aktionen zerlegen, weshalb eine weitere Tabelle namens tblAktionen mit einer 1:n-Beziehung an die Tabelle tblAufgaben angehängt wird. Hier finden sich einige Felder der Tabelle tblAufgaben wieder. Interessant ist hier vor allem das Feld Verbrauchte Zeit. Über die Summe der Zeiten aller Aktionen einer Aufgabe lässt sich ermitteln, ob die in der Tabelle tblAufgaben gemachte Angabe über die erwartete Dauer realistisch war oder nicht und wo es gegebenenfalls gehakt hat. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Aufgabenverwaltung.mdb.
2.6.11 Projektzeitverwaltung In eine ähnliche Richtung wie die Aufgabenplanung geht die Projektzeitverwaltung. Allerdings ist diese ein wenig projektorientierter und zielt konkret auf die Ermittlung des zeitlichen Aufwands zu Abrechnungszwecken ab. Im Mittelpunkt stehen die Projektzeiten – das sind die Zeiten, die ein Mitarbeiter mit dem Bearbeiten eines bestimmten Projekts verbringt.
Datenmodell-Muster
101
Abbildung 2.52: Datenmodell einer Aufgabenverwaltung
Die Tabelle tblProjektzeiten erfasst das Projekt, die Mitarbeiter, Startzeit und Endzeit sowie Tätigkeitsbeschreibung und Tätigkeitsart (siehe Abbildung 2.53). Die Projekte und Mitarbeiter stammen aus den verknüpften Tabellen tblProjekte und tblMitarbeiter. An der Tabelle tblProjekte hängt vorsichtshalber direkt die Tabelle mit den Kunden – falls Sie einmal das Budget überschreiten und den Kunden benachrichtigen müssen, haben Sie die notwendigen Informationen sofort zur Hand … Die Tabelle der Projekte enthält eine Projektbezeichnung und eine Beschreibung, Startund Enddatum sowie die Angabe, welcher Mitarbeiter Projektleiter und damit verantwortlich für die Zuweisung der Zeiten ist. Die Erfassung von Zeiten macht natürlich nur Sinn, wenn die Mitarbeiter diese kontinuierlich pflegen – und das machen sie vermutlich lieber, wenn dies schnell geht und nicht viel Zeit kostet. Dazu sollte der Mitarbeiter nicht erst lange nach »seinen« Projek-
102
2
Tabellen und Datenmodellierung
ten suchen müssen, sondern alle Projekte, an denen er beteiligt ist, direkt vorliegen haben und möglichst selbst sortieren können. Die Voraussetzung schaffen Sie mit der Tabelle tblProjekteMitarbeiter, mit der Sie erstens überhaupt festlegen, welcher Mitarbeiter Projektzeiten für welche Projekte anlegen kann. Zweitens können die Mitarbeiter »ihre« Projekte mit dem Feld Aktiviert ein- oder ausblenden und mit dem Feld ReihenfolgeID nach ihren eigenen Wünschen anordnen.
Abbildung 2.53: Datenmodell einer Projektzeitverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Projektzeitverwaltung.mdb.
2.6.12 Kunden und Weihnachtsgeschenke Alle Jahre wieder nähert sich das Weihnachtsfest und Hektik macht sich breit – und das nicht nur in Innenstädten und Einkaufszentren, sondern auch in bestimmten Abteilungen der Unternehmen. Dort sollen nämlich die alljährlichen Weihnachtsgeschenke zusammengestellt werden – und zwar möglichst nicht jedes Jahr das Gleiche und nach Beliebtheit der Kunden sortiert, sprich nach dem Umsatz.
Datenmodell-Muster
103
Das folgende Datenmodell liefert nicht nur die Ansprechpartner der einzelnen Unternehmen (tblPersonen und tblUnternehmen), sondern auch noch zwei Tabellen zum Verwalten der Präsente (siehe Abbildung 2.54). Dabei ist die Tabelle tblPersonenPraesente eine Verknüpfungstabelle zur Realisierung einer m:n-Beziehung zwischen den Tabellen tblPersonen und tblPraesente. Auf diese Weise lässt sich mehreren Personen das gleiche Präsent zuweisen, aber auch einer Person mehrere Präsente. Der Clou ist das zusätzliche Feld Jahr in der Verknüpfungstabelle: Darüber halten Sie zusätzlich nach, wer in welchem Jahr womit beglückt wurde – nicht, dass jemand denkt, er sei in der Gunst gesunken, nur weil er eine Flasche Wein weniger bekommt. Als Präsent können Sie hier im Übrigen auch die Weihnachtskarten erfassen.
Abbildung 2.54: Datenmodell einer Präsenteverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Praesenteverwaltung.mdb.
104
2
Tabellen und Datenmodellierung
2.6.13 Fahrtenbuch Ein Fahrtenbuch zu führen, ist eigentlich kein Problem. Man fährt, trägt seinen Namen, die Strecke und ein paar weitere Informationen ein und schon ist man fertig. Arbeit hat dann derjenige, der die im Fahrtenbuch enthaltenen Informationen für den Arbeitgeber oder das Finanzamt auswerten muss. Einfacher geht das mit einem elektronischen Fahrtenbuch – natürlich auf Access-Basis. Die Tabelle tblFahrten enthält den größten Teil der benötigten Informationen. Sie enthält zwei Verknüpfungen zu den Lookup-Tabellen tblFahrer und tblNutzungsarten und ist per 1:n-Beziehung mit der Tabelle tblFahrzeuge verknüpft. Diese sorgt dafür, dass das Datenmodell mehr als ein Fahrzeug verträgt und Fahrtenbücher für einen beliebig großen Fuhrpark verwalten kann (siehe Abbildung 2.55). In Zusammenhang mit den Fahrzeugen sind noch weitere Informationen interessant: Zum Beispiel Ausgaben für Reparaturen, Autowäsche und sonstiges Zubehör. Unter Ausgaben fallen eigentlich auch die Tankvorgänge. Wegen der vielen speziellen Informationen werden diese allerdings in einer eigenen Tabelle namens tblTankvorgaenge gespeichert. Zu diesen Informationen zählen das Datum, der Kilometerstand, die Anzahl Liter und der Preis je Liter. Durch das Boolean-Feld Vollgetankt lässt sich später der durchschnittliche Verbrauch zwischen mehreren Vollbetankungen ermitteln.
Abbildung 2.55: Datenmodell eines Fahrtenbuchs
Bilder und Dateien in Tabellen speichern
105
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter /Kap_02/Fahrtenbuch.mdb. Achtung: Wenn Sie mit dem Gedanken spielen, künftig mit einem per Rechner erstellten Fahrtenbuch beim Finanzamt vorstellig zu werden, erkundigen Sie sich dort auf jeden Fall vorher, welche Bedingungen dabei genau zu erfüllen sind.
2.7 Bilder und Dateien in Tabellen speichern Probleme mit dem Speichern von Dateien und insbesondere von Bildern in Dateien gehören in den Newsgroups und Foren zum täglichen Leben. Access bietet mit dem Datentyp OLE-Objekt die Möglichkeit, auch Dateien in einer Tabelle zu speichern (siehe Abbildung 2.56). Leider gibt es in Access keine eingebaute einfache Funktion, um Dateien ohne Weiteres in einer Tabelle zu speichern – zumindest keine, die nicht früher oder später die Größe der Datenbankdatei explodieren lässt. Beispieldatenbank: Die Tabellen tblBilder und tblDateien, die Formulare frmBilder, frmBilderBinaer, frmVerknuepfteBilder und frmDateien sowie die Module mdlDateienUndTabellen und mdlTools finden Sie unter Kap_02\TabellenDatenmodellierung.mdb.
Abbildung 2.56: Datentyp für Dateien und Bilder
106
2
Tabellen und Datenmodellierung
Die Möglichkeit zum Speichern von Dateien in einer Access-Datenbank wird meist zum Speichern von Bilddateien verwendet. Die folgenden Ausführungen beziehen sich größtenteils auf das Speichern von Bildern, die dabei vorgestellten Techniken lassen sich aber auch für andere Dateitypen verwenden.
2.7.1 Bilder im OLE-Feld speichern Wenn Sie beispielsweise ein Formular an die Tabelle aus Abbildung 2.56 binden und alle Felder in den Detailbereich des Entwurfs ziehen, können Sie der Tabelle bereits Dateien hinzufügen. Dazu wählen Sie einfach den Eintrag Objekt einfügen… des Kontextmenüs des entsprechenden Steuerelements aus (siehe Abbildung 2.57) und geben im nächsten Dialog an, welche Datei Sie darin speichern möchten. Da das Formular direkt an die Tabelle gebunden ist, wird die Datei direkt in der Tabelle gespeichert. Der hier verwendete Eintrag des Kontextmenüs steht auch in der Datenblattansicht der Tabelle zur Verfügung. Es gibt aber keinen Anlass, irgendeine Aktion direkt auf einer Tabelle auszuführen. Benutzerfreundlich wäre diese Variante allerdings nicht – einen Eintrag namens Objekt einfügen…im Kontextmenü eines Steuerelements kann man nicht gerade »intuitiv« nennen.
Abbildung 2.57: Einfügen eines Objekts
Bilder und Dateien in Tabellen speichern
107
OLE-Felder = Einbahnstraße? Einmal in ein OLE-Feld eingebettete Dateien lassen sich dort nicht ohne Weiteres wieder herausholen. Es gibt zwar verschiedene Verfahren, aber der Aufwand ist relativ hoch. Sie könnten das Objekt etwa per Doppelklick in der dafür vorgesehenen Anwendung öffnen und dann speichern, aber wenn Sie diesen Vorgang mehrere hundert oder gar tausend Mal durchführen müssen, um beispielsweise Ihre Urlaubsbildersammlung auf CD zu brennen, werden Sie wünschen, die Dateien niemals in der Datenbank untergebracht zu haben. Außerdem kann es hier weitere Probleme geben: Dateien, die über die Objekt einfügen-Funktion von Access in ein Tabellenfeld eingefügt wurden, benötigen einen OLEServer, um bearbeitet oder wieder in eine Datei umgewandelt werden zu können. Das ist die Anwendung, die auf dem aktuellen Windows-System für die jeweilige Dateiendung zuständig ist. Wenn keine für diese Dateiendung passende Anwendung eingetragen ist, wird die Datei als »Paket« in dem Feld gespeichert. In diesem Fall ist der »Objekt-Manager« von Windows der für das Paket zuständige OLE-Server und damit auch für die Wiederherstellung der Datei verantwortlich. Das ist der günstigere Fall, denn falls die Datei auf dem Ausgangssystem mit einem konkreten OLE-Server verknüpft ist, muss die entsprechende Anwendung auch auf anderen Rechnern vorhanden sein, um die Datei öffnen oder speichern zu können.
Bilder lassen die Datenbank wachsen Wenn es sich bei den zu speichernden Dateien um Bilder handelt, gibt es noch einen weiteren Nachteil: Bilddateien werden datenbankintern in einem speziellen bitmapähnlichen Format gespeichert, das wesentlich mehr Speicherplatz frisst als etwa das gängige für Fotos verwendete Format .jpg. Die Datenbank und die darin enthaltenen Bilder nehmen dann einen um ein Vielfaches größeren Platz auf der Festplatte ein als die Bilder im Ursprungsformat.
2.7.2 Dateien nicht in der Datenbank speichern Je nachdem, ob Sie die gewünschten Dateien einfach nur in Formularen oder Berichten der Datenbank anzeigen möchten, können Sie sich mit sehr einfachen Mitteln behelfen. Speichern Sie die Dateien einfach gar nicht erst in der Datenbank, sondern belassen Sie diese im Dateisystem. Statt der Datei speichern Sie lediglich den Pfad zu der gewünschten Datei und verwenden die Pfadangabe, um eine Datei in einem entsprechenden Steuerelement zu öffnen. Praktisch ist es dabei, wenn man die Dateien im Datenbankverzeichnis oder in einem darunter liegenden Verzeichnis speichert. Auf diese Weise können Sie die Daten zusammen weitergeben und gleichzeitig sicherstellen, dass die Datenbank die Dateien am gleichen Ort (relativ zum Datenbankverzeichnis) wie beim Anlegen der Datei findet.
108
2
Tabellen und Datenmodellierung
Ab Access 2000 liefert die Funktion CurrentProject.Path den gewünschten Pfad zurück. Für Access 97 bietet die folgende Funktion einen guten Ersatz: Public Function Datenbankpfad() Datenbankpfad = Left(CurrentDb.Name, Len(CurrentDb.Name) _ - Len(Dir(CurrentDb.Name))) End Function Listing 2.4: Hilfsfunktion zum Ermitteln des Datenbankpfades
Wohin mit dem Dateipfad? Wenn Sie nur den Dateipfad in der Datenbank speichern, stehen Sie vor der Frage, wie Sie die enthaltenen Informationen aufteilen. Es gibt verschiedene Varianten: Sie speichern den kompletten Pfad inklusive Dateiname in einem Feld. Nachteil: Änderungen am Verzeichnis erfordern immer den Einsatz von ZeichenkettenFunktionen. Sie speichern Verzeichnis und Dateiname in zwei Feldern. Sie legen zwei eigene Tabellen für Verzeichnisse und Dateinamen an und verknüpfen diese per 1:n-Beziehung. Vorteil: Änderungen an Verzeichnissen erfolgen auf einen Schlag. Nachteil: Änderungen an einzelnen Verzeichnissen bereiten mehr Aufwand.
Anzeigen externer Bilddateien im Formular Da sich Bilddateien nicht nur für das Speichern in einer Datenbank, sondern besonders zur Anzeige in Formularen und Berichten eignen, beschäftigt sich das folgende Beispiel mit der Anzeige von extern gespeicherten Bilddateien. Die Tabelle aus Abbildung 2.58 enthält neben dem Primärschlüsselfeld BildID nur ein Feld für eine Bezeichnung des Bildes und eines für den Dateinamen.
Abbildung 2.58: Tabelle zum Speichern von Bildverknüpfungen
In das Feld Dateiname tragen Sie entweder den kompletten Pfad ein oder Sie gehen davon aus, dass die Bilddateien irgendwo im gleichen Verzeichnis wie die Datenbank selbst liegen. Letzterer Ansatz scheint sinnvoller, da auf diese Weise Datenbank und
Bilder und Dateien in Tabellen speichern
109
Bilder leicht im Dateisystem verschoben oder weitergegeben werden können. Da die erste Variante aber einfacher zu realisieren ist, erhält sie für folgendes Beispiel den Vorzug. Zum Anzeigen der Bilder verwenden Sie ein Formular mit der Tabelle tblBildverknuepfungen als Datenherkunft. Das Formular soll alle Felder der Tabelle enthalten und – falls vorhanden – das angegebene Bild anzeigen. Dazu fügen Sie zusätzlich ein Bildsteuerelement hinzu. Dabei müssen Sie direkt die anzuzeigende Datei angeben – wählen Sie dazu irgendein Dummybild aus (im Verzeichnis der Nordwind-Datenbank befinden sich üblicherweise einige). Anschließend lassen Sie dieses wieder verschwinden, indem Sie in der Entwurfsansicht des Formulars die Eigenschaft Bild des Bildsteuerelements leeren. Stellen Sie außerdem den Namen des Bildsteuerelements auf ctlBild ein (siehe Abbildung 2.59).
Abbildung 2.59: Formular mit ungebundenem Bildsteuerelement
Nun müssen Sie noch dafür sorgen, dass das Bildsteuerelement die im Feld Dateiname angegebene Bilddatei anzeigt. Das Bildsteuerelement soll beim Anzeigen jedes Datensatzes aktualisiert werden, daher ist die Ereigniseigenschaft Beim Anzeigen der richtige Ort für die entsprechende Prozedur. Aber nicht nur beim Wechseln des Datensatzes, sondern auch nach der Eingabe des Dateinamens soll das Bild direkt angezeigt werden. Daher lagern Sie die Funktionalität zum Aktualisieren des Bildsteuerelements in eine eigene Prozedur aus, die Sie von mehreren Orten aus aufrufen können.
110
2
Tabellen und Datenmodellierung
Die Prozedur BildAktualisieren sorgt dafür, dass das Bildsteuerelement das aktuell unter Dateiname angegebene Bild anzeigt. Die Prozedur stellt sicher, dass das Bildsteuerelement leer ist, wenn kein Dateiname angegeben ist (das ist nicht selbstverständlich – wenn man den Datensatz wechselt, ohne den Inhalt des Bildsteuerelements zu leeren, zeigt es weiterhin das vorherige Bild an). Außerdem prüft die Prozedur, ob eine Bilddatei mit dem angegebenen Namen vorhanden ist. Wenn die Datei nicht existiert, leert die Prozedur nicht nur das Bildsteuerelement, sondern auch das Feld Dateiname. Private Sub BildAktualisieren() Dim strDateiname As String Dim strPicture As String 'Prüfen, ob Dateiname vorhanden ist If Not IsNull(Me!Dateiname) Then strDateiname = Me!Dateiname 'Prüfen, ob das angegebene Bild existiert... If Not Dir(strDateiname) = "" Then '... und Bild festlegen strPicture = strDateiname Else '... oder Meldung ausgeben 'und Textfeld Dateiname leeren MsgBox "Das Bild ist nicht vorhanden." Me!Dateiname = "" End If End If Me!ctlBild.Picture = strPicture End Sub Listing 2.5: Prozedur zum Aktualisieren des Bildsteuerelements
Der Aufruf dieser Prozedur erfolgt etwa beim Eintreten des Ereignisses Beim Anzeigen des Formulars oder Nach Aktualisierung des Textfeldes Dateiname: Private Sub Dateiname_AfterUpdate() BildAktualisieren End Sub Private Sub Form_Current() BildAktualisieren End Sub Listing 2.6: Aufruf der Prozedur BildAktualisieren durch verschiedene Ereignisprozeduren
Bilder und Dateien in Tabellen speichern
111
Das komplette Formular auf der Buch-CD enthält auch noch eine Schaltfläche, mit der sich ein Dialog zum Auswählen der Bilddatei anzeigen lässt (siehe Abbildung 2.60). Auch nach dem Auswählen der Datei ruft das Formular die Prozedur BildAktualisieren auf.
Abbildung 2.60: Das fertige Formular zur Anzeige verknüpfter Bilddateien
Anzeige externer Bilddateien in Berichten In Berichten ist die Vorgehensweise etwas anders. Hier sorgt lediglich das Ereignis Beim Drucken für das Füllen des Bildsteuerelements mit dem entsprechenden Bild (siehe Abbildung 2.61). Die Ereignisprozedur hat folgendes Aussehen: Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) Dim strDateiname As String Dim strPicture As String 'Prüfen, ob Dateiname vorhanden ist If Not IsNull(Me!Dateiname) Then strDateiname = Me!Dateiname 'Prüfen, ob das angegebene Bild existiert... If Not Dir(strDateiname) = "" Then '... und Bild festlegen strPicture = strDateiname End If End If 'Bildname dem Bildsteuerelement zuweisen Me!ctlBild.Picture = strPicture End Sub Listing 2.7: Ereignisprozedur zum Füllen des Bildsteuerelements eines Berichts
112
2
Tabellen und Datenmodellierung
Abbildung 2.61: Verknüpfte Bilddateien im Bericht
Wenn Bilder nicht angezeigt werden wollen … Manche .jpg-Datei und Bilddatei anderer Formate kann Access nicht in dem dafür vorgesehenen Bildsteuerelement anzeigen. In diesem Fall erscheint die Fehlermeldung aus Abbildung 2.62. Das Problem liegt entweder tatsächlich in dem fehlenden Grafikfilter oder aber das Format der Datei stimmt nicht exakt mit dem erwarteten Format überein. Im ersten Fall hilft das Nachinstallieren der benötigten Grafikfilter oder das Konvertieren der Bilder in ein anderes Format. Ersteres hilft allerdings nur auf dem lokalen Rechner weiter und schließt Probleme beim Einsatz auf anderen Rechnern nicht aus und Letzteres dürfte bei größeren Bildmengen wenig Spaß machen. Wenn das Bildsteuerelement allerdings manche .jpg-Dateien anzeigt, andere wiederum nicht, spricht vieles für Probleme mit dem Format der widerspenstigen Bilder.
Bilder und Dateien in Tabellen speichern
113
Abbildung 2.62: Dieser Fehler tritt gelegentlich beim Anzeigen von .jpg-Dateien im Bildsteuerelement auf.
Alternative zum Bildsteuerelement von Access Abhilfe schafft das Image-Steuerelement der MSForms-Bibliothek, die bei jeder AccessInstallation – auch bei Runtimes – mitinstalliert wird. Dieses Steuerelement fügen Sie über den Dialog ActiveX-Steuerelement einfügen ein (siehe Abbildung 2.63).
Abbildung 2.63: Einfügen eines Image-Steuerelements
Um in diesem Steuerelement ein Bildobjekt anzuzeigen, reicht ein Einzeiler, wobei strPicture wiederum der Pfad zur Bilddatei ist: Me!ctlBild.Picture = stdole.LoadPicture(strPicture)
114
2
Tabellen und Datenmodellierung
2.7.3 Dateien als Binärstrom in der Datenbank speichern Die Lösung, lediglich den Pfad zu einer Bilddatei zu speichern und diese bei Bedarf anzuzeigen, ist in vielen Fällen ausreichend – vor allem bei der Verwendung von Bilddateien. Andererseits ist die Vorstellung, die komplette Bildersammlung in einer Datenbankdatei zu speichern oder diese als Dokumenten-Management-System zu missbrauchen, schon interessant. In den folgenden Abschnitten lernen Sie einige Funktionen kennen, mit denen Sie Dateien in ein Feld einer Access-Datenbank importieren und die Datei wieder im Dateisystem speichern können.
Importieren einer Datei in ein OLE-Feld einer Tabelle Die Funktion DateiInTabelleSpeichern importiert eine Datei in ein OLE-Feld einer Tabelle. Die Funktion erwartet folgende Parameter: strTabelle: Tabelle, die das OLE-Feld enthält strPrimaerschluessel: Name des Primärschlüsselfelds dieser Tabelle strZiel: OLE-Feld, in das die Datei importiert werden soll lngID: Wert des Primärschlüssels des Datensatzes, in den das Feld importiert werden soll strFeldDateiname (optional): Feld, in dem sich der Dateiname der zu importierenden Datei befindet strDateiname (optional): Dateiname der zu importierenden Datei (alternativ zu strFeldDateiname) bolImDatenbankpfad (optional): Gibt an, ob dem Dateinamen aus strFeldDateiname oder strDateiname der aktuelle Datenbankpfad vorangestellt werden soll Die Funktion wertet zunächst den Parameter strFeldDateiname aus. Enthält er einen Wert, liest die Funktion den Speicherort der zu importierenden Datei aus dem angegebenen Feld der Tabelle ein. Dazu füllt sie im nächsten Schritt ein Recordset-Objekt mit dem Datensatz mit dem Primärschlüsselwert lngID der Tabelle strTabelle. Dabei liest sie nur die in den in den Parametern strZiel und strFeldDateiname angegebenen Feldern enthaltenen Werte ein. Ist der Parameter strFeldDateiname gefüllt, verwendet die Funktion den enthaltenen Wert als Speicherort, sonst den Wert aus dem Parameter strSpeicherort.
Bilder und Dateien in Tabellen speichern
115
In manchen Fällen liegen die zu importierenden Dateien im Datenbankverzeichnis. Trifft das zu, stellen Sie den Wert des Parameters bolImDatenbankpfad auf den Wert True ein. Die Funktion fügt den aktuellen Datenbankpfad dann vorne an den vorhandenen Speicherort an. Nach einer Prüfung, ob die angegebene Datei vorhanden ist, ruft die Routine eine weitere Funktion namens DateiInFeld auf und übergibt das Recordset-Objekt und den Namen des Feldes, in das die Datei importiert werden soll, und den Dateinamen. Public Function DateiInTabellenfeld(strTabelle As String, _ strPrimaerschluessel As String, strZielfeld As String, lngID As Long, _ Optional strFeldDateiname As String, _ Optional strDateiname As String, _ Optional bolImDatenbankpfad As Boolean) As Long Dim db As DAO.Database Dim rst As DAO.Recordset Dim strDateinameTemp As String On Error GoTo DateiInTabellenfeld_Err Set db = CurrentDb 'Ist der Speicherort in einem Feld der Tabelle gespeichert? If Not strFeldDateiname = "" Then strDateinameTemp = ", " & strFeldDateiname Else strDateinameTemp = "" End If 'Datensatzgruppe mit dem Zielfeld für die Datei öffnen Set rst = db.OpenRecordset("SELECT " & strZielfeld & strDateinameTemp _ & " FROM " & strTabelle _ & " WHERE " & strPrimaerschluessel & " = " & lngID, dbOpenDynaset) 'Speicherort aus der Tabelle auslesen, wenn Feld angegeben, 'sonst Speicherort aus dem Parameter strSpeicherort verwenden If Not strFeldDateiname = "" Then strDateiname = rst(strFeldDateiname) End If 'Falls relative Pfadangabe: Datenbankpfad voranstellen If bolImDatenbankpfad = True Then strDateiname = CurrentProject.Path & "\" & strDateiname End If 'Meldung, falls Datei nicht vorhanden ist If Dir(strDateiname) = "" Then
116
2
Tabellen und Datenmodellierung
MsgBox "Die Datei '" & strDateiname & "' existiert nicht." Exit Function End If 'Bearbeitung des Datensatzes beginnen rst.Edit 'Funktion zum Füllen des Feldes aufrufen If DateiInFeld(strDateiname, strZielfeld, rst) = True Then DateiInTabellenfeld = True End If 'Recordset aktualisieren rst.Update DateiInTabellenfeld_Exit: On Error Resume Next rst.Close Set rst = Nothing Set db = Nothing Exit Function DateiInTabellenfeld_Err: DateiInTabellenfeld = Err.Number Resume DateiInTabellenfeld_Exit End Function Listing 2.8: Funktion zum Importieren einer Datei in ein OLE-Feld einer Tabelle
Die Funktion DateiInFeld öffnet die Datei für den binären Zugriff, ermittelt die Größe der Datei, dimensioniert eine temporäre Variable (Byte-Array) entsprechend groß und verwendet dann die Get-Anweisung, um die Datei in einem Rutsch in die temporäre Variable einzulesen. Dann kommt die AppendChunk-Methode zum Einsatz und schreibt den Inhalt der Variablen in das angegebene Feld des übergebenen Recordset-Objekts. Public Function DateiInFeld(strDateiname As String, strZielfeld As String, _ rst As DAO.Recordset) Dim lngExportdateiID As Long Dim Buffer() As Byte Dim lngDateigroesse As Long On Error GoTo DateiInFeld_Err 'Dateinummer für die Dateioperationen festlegen lngExportdateiID = FreeFile 'Zu importierende Datei für den binären Zugriff öffnen
Bilder und Dateien in Tabellen speichern
117
Open strDateiname For Binary Access Read Lock Read Write _ As lngExportdateiID 'Dateigröße ermitteln lngDateigroesse = LOF(lngExportdateiID) 'Größe der Variablen für den Dateiinhalt anpassen ReDim Buffer(lngDateigroesse) 'Zielfeld leeren rst(strZielfeld) = Null 'Inhalt der Datei in Variable "Buffer" schreiben Get lngExportdateiID, , Buffer 'Inhalt der Variablen in das Zielfeld schreiben rst(strZielfeld).AppendChunk Buffer 'Datei schließen DateiInFeld = True DateiInFeld_Exit: Close lngExportdateiID Exit Function DateiInFeld_Err: Resume DateiInFeld_Exit End Function Listing 2.9: Die Funktion DateiInFeld speichert eine Datei in das angegebene Feld-Objekt.
Beispiele für den Import einer Datei Nachfolgend finden Sie einige Beispiele für das Importieren von Dateien in eine Tabelle. Im ersten Fall enthält die Tabelle keinen Dateinamen und ist wie in Abbildung 2.64 aufgebaut. Eine .pdf-Datei soll in das Feld Datei importiert werden.
Abbildung 2.64: Zieltabelle des Datei-Imports
118
2
Tabellen und Datenmodellierung
Der Aufruf sieht folgendermaßen aus und kann beispielsweise im Direktfenster abgesetzt werden: Debug.Print DateiInTabellenfeld ("tblDateien", "DateiID", "Datei", 1, ,"c:\Kapitel_2.pdf")
Sofern die Datei vorhanden und nicht zum Bearbeiten geöffnet ist (sonst kann sie nicht für den binären Zugriff geöffnet werden), liefert die Funktion den Wert -1 zurück und die Tabelle sieht nun wie in Abbildung 2.65 aus – das Feld Datei ist gefüllt.
Abbildung 2.65: Datensatz einer Tabelle nach dem Importieren einer Datei
Ob dieses Feld nun wirklich die angegebene .pdf-Datei enthält, kann abschließend nur der Gegenversuch mit der Funktion TabellenfeldInDateiAusgeben klären – so weit ist es allerdings noch nicht. Im zweiten Beispiel ist der Dateiname bereits in der Tabelle gespeichert (siehe Abbildung 2.66). Daher lässt der folgende Aufruf den Parameter strDateiname aus und gibt den Namen des Feldes an, in dem der Dateiname gespeichert ist: Debug.Print DateiInTabellenfeld ("tblDateien", "DateiID", "Datei", 1, ,"c:\Kapitel_2.pdf")
Abbildung 2.66: Dateidatensatz mit Dateiname
Fehlen noch zwei Varianten, denn die beiden vorherigen Beispiele lassen sich noch mit der Option bolImDatenbankpfad kombinieren. Wenn Sie für diesen Parameter den Wert True übergeben, stellt die Funktion dem per strDateiname übergebenen oder dem im Feld strFeldDateiname enthaltenen Dateinamen noch den Pfad der aktuellen Datenbank voran. Natürlich muss der Wert des Parameters strSpeicherort beziehungsweise
Bilder und Dateien in Tabellen speichern
119
des Feldes strFeldDateiname dann eine relative Pfadangabe enthalten, also etwa »Kapitel_2.pdf« oder »Buch\Kapitel_2.pdf«. Der Aufruf sieht dann beispielsweise folgendermaßen aus: Debug.Print DateiInTabellenfeld("tblDateien", "DateiID", "Datei", 1, , "Kapitel_2.pdf", True)
Die Funktion setzt den Pfad der Datei dann in dieser Zeile aus dem aktuellen Datenbankverzeichnis und dem angegebenen Dateinamen zusammen: strDateiname = CurrentProject.Path & "\" & strDateiname
Die letzte Variante ist die Kombination eines in der Datenbank gespeicherten Dateinamens mit dem aktuellen Datenbankpfad.
Speichern einer Datei aus einem OLE-Feld im Dateisystem Die Funktion TabellenfeldInDatei hilft nun zunächst dabei, die Funktionstüchtigkeit der Routine DateiInTabellenfeld zu beweisen. Darüber hinaus stellt sie natürlich die in der Datenbank gespeicherten Dateien wieder her. Dabei weist diese Funktion genauso viel Flexibilität wie die obige Funktion auf, was sich in genau den gleichen Parametern widerspiegelt. Deren Zweck sieht allerdings ein wenig anders aus: Die Parameter strTabelle, strPrimaerschluessel, strQuelle und lngID geben nun keine Informationen mehr über das Ziel, sondern die Herkunft der Datei und die Parameter strFeldDateiname, strDateiname und bolDatenbankpfad geben an, wo die Datei gespeichert werden soll. Dementsprechend ist auch die Funktion sehr ähnlich aufgebaut und tauscht im Wesentlichen Quelle und Ziel der Datei gegeneinander aus. Für weitere Informationen sei daher auf die Kommentare im Quelltext des folgenden Listings verwiesen. Public Function TabellenfeldInDatei(strTabelle As String, _ strPrimaerschluessel As String, strQuellfeld As String, lngID As Long, _ Optional strFeldDateiname As String, _ Optional strDateiname As String, _ Optional bolImDatenbankpfad As Boolean) As Long Dim db As DAO.Database Dim rst As DAO.Recordset Dim strDateinameTemp As String On Error GoTo TabellenfeldInDatei_Err Set db = CurrentDb 'Prüfen, ob der Dateiname in einem Feld der Tabelle gespeichert ist
120
2
Tabellen und Datenmodellierung
If Not strFeldDateiname = "" Then strDateinameTemp = ", " & strFeldDateiname Else strDateinameTemp = "" End If 'Datensatz mit der angegebenen ID öffnen Set rst = db.OpenRecordset("SELECT " & strQuellfeld & strDateinameTemp _ & " FROM " & strTabelle _ & " WHERE " & strPrimaerschluessel & " = " & lngID, dbOpenDynaset) 'Wenn Speicherort der Datei in der Tabelle gespeichert ist, 'wird dieser ausgelesen und ansonsten der im Parameter 'strFeldSpeicherort angegebene Speicherort verwendet If Not strFeldDateiname = "" Then strDateiname = rst(strFeldDateiname) End If 'Gegebenenfalls den aktuellen Datenbankpfad vor dem Dateinamen einfügen If bolImDatenbankpfad = True Then strDateiname = Datenbankpfad & strDateiname End If If FeldInDatei(strDateiname, strQuellfeld, rst) = True Then TabellenfeldInDatei = True End If TabellenfeldInDatei_Exit: rst.Close Set rst = Nothing Set db = Nothing Exit Function TabellenfeldInDatei_Err: TabellenfeldInDatei = Err.Number Resume TabellenfeldInDatei_Exit End Function Listing 2.10: Funktion zum Exportieren einer Datei aus einer Tabelle in das Dateisystem
Auch diese Routine ruft eine weitere Funktion auf. Diese heißt FeldInDatei und übernimmt die Feinarbeit: Das Auslesen des Inhalts des OLE-Felds und das Schreiben in die angegebene Datei. Public Function FeldInDatei(strDateiname As String, strQuellfeld As String, rst As DAO.Recordset) Dim lngExportdateiID As Long
Bilder und Dateien in Tabellen speichern
121
Dim Buffer() As Byte Dim lngDateigroesse As Long On Error GoTo FeldInDatei_Err 'Dateinummer für den Zugriff auf die zu erzeugende Datei ermitteln lngExportdateiID = FreeFile 'Dateigröße ermitteln lngDateigroesse = Nz(LenB(rst(strQuellfeld)), 0) 'Wenn die Dateigröße größer 0 ist: If lngDateigroesse > 0 Then 'Größe der Variable "Buffer" anpassen ReDim Buffer(lngDateigroesse) 'Datei zum Schreiben öffnen Open strDateiname For Binary Access Write As lngExportdateiID 'Feldinhalt in die Variable "Buffer" schreiben Buffer = rst(strQuellfeld).GetChunk(0, lngDateigroesse) 'Inhalt der Variablen "Buffer" in die Datei schreiben Put lngExportdateiID, , Buffer End If FeldInDatei = True FeldInDatei_Exit: 'Datei schließen Close lngExportdateiID Exit Function FeldInDatei_Err: Resume FeldInDatei_Exit End Function Listing 2.11: Die Funktion FeldInDatei speichert den Inhalt des angegebenen OLE-Felds in einer Datei.
Beispiele für das Speichern einer als OLE-Objekt vorliegenden Datei Die nachfolgenden Beispiele setzen voraus, dass Sie bereits eine oder mehrere Dateien in einer Tabelle ähnlich der in Abbildung 2.67 gespeichert haben. Die folgende Anweisung speichert die Datei im Datenbankverzeichnis unter dem Dateinamen, der im Feld Dateiname der Tabelle angegeben ist: Debug.Print TabellenfeldInDatei("tblDateien", "DateiID", "Datei", 1,"Dateiname", , True)
122
2
Tabellen und Datenmodellierung
Abbildung 2.67: Tabelle mit einer im OLE-Feld gespeicherten Datei
Beachten Sie, dass die Prozedur vorhandene Dateien ohne Rückfrage überschreibt. Die übrigen Varianten sehen ähnlich wie für die Funktion DateiInTabellenfeld aus.
Als Binärstrom gespeicherte Bilder verfügbar machen In der Praxis sieht es so aus, dass Sie die Funktionen vermutlich von anderen Prozeduren aus aufrufen werden. Das folgende Beispiel nimmt sich nochmals die Bildverwaltung vor die Brust. In diesem Fall geht es um eine Kombination der beiden vorherigen Beispiele zur Bildverwaltung – die Bilder sollen zwar in der Datenbank gespeichert werden, aber nicht in dem Platz fressenden Bildformat von Access. Natürlich sollen sie dennoch im Formular oder Bericht angezeigt werden können – was aber nicht direkt aus der Tabelle heraus funktioniert. Daher werden die Bilddateien vor dem Anzeigen aus der Tabelle ins Dateisystem exportiert und erst dann im Bildsteuerelement angezeigt. Das Beispielformular frmBilderBinaer sieht wie in Abbildung 2.68 aus und hat die Tabelle aus Abbildung 2.67 als Datenherkunft. Die Textfelder sind an die Felder BildID, Bezeichnung und Dateiname gebunden. Das Bildsteuerelement ist ungebunden.
Abbildung 2.68: Formular zur Anzeige von temporär gespeicherten Abbildungen
Bilder und Dateien in Tabellen speichern
123
Hinter der Schaltfläche mit der Beschriftung Neues Bild verbirgt sich die folgende Ereignisprozedur: Private Sub cmdNeuesBild_Click() Dim strDateiname As String Dim rst As DAO.Recordset 'Dateiname der zu importierenden Datei ermitteln strDateiname = DateiAuswaehlen Me!Dateiname = strDateiname 'Datensatz zwischenspeichern DoCmd.RunCommand acCmdSaveRecord 'Kopie der Datensatzgruppe anlegen... Set rst = Me.RecordsetClone '...und auf den aktuellen Datensatz einstellen rst.Bookmark = Me.Bookmark 'Datensatz zur Bearbeitung vorbereiten rst.Edit 'Funktion zum Importieren der angegebenen Bilddatei aufrufen If DateiInFeld(strDateiname, "Bild", Me.RecordsetClone) = -1 Then 'Datensatz speichern rst.Update 'Aktuelles Bild anzeigen BildAktualisieren Else MsgBox "Beim Import der Bilddatei ist ein Fehler aufgetreten." End If End Sub Listing 2.12: Prozedur zum Laden einer neuen Abbildung in das Bildsteuerelement
Die Prozedur ermittelt zunächst über die Funktion DateiAuswaehlen den Pfad der gewünschten Bilddatei. Diese Funktion verwendet das FileDialog-Objekt, das seit Office XP Bestandteil der Office-Objektbibliothek ist, zur Auswahl der Bilddatei: Private Function DateiAuswaehlen() As String Dim objFiledialog As Office.FileDialog Dim strDateiname As String Dim strDatenbankpfad As String
124
2
Tabellen und Datenmodellierung
'Pfad der aktuellen Datenbank ermitteln strDatenbankpfad = CurrentProject.Path 'Filedialog-Objekt festlegen Set objFiledialog = FileDialog(msoFileDialogOpen) With objFiledialog 'Auswahl auf ein Element beschränken .AllowMultiSelect = False 'Thumbnail-Ansicht aktivieren .InitialView = msoFileDialogViewThumbnail 'Aktueller Datenbankpfad als Standardpfad .InitialFileName = strDatenbankpfad 'Titel festlegen .Title = "Bild auswählen" 'Filter hinzufügen .Filters.Add "Alle Bilddateien", "*.*", 1 .Filters.Add "Windows Bitmap", "*.bmp", 2 .Filters.Add "JPEG", "*.jpg; *.jpeg", 3 .Filters.Add "TIFF", "*.tif; *.tiff", 4 .Filters.Add "GIF", "*.gif", 5 'Wenn Auswahl getroffen, erstes und einziges Element ermitteln If .Show = -1 Then strDateiname = .SelectedItems(1) End If End With DateiAuswaehlen = strDateiname Set objFiledialog = Nothing End Function Listing 2.13: Auswählen der Bilddatei mit dem FileDialog-Objekt
Es gibt allerdings noch eine wesentlich kürzere Variante, um einen Dialog zur Auswahl einer Datei zu öffnen. Dazu verwenden Sie ein nicht dokumentiertes Objekt namens WizHook (ab Access 2000). Wie bei nicht dokumentierten Funktionen üblich, besteht keine Garantie, dass es diese in den nächsten Versionen von Access noch gibt, daher erfolgt der Einsatz auf eigene Gefahr: Private Function DateiAuswaehlen() As String Dim strDateiname As String WizHook.Key = 51488399
Bilder und Dateien in Tabellen speichern
125
Call WizHook.OpenPictureFile(strDateiname, False) DateiAuswaehlen = strDateiname End Function Listing 2.14: File-Dialog in aller Kürze dank versteckter Funktion
Nach der Auswahl der Quelldatei schreibt die Funktion cmdNeuesBild_Click deren Namen in das Feld Dateiname und speichert den aktuellen Datensatz zwischen, da sonst beim folgenden Schreibzugriff ein Konflikt auftritt. Die Funktion erzeugt ein Recordset-Objekt aus dem RecordsetClone des Formulars, stellt dessen aktuellen Datensatz auf den aktuellen Datensatz des Formulars ein und aktiviert den Bearbeitungsmodus für das Recordset-Objekt. Dann übergibt sie das Recordset-Objekt sowie den Dateinamen und den Namen des Zielfelds an die weiter oben vorgestellte Routine DateiInFeld, die für den Import der Datei in das angegebene Feld zuständig ist. Fehlt nur noch das Speichern des Datensatzes und das Anzeigen des Bildes – das wiederum erledigt die Funktion BildAktualisieren. Die Funktion speichert die Bilddatei als temporäre Datei im gleichen Verzeichnis wie die Datenbank und zeigt diese im Bildsteuerelement an: Private Sub BildAktualisieren() Dim strDateiname As String Dim strDateiendung As String 'Wenn das Feld Bild Daten enthält... If Not IsNull(Me!Bild) Then '...falls ja: 'Dateiname ermitteln strDateiname = Me.Dateiname 'Dateiendung ermitteln strDateiendung = Mid(strDateiname, InStrRev(strDateiname, ".") + 1) 'Feld in eine temporäre Datei mit der passenden Endung speichern FeldInDatei "pic." & strDateiendung, "Bild", Me.Recordset 'Bildsteuerelement mit der temporären Bilddatei füllen Me!ctlBild.Picture = Datenbankpfad & "pic." & strDateiendung Else '... wenn kein Bild gespeichert ist, Bildsteuerelement leeren Me!ctlBild.Picture = ""
126
2
Tabellen und Datenmodellierung
End If End Sub Listing 2.15: Die Prozedur BildAktualisieren füllt das Bildsteuerelement mit einer zuvor temporär gespeicherten Bilddatei.
Wenn Sie binär gespeicherte Bilder in einem Bericht anzeigen möchten, gehen Sie genauso wie in Kapitel 2.7.2, »Dateien nicht in der Datenbank speichern«, Abschnitt »Anzeige externer Bilddateien in Berichten«, vor. Sie müssen lediglich, wie soeben beschrieben, die Bilddatei vor der Anzeige aus der Datenherkunft auf die Festplatte kopieren. Mit der folgenden kleinen Prozedur, die beim Entladen des Formulars ausgelöst wird, sorgen Sie für die Beseitigung der Rückstände – sprich der temporären Bilddateien auf der Festplatte: Private Sub Form_Unload(Cancel As Integer) Kill CurrentProject.Path & "\pic.*" End Sub Listing 2.16: Entfernen temporärer Bilder von der Festplatte
Weitere Einsatzmöglichkeiten Aus den hier vorgestellten Funktionen zum Speichern von Dateien in der Datenbank und zum Extrahieren von in der Datenbank gespeicherten Dateien lassen sich weitere Anwendungen ableiten: Verwenden einer Access-Datenbank für die Verwaltung von Dokumenten, MP3Dateien, Bildern etc. Bereitstellen von Symbolbildern für Schaltflächen oder Symbolleisten der Datenbank (siehe auch Kapitel 10, »Menüleisten«) Integration von setup-ähnlichen Funktionen, etwa um für das Benutzen einer Anwendung notwendige Tools oder .dlls automatisch bereitzustellen. Auf diese Weise ließe sich auch leicht ein Anwendungssymbol mit einer Datenbank mitliefern, ohne mehr als die reine .mdb-Datei zu übergeben.
3 Abfragen Abfragen sind die Schnittstelle zwischen den in den Tabellen enthaltenen Daten und den für die Bearbeitung und Anzeige zuständigen Formularen und Berichten sowie für die Weiterverarbeitung per VBA. Genau wie Tabellen sollten die Benutzer einer professionellen Datenbank auch eine Abfrage nie zu Gesicht bekommen. Sie dient lediglich dazu, die Daten für die eigentliche Bearbeitung aufzubereiten. Dabei gibt es verschiedene Möglichkeiten: Einschränken der Felder einer Tabelle: Mit einer Abfrage können Sie die Felder auswählen, deren Inhalte als Abfrageergebnis angezeigt werden sollen. Einschränken der Daten einer Tabelle: Genau wie die Felder können Sie auch die anzuzeigenden Datensätze einer Tabelle per Abfrage einschränken. Dazu verwenden Sie ein geeignetes Kriterium. Zusammenführen der Daten verschiedener Tabellen: Nach der Normalisierung liegen Daten in vielen Fällen in mehreren, miteinander verknüpften Tabellen vor. In einer Abfrage können Sie die Felder der verknüpften Tabellen wieder zusammenführen und – in den meisten Fällen – wie eine einzige Tabelle verwenden. Zusammenführen der Daten gleichartiger Tabellen: Auch wenn man gleichartige Daten aus verschiedenen Tabellen benötigt – etwa die Namen der Mitarbeiter aus der Mitarbeitertabelle und diejenigen aus der Kunden-Tabelle –, hilft eine Abfrage weiter (siehe Abschnitt 3.3, »UNION-Abfragen«). Spezielle Aufbereitung von Daten: Mit Hilfe von Kreuztabellenabfragen lassen sich Daten wie beispielsweise die Verkaufszahlen von Produkten in bestimmten Zeiträumen in einer Art Matrix ausgeben. Berechnungen auf Basis der Felder der zugrunde liegenden Tabellen ausführen: Berechnete Werte in Tabellenfeldern sind bekanntlich tabu, da diese zu redundanten Daten führen. Daher gehören Berechnungen in Abfragen.
128
3
Abfragen
Abfragen werden Sie überall antreffen: Als Datenherkunft von Formularen und Berichten, als Datensatzherkunft von Kombinations- und Listenfeldern, als Datenquelle von Recordset-Objekten in VBA-Code und vielleicht auch als Bestandteil einer weiteren Abfrage. Weil Abfragen so wichtig sind, stellt Access eine mächtige Entwurfsansicht dafür bereit, die nur wenig Wünsche offen lässt. So lassen sich dort keine UNION-Abfragen oder PassThrough-Abfragen eingeben; dafür ist aber eine zusätzliche SQL-Ansicht vorhanden, in die man nicht nur diese Abfragen eingeben, sondern mit der man auch den SQL-Text der anderen Abfragen ausgeben kann. Wie Sie vielleicht zu Beginn des Buches gelesen haben, setzt dies grundlegende Kenntnisse im Umgang mit Access voraus. Daher finden Sie im Folgenden auch keine detaillierte Beschreibung für die Anwendung der Abfrage-Entwurfsansicht, sondern Informationen zu oft benötigten Vorgehensweisen im Zusammenhang mit Abfragen. Dabei findet gelegentlich ein Vorgriff auf die in Kapitel 8, »Access-SQL«, enthaltene umfassende Beschreibung der Abfragesprache SQL statt. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbank Kap_03\Abfragen.mdb.
3.1 Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft Formulare und Berichte beziehen ihre Daten aus der unter der Eigenschaft Datenherkunft angegebenen Tabelle oder Abfrage, bei Kombinations- und Listenfeldern heißt die entsprechende Eigenschaft Datensatzherkunft. Zum Füllen dieser beiden Eigenschaften gibt es verschiedene Techniken, die nachfolgend erläutert werden. Die einfachste ist das Setzen der entsprechenden Eigenschaft auf eine bestehende Tabelle oder Abfrage. Der Einsatz einer Tabelle ist dabei nur sinnvoll, wenn alle Felder und alle Datensätze der Tabelle benötigt werden. Wenn nicht alle Felder der Tabelle Verwendung finden oder nicht alle Datensätze angezeigt werden sollen, verwenden Sie eine Abfrage. Das kommt auch der Performance Ihrer Anwendung zu Gute. Die Herkunft der Daten heißt in Formularen und Berichten Datenherkunft und in Steuerelementen wie dem Kombinationsfeld oder Listenfeld Datensatzherkunft. Wenn es im Folgenden nicht explizit um die Datensatzherkunft solcher Steuerelemente geht, wird verallgemeinernd der Begriff Datenherkunft verwendet.
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft
129
Tabelle als Datenherkunft Die einfachste Art der Datenherkunft ist eine Tabelle. In vielen Fällen haben Tabellen aber mehr Felder oder enthalten mehr Datensätze als tatsächlich angezeigt werden sollen. Lookup-Tabellen, die nur aus einem Primärschlüsselfeld und einem weiteren Feld bestehen, können aber durchaus ohne Verwendung einer Abfrage eingesetzt werden – etwa als Datensatzherkunft von Kombinationsfeldern. Erst wenn die enthaltenen Daten auch noch sortiert werden sollen, ist eine Abfrage erforderlich. Beispiel für diese und die folgenden Möglichkeiten zur Bestückung von Formularen und Steuerelementen mit Datenherkünften ist ein Formular, das die Abwesenheit von Mitarbeitern nach Jahren filtert (siehe Abbildung 3.1).
Abbildung 3.1: Beispiel für das Zuweisen der Datensatzherkunft
Ein gutes Beispiel für die Verwendung einer Tabelle als Datensatzherkunft ist das Kombinationsfeld cboJahr. Voraussetzung für den Einsatz einer Tabelle ist, dass alle Felder benötigt werden und dass die Daten in der richtigen Reihenfolge vorliegen. Das ist hier der Fall: Das Feld JahrID dient als nicht sichtbares, gebundenes Feld und der Inhalt des Feldes Jahr wird im Kombinationsfeld angezeigt (siehe Abbildung 3.2).
SQL-Ausdruck als Datenherkunft Die zweite Möglichkeit sind reine SQL-Ausdrücke. Diese können statt des Namens der Tabelle oder der Abfrage für die Datenherkunft- oder Datensatzherkunft-Eigenschaft angegeben werden. In vielen Fällen geht es einfach schneller, wenn man mal eben eine kurze SQL-Anweisung für die entsprechende Eigenschaft einträgt, als wenn man zunächst eine Abfrage erstellt, diese speichert und dann die Abfrage als Wert der jeweiligen Eigenschaft einträgt. Im Beispielformular ist das Kombinationsfeld cboMitarbeiter mit einem solchen SQLAusdruck ausgestattet. Dieser lautet folgendermaßen: SELECT tblMitarbeiter.MitarbeiterID, [Nachname] & ", " & [Vorname] AS Mitarbeiter FROM tblMitarbeiter;
130
3
Abfragen
Abbildung 3.2: Tabelle als Datensatzherkunft
Dieser Ausdruck wurde über die Entwurfsansicht für Abfragen erstellt, aber nicht als Abfrage gespeichert. In diesem Fall wird der reine SQL-Ausdruck in die Eigenschaft Datensatzherkunft des Kombinationsfeldes eingetragen.
Gespeicherte Abfrage als Datenherkunft Die einfachste, weil ohne VBA- und SQL-Kenntnisse zu bewältigende und daher auch für Einsteiger geeignete Möglichkeit zur Erstellung einer Abfrage bietet die dafür vorgesehene Entwurfsansicht für Abfragen. Damit ist die Einschränkung der Datenherkunft sowohl bezüglich der Felder als auch der Datensätze möglich und auch die Verwendung von Parametern ist relativ einfach. Die Datensatzherkunft des Kombinationsfeldes cboMitarbeiter lässt sich natürlich ebenso mit einer gespeicherten Abfrage wie mit einem SQL-Ausdruck füllen. Um den SQLAusdruck in eine gespeicherte Abfrage zu überführen, klicken Sie einfach auf die Schaltfläche mit den drei Punkten (…) und speichern die nun in der Entwurfsansicht angezeigte Abfrage ab – beispielsweise unter dem Namen qryFrmAbwesenheitenCboMitarbeiter. Auf diese Weise erkennen Sie später schnell, wofür Sie diese Abfrage benötigen.
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft
131
Verwenden Sie nun besser einen SQL-Ausdruck oder eine gespeicherte Abfrage als Datenherkunft? Performancetechnisch betrachtet besteht kein großer Unterschied – in beiden Fällen wird die Abfrage beim ersten Aufruf kompiliert und damit optimiert (siehe auch Kapitel 12, Abschnitt 12.2.1, »Abfragen und die Jet-Engine«). Bleiben zwei Gründe, die für das Speichern der Abfrage sprechen: Entweder benötigen Sie die Abfrage an mehreren Stellen oder Sie möchten die Abfrage testen beziehungsweise optimieren, während das Formular in der Formularansicht angezeigt wird. In allen anderen Fällen scheint die Verwendung eines SQL-Ausdrucks nahe liegender, zumal die Abfragen-Ansicht des Datenbankfensters sonst ziemlich schnell unübersichtlich werden dürfte.
Datenherkunft per VBA zuweisen Die Eigenschaften Datenherkunft und Datensatzherkunft stehen auch unter VBA zur Verfügung – dort verwendet man die Eigenschaftsnamen RowSource und RecordSource. Sie können diesen Eigenschaften von Formularen sowie Kombinations- und Listenfeldern per VBA den Namen einer Tabelle oder Abfrage oder auch einen SQL-Ausdruck zuweisen. Was macht das für einen Sinn? Manchmal weiß man noch nicht genau, wie die anzuzeigenden Daten gestaltet sind. Das ist meist bei Suchformularen der Fall: Ein Formular bietet mehrere Such- und Sortierkriterien an, die der Benutzer mit den gewünschten Werten füllen kann. Man könnte die Kriterien einfach in Form von Parametern an die der Suche zugrunde liegende Abfrage übergeben, aber je nach Komplexität und Anzahl der enthaltenenzugrunde liegenden Tabellen gerät die Abfrage recht komplex. Eine Abfrage über die drei per m:n-Beziehung verknüpften Tabellen tblBestellungen, tblBestelldetails und tblArtikel, deren Suchkriterien sich über die äußeren Tabellen erstrecken, benötigt auch alle Tabellen der Abfrage. Soll eine Suche allerdings alle Artikel liefern, deren Suchkriterien sich lediglich auf die Tabellen tblBestelldetails und tblArtikel erstrecken, kann man die Tabelle tblBestellungen und die notwendige Verknüpfung und damit Zeit und Ressourcen sparen – vorausgesetzt, das Abfrageergebnis gibt keine darin enthaltenen Felder zurück. Hier würde dann eine VBA-Routine zum Einsatz kommen, die einen SQL-Ausdruck mit den benötigten Tabellen zusammensetzt.
Parameter statt Zusammensetzen von SQL-Ausdrücken Auch in anderen, einfacheren Fällen, in denen eine Abfrage lediglich aus einer einzigen Tabelle besteht und nur ein Parameter eingesetzt werden muss, verwenden viele Entwickler VBA, um einen SQL-String zusammenzusetzen und diesen als Datenherkunft zu verwenden. Das sieht dann beispielsweise so aus:
132
3 Private Dim Dim Dim
Abfragen
Sub ListenfeldAktualisieren() strSQL As String strSQLSelect As String strSQLWhere As String
'Basisabfrage (SELECT-Teil) strSQLSelect = "SELECT AbwesenheitID, StartDatum, EndDatum, " _ & " Abwesenheitsart FROM tblAbwesenheitsarten " _ & "INNER JOIN tblAbwesenheiten " _ & "ON tblAbwesenheitsarten.AbwesenheitsartID = " _ & "tblAbwesenheiten.Abwesenheitart " 'Erstes Kombinationsfeld auswerten If Not Nz(Me!cboMitarbeiter, 0) = 0 Then strSQLWhere = "MitarbeiterID = " & Me!cboMitarbeiter End If 'Zweites Kombinationsfeld auswerten If Not Nz(Me!cboJahr, 0) = 0 Then If Len(strSQLWhere) > 0 Then strSQLWhere = strSQLWhere & " AND " End If strSQLWhere = strSQLWhere & "Year(tblAbwesenheiten.StartDatum) = " _ & Me!cboJahr.Column(1) End If 'SELECT-Teil zum SQL-Ausdruck hinzufügen strSQL = strSelect 'Falls WHERE-Bedingung vorhanden, WHERE-Teil zum SQL-Ausdruck hinzufügen If Len(strSQLWhere) > 0 Then strSQL = strSQL & " WHERE " & strSQLWhere End If Debug.Print strSQL 'Neue Datensatzherkunft zuweisen und Listenfeld aktualisieren Me!lstAbwesenheiten.RowSource = strSQL Me!lstAbwesenheiten.Requery End Sub Listing 3.1: Datensatzherkunft für ein Listenfeld per zusammengesetztem SQL-Ausdruck ermitteln
Die Prozedur wird von den beiden Prozeduren aufgerufen, die durch die Ereigniseigenschaft Nach Aktualisierung der beiden Kombinationsfelder cboMitarbeiter und cboJahr ausgelöst werden. Die hier ermittelte SQL-Anweisung ist bei keiner Ausführung kompiliert.
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft
133
Eine Alternative ist die Verwendung einer gespeicherten Abfrage mit Parametern. Die Parameter, die normalerweise die Anzeige eines Dialogs zum Eingeben des Parameters hervorrufen, füllen Sie ebenfalls per VBA. Beim späteren Aufruf weisen Sie dem Listenfeld ein Recordset zu, das auf der kompilierten Abfrage inklusive Parametern basiert. Dies funktioniert übrigens nur unter Access XP und Access 2003. Unter Access 2000 und älteren Versionen von Access haben Kombinations- und Listenfelder noch keine Recordset-Eigenschaft. Die Abfrage sieht wie in Abbildung 3.3 aus. Die ersten vier Felder der Abfrage werden angezeigt, die letzten beiden sind lediglich Kriterienfelder. Als Kriterien dienen die per VBA zu füllenden Parameter [cboMitarbeiter] und [cboJahr]. Ersterer wird direkt mit dem Inhalt des Feldes MitarbeiterID verglichen, Letzterer mit dem Ausdruck, der durch die Anwendung der Jahr-Funktion auf dem Inhalt des Feldes StartDatum erzeugt wird. Dabei handelt es sich um die dem Datum entsprechende Jahreszahl.
Abbildung 3.3: Abfrage mit Parametern
Nun fehlt noch die Prozedur, mit der die Parameter per Code gefüllt werden und das Ergebnis der Abfrage dem Listenfeld zugewiesen wird. Diese Prozedur erstellt ein QueryDef-Objekt auf Basis der Abfrage qryFrmAbwesenheitLstAbwesenheitParameter. Dieses Objekt enthält eine Auflistung namens Parameters, mit der Sie die in der Abfrage gespeicherten Parameter referenzieren und die gewünschten Werte zuweisen können. Der Parameter [cboMitarbeiter] soll mit dem gebundenen Feld der Datensatzherkunft des Kombinationsfeldes cboMitarbeiter gefüllt werden, der Parameter [cboJahr] wird mit dem im Kombinationsfeld cboJahr angezeigten Wert bestückt. Beachten Sie, dass der angezeigte Wert nicht mit dem Wert des gebundenen Feldes übereinstimmt, sondern das Jahr und nicht dessen ID enthält!
134
3
Abfragen
Nach dem Füllen der Parameter wird die Abfrage mit der OpenRecordset-Methode ausgeführt und das Ergebnis in ein Recordset-Objekt geschrieben, das schließlich der entsprechenden Eigenschaft des Listenfeldes zugewiesen wird. Die Prozedur verwendet einige Objekte, Methoden und Eigenschaften der DAOBibliothek von Access. Detaillierte Informationen finden Sie in Kapitel 8, »DAO«. Private Dim Dim Dim
Sub ListenfeldAktualisierenParameter() db As DAO.Database qdf As DAO.QueryDef rst As DAO.Recordset
'Database- und Querydef-Objekt festlegen Set db = CurrentDb Set qdf = db.QueryDefs("qryFrmAbwesenheitenLstAbwesenheitenParameter") 'Parameter [cboMitarbeiter] mit der im Kombinationsfeld 'cboMitarbeiter ausgewählten MitarbeiterID füllen qdf.Parameters("cboMitarbeiter").Value = Me!cboMitarbeiter 'Parameter [cboJahr] mit dem im Kombinationsfeld angezeigten Jahr füllen qdf.Parameters("cboJahr").Value = Me!cboJahr.Column(1) 'Abfrage ausführen und Ergebnis in Recordset-Objekt ablegen Set rst = qdf.OpenRecordset 'Recordset der gleichnamigen Eigenschaft des Listenfelds zuweisen Set Me!lstAbwesenheiten.Recordset = rst Set rst = Nothing Set qdf = Nothing Set db = Nothing End Sub Listing 3.2: Listenfeld mit Parameterabfrage füllen
Der erste Test mit dieser Routine läuft nur zufrieden stellend, wenn in beiden Kombinationsfeldern ein Wert ausgewählt ist. Eigentlich soll das Listenfeld beim Öffnen des Formulars alle Abwesenheiten anzeigen, beim Auswählen eines Mitarbeiters ohne Jahr alle Abwesenheiten dieses Mitarbeiters für alle Jahre, um beim Auswählen lediglich eines Jahres alle Abwesenheiten dieses Jahres für alle Mitarbeiter zu berücksichtigen. Das funktioniert deshalb nicht, weil die Abfrage beispielsweise bei fehlender Auswahl des Mitarbeiters den Wert Null als Parameter übergibt. Und da es keine Mitarbeiter mit der MitarbeiterID Null gibt, werden auch keine Abwesenheiten angezeigt. Das Gleiche gilt für die Auswahl des Jahres.
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft
135
Die Verwendung von Null als Standardwert bei fehlender Eingabe eines Parameters ist übrigens nicht zwingend, sondern in diesem Fall durch die Verwendung des Standarddatentyps Variant bedingt. Sie können für einen Parameter in der Abfragedefinition durchaus andere Datentypen angeben; folglich werden dann auch die entsprechenden Standardwerte bei fehlendem Wert verwendet (etwa 0 bei Zahlentypen oder eine leere Zeichenkette bei String-Variablen). Sie müssen also dafür sorgen, dass die Parameter im Falle einer fehlenden Auswahl neutralisiert werden. Folgender Trick hilft dabei weiter: Fügen Sie in die Kriterienspalte der betroffenen Felder die folgenden abgewandelten Ausdrücke ein. Ein Datensatz wird angezeigt, wenn das Kriterium wahr ist – und das ist entweder bei passendem Parameterwert oder bei der Übergabe des Wertes Null der Fall: [cboMitarbeiter] Oder [cboMitarbeiter] Ist Null [cboJahr] Oder [cboJahr] Ist Null
Interessant ist, was Access nach dem Schließen und erneutem Öffnen aus den Kriterien macht. Abbildung 3.4 zeigt, wie Sie die Kriterien alternativ formulieren können.
Abbildung 3.4: Zwei harmlose Kriterienausdrücke nach der Überarbeitung durch Access
Abfragen mit Parameter oder zusammengesetzte SQL-Ausdrücke? Welche der beiden Varianten Sie verwenden, hängt von der Menge der Parameter ab. Je mehr Parameter vorkommen, desto langsamer wird die Abfrage und umso komplizierter wird der Abfrageentwurf. Wenn Sie sich den Abfrageentwurf aus Abbildung
136
3
Abfragen
3.4 ansehen und sich vorstellen, wie eine Abfrage mit vier oder mehr Parametern aussehen wird, können Sie sich vermutlich ausmalen, wie viel Spaß eine nachträgliche Änderung am Abfrageentwurf machen wird. Für Abfragen mit mehreren Parametern empfiehlt sich daher eher die Verwendung eines per VBA zusammengesetzten SQLAusdrucks.
3.1.1 Probleme mit Kriterienausdrücken bei SQL-Ausdrücken in VBA Viele Fehler bei der Verwendung von SQL-Ausdrücken unter VBA passieren im Zusammenhang mit den Kriterien. Mal meldet Access das Problem, dass zu wenig Parameter übergeben wurden (siehe Abbildung 3.5), ein anderes Mal funktionieren die Vergleiche mit übergebenen Datumsangaben nicht.
Abbildung 3.5: Fehlermeldung beim Verwenden einer SQL-Anweisung per VBA
Zeichenkette oder Zahlenwert? Der Fehler aus Abbildung 3.5 resultiert fast immer aus dem Fehlen von Anführungszeichen im SQL-Ausdruck beim Verwenden von Zeichenketten als Kriterium. Das folgende Listing zeigt einen solchen Fehler: Public Function MitarbeiterSuchen(strNachname As String) … Set rst = CurrentDB.OpenRecordset("SELECT MitarbeiterID, Vorname", "Nachname FROM tblMitarbeiter WHERE Nachname = " & strNachname) … End Function Listing 3.3: Falsche Verwendung einer Zeichenkette als Kriterium
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft
137
In dieses Listing wurden keine Anführungszeichen für die Zeichenkette integriert. Der SQL-Ausdruck sieht für den Aufruf MitarbeiterSuchen "Müller" wie folgt aus: SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = Müller
»Müller« wird hierbei nicht als Zeichenkette, sondern als Parameter ausgelegt. Da für diesen kein Wert vorliegt, erscheint obige Fehlermeldung. Die Lösung des Problems ist einfach: Fassen Sie den Parameter einfach in Anführungszeichen oder Hochkommata ein (in einer Zeile): Set rst = db.OpenRecordset("SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = '" & strNachname & "'")
oder Set rst = db.OpenRecordset("SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = """ & strNachname & """")
Probleme mit Datumsangaben Auch Datumsangaben führen immer wieder zu Problemen. Die folgende Routine soll beispielsweise Informationen über Abwesenheiten ausgeben, deren Beginn in einem bestimmten Zeitraum liegt, der durch die Parameter datStart und datEnde angegeben werden kann. Public Function AbwesenheitenZeitraum(datStart As Date, datEnde As Date) … Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE" "Startdatum BETWEEN " & datStart & " AND " & datEnde, dbOpenDynaset) … End Function Listing 3.4: Ermitteln von Abwesenheiten in einem bestimmten Zeitraum
Wenn Sie die Routine mit folgendem Aufruf starten, erscheint die Fehlermeldung aus Abbildung 3.6. Auf den ersten Blick scheinen hier die Anführungszeichen zu fehlen. AbwesenheitenZeitraum "1.1.2004", "31.1.2004"
Ändern Sie den Aufruf der SQL-Anweisung wie folgt um, gibt es allerdings eine andere Fehlermeldung (siehe Abbildung 3.7): Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE Startdatum BETWEEN '" & datStart & "' AND '" & datEnde & "'", dbOpenDynaset)
138
3
Abfragen
Abbildung 3.6: Fehlermeldung bei der Verwendung von Datumsangaben in SQL-Ausdrücken
Diesmal hat Access Probleme mit dem Datentyp – ein String scheint das Bedürfnis nach einem Wert des Typs DATETIME nicht zu befriedigen.
Abbildung 3.7: Auch die Zeichenkette taugt nicht als Datumskriterium.
Mit dem Wissen, dass Access Datumsangaben intern als Zahlenwerte behandelt, erscheint dies schnell logisch. Dabei gibt es zwei Möglichkeiten: Verwenden Sie den Datentyp Double, um Datumsangaben inklusive Uhrzeit zu verwalten, dann entspricht die Zahl vor dem Komma der Anzahl Tage seit dem 31.12.1899 und die Zahl nach dem Komma der Anzahl Sekunden, die am angegebenen Tag verstrichen sind. Für Datumsangaben ohne Uhrzeit reicht dementsprechend der Datentyp Long aus. Nun müssen Sie aber nicht alle Datumsangaben explizit in einen Zahlen-Datentyp umwandeln. Es reicht, wenn Sie ein standardisiertes Format verwenden. Dieses hat die Form yyyy-mm-dd. Zusätzlich fassen Sie diesen Ausdruck in der Abfrage in RauteZeichen (#) ein. In obigem Code sieht das Ganze dann wie folgt aus (in einer Zeile): Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE Startdatum BETWEEN #" & Format(strStart, "yyyy-mm-dd") & "# AND #" & Format(strEnde, "yyyy-mm-dd") & "#", dbOpenDynaset)
Das Formatieren des Datums und das Einfassen in Rauten lässt sich auch per Funktion erledigen:
Verwendung von Abfragen als Datenherkunft oder Datensatzherkunft
139
Public Function ISODatum(strDatum As String) As String ISODatum = Format(strDatum, "\#yyyy-mm-dd\#") End Function Listing 3.5: Funktion zum Standardisieren von Datumsangaben
Die Zeile aus obiger Routine sähe dann so aus (in einer Zeile): Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE Startdatum BETWEEN " & ISODatum(strStart) & " AND " & ISODatum(strEnde), dbOpenDynaset)
Verweis auf Steuerelemente Access-SQL erlaubt direkte Verweise auf Formulare und Steuerelemente. Das ist hilfreich, wenn Sie etwa den Feldinhalt eines Formulars als Abfragekriterium verwenden möchten. Eine solche Abfrage sieht beispielsweise wie in Abbildung 3.8 aus.
Abbildung 3.8: Abfrage mit einem Verweis auf ein Formularsteuerelement
Ein Blick in die entsprechende SQL-Anweisung zeigt, dass der Ausdruck tatsächlich im gleichen Format wie in VBA in die Abfrage integriert wurde: SELECT MitarbeiterID, Nachname, Vorname, AbteilungID FROM tblMitarbeiter WHERE AbteilungID=[Forms]![frmMitarbeiter]![cboAbteilungen];
Dies funktioniert sogar, wenn Sie mit anderen Backends wie etwa MySQL arbeiten. Beim Zusammensetzen von SQL-Abfragen via VBA bietet es sich allerdings an, die entsprechenden Ausdrücke direkt auszulesen und als festen Parameterwert in die Abfrage zu integrieren.
140
3
Abfragen
Die Verwendung einer fest in eine Abfrage eingebauten Referenz bringt den Nachteil mit sich, dass Sie diese Abfrage nicht einsetzen können, wenn Sie einen anderen als diesen Parameter verwenden möchten. Die Variante, eine Abfrage ohne Formularreferenzen zu erstellen und derartige Kriterien erst per VBA oder direkt in einer Datenherkunft- oder Datensatzherkunft-Eigenschaft zuzuweisen, ist flexibler.
3.2 Aktualisierbarkeit von Abfragen Je nachdem, wie die in einer Abfrage enthaltenen Tabellen beschaffen sind und wie Sie diese zusammensetzen, sind Abfragen nicht aktualisierbar, das heißt, dass Sie über die Datenblattansicht und damit auch über die Anzeige in Formularen keine Datensätze bearbeiten oder hinzufügen können. Es ist sicher jedem Access-Entwickler schon einmal passiert, dass er per VBA einen Datensatz einer Abfrage ändern oder hinzufügen wollte und eine entsprechende Fehlermeldung erschien, für die es scheinbar keine Erklärung gab.
Wie erkennen Sie, ob das Abfrageergebnis aktualisierbar ist? Wenn Sie eine Abfrage in der Datenblattansicht öffnen, erkennen Sie recht schnell, ob diese aktualisierbar ist – die Schaltfläche zum Springen auf einen neuen Datensatz ist ausgeblendet und unter dem letzten Datensatz befindet sich keine leere Zeile zum Anlegen eines neuen Datensatzes (siehe Abbildung 3.9).
Abbildung 3.9: Eine nicht aktualisierbare Abfrage erkennt man an der deaktivierten Schaltfläche »Neuer Datensatz« und an der fehlenden Zeile mit einem leeren neuen Datensatz.
Aktualisierbarkeit von Abfragen
141
Nicht aktualisierbare Abfragen Nachfolgend finden Sie Beispiele für Abfragen, die niemals aktualisierbar sind: Abfragen mit zwei nicht verknüpften Tabellen. Beispiele sind alle Abfragen, die alle Kombinationen aus den Datensätzen zweier Tabellen anzeigen sollen (siehe Abbildung 3.10). Abfragen mit drei oder mehr Tabellen, deren innere Tabelle die 1-Seite für die beiden äußeren Tabellen stellt. Beispiel: Sie möchten zu einem Mitarbeiter gleichzeitig die Abwesenheiten und den Urlaubsanspruch ausgeben (siehe Abbildung 3.11). Abfragen mit Gruppierungen und Aggregatfunktionen Abfragen, bei denen die Eigenschaft Keine Duplikate auf Ja eingestellt ist Sollte sich einmal eine Abfrage als nicht aktualisierbar erweisen, von der Sie es eigentlich erwarten, prüfen Sie diese zunächst auf die oben genannten Eigenschaften.
Abbildung 3.10: Diese Abfrage liefert kein aktualisierbares Ergebnis.
Abbildung 3.11: Auch diese Abfrage ist nicht aktualisierbar.
142
3
Abfragen
3.3 UNION-Abfragen UNION-Abfragen bieten die Möglichkeit, die Daten mehrerer gleichartig aufgebauter Tabellen mit einer Abfrage zu vereinen. Dazu erstellen Sie zwei oder mehr gleichartig aufgebaute Abfragen und verketten diese mit dem UNION-Schlüsselwort. Entscheidend ist, dass alle beteiligten Abfragen die gleiche Anzahl Felder haben und dass die jeweils an der gleichen Stelle befindlichen Felder den gleichen Datentyp besitzen. Haben die Daten verschiedene Datentypen, konvertiert Jet in den meisten Fällen beide in einen Variant-Wert und gibt anschließend einen String-Wert aus. Probleme gibt es hier, wenn GUIDs und andere Datentypen gemischt werden. Weitere Grundlagen zu UNION-Abfragen finden Sie weiter hinten in Kapitel 7, Abschnitt 7.2.9, »Zusammenfassen von Abfrageergebnissen mit UNION«.
3.3.1 UNION-Abfragen zur Optimierung von Kombinationsfeldern Sie können eine UNION-Abfrage beispielsweise dazu verwenden, Kombinationsfelder zu optimieren. Wenn Kombinationsfelder keinen Eintrag enthalten, zeigen diese ein leeres Feld an. Praktischer und eine eindeutige Aufforderung an den Benutzer wäre es, wenn Kombinationsfelder ohne Wert etwa die Zeichenkette anzeigen würden (siehe Abbildung 3.12).
Abbildung 3.12: Vorgefülltes Kombinationsfeld
Als Datensatzherkunft des Kombinationsfeldes dient dabei die folgende SQL-Abfrage, die Sie direkt in die SQL-Ansicht der Abfrage eingeben müssen: SELECT 0 AS AbteilungID, '' AS Abteilung FROM tblAbteilungen UNION SELECT AbteilungID, Abteilung FROM tblAbteilungen;
Dies ist ein gutes Beispiel für die Zweckentfremdung einer UNION-Abfrage, denn der aus dem ersten Teil der Abfrage stammende Wert ist eigentlich gar nicht in der Tabelle vorhanden. Deshalb gibt man dort nicht nur die Feldnamen, sondern die konkreten
UNION-Abfragen
143
Werte an. Der Übersicht halber versieht man die einzelnen Feldwerte noch mit dem AS-Schlüsselwort und fügt den eigentlichen Feldnamen hinzu. Letzteres ist aber nicht unbedingt notwendig. Wenn Sie einen Dummy-Datensatz wie im ersten Teil der obigen UNION-Abfrage benötigen, brauchen Sie in allein stehenden Tabellen nur den ersten Teil der Abfrage zu verwenden: SELECT 0 AS AbteilungID, '' AS Abteilung. Die Angabe einer Ursprungstabelle mit FROM tblAbteilungen ist nur in Zusammenhang mit UNION-Abfragen erforderlich. Wichtig ist bei diesem Beispiel, dass der im ersten Abfrageteil verwendete Wert für die gebundene Spalte der zukünftigen Datensatzherkunft kleiner ist als alle Werte, die aus der oder den anderen Tabellen noch hinzukommen. Anderenfalls lässt sich nur schwer eine sinnvolle Sortierung festlegen – es sei denn, man fügt noch ein individuelles Sortierfeld hinzu. Wenn Sie hingegen nach dem angezeigten Feld sortieren möchten, müssen Sie erstens das Sortierkriterium an den letzten Teil der UNION-Abfrage anhängen und zweitens dafür sorgen, dass der ohne Auswahl angezeigte Datensatz der erste unter der angegebenen Sortierung ist: SELECT 0 AS AbteilungID, '' AS Abteilung FROM tblAbteilungen UNION SELECT AbteilungID, Abteilung FROM tblAbteilungen ORDER BY Abteilung;
Da das Kleiner-Zeichen (<) im ASCII-Code vor den Buchstaben angeordnet ist, sind hier keine weiteren Maßnahmen erforderlich. Wollten Sie hingegen nur den Eintrag Auswählen ohne spitze Klammern verwenden, müssten Sie ein zusätzliches Sortierfeld anhängen, bei dem Sie für den ersten Teil der Abfrage einen Wert angeben, der auf jeden Fall vor allen anderen liegt. Außerdem legen Sie dieses Sortierfeld als ORDER BY-Kriterium fest: SELECT 0 AS AbteilungID, 'Auswählen' AS Abteilung, 'AAAA' AS Sortierung FROM tblAbteilungen UNION SELECT AbteilungID, Abteilung, Abteilung As Sortierung FROM tblAbteilungen ORDER BY Sortierung;
3.3.2 Eindeutige Schlüssel mit UNION-Abfragen Einen gravierenden Nachteil haben UNION-Abfragen: Wenn die zumeist aus mehreren Tabellen zusammengeführten Daten eindeutig identifiziert werden sollen, um etwa einen ausgewählten Datensatz zu löschen, ist »Hängen im Schacht«. Das Problem
144
3
Abfragen
ist, dass auch die Primärschlüssel aus mehreren Tabellen zusammengeführt werden und diese daher nicht zwangsläufig eindeutig sind, denn es können durchaus mehrere Tabellen etwa den Schlüsselwert 1 besitzen. Die einfachste Lösung ist die Verwendung von GUIDs als Primärschlüssel. Diese Werte sind nicht nur über mehrere Tabellen, sondern sogar weltweit eindeutig – sofern sie mit der entsprechenden Systemfunktion erzeugt wurden (weitere Informationen finden Sie in Kapitel 2, Abschnitt 2.5, »Autowerte als Long oder GUID?«). Selbst das Löschen eines Datensatzes, der aus einer UNION-Abfrage ausgewählt wurde, ist bei Vorhandensein einer GUID einfach: Löschen Sie einfach aus allen beteiligten Tabellen den Datensatz mit der betroffenen GUID – irgendwo werden Sie schon den richtigen treffen und falsche Datensätze löschen Sie damit auch nicht. Ohne GUID ist die eindeutige Identifikation von Daten aus UNION-Abfragen noch schwieriger. Als Beispiel dient die folgende Abfrage: SELECT KundeID, Vorname, Nachname FROM tblKunden UNION SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter;
Abbildung 3.13 zeigt auf, was passieren kann: Der Wert 3 kommt in je einem Datensatz der beteiligten Tabellen vor.
Abbildung 3.13: UNION-Abfragen garantieren keinen eindeutigen Index.
Um dies zu verhindern, verwenden Sie einen kombinierten Wert aus KundeID beziehungsweise MitarbeiterID und dem jeweiligen Tabellennamen. Die UNION-Abfrage sieht nun so aus: SELECT 'Kunde' & KundeID AS PersonID, Vorname, Nachname FROM tblKunden UNION SELECT 'Mitarbeiter' & MitarbeiterID AS PersonID, Vorname, Nachname FROM tblMitarbeiter;
Suchen in m:n-Beziehungen
145
Das Ergebnis überzeugt ebenfalls, die Datensätze verfügen nun über ein eindeutiges Feld (siehe Abbildung 3.14).
Abbildung 3.14: Eine UNION-Abfrage mit eindeutigem Feld
Gegenüber der Variante mit dem GUID-Wert als Primärschlüssel fällt diese Variante jedoch deutlich ab. Allein das Handling ist wesentlich aufwändiger: Wenn Sie etwa einen mit dieser UNION-Abfrage ausgewählten Datensatz löschen möchten, müssten Sie zunächst über den zusammengesetzten Schlüssel ermitteln, aus welcher Tabelle der Datensatz stammt, und diesen dann dort löschen. Daraus resultiert letzten Endes die Empfehlung, Daten, die gleich aufgebaut sind, auch in einer einzigen Tabelle zu speichern. Wenn die Daten gelegentlich unterschiedliche Eigenschaften aufweisen und daher unterschiedliche Felder benötigen, lässt sich dies über 1:1-Beziehungen vermutlich leichter realisieren.
3.3.3 INSERT INTO mit UNION-Abfragen Abfragen sind nun einmal nichts Beständiges und deshalb sind auch die mit einer UNION-Abfrage ermittelten Daten grundsätzlich erst einmal nicht zur Weitergabe geeignet. Jeglicher Versuch, die Daten per INSERT INTO-Abfrage einfach in eine temporäre Tabelle zu schreiben, schlägt fehl: SQL mag keine INSERT-Queries mit UNIONAbfragen als Datenherkunft (siehe Abbildung 3.15). Die Lösung ist, wie in Abbildung 3.16 die UNION-Abfrage zu speichern und die Anfügeabfrage auf diese Abfrage zugreifen zu lassen.
3.4 Suchen in m:n-Beziehungen Wenn Sie die Anleitungen zur Normalisierung in Kapitel 2 sorgfältig berücksichtigen, dann haben Sie beispielsweise eine Menge triviale Eigenschaften, zu denen jederzeit neue hinzukommen können, in eine m:n-Beziehung ausgelagert. Das dortige Beispiel bezog sich auf Fahrzeuge und ihre Ausstattungsmerkmale.
146
3
Abfragen
Abbildung 3.15: UNION-Abfragen in INSERT INTO-Statements funktionieren nicht.
Abbildung 3.16: INSERT INTO mit UNION-Abfrage
Nach diesem Schritt haben Sie zwar ein sauberes Datenmodell, aber dummerweise lässt sich nicht mehr so einfach per Abfrage ermitteln, welche Autos beispielsweise mit einer Klimaanlage und Servolenkung ausgestattet sind, aber etwa kein Navigationssystem besitzen. Hätte man in der Version mit allen Eigenschaften in einer einzigen Tabelle nur die entsprechenden Felder mit Kriterien wie True oder False ausstatten müssen, ist nun ein wenig mehr Aufwand notwendig.
Suchen von Rezepten mit bestimmten Zutaten Als Beispiel dient nun aber eine neue Variante: Die Zuordnung von Zutaten zu einzelnen Rezepten. Stellen Sie sich vor, was Sie alles anstellen könnten, wenn Sie nur noch in Kühl- und Vorratsschrank schauen, die dort vorhandenen Zutaten in ein Formular eingeben (ok, nicht alle, aber zumindest die, auf die Sie gerade Appetit haben) und dann umgehend alle Rezepte erhalten, die sich mit diesen Zutaten zaubern lassen.
Suchen in m:n-Beziehungen
147
Betrachtet man das Datenmodell aus Abbildung 3.17, fällt einem nur leider gerade kein Rezept ein, auf dessen Basis man die passende Abfrage basteln könnte.
Abbildung 3.17: Aufbau der Beziehung zwischen Rezepten und Zutaten
Mit einer Zutat jedenfalls funktioniert der Ansatz aus Abbildung 3.18. Das Ergebnis liefert alle Rezepte, welche die Zutat »Pfeffer« enthalten – das ist einfach.
Abbildung 3.18: Abfrage zur Ermittlung aller Rezepte mit einer bestimmten Zutat
Jetzt fügen Sie eine Zutat hinzu: Mal sehen, welche Rezepte übrig bleiben, wenn auch noch Salz enthalten sein soll. Nur wohin mit dem neuen Kriterium? Wenn man es ganz naiv als »Oder«-Kriterium in die Spalte Zutat einfügt, enthält das Abfrageergebnis mehr Datensätze als vorher – das war abzusehen und ist falsch. Schließlich sollte eine weitere Zutat die Ergebnismenge einschränken oder maximal gleich viele Ergebnisse zurückliefern. Also verwenden Sie als Kriterium nun ="Pfeffer" Und "Salz". Das Ergeb-
148
3
Abfragen
nis? Es enthält gar keine Datensätze mehr. Gibt es keine Rezepte, die Pfeffer und Salz enthalten? Doch, natürlich, aber es gibt bei keinem Rezept ein Zutatenfeld, das zwei Zutaten gleichzeitig enthält. Schauen Sie sich noch einmal genauer das Ergebnis der Variante mit »Oder« an (siehe Abbildung 3.19). Logischerweise sind dort alle Rezepte, die beide angegebenen Zutaten enthalten, zweimal aufgeführt; alle Rezepte, die nur eine der beiden Zutaten enthalten, findet man nur einmal.
Abbildung 3.19: Ergebnis der »Oder«-Variante der Zutaten-Suche
Damit lässt sich doch etwas anfangen. Gleiche Einträge in einer Abfrage lassen sich gruppieren und zu einer Gruppierung lässt sich auch die Anzahl der enthaltenen Datensätze ausgeben. Passen Sie also die Abfrage wie in Abbildung 3.20 an: 1. Blenden Sie mit dem Menüeintrag Ansicht/Funktionen bei aktivierter Entwurfsansicht die Funktionszeile ein. 2. Stellen Sie für das Feld RezeptID die Funktion Anzahl ein. 3. Das Feld Zutat dient nur als Bedingung und darf nicht angezeigt werden. Dazu deaktivieren Sie die Zeile Anzeigen und stellen als Funktion den Wert Bedingung ein. Das Ergebnis aus Abbildung 3.21 überzeugt: Die Rezepte, die beide Zutaten enthalten, werden mit der Anzahl 2 ausgegeben. Damit erhalten Sie nicht nur die Rezepte mit allen gewünschten Zutaten, sondern auch andere – und zwar nach der Qualität des Treffers sortiert.
Handhabung von 1:1-Beziehungen
149
Abbildung 3.20: Diese Abfrage sortiert die Rezepte nach der Anzahl der enthaltenen Wunschzutaten.
Abbildung 3.21: Rezepte und die Anzahl der enthaltenen Wunschzutaten
3.5 Handhabung von 1:1-Beziehungen In Kapitel 2, Abschnitt 2.4.7, »1:1-Beziehungen« haben Sie erfahren, wie Sie Daten von Tabellen in zwei oder mehr per 1:1-Beziehung verknüpfte Tabellen aufteilen. Offen ist noch, wie Sie mit solchen Tabellen arbeiten, wenn es um die Eingabe, das Bearbeiten und Löschen von Daten geht. Mit einer geeigneten Abfrage lassen sich die Daten in per 1:1-Beziehung verknüpften Tabellen genau so bearbeiten wie die Daten einer einzelnen Tabelle.
150
3
Abfragen
Als Beispiel dient die in Kapitel 2 vorgestellte Beziehung, bei der die erste Tabelle der 1:1-Beziehung Personendaten und die zweite Tabelle Erweiterungsdaten zu den in der ersten Tabelle enthaltenen Daten enthält (siehe Abbildung 3.22).
Abbildung 3.22: 1:1-Beziehung zwischen Personen auf der einen und Angestellten und freien Mitarbeitern auf der anderen Seite
Für die Bearbeitung ist lediglich interessant, ob Sie Personen mit der Ausprägung »Angestellter« oder mit der Ausprägung »freier Mitarbeiter« behandeln möchten. Wenn Sie die Daten der Angestellten bearbeiten wollen, verwenden Sie die in Abbildung 3.23 abgebildete Abfrage. Die Abfrage enthält alle Felder der beiden Tabellen mit Ausnahme des Primärschlüsselfeldes der Tabelle tblAngestellte. Auch das Fremdschlüsselfeld dieser Tabelle muss nicht angezeigt werden, aber zu Beispielzwecken sollten Sie es einbauen. In der Abbildung enthält die Abfrage aus Platzgründen nur die wichtigsten Felder der zugrunde liegenden Tabellen. Wenn Sie die Abfrage in der Datenblattansicht anzeigen, können Sie dort Daten wie in einer ganz normalen Tabelle einfügen. Dabei kann allerdings folgendes Problem entstehen: Geben Sie einmal nur Daten in Felder der Tabelle tblPersonen ein (also etwa Vorname und Nachname), schließen Sie die Abfrage und öffnen Sie diese erneut. Der Datensatz scheint verschwunden, zumindest ist er im Sinne der 1:1-Beziehung nicht vorhanden. Der Grund ist ganz einfach: Mit der getätigten Eingabe wird in der Tabelle tblAngestellte kein Datensatz angelegt und die Abfrage zeigt nur jene Kombinationen von Datensätzen der Tabellen tblPersonen und tblAngestellte an, bei denen der Inhalt des Feldes PersonID in beiden Tabellen gleich ist. Den Beweis liefert ein kurzer Blick in die Tabelle tblPersonen: Dort findet sich nämlich der frisch angelegte Datensatz, allein das Pendant in der Tabelle tblAngestellte fehlt.
Handhabung von 1:1-Beziehungen
151
Abbildung 3.23: Zusammenführen der Tabellen einer 1:1-Beziehung per Abfrage
Geben Sie nun einen kompletten Datensatz ein und füllen Sie die Felder von links nach rechts, werden Sie feststellen, dass das Feld PersonID der Tabelle tblAngestellte automatisch mit dem gleichen Wert wie in der Tabelle tblPersonen gefüllt wird, sobald Sie die Eingabe in eines der Felder der Tabelle tblAngestellte abgeschlossen haben (siehe Abbildung 3.24).
Abbildung 3.24: Das Eingeben von Daten
Wenn Sie zuerst ein Feld der Tabelle tblAngestellte füllen, erhält das Feld PersonID dieser Tabelle zunächst den Wert 0. Erst wenn Sie mindestens ein Feld der Tabelle tblPersonen gefüllt haben, erhalten beide PersonID-Felder den über die Tabelle tblPersonen generierten Autowert. Bleibt die letzte Möglichkeit: Sie füllen lediglich die Felder der Tabelle tblAngestellte und versuchen, den Datensatz zu speichern. Dies lässt Access nicht zu: Es weist mit einer entsprechenden Meldung darauf hin, dass zunächst ein passender Datensatz in der Tabelle tblPersonen angelegt werden muss (siehe Abbildung 3.25).
152
3
Abfragen
Abbildung 3.25: Diese Meldung erscheint, wenn Sie unvollständige Daten in eine Abfrage mit zwei per 1:1-Beziehung verknüpften Daten eingeben.
Wie sorgen Sie nun dafür, dass vor dem Speichern alle benötigten Daten eingegeben werden, ohne dass Access seine eigenen Meldungen anzeigt? In der Abfrage funktioniert dies gar nicht: Es besteht keine Möglichkeit, die Meldung aus Abbildung 3.25 abzufangen – weder durch Setzen der Eingabe erforderlich-Eigenschaft noch durch die Verwendung der Gültigkeitsregel. Das ist aber auch nicht schlimm, denn wie bereits erwähnt, sollen Tabellen und Abfragen ohnehin nicht zur direkten Dateneingabe verwendet werden. Und in den dafür vorgesehenen Formularen gibt es sowieso ganz andere Mittel. Mehr dazu erfahren Sie in Kapitel 4, Abschnitt 4.4.5, »1:1-Beziehungen«.
Behandlung von 1:1-Beziehungen mit ergänzenden Feldern Die oben vorgestellte 1:1-Beziehung dient dem »Vererben« der in der Tabelle tblPersonen gespeicherten Eigenschaften an speziellere Personentypen wie Angestellte oder freie Mitarbeiter. Es gibt auch 1:1-Beziehungen mit wesentlich weniger anspruchsvollem Hintergrund. Die 1:1-Beziehung aus Abbildung 3.26 dient beispielsweise nur dazu, einer Tabelle ein Drucken-Feld hinzuzufügen, ohne dass die Tabelle tatsächlich erweitert wird. Die passende Verknüpfung sieht wie in Abbildung 3.26 aus. Wenn Sie die beiden Tabellen so wie in Abbildung 3.27 in den Abfrageentwurf übernehmen, werden Sie nach dem Wechsel in die Datenblattansicht ein leeres Abfrageergebnis vorfinden (zumindest, wenn Sie noch keine Datensätze in der Tabelle tblMitarbeiterDrucken angelegt haben). Der Grund ist einfach: Die Abfrage zeigt nur Daten, für die in beiden Tabellen ein Datensatz vorliegt. Da die Tabelle tblMitarbeiterDrucken im Urzustand zunächst keine Daten enthält, ist das Abfrageergebnis noch leer. Leider lässt sich über diese Abfrage auch kein Datensatz zur Tabelle tblMitarbeiterDrucken hinzufügen. Um dies zu ermöglichen, ändern Sie in der Abfrage den Beziehungstyp wie in Abbildung 3.28 und erzeugen somit einen LEFT JOIN. Damit werden nun definitiv alle Datensätze der Tabelle tblMitarbeiter angezeigt, auch wenn es nicht für alle einen verknüpften Datensatz in der Tabelle tblMitarbeiterDrucken gibt.
Handhabung von 1:1-Beziehungen
153
Abbildung 3.26: 1:1-Beziehung zum Anfügen eines einzelnen Feldes
Abbildung 3.27: Abfrage mit Drucken-Feld in der Entwurfsansicht …
Nun sieht die Datenblattansicht der Abfrage schon wesentlich erfreulicher aus: Zu jedem Mitarbeiter wird das Drucken-Feld angezeigt, obwohl es eigentlich gar keine verknüpften Datensätze gibt. In der Tabelle tblMitarbeiterDrucken wird dann auch tatsächlich erst ein Datensatz angelegt, wenn Sie in der Abfrage einen Haken in das entsprechende Feld setzen (siehe Abbildung 3.29).
154
3
Abfragen
Abbildung 3.28: Ändern der Verknüpfungseigenschaften
Wenn Sie das Feld in der Abfrage wieder deaktivieren, bleibt der Datensatz in der Tabelle tblMitarbeiterDrucken allerdings vorhanden. Um Platz in der Datenbank zu schaffen, empfiehlt es sich daher, regelmäßig alle Datensätze aus der Tabelle tblMitarbeiterDrucken zu entfernen, deren Feld Drucken den Wert False enthält.
Abbildung 3.29: Das Drucken-Feld ist für alle Mitarbeiter verfügbar.
3.6 Extremwerte per Abfrage ermitteln Unterabfragen sind oft ein bewährtes Mittel, um Kriterien für Abfragen zu ermitteln – sei es zur Ermittlung nur eines oder auch mehrerer Kriterien (IN-Operator). Besonders interessant ist die Möglichkeit, in einer Unterabfrage auf den Inhalt des in der Hauptabfrage enthaltenen Datensatzes zuzugreifen. So lassen sich beispielsweise Informationen ermitteln, die auf eine Gruppe von Datensätzen bezogen sind.
Extremwerte per Abfrage ermitteln
155
Extremwert einer Gruppierung ermitteln Ein Beispiel sind Extremwerte von Gruppierungen. Wenn Sie in den Tabellen Artikel und Kategorien der Nordwind-Datenbank den teuersten Artikel einer Kategorie ermitteln möchten, ist das kein Problem. Die Abfrage aus Abbildung 3.30 hilft dann weiter – hier wird beispielsweise der höchste Preis eines Artikels der Kategorie »Getränke« ermittelt.
Abbildung 3.30: Ermitteln des maximalen Preises von Artikeln der Kategorie Getränke
Leider gibt die Abfrage nur den Preis, aber keine weiteren Informationen zum Namen des Artikels aus, da das Ergebnis über eine Gruppierung herbeigeführt wurde. Sie können noch nicht einmal das Abfrageergebnis als Unterabfrage einer Abfrage verwenden, die dann alle gewünschten Daten enthält, da diese Unterabfrage kein eindeutiges Feld enthielte.
Extremwert per TOP und ORDER BY Also tricksen Sie ein wenig: Verwenden Sie statt der Gruppierung eine nach dem Preis sortierte Abfrage und die TOP-Option der SELECT-Klausel, um nur den ersten und damit teuersten Datensatz zurückzugeben (siehe Abbildung 3.31).
156
3
Abfragen
Abbildung 3.31: Variante 2 zur Ermittlung des teuersten Artikels der Kategorie »Getränke«
Extremwerte per Unterabfrage Diese Abfrage liefert das richtige Ergebnis einschließlich Artikelinformationen zurück. Im nächsten Schritt sollen nun in einer Abfrage alle teuersten Artikel ihrer Kategorie ausgegeben werden – und hier kommen die korrelierten Haupt- und Unterabfragen zum Zuge. Zum besseren Verständnis findet noch ein Zwischenschritt statt: Die Abfrage aus Abbildung 3.32 enthält eine Unterabfrage zur Ermittlung des teuersten Artikels, dessen Detaildaten in der Hauptabfrage angezeigt werden.
Abbildung 3.32: Variante zum Ermitteln des teuersten Artikels über eine Unterabfrage
Datensätze mehrfach anzeigen
157
Extremwerte von Gruppierungen Von dieser Abfrage aus gelangen Sie mit wenigen Änderungen zu einer Variante, die die teuersten Artikel einer jeden Kategorie anzeigt. Dazu müssen Sie eine zusätzliche Beziehung zwischen der Haupt- und der Unterabfrage einbauen, die dazu führt, dass die Unterabfrage jeweils den teuersten Artikel der Kategorie des aktuellen Datensatzes des Hauptformulars zurückgibt. Hört sich kompliziert an, ist es aber nicht: Dazu müssen Sie lediglich die Tabellen in Haupt- und Unterabfrage mit einem Alias-Namen versehen und die Inhalte des Feldes Kategorie-Nr der beiden Abfragen gleichsetzen. Der SQL-Ausdruck dieser Abfrage sieht wie folgt aus: SELECT t1.[Artikel-Nr], t1.Artikelname, t1.Einzelpreis FROM Artikel AS t1 WHERE t1.[Artikel-Nr]=( SELECT TOP 1 t2.[Artikel-Nr] FROM Artikel AS t2 WHERE t1.[Kategorie-Nr] = t2.[Kategorie-Nr] ORDER BY t2.Einzelpreis DESC );
In der Entwurfsansicht sieht die Abfrage wie in Abbildung 3.33 aus. Das Ergebnis ist in Abbildung 3.34 abgebildet.
Abbildung 3.33: Korrelierte Haupt- und Unterabfrage zur Ermittlung des teuersten Artikels je Kategorie
3.7 Datensätze mehrfach anzeigen Für manche Aufgaben kann es erforderlich sein, Datensätze mehr als einmal auszugeben – das Paradebeispiel ist wohl die Ausgabe eines Berichtes mit Adressetiketten. Es gibt einige Lösungen, die dieses Problem per VBA-Code angehen. Es funktioniert aber auch mit einer ausgefeilten Abfrage. Voraussetzung ist, dass sich ein Feld mit der auszugebenden Anzahl in der Tabelle befindet.
158
3
Abfragen
Abbildung 3.34: Ergebnis der korrelierten Unterabfrage
Außerdem benötigen Sie eine Tabelle, die lediglich ein einziges Feld namens Anzahl enthält, das mit den Zahlen von 0 bis 10 (oder der maximalen Anzahl vorgesehener Exemplare) gefüllt ist (siehe Abbildung 3.35).
Abbildung 3.35: Die Kombination dieser beiden Tabellen liefert die im Feld Anzahl der Mitarbeitertabelle angegebenen Exemplare eines jeden Datensatzes.
Abbildung 3.36 zeigt, wie Sie die Tabellen zusammenführen müssen. Beim Hinzufügen der beiden Tabellen fügt Access gegebenenfalls eine Verknüpfung zwischen den gleichnamigen Feldern (Anzahl) hinzu, diese entfernen Sie umgehend wieder. Dadurch zeigt Access alle Kombinationen der Datensätze aus der ersten und der Datensätze aus der zweiten Tabelle an. Das sind natürlich ein paar zu viel: Daher schränken Sie die Ergebnismenge noch ein wenig ein. Ziehen Sie neben den auszugebenden Daten das Feld Anzahl der Tabelle tblAnzahl in das Entwurfsraster – nicht jedoch das der Mitarbeitertabelle (!) – und fügen Sie diesem Feld das Kriterium aus der Abbildung hinzu.
Datensätze mehrfach anzeigen
159
Somit enthält das Abfrageergebnis alle Kombinationen der Datensätze, für die das Feld Anzahl der Tabelle tblAnzahl kleiner als die in der Mitarbeitertabelle angegebene Anzahl ist.
Abbildung 3.36: Abfrage zur Anzeige einer bestimmten Anzahl je Datensatz
Abbildung 3.37 zeigt das nach dem Feld MitarbeiterID sortierte Abfrageergebnis.
Abbildung 3.37: Das Abfrageergebnis enthält jeden Datensatz in der gewünschten Anzahl. Die letzte Spalte zeigt den jeweiligen Wert des Feldes Anzahl der Tabelle tblAnzahl.
160
3
Abfragen
3.8 Nummerierung von Datensätzen Felder wie der Primärindex taugen in den seltensten Fällen zum Bereitstellen einer lückenlosen Nummerierung von Datensätzen. Erstens sorgt jeder gelöschte Datensatz für eine Lücke (sofern es sich nicht um den letzten Datensatz handelt) und zweitens sollte eine Nummerierung auch mal eine Umsortierung oder das Setzen eines Kriteriums mitmachen. Da hierzu eine gewisse Dynamik erforderlich ist, funktioniert dies nur in Verbindung mit einer Abfrage auf Basis der zu nummerierenden Tabelle. Die Abfrage verwendet einen Ausdruck, der die Anzahl der vor dem aktuellen Datensatz liegenden Datensätze berechnet (siehe Abbildung 3.38). Das Ergebnis der Abfrage finden Sie in Abbildung 3.39.
Abbildung 3.38: Nummerieren von Daten ohne Sortierung und Gruppierung
Abbildung 3.39: Nummerierte Datensätze per Abfrage
Nummerierung von Datensätzen
161
Alternative: Nummerieren per Unterabfrage Die gleiche Abfrage können Sie mit einem anderen Nummerierungsfeld ausstatten. Diesmal verwenden Sie eine Unterabfrage: Nummer: (SELECT Count(*) FROM Artikel As Temp WHERE Temp.[Artikel-Nr] < Artikel.[Artikel-Nr])+1
Ausnahmsweise ist die Alternative mit der Domänen-Funktion mal nicht die langsamere. Die Variante mit DCount ist je nach Datensatzmenge bis zu ca. 10% schneller als die Version mit der Unterabfrage.
Nummerierung von Abfrageergebnissen mit alternativen Sortierungen Wenn die Daten nach einem anderen Feld als dem Primärschlüsselfeld der Tabelle sortiert werden sollen, geben Sie im WHERE-Teil der Unterabfrage einfach auf beiden Seiten der Bedingung das gewünschte Feld an und legen die Sortierung auch in der Hauptabfrage fest.
Nummerierung von Abfrageergebnissen mit eingeschränkten Ergebnismengen Wenn das zu nummerierende Abfrageergebnis Kriterien enthält, fügen Sie diese auch der Unterabfrage hinzu. Um alle Artikel, deren Artikelname mit »C« beginnt, zu nummerieren, verwenden Sie die Abfrage aus Abbildung 3.40. Der dort aus Platzgründen nicht vollständig abgebildete Nummer-Ausdruck lautet folgendermaßen: Nummer: (SELECT Count(*) FROM Artikel As Temp WHERE (Temp.Artikelname LIKE "C*") AND (Temp.[Artikelname] < Artikel.[Artikelname]))+1
Abbildung 3.40: Nummerierung einer sortierten und eingeschränkten Abfrage
162
3
Abfragen
3.9 Reflexive 1:n-Beziehungen In Kapitel 2, Abschnitt 2.4.8, »Reflexive Beziehungen« haben Sie die Grundlagen zu reflexiven Abfragen erhalten. Die Anzeige und Behandlung der Daten reflexiver Beziehungen mit Abfragen ist relativ eingeschränkt, da es sich bei Access-SQL nicht um eine prozedurale Sprache handelt. Die meisten Aufgaben in Zusammenhang mit reflexiven Beziehungen lassen sich nur mit VBA und passenden rekursiven Funktionen erledigen. Für einige Aufgaben ist jedoch auch eine Abfrage geeignet. Wenn Sie etwa ermitteln möchten, in welcher Ebene der Hierarchie sich ein Mitarbeiter befindet, können Sie die Abfrage aus Abbildung 3.41 verwenden. Wichtig ist dabei, dass Sie jeweils einen LEFT JOIN zwischen den Tabellen einrichten. Das Feld Ebene enthält den folgenden Ausdruck: Ebene: Anzahl([tblMitarbeiterVorgesetzter].[VorgesetzterID]) +Anzahl([tblMitarbeiterVorgesetzter_1].[VorgesetzterID]) +Anzahl([tblMitarbeiterVorgesetzter_2].[VorgesetzterID]) +1
Die Abfrage geht von der Tabelle tblMitarbeiterVorgesetzter aus. Die weiteren Tabellen sind Kopien der ersten Tabelle, die Sie durch wiederholtes Ziehen der Tabelle tblMitarbeiterVorgesetzter zum Abfrageentwurf hinzufügen. Jeder Anzahl(…)-Ausdruck ermittelt, ob das Feld VorgesetzterID in den verknüpften Tabellen einen Wert enthält. Ist das der Fall, liefert die Anzahl-Funktion den Wert 1.
Abbildung 3.41: Ermittlung der Anzahl der Hierarchieebenen eines Mitarbeiters
Reflexive m:n-Beziehungen
163
Abbildung 3.42 hilft beim Verständnis dieser Abfrage. Die letzten drei Spalten sind im Abfrageentwurf nicht zu sehen. Sie enthalten den direkten Vorgesetzten, den Vorgesetzten dieses Vorgesetzten und dessen Vorgesetzten.
Abbildung 3.42: Hierarchieebene der Mitarbeiter und Anzeige der jeweiligen Vorgesetzten
Der Nachteil solcher Abfragen ist, dass Sie diese in der Regel schlecht statisch anlegen können, da Sie nie wissen, ob nicht einmal eine Ebene hinzukommt oder wegfällt. Daher sollten Sie Vorgänge im Zusammenhang mit reflexiven Beziehungen in der Regel mit VBA und rekursiven Funktionen durchführen.
3.10 Reflexive m:n-Beziehungen Mit reflexiven m:n-Beziehungen verhält es sich genauso wie mit 1:n-Beziehungen. Am flexibelsten lassen sich die enthaltenen Daten auswerten, wenn Sie dies mit rekursiven Funktionen unter VBA erledigen.
4 Formulare Formulare sind das wichtigste Element der Benutzungsoberfläche von Access-Anwendungen. Sie sorgen dafür, dass der Benutzer bei der Dateneingabe nicht mit ellenlangen Tabellen oder Abfragen mit Tausenden von Datensätzen hantieren muss, sondern die Daten übersichtlich dargestellt bekommt – je nach Anforderung als Liste oder Detailansicht. Mit Kombinationsfeldern, Listenfeldern und Unterformularen lassen sich auch die Daten verknüpfter Tabellen problemlos darstellen. Der große Vorteil von Access-Formularen gegenüber denen von Visual Basic oder den .NET-Programmiersprachen liegt in den speziellen Funktionen für die Verwendung mit Tabellen und Abfragen als Datenherkunft. Diese Techniken sind mit dafür verantwortlich, dass Access heute das am weitesten verbreitete Datenbank-Managementsystem für Windows-Rechner ist. Abgerundet wird dies durch die Möglichkeit, Formulare und die enthaltenen Steuerelemente zu programmieren und dabei die zahlreichen Ereignisse zu nutzen. In diesem Kapitel erfahren Sie, wie Sie Daten auf Basis einzelner oder verknüpfter Tabellen in Abfragen darstellen können und wie Sie VBA zur Realisierung wichtiger Funktionen wie etwa zur Suche von Datensätzen verwenden. Da alle beschriebenen Funktionen mehr oder weniger den Einsatz von VBA und Ereigniseigenschaften von Formularen und Steuerelementen benötigen, finden Sie zunächst eine Beschreibung der wichtigsten Ereignisse und deren Abfolge für das Formular selbst und einige Steuerelemente. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbank Kap_04\Formulare.mdb.
4.1 Formulare öffnen Formulare öffnen Sie in der Regel mit der DoCmd.OpenForm-Anweisung. Es geht zwar auch über das Instanzieren des entsprechenden Formularobjekts und anschließendes
166
4
Formulare
Einstellen der Eigenschaft Visible auf den Wert True, das ist aber eher Spezialfällen vorbehalten (siehe Kapitel 14, Abschnitt 14.2.3, »Öffnen mehrerer Instanzen eines Formulars«). Da die DoCmd.OpenForm-Anweisung allgemein bekannt sein dürfte (ansonsten liefert die Online-Hilfe einen guten Überblick), soll sie hier nicht im Detail beschrieben werden. Im Hinblick auf die nachfolgenden Beispiele sei nur erwähnt, dass die dortigen Aufrufe »benannte Parameter« verwenden. Das bedeutet, dass die optionalen Parameter nicht in der vorgegebenen Reihenfolge durch Kommata getrennt an die Anweisung angefügt werden, sondern unter Angabe des jeweiligen Parameternamens: DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog
Mit einer herkömmlichen Parameterliste hätte diese Anweisung so ausgesehen: DoCmd.OpenForm "frmKontakteDetailansicht", , , , acFormAdd, acDialog
4.2 Ereignisse in Formularen und Steuerelementen Formulare lassen sich wie auch Berichte bis zu einem gewissen Grad ohne den Einsatz von Ereigniseigenschaften und VBA verwenden. Sobald Sie aber auch nur eine OKSchaltfläche hinzufügen möchten, ist es so weit: Die Programmierung der ersten Ereignisprozedur steht an. In diesem Abschnitt erfahren Sie, welche der zahlreichen Ereignisse von Formularen und den enthaltenen Steuerelementen oft zum Einsatz kommen und wie Sie diese optimal einsetzen – dazu gehört auch, dass Sie sich mit der Reihenfolge der Ereignisse vertraut machen.
4.2.1 Ereignisse in Formularen Formulare bieten eine unüberschaubare Menge Ereignisse. Gerade seit Access 2003 sind noch einige Eigenschaften hinzugekommen. Die meisten neuen Ereignisse hängen mit der Anwendung der Web Components und der Pivot-Ansichten zusammen, die in diesem Buch leider nicht vorgestellt werden können. Im Rahmen dieses Kapitels können leider nur die wichtigsten Ereignisse besprochen werden. Für Informationen zu speziellen Ereignissen klicken Sie einfach in das Feld der gewünschten Eigenschaft im Eigenschaftsfenster und betätigen anschließend die Taste (F1). Die Online-Hilfe steht Ihnen dann mit Rat und Tat zur Seite.
Ereignisse in Formularen und Steuerelementen
167
Anlegen eines Ereignisses Abbildung 4.1 zeigt die Ereigniseigenschaften von Formularen – hier aus Platzgründen auf zwei Fenster verteilt.
Abbildung 4.1: Übersicht der Ereigniseigenschaften eines Formulars
Zum Anlegen eines Ereignisses klicken Sie doppelt in das entsprechende Textfeld, sodass dieses den Wert [Ereignisprozedur] erhält, und betätigen dann die Schaltfläche mit den drei Punkten (…) wie in Abbildung 4.2. Wenn Sie es noch einfacher haben möchten, aktivieren Sie auf der Registerseite Formulare/Berichte des Optionen-Dialogs (Menüeintrag Extras/Optionen) die Option Ereignisprozedur immer verwenden. Dann müssen Sie nur noch auf die beim Aktivieren des Feldes erscheinende Schaltfläche klicken, um die Ereignisprozedur anzulegen. Wenn Sie dies beispielsweise mit der ersten aufgeführten Ereigniseigenschaft Beim Anzeigen machen, öffnet sich der VBA-Editor und zeigt direkt den Rumpf der gewünschten Prozedur an:
168
4
Formulare
Abbildung 4.2: Anlegen einer Ereignisprozedur
Private Sub Form_Current() End Sub Listing 4.1: Leerer Rumpf einer frisch angelegten Ereignisprozedur
Neben den Ereignisprozeduren können Sie auch Access-Makros und Funktionen per Ereignis auslösen. Dazu tragen Sie einfach den Namen des Access-Makros oder der Funktion ein. Access-Makros bleiben in diesem Buch weitgehend außen vor; der Einsatz von Funktionen kann jedoch durchaus sinnvoll sein. So können Sie beispielsweise für OK-Schaltflächen eine globale Funktion schreiben, die eine Anweisung zum Schließen des Formulars enthält, oder eine Funktion im Formularmodul anlegen, die durch mehrere Ereignisse des gleichen Formulars ausgelöst wird.
Ereigniseigenschaft und Ereignisprozedur – ein unzertrennliches Paar Damit ein Ereignis durch die entsprechende Aktion – also etwa Betätigen einer Schaltfläche – ausgelöst wird, muss die Ereigniseigenschaft den Eintrag [Ereignisprozedur] enthalten und eine Ereignisprozedur mit dem für dieses Ereignis vorgesehenen Namen im Klassenmodul des Formulars oder des Berichts vorliegen – beispielsweise cmdOK_Click.
4.2.2 Abfolge und Bedeutung der Ereignisse beim Öffnen und Schließen eines Formulars Besonders wichtig beim Umgang mit Ereigniseigenschaften ist die Reihenfolge ihrer Abarbeitung und der Zusammenhang mit den im Hintergrund ausgelösten internen Prozessen wie Laden der angezeigten Daten, Übergeben von Öffnungsargumenten und dergleichen in gebundenen Formularen.
Ereignisse in Formularen und Steuerelementen
169
Selbst wenn Sie eine Access-Anwendung völlig ohne Dokumentation programmieren müssen, können Sie sich hier selbst weiterhelfen: Schreiben Sie einfach für alle Ereigniseigenschaften, die Sie interessieren, eine kleine Prozedur, die den jeweiligen Ereignisnamen ausgibt. Wenn Sie dann das Formular öffnen oder die gewünschte Aktion durchführen, können Sie anschließend oder währenddessen beobachten, in welcher Reihenfolge die Ereignisse ablaufen. Wenn Sie beispielsweise herausfinden, welche Sequenz von Ereignissen beim einfachen Öffnen und Schließen eines Formulars abläuft, legen Sie die Ereignisprozeduren aus folgendem Listing an: Private Sub Form_Activate() Debug.Print "Beim Aktivieren" End Sub Private Sub Form_Close() Debug.Print "Beim Schließen" End Sub Private Sub Form_Current() Debug.Print "Beim Anzeigen" End Sub Private Sub Form_Deactivate() Debug.Print "Beim Deaktivieren" End Sub Private Sub Form_Load() Debug.Print "Beim Laden" End Sub Private Sub Form_Open(Cancel As Integer) Debug.Print "Beim Öffnen" End Sub Private Sub Form_Resize() Debug.Print "Beim Größenänderung" End Sub Private Sub Form_Unload(Cancel As Integer) Debug.Print "Beim Entladen" End Sub Listing 4.2: Ausgabe von Meldungen beim Auslösen unterschiedlicher Ereignisprozeduren
170
4
Formulare
Als Ergebnis erhalten Sie beim Öffnen des Formulars folgende Abfolge: Beim Öffnen: Tritt beim Öffnen ein. Bietet die Möglichkeit, das Öffnen abzubrechen, wenn beispielsweise keine Daten vorhanden sind – diese werden dementsprechend bereits vor dem Beim Öffnen-Ereignis eingelesen. Beim Laden: Tritt ein, wenn das Formular einschließlich Steuerelementen geöffnet ist. Eignet sich für Aktionen wie Setzen von Standardwerten oder Werten von Steuerelementen. Bei Größenänderung: Wird beim Öffnen und bei der Änderung der Größe des Formulars ausgelöst. Kann zum Anpassen der Größe von Steuerelementen an die Formulargröße verwendet werden. Beim Aktivieren: Wird ausgelöst, wenn das Formular den Fokus erhält. Beim Anzeigen: Wird beim Anzeigen, bei jedem Datensatzwechsel und beim Aktualisieren ausgelöst. Beim Schließen sieht der Ablauf so aus: Beim Entladen: Wird durch einen Klick auf eine Schließen-Schaltfläche oder die DoCmd.Close-Anweisung ausgelöst, findet aber vor dem eigentlichen Schließen statt. Der Schließen-Vorgang kann hier unterbrochen werden. Beim Deaktivieren: Wird ausgelöst, wenn das Formular den Fokus verliert. Beim Schließen: Wird im Moment des Schließens ausgelöst.
4.2.3 Abfolge und Bedeutung der Ereignisse beim Bearbeiten von Datensätzen Das Bearbeiten eines Datensatzes beginnt mit der ersten Änderung an einem der Felder und endet mit dem Speichervorgang des Datensatzes.
Ändern von Feldinhalten Änderungen von Datensätzen beginnen mit dem Ändern des Inhalts mindestens eines Feldes. Dies löst – je nachdem, ob es sich um einen neuen Datensatz handelt – ein oder zwei der folgenden Ereignisse aus: Vor Eingabe: Wird vor der Eingabe des ersten Zeichens ausgelöst, aber nur in Verbindung mit neuen Datensätzen. Kann abgebrochen werden. Bei Geändert: Wird beim ersten Eingeben oder Löschen eines Zeichens in einem der Datensätze ausgelöst. Kann abgebrochen werden.
Ereignisse in Formularen und Steuerelementen
171
Speichern der Änderungen Wird die Änderung gespeichert, werden in der Regel die folgenden drei Ereignisse ausgelöst. Das erste Ereignis Vor Aktualisierung kann den Ablauf allerdings abbrechen, etwa wenn eine Validierung fehlschlägt. Vor Aktualisierung: Wird nach dem Auslösen des Speicherns, aber vor dem eigentlichen Speichervorgang ausgelöst. Ist ein gängiger Platz für die Durchführung von Validierungen. Folgendes Beispiel zeigt, wie Sie die Aktualisierung abbrechen, wenn bestimmte Daten nicht vorhanden sind (weitere Informationen zum Validieren siehe Abschnitt 4.7.2, »Validieren vor dem Speichern«): If IsNull(Me!Telefon) And IsNull(Me!EMail) Then MsgBox "Bitte geben Sie eine Telefonnummer oder eine " _ & "E-Mail-Adresse ein.", vbExclamation + vbOKOnly, _ "Fehlende Daten" Me!Telefon.SetFocus Cancel = True End If
Nach Aktualisierung: Wird nach dem Speichern des Datensatzes ausgelöst. Nach Eingabe: Wird nach dem Speichern eines neuen Datensatzes ausgelöst; gilt nicht für bestehende Datensätze. Beim Anzeigen: Wird beim Wechseln beziehungsweise Speichern des Datensatzes aufgerufen.
Abbruch des Änderungsvorgangs Neben dem Speichern bietet sich nach Änderungen an Feldern eines Datensatzes auch die Möglichkeit, den Vorgang abzubrechen und die vorgenommenen Änderungen zu verwerfen. Dies löst das folgende Ereignis aus: Bei Rückgängig: Wird beim Abbrechen einer Datensatzänderung ausgelöst. Das Ereignis kann abgebrochen werden.
Löschen eines Datensatzes Das Löschen von Datensätzen ist keine triviale Geschichte. Wenn Sie auf eine LöschenSchaltfläche klicken, ist der Datensatz noch lange nicht gelöscht. Zunächst wird die Anzeige aktualisiert, die auf einem temporären zwischengespeicherten Datenbestand basiert, und dann noch abgewartet, ob die Ereignisse Bei Löschbestätigung oder Nach Löschbestätigung Änderungen bringen. Erst dann überträgt Access die Änderungen in die Tabellen der Datenbank.
172
4
Formulare
Beim Löschen: Wird beim Aufrufen des Löschvorgangs und unmittelbar vor dem Löschen ausgelöst. Wenn mehrere Datensätze gleichzeitig gelöscht werden, wird dieses Ereignis für jeden Datensatz einmal aufgerufen. Beim Anzeigen: Wird nach dem Löschen, aber vor dem Übernehmen der Änderungen in die Tabelle ausgelöst. Bei Löschbestätigung: Wird nur ausgelöst, wenn Access eine Bestätigung der Datensatzänderung anzeigt – diese Option lässt sich im Dialog aus Abbildung 4.3 einstellen. Die Bestätigungsmeldung kann unterbunden und durch eine eigene Meldung ersetzt werden. Beispiel: If MsgBox("Möchten Sie den Datensatz wirklich löschen?", _ vbExclamation + vbOKCancel, _ "Löschbestätigung") = vbCancel Then Cancel = True End If Response = acDataErrContinue
Nach Löschbestätigung: Wird nur ausgelöst, wenn eine angezeigte Bestätigung einer Datensatzänderung auch bestätigt wurde beziehungsweise wenn der Löschvorgang nach einer benutzerdefinierten Meldung im Ereignis Bei Löschbestätigung nicht abgebrochen wurde.
Abbildung 4.3: Aktivieren der Anzeige einer Meldung beim Ändern von Datensätzen
Ereignisse von Steuerelementen
173
4.3 Ereignisse von Steuerelementen Neben dem Formular selbst lösen auch die Steuerelemente Ereignisse aus. Nachfolgend finden Sie einige wichtige Ereigniseigenschaften von Steuerelementen.
Unterformulare Wenn Sie mit Unterformularen arbeiten, sollten Sie wissen, in welcher Reihenfolge die Ereignisse beim Öffnen und Schließen von Unterformularen in Bezug auf die Ereignisse des Hauptformulars ausgelöst werden. Die Abfolge sieht folgendermaßen aus: Unterformular Beim Öffnen Unterformular Beim Laden Unterformular Bei Größenänderung Unterformular Beim Anzeigen Hauptformular Beim Öffnen Hauptformular Beim Laden Hauptformular Bei Größenänderung Hauptformular Bei Aktivierung Hauptformular Beim Anzeigen Das Unterformular wird in der Tat komplett vor dem Hauptformular geladen. Lediglich das Filtern der Datensätze des Unterformulars in Abhängigkeit von den für das Hauptformular vorgesehenen Datensätzen erfolgt noch vor dem Beim Öffnen-Ereignis des Unterformulars. Beim Schließen des Formulars wird zuerst das Hauptformular und dann das Unterformular geschlossen: Hauptformular Beim Entladen Hauptformular Bei Deaktivierung Hauptformular Beim Schließen Unterformular Beim Entladen Unterformular Beim Schließen
174
4
Formulare
Textfelder Beim Setzen der Einfügemarke in ein Textfeld, beim Ändern des Inhalts und beim anschließenden Verlassen treten die folgenden Ereignisse auf: Beim Hingehen: Beim Eintreten in das Feld Bei Fokuserhalt: Bei Geändert: Beim Ändern des ersten Zeichens. Stellt die Eigenschaft Dirty auf den Wert True ein. Bei Änderung: Beim Ändern jedes Zeichens Vor Aktualisierung: Bei jeder Aktion, die zum Verlassen des Feldes führt. Dieses Ereignis bietet die Möglichkeit, feldbezogene Validierungen durchzuführen und die Aktualisierung abzubrechen. Beispiel: If IsNumeric(Left(Me.Projekt, 1)) Then MsgBox "Der Projektname darf nicht mit einer Zahl " _ & "beginnen.", vbOKOnly + vbExclamation, _ "Fehlerhafte Eingabe" Cancel = True End If
Nach Aktualisierung: Nach dem Ereignis Vor Aktualsisierung, wenn dieses nicht abgebrochen wurde. Bietet die Möglichkeit, den eingegebenen Wert weiter zu verwerten oder zu ändern. Beim Verlassen: Nach Abarbeitung der Ereignisse Vor Aktualisierung und Nach Aktualisierung Bei Fokusverlust: Beim Setzen des Fokus auf ein anderes Steuerelement In Zusammenhang mit diesen Ereignissen sind drei Eigenschaften von Textfeldern wichtig: Value, OldValue und Text. OldValue enthält während des ganzen Änderungsvorgangs den vorherigen Wert des Feldes. Text enthält den aktuell im Textfeld angezeigten Wert und Value den alten Wert, bis es mit dem Auslösen des Ereignisses Vor Aktualisierung den aktuellen Inhalt des Feldes beziehungsweise der Eigenschaft Text zugewiesen bekommt.
Kombinationsfelder Das Auswählen eines Eintrags und das anschließende Verlassen eines Kombinationsfeldes löst die folgenden Ereignisse aus (wenn dieses noch nicht den Fokus hat). In runden Klammern finden Sie Ereignisse, die nur bei der manuellen Eingabe von Zeichen ausgeführt werden, in eckigen Klammern die Ereignisse, die nur bei manueller Eingabe nicht vorhandener Listeneinträge ausgelöst werden.
Abbildung verschiedener Beziehungsarten
175
Beim Hingehen Bei Fokuserhalt Bei Änderung: Beim Auswählen eines Eintrags oder bei der ersten Eingabe eines Zeichens (Bei Geändert: Bei der manuellen Eingabe von Zeichen) [Bei nicht in Liste: Nach der manuellen Eingabe eines Wertes, der nicht in der Liste enthalten ist. Hier können Sie eine Prozedur hinterlegen, die nicht in der Liste enthaltene Einträge in der zugrunde liegenden Datensatzherkunft anlegt und den neuen Wert im Kombinationsfeld festlegt. In diesem Fall folgen die übrigen Ereignisse, sonst wird der Änderungsvorgang unterbrochen.] Vor Aktualisierung Nach Aktualisierung Beim Klicken Bei Geändert Beim Verlassen Bei Fokusverlust
Weitere Steuerelemente Die übrigen Steuerelemente wie Listenfelder, Kontrollkästchen oder Optionsgruppen haben je nach Typ unterschiedliche Ereignisse. Nachdem Sie erfahren haben, wie Sie die Abfolge der Ereignisse von Formularen und Steuerelementen ermitteln, soll an dieser Stelle nicht weiter auf die Ereignisse der noch nicht besprochenen Steuerelemente und deren Abfolge eingegangen werden.
4.4 Abbildung verschiedener Beziehungsarten Die vorhandene Fachliteratur beschränkt sich weitgehend auf die Darstellung von Daten aus einzelnen oder aus per 1:n-Beziehung verknüpften Tabellen. Daher erhalten Sie in diesem Abschnitt des Kapitels einen Überblick über die Realisierung der verschiedenen Beziehungsarten in Formularen.
4.4.1 Einfache Daten in der Detailansicht Die Daten aus einzelnen Tabellen oder einfachen Abfragen lassen sich mit wenigen Schritten in einem Formular darstellen. Üblicherweise sind hier zwei Darstellungen gefragt: Detailansichten oder Übersichtslisten.
176
4
Formulare
Mit »einfachen Abfragen« sind hier Abfragen gemeint, deren Daten quasi wie eine Tabelle gehandhabt werden – also etwa Abfragen, die zwei per 1:1-Beziehung verknüpfte Tabellen darstellen oder die eine einzelne Tabelle mit einem oder mehreren Lookup-Feldern beinhalten. Detailansichten dienen dazu, die sich oft über viele Felder erstreckenden Daten in einer Form anzuzeigen, die die Daten einerseits vernünftig strukturiert und andererseits eine komfortable Bearbeitung ermöglicht. Übersichtslisten dienen selten dazu, die Daten kompletter Tabellen anzuzeigen – es sei denn, diese enthalten nicht besonders viele Felder. Wenn der Benutzer scrollen muss, um alle Felder eines Formulars sehen zu können, ist die Menge der angezeigten Felder zu groß. Meist dienen solche Übersichtslisten dazu, wichtige Informationen zu den angezeigten Datensätzen sowie die Anzeige einer Detailansicht zu bieten. Übersichtlisten gibt es in zwei Formen: als Endlosformular oder als Datenblattansicht.
Detailansicht einfacher Daten in Formularen Die Erstellung einer Detailansicht einfacher Daten läuft in folgenden Schritten ab: 1. Anlegen eines neuen, leeren Formulars 2. Festlegen der Datenherkunft im Eigenschaftsfenster 3. Einfügen der benötigten Felder (Menüeintrag Ansicht/Feldliste, siehe Abbildung 4.4) 4. Anpassen und Ausrichten der Felder und Anpassen der Beschriftungen Das ist im Prinzip einfach. Zu beachten ist lediglich, dass Sie beim Anlegen der Datenherkunft in Schritt 2 darauf achten, dass die Datenherkunft nur die Felder enthält, die Sie auch wirklich benötigen.
Muss man in Detailansichten blättern können? Die oben erstellte Detailansicht enthält einige Elemente, die die Navigation in den Datensätzen des Formulars erleichtern: Bildlaufleisten Datensatzmarkierer Navigationsschaltflächen Trennlinien
Abbildung verschiedener Beziehungsarten
177
Abbildung 4.4: Das Hinzufügen der Einträge der Feldliste in das Formular kann durch Ziehen mit der Maus erfolgen.
Das Formular aus Abbildung 4.5 enthält all diese Elemente. Die Frage ist nun: Welche davon benötigen Sie in einer Detailansicht? Was zu einer anderen Frage führt: Was macht der Benutzer eigentlich mit dieser Detailansicht? Nun, er soll die Details eines Datensatzes betrachten und gegebenenfalls die enthaltenen Daten ändern beziehungsweise neue Datensätze anlegen oder bestehende löschen können. Soll er mit der Navigationsleiste arbeiten, um innerhalb der Datensätze zu navigieren? Eher weniger. Optimal wäre es sogar, wenn er die oben genannten Elemente gar nicht benötigt, sondern mit anderen Mitteln zu einem gesuchten oder einem neuen Datensatz gelangt – immerhin ist nicht jeder Benutzer mit diesen Steuerelementen vertraut und intuitiv sind diese auch nicht unbedingt.
Abbildung 4.5: Die Formularansicht des Detailformulars
178
4
Formulare
Angenommen, der Benutzer benötigt diese zusätzlichen Elemente gar nicht: Dann zeigen Sie diese doch gar nicht an. Vier Mausklicks im Eigenschaftsfenster der Entwurfsansicht des Formulars, einer noch, wenn das Formular beim Öffnen zentriert angezeigt werden soll, was immer Sinn macht, und noch eine Beschriftung hinzufügen – blitzschnell sieht das Eigenschaftsfenster wie in Abbildung 4.6 und das Formular wie in Abbildung 4.7 aus.
Abbildung 4.6: Eigenschaften eines Formulars …
Abbildung 4.7: … ohne unnötigen Schnickschnack (»frmKontakteDetailansicht«)
Primärschlüssel: Anzeigen oder nicht? Wenn die anzuzeigende Tabelle einen Primärschlüssel enthält, der keine weiteren Informationen wie etwa eine Kundennummer beinhaltet, sollte dieser nicht angezeigt werden. Der Benutzer kann ihn ohnehin nicht ändern, da er meist als Autowert ausge-
Abbildung verschiedener Beziehungsarten
179
legt ist. Die Beispiele in diesem Buch zeigen das Primärschlüsselfeld zwar meist an, aber in einer Anwendung, die Sie weitergeben, sollten Sie auf die Ausgabe des Primärschlüsselfeldes verzichten.
Navigation und Aktion in Detailformularen Nun fehlen noch die Möglichkeiten zum Auswählen eines Datensatzes, zum Anlegen eines neuen und Löschen des bestehenden Datensatzes. Nur: Müssen diese Elemente auf das Detailformular? Das ist sicher Geschmackssache, aber Sie sollten sich für eine der folgenden beiden Möglichkeiten entscheiden: Entweder Sie verwenden nur eine OK- und eine Abbrechen-Schaltfläche oder Sie fügen neben einer Möglichkeit zur Auswahl von Datensätzen auch noch je eine Schaltfläche zum Löschen und zum Anlegen von Datensätzen hinzu. Nachfolgend finden Sie eine Beschreibung der ersten Variante. Die Schaltflächen zum Auswählen, Anlegen und Löschen eines Datensatzes werden in Zusammenhang mit den unten beschriebenen Übersichtsformularen in der Endlosund der Datenblattansicht beschrieben.
OK- und Abbrechen-Schaltflächen Diese beiden Schaltflächen lösen jeweils Prozeduren mit nur einer Zeile VBA-Code aus. Legen Sie die beiden Schaltflächen an, stellen Sie die Eigenschaft Beschriftung auf die Werte OK und Abbrechen und die Eigenschaft Name auf die Werte cmdOK und cmdAbbrechen ein. Legen Sie dann für beide je eine Prozedur für die Ereigniseigenschaft Beim Klicken an (siehe Abbildung 4.8): Private Sub cmdOK_Click() DoCmd.Close acForm, Me.Name End Sub Private Sub cmdAbbrechen_Click() Me.Undo DoCmd.Close acForm, Me.Name End Sub Listing 4.3: Schließen mit und ohne Übernahme der Änderungen
Die OK-Schaltfläche sorgt in der Regel nur dafür, dass ein Formular geschlossen wird. Es gibt aber auch Ausnahmen: Wenn ein Formular geöffnet wurde, um Daten zu ermitteln, die in der aufrufenden Routine weiter verarbeitet werden sollen, müssen diese vor dem Schließen natürlich erst abgefragt werden. Wie dies funktioniert, erfahren Sie weiter unten in Abschnitt 4.5, »Von Formular zu Formular«.
180
4
Formulare
Abbildung 4.8: Anlegen der Beim Klicken-Ereigniseigenschaft für eine Schaltfläche (»frmDetailansicht«)
Achtung, Eigenschaftsfenster! Das Eigenschaftsfenster ist eigentlich dem Entwurf von Formularen und Berichten vorbehalten und sollte dementsprechend für den Benutzer nicht sichtbar sein. Um sicherzugehen, dass beim Öffnen eines Formulars in der Formular- oder Datenblattansicht keine Eigenschaften angezeigt werden, stellen Sie einfach die Eigenschaft Entwurfsänderungen zulassen auf den Wert Nur Entwurfsansicht ein.
4.4.2 Einfache Daten in der Übersicht mit Endlosformularen Ein Formular zur Anzeige der Übersicht von Datensätzen enthält die gleiche Datenherkunft wie das Formular zur Anzeige der Detailansicht, in der Regel jedoch mit weniger Feldern. Für Tabellen, die so wenige Felder enthalten, dass diese leicht nebeneinander angezeigt werden können, braucht prinzipiell gar keine Detailansicht erstellt zu werden. Die Felder sind meist wie in Abbildung 4.9 nebeneinander angeordnet. In der Abbildung finden Sie direkt die Elemente zum Steuern von Aktionen wie Neuanlegen, Löschen und Bearbeiten von Datensätzen. Die OK-Schaltfläche dient wie die Schaltfläche des zuvor beschriebenen Formulars lediglich dem Schließen des Formulars. Die Neu-Schaltfläche soll das oben beschriebene Formular zur detaillierten Ansicht eines Datensatzes öffnen und einen leeren Datensatz anzeigen. Dafür ist die folgende Ereigniseigenschaft verantwortlich, die durch das Beim Klicken-Ereignis der Schaltfläche ausgelöst wird.
Abbildung verschiedener Beziehungsarten
181
Abbildung 4.9: Entwurfsansicht eines Übersichtsformulars (»frmKontakteListenfeld«)
Formular mit leerem Datensatz öffnen Die Routine verwendet die OpenForm-Methode des DoCmd-Objekts zum Öffnen des Formulars frmKontakteDetailansicht. Der Parameter DataMode erhält dabei den Wert acFormAdd, damit das Formular beim Öffnen direkt einen leeren Datensatz anzeigt.
Formular als modalen Dialog öffnen Der Wert des Parameters WindowMode liefert die Voraussetzung dafür, dass das aufrufende Formular – die Übersicht – den angezeigten Datenbestand direkt nach dem Eingeben des neuen Datensatzes und Schließen des Detailformulars aktualisieren kann (siehe Abbildung 4.10). Durch den Wert acDialog wird das Formular frmKontakteDetailansicht als modaler Dialog geöffnet, was bedeutet, dass innerhalb der Access-Anwendung nichts mehr geht, solange dieses Formular geöffnet ist – selbst der aufrufende Code wird währenddessen angehalten. Erst wenn das Formular den Fokus verliert, also geschlossen oder unsichtbar gemacht wird, läuft die aufrufende Routine weiter. Dadurch kann die Übersicht direkt nach dem Schließen der Detailansicht aktualisiert werden. Private Sub cmdNeu_Click() DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog Me.Requery End Sub Listing 4.4: Aufrufen des Detailformulars zum Anlegen eines neuen Datensatzes
182
4
Formulare
Abbildung 4.10: Öffnen eines Eingabeformulars vom Übersichtsformular aus (»frmKontakteEndlosformular«, »frmKontakteDetailansicht«)
Löschen von Datensätzen Die nächste Schaltfläche dient dem Löschen des aktuell markierten Datensatzes. Die einfachste Variante des benötigten Codes sieht wie folgt aus: Private Sub cmdLoeschen_Click() On Error Resume Next DoCmd.RunCommand acCmdDeleteRecord End Sub Listing 4.5: Löschen eines Datensatzes
Die Routine verwendet die RunCommand-Methode des DoCmd-Objekts mit dem Parameter acCmdDeleteRecord, um den aktuell markierten Datensatz zu löschen. Da hier eigentlich nur der Fehler auftreten kann, dass kein Datensatz markiert ist, verhindern Sie diesen mit der On Error Resume Next-Anweisung im Vorhinein. Alternativ können Sie den aktuellen Datensatz im Übrigen auch mit folgender Anweisung löschen: Me.Recordset.Delete
Etwas weniger rudimentär gestalten Sie das Abfangen dieses Fehlers, wenn Sie die On Error Resume Next-Anweisung durch folgenden Code ersetzen. Dieser prüft, ob aktuell kein Datensatz ausgewählt ist, und bricht in diesem Fall mit einer entsprechenden Meldung ab:
Abbildung verschiedener Beziehungsarten
183
If IsNull(Me!KontaktID) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If
Sollte die Anzeige von Warnmeldungen bei Datensatzänderungen aktiviert sein (siehe Abbildung 4.3), erscheint hier allerdings eine unschöne Meldung (siehe Abbildung 4.11).
Abbildung 4.11: Access-Meldung beim Löschen eines Datensatzes
Wenn Sie diese Meldung durch eine eigene Meldung ersetzen möchten, verwenden Sie die folgende Prozedur. Die SetWarnings-Methode des DoCmd-Objekts deaktiviert dabei die Anzeige der eingebauten Meldung und aktiviert diese anschließend wieder. Private Sub cmdLoeschen_Click() On Error Resume Next DoCmd.SetWarnings False If MsgBox("Möchten Sie den Kontakt '" & Me!Vorname & " " & Me!Nachname _ & "' wirklich löschen?", vbYesNo + vbExclamation, _ "Löschbestätigung") = vbYes Then DoCmd.RunCommand acCmdDeleteRecord End If DoCmd.SetWarnings True End Sub Listing 4.6: Löschen eines Datensatzes mit benutzerdefinierter Warnmeldung
Anzeigen der Details zu einem Datensatz Die letzte Schaltfläche des Übersichtsformulars dient dem Anzeigen der Detailansicht des ausgewählten Datensatzes. Die dadurch ausgelöste Routine prüft zunächst, ob ein Datensatz markiert ist. Falls ja, öffnet sie das Detailformular wiederum mit der DoCmd.OpenForm-Methode. In diesem Fall kommt für den Parameter DataMode jedoch der
184
4
Formulare
Wert acFormEdit zum Zuge und der anzuzeigende Datensatz wird mit dem Parameter WhereCondition festgelegt. Dieser erwartet als Wert – wie der Name schon sagt – eine Bedingung, die den anzuzeigenden Datensatz festlegt. Hier lautet die Bedingung, dass der Wert des Feldes KontaktID dem des ausgewählten Datensatzes im Übersichtsformular entsprechen muss. Private Sub cmdBearbeiten_Click() If IsNull(Me!KontaktID) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="KontaktID = " & Me!KontaktID End Sub Listing 4.7: Öffnen eines Datensatzes in der Detailansicht
4.4.3 Einfache Daten in der Übersicht als Datenblatt Neben der Endlosansicht ist auch die Datenblattansicht von Formularen für die Verwendung als Übersichtsformular geeignet. Die Datenblattansicht hat den Vorteil, dass der Benutzer dort selbst die Spaltenbreite einstellen kann – das ist bei Endlosformularen mitunter ein Problem, wenn wenig Platz vorhanden oder die maximale Länge der anzuzeigenden Zeichenketten nicht bekannt ist. Auch die Performance ist in dieser Ansicht besser. Allerdings hat auch das Endlosformular Vorteile. Warum das so ist, erfahren Sie, wenn Sie das Formular frmKontakteUebersicht einmal in der Datenblattansicht anzeigen (siehe Abbildung 4.12). Das Datenblatt ist zwar als Übersicht akzeptabel, aber Kopf- und Fußbereich des Formulars sind völlig verschwunden. Tatsache ist: Beide können in der Datenblattansicht schlicht nicht verwendet werden.
Abbildung 4.12: Übersichtsformular in der Datenblattansicht (»sfmKontakteDatenblattansicht«)
Abbildung verschiedener Beziehungsarten
185
Datenblattansicht im Unterformular Wer dennoch ein Formular in der Datenblattansicht verwenden möchte, setzt dies als Unterformular in ein Hauptformular ein, das die Navigationselemente zum Anlegen, Löschen und Bearbeiten der Einträge bereitstellt. Das Unterformular enthält einfach nur die Felder, die in der Datenblattansicht angezeigt werden sollen (siehe Abbildung 4.13). Es gibt lediglich drei Punkte, auf die Sie achten sollten: Stellen Sie die Eigenschaft Standardansicht auf Datenblatt ein. Versehen Sie die Bezeichnungsfelder der einzelnen Steuerelemente mit ordentlichen Beschriftungen, da diese später als Feldüberschriften dienen. Sorgen Sie für die richtige Aktivierreihenfolge, da diese festlegt, in welcher Reihenfolge die einzelnen Felder später angezeigt werden.
Abbildung 4.13: Der Entwurf eines Formulars in der Datenblattansicht braucht keine besonderen optischen Ansprüche zu erfüllen.
Hauptformular als Container für ein Unterformular in der Datenblattansicht Bereiten Sie dann das Hauptformular vor. Dazu legen Sie ein neues, leeres Formular an und kopieren die bereits erstellten Schaltflächen des Formulars frmKontakteEndlosformular in den Detailbereich des neuen Formulars. Lassen Sie dabei ein wenig Platz nach oben. Ziehen Sie dann das neue Unterformular aus dem Datenbankfenster in den Detailbereich (siehe Abbildung 4.14).
186
4
Formulare
Abbildung 4.14: Ziehen des Unterformulars aus dem Datenbankfenster in den Detailbereich des neuen Hauptformulars
Anpassen der Datenblattansicht eines Formulars Das Erste, was Ihnen beim Blick auf die Formularansicht auffallen wird, ist, dass standardmäßig alle Steuerelemente in Formularen in Schriftgröße 8 dargestellt werden, das Unterformular in der Datenblattansicht aber mit Schriftgröße 10 aufwartet. Außerdem ist die Breite der Felder des Unterformulars natürlich nicht den Inhalten angepasst. Beides holen Sie nach – allerdings nicht in der Entwurfsansicht, sondern in der Formularansicht des Unterformulars. Dieses müssen Sie dafür allerdings noch einmal separat öffnen. Wenn Tabellen, Abfragen und Formulare in der Datenblattansicht grundsätzlich in einer anderen Schriftgröße oder in anderem Layout geöffnet werden sollen, können Sie die gewünschten Einstellungen auf der Registerseite des Dialogs Optionen vornehmen (Menüeintrag Extras/Optionen). Die Spaltenbreiten bändigen Sie durch Ziehen des vertikalen Trennstriches zwischen zwei Spalten und die Schriftgröße passen Sie in einem Dialog an, den Sie per Kontextmenü der Titelleiste des Formulars öffnen. Nachdem Sie im Hauptformular auch noch die Bildlaufleisten, Datensatzmarkierer, Navigationsschaltflächen und Trennlinien deaktiviert haben, sieht das Formular schon viel besser aus (siehe Abbildung 4.15).
Bezug auf Steuerelemente im Unterformular Nun fehlen noch die Funktionen der Schaltflächen. Da sich die betroffenen Daten nun in einem Unterformular befinden, können Sie nicht ohne Weiteres die Prozeduren aus dem Formular frmKontakteEndlosformular einsetzen – aber fast. Die Prozedur hinter der Schaltfläche OK können Sie komplett übernehmen. Die Prozeduren zum Anlegen, Löschen und Bearbeiten des aktuell markierten Datensatzes sehen nun folgendermaßen aus:
Abbildung verschiedener Beziehungsarten
187
Abbildung 4.15: Datenblattansicht als Übersicht (»frmKontakteDatenblattansicht«)
Private Sub cmdBearbeiten_Click() If IsNull(Me!sfmKontakteDatenblatt!KontaktID) Then
MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="KontaktID = " _ & Me!sfmKontakteDatenblatt!KontaktID
Me.Requery End Sub Private Sub cmdLoeschen_Click() If IsNull(Me!sfmKontakteDatenblatt.Form!KontaktID) Then
MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.SetWarnings False If MsgBox("Möchten Sie den Kontakt '" _ & Me!sfmKontakteDatenblatt!Vorname & " " _ & Me!sfmKontakteDatenblatt!Nachname & "' wirklich löschen?", _ vbYesNo + vbExclamation, "Löschbestätigung") = vbYes Then Me!sfmKontakteDatenblatt.SetFocus
DoCmd.RunCommand acCmdDeleteRecord End If DoCmd.SetWarnings True End Sub
Private Sub cmdNeu_Click() DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog Me!sfmKontakteDatenblatt.Requery
End Sub Listing 4.8: Änderungen an den Prozeduren zum Löschen und Bearbeiten von Datensätzen im Detailformular
188
4
Formulare
Die Änderungen gegenüber den Prozeduren des Formulars mit der Endlosansicht sind fett gedruckt. Dabei wird jeweils auf die im Unterformular befindlichen Steuerelemente Bezug genommen, die im Formular frmKontakteEndlosformular alle im gleichen Formular wie die Schaltflächen angesiedelt waren. Bei den Steuerelementen wird statt Me!KontaktID etwa Me!sfmKontakteDatenblatt!KontaktID verwendet. Statt dessen könnte man auch ausführlicher schreiben: Me!sfmKontakteDatenblatt.Form!KontaktID
oder Me.Controls("sfmKontakteDatenblatt").Form.Controls("KontaktID")
Vorteil Datenblattansicht Ein klarer Vorteil der hier konstruierten erweiterten Datenblattansicht gegenüber der Endlosansicht ist, dass nach dem Bearbeiten von Daten im Detailformular und der Aktualisierung der überarbeiteten Daten im Übersichtsformular nicht der Datensatz gewechselt wird. Im Formular frmKontakteEndlosformular springt der Datensatzzeiger nach dem Aktualisieren mit der Requery-Methode immer wieder auf den ersten Datensatz. Das ist in der Datenblattansicht nicht der Fall. Hier ist darüber hinaus nur eine Aktualisierung per Requery nach dem Anlegen eines neuen Datensatzes erforderlich, im Detailformular vorgenommene Änderungen werden automatisch übernommen.
4.4.4 Daten in der Übersicht als Listenfeld Die dritte Möglichkeit der Darstellung von Daten in der Übersicht ist die Verwendung eines Listenfeldes. Wie Sie bemerkt haben werden, kann man mit den beiden zuvor beschriebenen Varianten auch bereits in der Übersicht Daten verändern, wenn man nicht gerade die einzelnen Steuerelemente sperrt oder die Eigenschaft RecordsetTyp des Formulars auf Snapshot einstellt. Hier bringt das Listenfeld Vorteile: Es ist keine direkte Bearbeitung möglich und außerdem scheint das Markieren zu bearbeitender oder zu löschender Datensätze intuitiver zu sein. Davon abgesehen kann man den Doppelklick für die schnelle Anzeige des Detailformulars auslegen. Mit den folgenden Schritten haben Sie schnell ein Listenfeld als Übersicht erstellt: Legen Sie ein Listenfeld in einem leeren Formular an und vergrößeren Sie es auf etwa 12 cm Breite. Stellen Sie die Eigenschaft Name auf lstKontakte ein. Legen Sie als Datensatzherkunft eine Abfrage an, die auf der Tabelle tblKontakte basiert und nur die wichtigsten Felder KontaktID, Nachname, Vorname, Telefonnummer und EMail enthält. Gegebenenfalls können Sie auch eine Sortierung festlegen – etwa nach dem Nachnamen.
Abbildung verschiedener Beziehungsarten
189
Stellen Sie die Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 5 und 0cm;2cm:2cm;3cm ein. Dadurch wird die gebundene Spalte KontaktID ausgeblendet (Primärschlüsselfelder soll man, wie oben bereits erläutert, nur anzeigen, wenn diese geschäftliche Informationen enthalten). Das Feld muss aber dennoch in der Abfrage und auch im Listenfeld enthalten sein, da sonst kein Bezug zum entsprechenden Datensatz hergestellt werden kann. Die weiteren Spalten werden in der entsprechenden Breite angezeigt und die letzte Spalte füllt den restlichen Platz aus. Je nach Geschmack stellen Sie die Eigenschaft Spaltenüberschriften auf Ja ein oder legen selbst Beschriftungsfelder oberhalb des Listenfeldes an. Bedenken Sie, dass die Spaltenüberschriften – falls angezeigt – eine Zeile belegen und damit der Index des ersten angezeigten Datensatzes nicht 0, sondern 1 ist. Wenn Sie noch die obligatorischen Schaltflächen wie in den beiden vorherigen Beispielen hinzufügen und die Eigenschaften Bildlaufleisten, Datensatzmarkierer, Navigationsschaltflächen und Trennlinien auf Nein einstellen, sieht das Formular wie in Abbildung 4.16 aus.
Abbildung 4.16: Übersicht per Listenfeld (»frmKontakteListenfeld«)
Nun fehlen noch die dem Listenfeld angepassten Prozeduren zum Anlegen, Löschen und Bearbeiten des aktuell ausgewählten Datensatzes. Hierbei ist zu beachten, dass nach Änderungen immer die Requery-Methode des Listenfeld-Steuerelements ausgeführt werden muss. Die fett gedruckten Zeilen enthalten die Anweisungen, die sich speziell auf das Listenfeld beziehen. Um den Wert des Feldes KontaktID für den aktuellen Listenfeldeintrag zu ermitteln, lesen Sie einfach den Wert des Listenfeldes aus. Das Feld KontaktID ist das gebundene Feld der Datensatzherkunft und dadurch mit dem Wert des Listenfeldes identisch. Eine weitere Änderung ist zum Löschen des markierten Datensatzes notwendig: Mit DoCmd.RunCommand acCmdDeleteRecord ist einem Eintrag im Listenfeld nicht beizukommen, hier müssen Sie direkt mit einer DELETE-Anweisung auf die zugrunde liegende Tabelle zugreifen.
190
4
Formulare
Private Sub cmdBearbeiten_Click() If IsNull(Me!lstKontakte) Then
MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="KontaktID = " & Me!lstKontakte Me!lstKontakte.Requery
End Sub Private Sub cmdLoeschen_Click() If IsNull(Me!lstKontakte) Then
MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.SetWarnings False If MsgBox("Möchten Sie den Kontakt '" & Me!lstKontakte & " " _ & Me!lstKontakte & "' wirklich löschen?", vbYesNo + vbExclamation, _ "Löschbestätigung") = vbYes Then CurrentDb.Execute "DELETE FROM tblKontakte WHERE KontaktID = " _ & Me!lstKontakte Me!lstKontakte.Requery
End If DoCmd.SetWarnings True End Sub Private Sub cmdNeu_Click() DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog Me!lstKontakte.Requery
End Sub Listing 4.9: Anlegen, Löschen und Bearbeiten von Datensätzen eines Listenfeldes
4.4.5 1:1-Beziehungen Die Daten aus 1:1-Beziehungen lassen sich genau wie die oben beschriebenen »einfachen Daten« in Formularen abbilden. Sie können eine Abfrage über die an der 1:1Beziehung beteiligten Tabellen erstellen und genau die gleichen Formulare wie oben verwenden. Sie können aber auch beispielsweise im Übersichtsformular nur die Daten der einen Seite der Beziehung anzeigen und die Details erst im Detailformular offenbaren. Einen kleinen Trick müssen Sie anwenden, wenn Sie sicherstellen wollen, dass auch in der Erweiterungstabelle zu jedem Datensatz der Haupttabelle ein passender Datensatz angelegt wird – auch ohne, dass der Benutzer explizit Daten für die Erweiterungstabelle eingibt. Dazu legen Sie eine kleine Prozedur an, die durch die Ereigniseigenschaft
Abbildung verschiedener Beziehungsarten
191
Vor Aktualisierung des Formulars ausgelöst wird. Dieses Ereignis stellt einfach den Wert des Verknüpfungsfeldes der Erweiterungstabelle auf den Wert des Primärschlüsselfeldes der Haupttabelle ein: Private Sub Form_BeforeUpdate(Cancel As Integer) Me!tblAngestellte_PersonID = Me!tblPersonen_PersonID End Sub Listing 4.10: Synchronisieren von Haupt- und Erweiterungstabelle im Formular
Das Beispielformular finden Sie nicht in der Datenbank zu diesem Kapitel, sondern unter Kap_03\Abfragen.mdb unter dem Formularnamen frmAngestellte.
4.4.6 n:1-Beziehungen n:1-Beziehungen bestehen aus einer Detailtabelle, von der ein Feld in Form einer Mastertabelle ausgelagert wurde – das einfachste Beispiel ist wohl das Feld Anrede in einer Kontakt-, Adress- oder Mitarbeitertabelle. Die Darstellung und Auswahl der Daten aus der Mastertabelle erfolgt im Formular über ein Kombinationsfeld. Wer gute Vorarbeit leistet und per Nachschlage-Assistent oder von Hand bereits beim Datenentwurf dafür sorgt, dass die Daten aus verknüpften Tabellen in einem Kombinationsfeld ausgewählt werden können, hat beim Erstellen eines Formulars für diese Beziehungsart leichtes Spiel: Das Kombinationsfeld wird dann automatisch angelegt (siehe Abbildung 4.17). Wie Sie eine Tabelle mit einem Kombinationsfeld zur Darstellung einer n:1-Beziehung erstellen, erfahren Sie in Kapitel 2, Abschnitt 2.4.5, »n:1-Beziehungen oder Lookup-Beziehungen«.
Abbildung 4.17: Darstellung einer n:1-Beziehung im Formular
192
4
Formulare
Sollte es aus irgendwelchen Gründen nicht gewünscht sein, dass das Kombinationsfeld direkt in der Tabelle angelegt wird, müssen Sie selbst Hand anlegen. In diesem Fall zeigt das Formular nach dem Hinzufügen der Felder aus der Feldliste zunächst ein herkömmliches Textfeld statt eines Kombinationsfeldes an. Wenn Sie wissen, aus welcher Datei das Fremdschlüsselfeld seine Daten beziehen soll, gehen Sie folgendermaßen vor: Konvertieren Sie das Textfeld in ein Kombinationsfeld, indem Sie aus dessen Kontextmenü den Eintrag Ändern zu/Kombinationsfeld auswählen. Stellen Sie die Eigenschaft Datensatzherkunft des frisch gebackenen Kombinationsfeldes auf die Tabelle tblAnreden ein. Legen Sie für die Eigenschaften Spaltenanzahl und Spaltenbreiten die Werte 2 und 0 fest. Es sollen beide Felder der Tabelle enthalten sein, aber nur die zweite mit den Anreden selbst angezeigt werden. Das Kombinationsfeld zeigt nun die vorhandenen Einträge wie in Abbildung 4.17 an.
4.4.7 1:n-Beziehungen 1:n-Beziehungen trifft man während des Entwicklerlebens vermutlich am häufigsten an, vor allem, wenn man n:1-Beziehungen auch darunter einstuft. Ein klassisches Beispiel für eine 1:n-Beziehung sind Projekte und Kunden, wobei die Projekte in der Detailtabelle und die Kunden in der Mastertabelle gespeichert werden (siehe Abbildung 4.18).
Abbildung 4.18: 1:n-Beziehung zwischen Projekten und Kunden
Die Gestaltung des Formulars für die Anzeige der Projekte entspricht vom Aufbau her dem Formular aus Abbildung 4.17 – es enthält die Daten zum jeweiligen Projekt und der Kunde kann mit einem Kombinationsfeld ausgewählt werden.
Abbildung verschiedener Beziehungsarten
193
Interessanter ist die Darstellung eines Kunden und seiner Projekte. Der Kunde wird dabei wie in Abschnitt 4.4.1, Teilabschnitt »Detailansicht einfacher Daten in Formularen« angezeigt. Zusätzlich sollen aber nun die Projekte des Kunden im Überblick dargestellt werden – und dazu gibt es wiederum verschiedene Möglichkeiten. Auch diese Möglichkeiten kennen Sie bereits – wenn auch in vereinfachter Form – aus den Abschnitten 4.4.2, »Einfache Daten in der Übersicht mit Endlosformularen«, 4.4.3, »Einfache Daten in der Übersicht als Datenblatt« und 4.4.4, »Daten in der Übersicht als Listenfeld«. Dabei müssen Sie in diesem Fall sowohl das Endlosformular als auch die Datenblattansicht in ein Unterformular ausgliedern; lediglich das Listenfeld können Sie direkt in das Kunden-Formular integrieren.
Verknüpfte Daten direkt bearbeiten oder nicht? Welche Variante Sie verwenden, hängt zunächst einmal davon ab, ob Sie die verknüpften Daten – also die Projekte – direkt im Kundenformular bearbeiten möchten oder nicht. Falls ja, verwenden Sie ein Unterformular mit den Projekten in einem Endlosformular oder der Datenblattansicht, ansonsten entscheiden Sie sich für ein Unterformular mit dem Recordset-Typ Snapshot oder ein Listenfeld. Da die Unterformular-Varianten technisch ähnlich sind und beide in einfacher Form bereits ausführlich erläutert wurden, soll hier nur eine der beiden Möglichkeiten beschrieben werden – die etwas einfacher zu realisierende Datenblattansicht.
4.4.8 1:n-Beziehung per Unterformular und Datenblattansicht Das Unterformular zur Anzeige der Projekte in der Datenblattansicht ist genauso wie das aus Abschnitt 4.4.3, »Einfache Daten in der Übersicht als Datenblatt«, aufgebaut. Es basiert allerdings auf einer Abfrage, die alle Felder der Tabelle tblProjekte enthält und diese zusätzlich nach dem Startdatum sortiert (siehe Abbildung 4.19).
Vorbereiten des Unterformulars Das Unterformular selbst enthält lediglich die drei Felder Projekt, Startdatum und Enddatum. Die anderen Felder werden zwar für die Funktionalität benötigt, sollen aber nicht angezeigt werden – und die Datenblattansicht zeigt immer alle im Entwurf enthaltenen Felder an. Daran ändert auch das Einstellen der Sichtbar-Eigenschaft auf den Wert Nein nichts. Die einzige Möglichkeit, ein Feld »unsichtbar« zu machen, ist das Verkleinern der Spaltenbreite auf den Wert 0. Dies erreichen Sie mit dem Menüeintrag Format/Spalten ausblenden oder mit der VBA-Eigenschaft ColumnWidth.
194
4
Formulare
Abbildung 4.19: Datenherkunft des Unterformulars zur Anzeige der Projekte eines Kunden (»sfmProjekte«)
Vergessen Sie nicht, die Eigenschaft Standardansicht auf Datenblatt einzustellen und die Spaltenbreiten und Schriftgröße Ihren Bedürfnissen anzupassen, Speichern Sie das Formular unter dem Namen sfmProjekte (siehe Abbildung 4.20).
Abbildung 4.20: Das Unterformular zur Anzeige der Projekte in der Entwurfsansicht
Erstellen des Hauptformulars Das Hauptformular basiert auf der Tabelle tblKunden und zeigt alle darin enthaltenen Felder an. Ordnen Sie die Felder wie in Abbildung 4.21. Anschließend fügen Sie das Unterformular in das Hauptformular ein, indem Sie dieses aus dem Datenbankfenster in den Entwurf des Hauptformulars ziehen.
Abbildung verschiedener Beziehungsarten
195
Damit das Unterformular nur die Projekte des im Hauptformular angezeigten Kunden ausgibt, müssen zwei Eigenschaften des Unterformularsteuerelements entsprechend eingestellt werden, was beim Einfügen des Unterformulars normalerweise automatisch geschieht. Die beiden Eigenschaften heißen Verknüpfen von und Verknüpfen nach und erwarten den Namen des Fremdschlüsselfeldes der Datenherkunft des Unterformulars und den Namen des Primärschlüsselfeldes der Datenherkunft des Hauptformulars. Diese beiden Felder haben üblicherweise den gleichen Namen – in diesem Fall KundeID. Die Verknüpfungsfelder müssen nicht zwangsläufig mit dem Primärschlüsselfeld des Hauptformulars und dem Fremdschlüsselfeld des Unterformulars gefüllt werden. Es gibt auch Fälle, in denen beispielsweise der Wert eines Kombinationsfeldes im Hauptformular zur Auswahl der passenden Datensätze im Unterformular herangezogen wird – hier würde man einfach den Namen des Kombinationsfeldes als Wert der Eigenschaft Verknüpfen nach verwenden. Um die Eigenschaften einzusehen oder anzupassen, markieren Sie das Unterformularsteuerelement – dieses ist keinesfalls mit dem Unterformular selbst zu verwechseln! Am einfachsten markieren Sie es, wenn Sie erst ein anderes Steuerelement des Hauptformulars markieren und dann einfach auf das Unterformular klicken. Wenn das Eigenschaftsfenster im Register Daten die beiden Eigenschaften anzeigt, liegen Sie richtig.
Abbildung 4.21: Haupt- und Unterformular zur Anzeige von Kunden und Projekten
196
4
Formulare
Wenn Sie das Formular nun in der Formularansicht öffnen, sieht dieses etwa wie in Abbildung 4.22 aus. Beim Wechseln zu einem anderen Datensatz des Hauptformulars zeigt das Unterformular automatisch die passenden Projekte an.
Abbildung 4.22: Das fertige Formular zur Anzeige von Kunden und Projekten (»frmKunden«)
Anlegen, bearbeiten und löschen im Detailformular Eine Projekttabelle wird in der Regel nicht mit derart wenig Feldern auskommen. Sie werden also normalerweise wie weiter oben in Abschnitt 4.4.3, »Einfache Daten in der Übersicht als Datenblatt«, ein Detailformular zur Anzeige der Projekte benötigen. Um dieses anzuzeigen, neue Projekte anzulegen oder Projekte zu löschen, können Sie die ebenfalls in dem genannten Abschnitt vorgestellten Schaltflächen einschließlich VBACode für das Formular frmKunden anpassen. Die Prozeduren zum Löschen eines Datensatzes und für die Anzeige zum Bearbeiten sind prinzipiell mit den oben beschriebenen identisch; bei Bedarf finden Sie diese im Klassenmodul des Formulars frmKunden. Eine wichtige Neuerung liefert das Neuanlegen per Detailformular. In der Routine, die durch das Anklicken der Schaltfläche cmdNeu ausgelöst wird, enthält die DoCmd.OpenAnweisung einen bislang nicht verwendeten Parameter. Mit OpenArgs kann man einen einzelnen Wert, auch Öffnungsparameter genannt, an das aufgerufene Formular übergeben. In diesem Fall handelt es sich dabei um den Wert des Feldes KundeID.
Abbildung verschiedener Beziehungsarten
197
Private Sub cmdNeu_Click() DoCmd.OpenForm "frmProjekteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog, OpenArgs:=Me!KundeID Me.Requery End Sub Listing 4.11: Aufruf des Detailformulars mit Öffnungsargument
Der Hintergrund ist, dass neue Datensätze im Detailformular zur Anzeige der Projekte direkt mit dem richtigen Kunden ausgestattet werden sollen (siehe Abbildung 4.23).
Abbildung 4.23: Das Kunden-Formular und die Detailansicht für Projekte mit einem neuen Datensatz (»frmKunden«, »frmProjekteDetailansicht«)
Damit das Detailformular den Öffnungsparameter auch auswertet, legen Sie dort für die Ereigniseigenschaft Beim Öffnen die folgende Prozedur an. Die Prozedur prüft, ob ein Öffnungsparameter übergeben wurde (das ist notwendig, da das Formular etwa zum Bearbeiten auch ohne Öffnungsparameter geöffnet werden soll – das Fehlen des Öffnungsarguments würde sonst einen Fehler auslösen) und weist diesen dann der Eigenschaft DefaultValue des Feldes KundeID zu. Dadurch wird der entsprechende Eintrag im Kombinationsfeld zur Anzeige der verknüpften Werte voreingestellt. Private Sub Form_Open(Cancel As Integer) If Not IsNull(Me.OpenArgs) Then Me!KundeID.DefaultValue = Me.OpenArgs End If End Sub Listing 4.12: Auswerten des Öffnungsarguments beim Öffnen eines Formulars
198
4
Formulare
4.4.9 1:n-Beziehung per Listenfeld Wenn Sie ohnehin ein Formular für die Anzeige der Details der verknüpften Datensätze des Hauptformulars verwenden, müssen beziehungsweise sollten Sie im Hauptformular nicht die Möglichkeit zur Bearbeitung der verknüpften Daten bieten. In diesem Fall reicht ein Listenfeld zu deren Anzeige aus; außerdem bietet es die Möglichkeit, Datensätze zur Bearbeitung und zum Löschen auszuwählen – alles wie bereits in Abschnitt 4.4.4, »Daten in der Übersicht als Listenfeld« beschrieben. Eine Besonderheit gibt es jedoch: Die im Listenfeld angezeigten Daten hängen nun von dem im Hauptformular angezeigten Datensatz ab. Dementsprechend muss bei einem Wechsel des Hauptformulars auch der Inhalt des Listenfeldes aktualisiert werden (siehe Abbildung 4.24).
Abbildung 4.24: Darstellung einer 1:n-Beziehung per Listenfeld (»frmKundenListenfeld«)
Damit das Listenfeld jeweils die Projekte zu dem im Hauptformular angezeigten Kunden ausgibt, legen Sie für die Ereigniseigenschaft Beim Anzeigen die folgende Routine an. Diese stellt einen SQL-Ausdruck mit einer Abfrage zusammen, die alle anzuzeigenden Felder der Tabelle tblProjekte ausgibt und dabei nur jene Datensätze berücksichtigt, deren Feld KundeID mit dem Wert des Formularfeldes KundeID übereinstimmt. Diesen SQL-Ausdruck weist die Routine dann der Eigenschaft RowSource des Listenfeldes zu: Private Sub Form_Current() Dim strSQL As String strSQL = "SELECT ProjektID, Projekt, Startdatum, Enddatum " _
Abbildung verschiedener Beziehungsarten
199
& "FROM tblProjekte WHERE KundeID = " & Me!KundeID Me!lstProjekte.RowSource = strSQL End Sub Listing 4.13: Synchronisieren des Listenfeldinhalts mit dem Hauptformular
Projekt anzeigen per Doppelklick Wenn Sie dem Benutzer zusätzlichen Komfort bieten möchten, legen Sie für das Listenfeld eine Prozedur an, die beim Doppelklick auf das Listenfeld den aktuell markierten Eintrag im Detailformular anzeigt. Dazu verwenden Sie die Ereigniseigenschaft Beim Doppelklicken: Private Sub lstProjekte_DblClick(Cancel As Integer) If IsNull(Me!lstProjekte) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmProjekteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="ProjektID = " & Me!lstProjekte Me!lstProjekte.Requery End Sub Listing 4.14: Anzeigen der Detailansicht eines Projekts
Die Routine hat exakt den gleichen Inhalt wie die Ereignisprozedur, die durch einen Klick auf die Schaltfläche Bearbeiten ausgelöst wird. Den enthaltenen Code extrahieren Sie am besten in eine separate neue Routine, die Sie von beiden Ereignisprozeduren aufrufen. Wie das aussieht, können Sie dem Klassenmodul des Formulars frmKundenListenfeld in der Beispieldatenbank Kap_04\Formulare.mdb auf der Buch-CD entnehmen.
4.4.10 m:n-Beziehungen in Haupt- und Unterformular Wie bereits erwähnt, bieten Listenfelder zur Verwaltung von Daten aus m:n-Beziehungen nicht den Komfort, dass man die im Listenfeld angezeigten Daten direkt bearbeiten kann. Dafür ist eine andere Lösung wesentlich sinnvoller: Die Darstellung einer m:n-Beziehung in einer Kombination aus Formular und Unterformular. Dabei zeigt das Hauptformular die Daten der einen Seite der Beziehung an, während das Unterformular die Daten der Verknüpfungstabelle und der anderen Seite der Beziehung enthält. Das bekannteste Beispiel für eine solche Darstellung ist vermutlich das Formular Bestellungen aus der Nordwind-Datenbank (siehe Abbildung 4.25).
200
4
Formulare
Abbildung 4.25: Beispiel für die Verwaltung von Daten in einer m:n-Beziehung
Zu Demonstrationszwecken reicht eine einfache m:n-Beziehung mit den notwendigsten Feldern wie in Abbildung 4.26. Die beiden Tabellen tblProjekte und tblMitarbeiter werden über die Verknüpfungstabelle tblProjektzeiten miteinander verbunden. Interessant wird dieses Beispiel durch die Tatsache, dass auch die Verknüpfungstabelle einige Felder enthält – in diesem Fall sogar die wichtigsten. Die Verknüpfungstabelle dient dem Speichern der Zeiten, die die Mitarbeiter mit den jeweiligen Projekten verbracht haben. Um diese Informationen nicht nur quantitativ, sondern auch qualitativ auswerten zu können, wird nicht nur die Zeit, sondern auch das Datum der Tätigkeit gespeichert. Beispieldatenbank: Die Tabellen tblProjekte, tblMitarbeiter und tblProjektzeiten, die Abfrage qryProjektzeiten sowie die Formulare frmProjektzeiten und sfmProjektzeiten finden Sie unter Kap_04\Formulare.mdb. Für die Beziehungen legen Sie referentielle Integrität fest, da so sichergestellt ist, dass für jeden Eintrag in die Tabelle tblProjektzeiten ein Projekt und ein Mitarbeiter ausgewählt werden. Wer dieses Beispiel ausbauen möchte, wird natürlich die Projekt- und die Mitarbeitertabelle noch um einige Felder erweitern. Aber auch die Verknüpfungstabelle tblProjektzeiten kann noch weitere wichtige Informationen speichern: etwa eine Kurzbeschreibung der jeweiligen Tätigkeit und eine Tätigkeitsart wie »Konzeption«, »Programmierung« oder »Test«.
Abbildung verschiedener Beziehungsarten
201
Abbildung 4.26: Datenmodell der Beispieltabellen
Hauptformular der m:n-Beziehung Bevor Sie sich der Erstellung des Hauptformulars zuwenden, müssen Sie sich bei m:nBeziehungen jeweils zunächst überlegen, von welcher der beiden Tabellen Sie nur je einen Datensatz im Hauptformular anzeigen möchten und welche Tabelle die Datensätze für das Unterformular liefert. Im vorliegenden Fall sind beide Varianten interessant: Man könnte sich sowohl zu jedem Projekt die Zeiten ansehen, die die einzelnen Mitarbeiter damit verbracht haben, andererseits ist es vielleicht nicht ganz uninteressant, womit der eine oder andere Mitarbeiter seine Zeit verbringt. Wichtiger für die Auswertung von Projekten ist sicher die erste Variante. Das Hauptformular zeigt also die Daten der Tabelle tblProjekte an, die dementsprechend auch als Datenherkunft des Formulars dient. Wenn Sie aus der Feldliste die beiden Felder ProjektID und Projekt in den Detailbereich des Formulars ziehen, ist der erste Teil der Arbeit bereits erledigt (siehe Abbildung 4.27).
Abbildung 4.27: Hauptformular der m:n-Beziehung (»frmProjektzeiten«)
202
4
Formulare
Als Nächstes fügen Sie nun noch das Unterformular zur Anzeige der Mitarbeiter und der jeweiligen Projektzeiten hinzu.
Unterformular der m:n-Beziehung Das Unterformular soll anzeigen, welcher Mitarbeiter an welchem Datum wie viele Stunden mit einem Projekt verbracht hat. Dazu legen Sie für das Unterformular die Abfrage aus Abbildung 4.28 als Datenherkunft an.
Abbildung 4.28: Datenherkunft des Unterformulars der m:n-Beziehung
Der Aufbau dieser Abfrage ist für alle Unterformulare von Formularen zur Darstellung von m:n-Beziehungen gleich. Die Abfrage enthält jeweils die Verknüpfungstabelle und die Tabelle, deren Daten nicht im Hauptformular angezeigt werden – in diesem Fall also die Tabellen tblProjektzeiten und tblMitarbeiter. Auch für die Felder gibt es feste Regeln. Sie benötigen immer folgende Felder: Fremdschlüsselfeld der Verknüpfungstabelle zur Tabelle, die im Hauptformular angezeigt wird (hier ProjektID). Fremdschlüsselfeld der Verknüpfungstabelle zur anderen Tabelle der m:n-Beziehung (hier MitarbeiterID). Dieses Feld wird in der Regel als Kombinationsfeld ausgeführt, damit ein Datensatz dieser Tabelle ausgewählt werden kann. Alle sonstigen Felder der Verknüpfungstabelle und der im Unterformular anzuzeigenden Tabelle, die im Unterformular angezeigt werden sollen (hier die Felder Datum und Zeit der Tabelle tblProjektzeiten und Telefon der Tabelle tblMitarbeiter).
Abbildung verschiedener Beziehungsarten
203
Im Unterformular stellen Sie dann die Eigenschaft Datensatzherkunft auf die soeben erstellte Abfrage ein. Im Detailbereich des Unterformulars finden nicht alle Felder der zugrunde liegenden Abfrage Platz: Das Fremdschlüsselfeld, das auf den entsprechenden Datensatz des Hauptformulars verweist, muss nicht angezeigt werden. Lediglich das Feld zur Auswahl des Mitarbeiters sowie einige weitere Felder sollen später sichtbar sein (siehe Abbildung 4.29).
Abbildung 4.29: Das Unterformular der m:n-Beziehung in der Entwurfsansicht
Datenblatt- oder Endlosansicht? Da das Unterformular mehrere Datensätze anzeigen soll, stehen die Datenblatt- und die Endlosansicht zur Verfügung. Wenn Sie keine besonderen Ansprüche an das Layout der Steuerelemente im Unterformular haben und außer Text- und Kombinationsfeldern keine Steuerelemente benötigen, sind Sie mit der Datenblattansicht gut bedient. In diesem Beispiel ist das der Fall; stellen Sie daher die Eigenschaft Standardansicht auf Datenblattansicht ein. Das Unterformular sieht in der Datenblattansicht nun wie in Abbildung 4.30 aus. Wenn Sie bereits Daten in die Tabelle tblMitarbeiter eingegeben haben oder die Tabellen und Formulare aus der Beispieldatenbank verwenden, können Sie das Unterformular bereits testen. Die Auswahl eines Mitarbeiters per Kombinationsfeld sorgt automatisch für das Füllen der übrigen Felder, die Sie aus der Tabelle tblMitarbeiter in das Unterformular übernommen haben. Fehlt noch eine Eingabe in die beiden Felder Datum und Zeit und das Speichern des Datensatzes. Letzteres schlägt wegen der Regeln zur referentiellen Integrität freilich fehl, da noch kein Wert für das Feld ProjektID der Abfrage qryProjektzeiten angegeben wurde – was ja auch gar nicht geht, da das Feld überhaupt nicht im Formular angezeigt wird.
204
4
Formulare
Abbildung 4.30: Das Unterformular in der Datenblattansicht
Wie sollen Sie also Datensätze im Unterformular anlegen, wenn Sie gar keinen Zugriff auf eines der wichtigsten Felder haben? Ganz einfach: Sie automatisieren die Eingabe der Werte für dieses Feld. Das Unterformular ist ja eigentlich für den Einsatz im Hauptformular frmProjektzeiten gedacht, in das Sie es nun einfügen können. Es gibt mehrere Möglichkeiten, ein Formular als Unterformular in ein anderes Formular einzubauen. Am schnellsten geht es aber vermutlich, indem Sie das Zielformular in der Entwurfsansicht neben dem Datenbankfenster platzieren und das zukünftige Unterformular aus dem Datenbankfenster einfach an die gewünschte Stelle ziehen. Nachdem Sie das Unterformular in das Hauptformular eingefügt haben, werfen Sie einen Blick auf die Eigenschaften Verknüpfen von und Verknüpfen nach (siehe Abbildung 4.31). Dort sollte Access beim Einfügen des Unterformulars automatisch jeweils den Wert ProjektID eingetragen haben. Diese Einstellung gewährleistet Folgendes: Das Feld ProjektID wird bei neuen Datensätzen im Unterformular automatisch mit dem entsprechenden Wert des Hauptformulars gefüllt. Das Unterformular zeigt nur Datensätze an, deren ProjektID dem entsprechenden Wert des Hauptformulars entspricht. Ein Wechsel in die Formularansicht demonstriert die Funktion des Formulars. Sie können nun einen neuen Datensatz im Hauptformular anlegen und im Unterformular die gewünschten Projektzeiten eintragen. Dazu wählen Sie dort einfach den Mitarbeiter aus und geben Datum und Zeit ein – um den Inhalt des Feldes ProjektID kümmert sich Access selbst (siehe Abbildung 4.32).
Abbildung verschiedener Beziehungsarten
205
Abbildung 4.31: Verknüpfen von Haupt- und Unterformular
Abbildung 4.32: m:n-Beziehung im Einsatz
4.4.11 m:n-Beziehungen per Listenfeld Die Verwaltung von Daten aus m:n-Beziehungen erfolgt verhältnismäßig selten mit Hilfe von Listenfeldern. Der Grund ist, dass Listenfelder keine direkte Bearbeitung der angezeigten Daten ermöglichen. Daher sind Listenfelder eher zum übersichtlichen Zuweisen von Datensätzen der n-Seite zu den Datensätzen der m-Seite einer Beziehung einsetzbar. Ein gutes Beispiel findet sich in der Access-Anwendung selbst: Die Zuteilung der Gruppen zu einem Benutzer erfolgt dort wie in Abbildung 4.33. Die Assistenten von Access basieren im Übrigen auch alle auf Access-Formularen und VBA.
206
4
Formulare
Beispieldatenbank: Die Tabellen tblFahrzeuge, tblFahrzeugeSonderausstattungen, tblAusstattungen und das Formular frmFahrzeuge finden Sie unter Kap_04\Formulare.mdb.
Abbildung 4.33: Beispiel für den Einsatz von Listenfeldern für die Zuordnung von Datensätzen
In diesem Fall enthält das eine Listenfeld alle vorhandenen Gruppen, während das zweite Listenfeld nur die Gruppen anzeigt, die dem aktuell angezeigten Benutzer zugeordnet sind. Nachfolgend erfahren Sie, wie Sie solche Dialoge zur Verwaltung von m:n-Beziehungen mit bestehenden Daten selbst erstellen können. Einsatzzwecke für die Anwendung von Listenfeldern in Zusammenhang mit m:nBeziehungen gibt es genug: Die Verwaltung von Verteilerlisten, die das Zuordnen von Empfängern zu einer Publikation ermöglichen, Benutzer und Benutzergruppen (wie in obigem Beispiel) oder Fahrzeuge und Sonderausstattungen. Das folgende Formular soll anhand des Beispiels der Fahrzeuge und Ausstattungsmerkmale hergeleitet werden. Zum Nachvollziehen benötigen Sie drei Tabellen: Die beiden Tabellen tblFahrzeuge und tblAusstattungen werden über die Tabelle tblFahrzeugeAusstattungen miteinander verknüpft (siehe Abbildung 4.34). Für die beiden Beziehungen legen Sie jeweils referentielle Integrität mit Löschweitergabe fest.
Abbildung verschiedener Beziehungsarten
207
Abbildung 4.34: Datenmodell der Beispieltabellen
Das Formular zur Verwaltung der enthaltenen Daten besitzt als Datenherkunft die Tabelle tblFahrzeuge und enthält die folgenden Steuerelemente (siehe Abbildung 4.35): FahrzeugID und Fahrzeug: Textfelder, gebunden an die Datenherkunft des Formulars. lstVorhandeneAusstattung: Zeigt alle Datensätze der Tabelle tblAusstattungen an, die über die Tabelle tblFahrzeugeAusstattungen mit der Tabelle tblFahrzeuge verknüpft sind. lstNichtVorhandeneAusstattung: Zeigt alle Datensätze der Tabelle tblAusstattungen an, die das Listenfeld lstVorhandeneAusstattung nicht anzeigt. cmdEntfernen: Verschiebt das aktuell im linken Listenfeld markierte Ausstattungsmerkmal in das rechte Listenfeld. cmdAlleEntfernen: Verschiebt alle Einträge des linken Listenfeldes in das rechte Listenfeld. cmdHinzufuegen: Verschiebt das aktuell im rechten Listenfeld markierte Ausstattungsmerkmal in das linke Listenfeld. cmdAlleHinzufuegen: Verschiebt alle Einträge des rechten Listenfeldes in das linke Listenfeld. Zusätzlich zu den bereits erwähnten Funktionen soll ein Doppelklick auf einen Eintrag eines der Listenfelder den betroffenen Eintrag in das jeweils andere Listenfeld verschieben.
208
4
Formulare
Abbildung 4.35: Entwurfsansicht des Formulars zum Festlegen der Ausstattungsmerkmale eines Fahrzeugs
Datensatzherkunft der Listenfelder Die Beschreibung der Steuerelemente des Formulars hat den Inhalt der beiden Listenfelder bereits scharf umrissen. Da der Inhalt des rechten Listenfeldes vom Inhalt des linken Listenfeldes abhängt, beginnen Sie mit dem linken Listenfeld. Es soll alle Datensätze der Tabelle tblAusstattungen enthalten, die über die Tabelle tblFahrzeugeAusstattungen mit der Tabelle tblFahrzeuge verknüpft sind. Das aktuell angezeigte Fahrzeug lässt sich über das Feld FahrzeugID identifizieren. Da die Verknüpfungstabelle den entsprechenden Wert bereits enthält, besteht die Abfrage für die Datensatzherkunft aus den beiden Tabellen tblFahrzeugeAusstattungen und tblAusstattungen (siehe Abbildung 4.36). Das Listenfeld soll die Bezeichnung des Ausstattungsmerkmals anzeigen, aber das Feld AusstattungID als gebundene Spalte verwenden; das Feld FahrzeugID ist nur als Kriterium vorgesehen und muss gar nicht angezeigt werden. Damit das Listenfeld die erste Spalte mit dem Feld AusstattungID ausblendet, stellen Sie die Eigenschaften Spaltenanzahl und Spaltenbreite auf die Werte 2 beziehungsweise 0cm ein. Wenn Sie die Tabellen aus der Beispieldatenbank verwenden oder bereits selbst Beispieldaten eingegeben haben, können Sie nun ausprobieren, ob das Listenfeld die gewünschten Daten anzeigt. Beim ersten angezeigten Fahrzeug sollte dies funktionieren, beim Blättern zum nächsten Fahrzeug verändert sich der Inhalt des Listenfeldes allerdings nicht. Das liegt daran, dass seine Datensatzherkunft nicht aktualisiert wird.
Abbildung verschiedener Beziehungsarten
209
Abbildung 4.36: Datensatzherkunft des Listenfeldes lstVorhandeneAusstattung (»frmFahrzeuge«)
Legen Sie also die folgende Prozedur an, die durch das Ereignis Beim Anzeigen des Formulars ausgelöst wird: Private Sub Form_Current() 'Aktualisieren des linken Listenfeldes Me!lstVorhandeneAusstattung.Requery End Sub Listing 4.15: Diese Prozedur sorgt für die Aktualisierung des Listenfeldes beim Datensatzwechsel.
Legen Sie nun die Abfrage an, die als Datensatzherkunft für das zweite Listenfeld dient. Das Listenfeld soll alle Ausstattungen anzeigen, die nicht zu einem Fahrzeug gehören und die nicht über die Tabelle tblFahrzeugeAusstattungen mit der Tabelle tblFahrzeuge verknüpft sind. Das sind alle Datensätze der Tabelle tblAusstattungen, die nicht im linken Listenfeld angezeigt werden – also formulieren Sie die Abfrage auch einfach so. Diese enthält zunächst lediglich die beiden Felder der Tabelle tblAusstattungen. Den Bezug zu der Abfrage, die als Datensatzherkunft des linken Listenfeldes dient, erstellen Sie über das Kriterium für das Feld AusstattungID: Dort schließen Sie über das Schlüsselwort NOT IN alle Ausstattungen der Abfrage des linken Listenfeldes aus. Allerdings müssen Sie die dortige Abfrage noch ein wenig bearbeiten, da die für IN-Bedingungen verwendeten Unterabfragen nur ein Feld ausgeben dürfen. Den in Abbildung 4.37 nicht komplett zu erkennenden Ausdruck für die Bedingung finden Sie hier: Nicht In (SELECT tblAusstattungen.AusstattungID FROM tblAusstattungen INNER JOIN tblFahrzeugeAusstattungen ON tblAusstattungen.AusstattungID = tblFahrzeugeAusstattungen.AusstattungID WHERE tblFahrzeugeAusstattungen.FahrzeugID=[Forms]![frmFahrzeuge]![FahrzeugID])
210
4
Formulare
Abbildung 4.37: Datensatzherkunft des Listenfeldes der nicht vorhandenen Ausstattungsmerkmale (»frmFahrzeuge«)
Nun zeigen die beiden Listenfelder bereits die gewünschten Daten an (siehe Abbildung 4.38). Als Nächstes bringen Sie ein wenig Leben in die Schaltflächen und Listenfelder, um das Formular zum Hinzufügen und Entfernen von Datensätzen in der Tabelle tblFahrzeugeAusstattungen verwenden zu können. Damit auch das rechte Listenfeld beim Datensatzwechsel aktualisiert wird, ergänzen Sie die Ereignisprozedur Form_Current wie in folgendem Listing: Private Sub Form_Current() 'Aktualisieren des linken Listenfeldes Me!lstVorhandeneAusstattung.Requery 'Aktualisieren des rechten Listenfeldes Me!lstNichtVorhandeneAusstattung.Requery
End Sub Listing 4.16: Aktualisieren der Listenfelder
Hinzufügen eines Ausstattungsmerkmals Das Formular bietet zwei Möglichkeiten zum Hinzufügen eines einzelnen Ausstattungsmerkmals zu einem Fahrzeug: per Doppelklick auf den gewünschten Eintrag im Listenfeld lstNichtVorhanden oder durch Markieren des Eintrags im selben Listenfeld und anschließendes Betätigen der Schaltfläche cmdHinzufuegen. Da in beiden Varianten die gleiche Aktion ausgelöst werden soll, legen Sie diese in einer separaten Prozedur an, die von der jeweiligen Ereignisprozedur der beiden Steuerelemente aus aufgerufen wird.
Abbildung verschiedener Beziehungsarten
211
Abbildung 4.38: Die Listenfelder zeigen bereits die richtigen Daten an (»frmFahrzeuge«).
Für das Anlegen des neuen Datensatzes in der Tabelle tblFahrzeugeAusstattungen müssen Sie die zukünftigen Werte der Felder FahrzeugID und AusstattungID kennen. Diese ermitteln Sie in den beiden Ereignisprozeduren Beim Klicken der Schaltfläche cmdHinzufuegen und Beim Doppelklicken des Listenfeldes lstNichtVorhanden und rufen von dort aus die Prozedur Hinzufuegen auf. Sollte einer der beiden Werte nicht vorhanden sein, was der Fall ist, wenn entweder kein hinzuzufügender Eintrag des Listenfeldes markiert ist oder das Formular einen neu angelegten Fahrzeug-Datensatz enthält, wird die Hinzufuegen-Prozedur nicht aufgerufen. Private Sub cmdHinzufuegen_Click() If Not IsNull(Me!FahrzeugID) And _ Not IsNull(Me!lstNichtVorhandeneAusstattung) Then Hinzufuegen Me!FahrzeugID, Me!lstNichtVorhandeneAusstattung End If End Sub Private Sub lstNichtVorhandeneAusstattung_DblClick(Cancel As Integer) If Not IsNull(Me!FahrzeugID) And _ Not IsNull(Me!lstNichtVorhandeneAusstattung) Then Hinzufuegen Me!FahrzeugID, Me!lstNichtVorhandeneAusstattung End If End Sub Listing 4.17: Aufrufen der Prozedur zum Anlegen eines neuen Datensatzes in der Tabelle tblFahrzeugeAusstattungen
212
4
Formulare
Die Prozedur Hinzufuegen erwartet als Parameter die Fahrzeug-ID und die Ausstattungs-ID des zu erstellenden Datensatzes. Nach der Durchführung der Aktionsabfrage aktualisiert die Prozedur die beiden Listenfelder. Private Sub Hinzufuegen(lngFahrzeugID As Long, _ lngAusstattungID As Long) Dim db As DAO.Database Dim strSQL As String Set db = CurrentDb 'Zusammen der Anfügeabfrage strSQL = "INSERT INTO tblFahrzeugeAusstattungen" _ & "(FahrzeugID, AusstattungID) VALUES(" _ & lngFahrzeugID & ", " & lngAusstattungID & ")" 'Ausführen der Anfügeabfrage db.Execute strSQL 'Aktualisieren der Listenfelder Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End Sub Listing 4.18: Prozedur zum Hinzufügen eines Datensatzes zur Tabelle tblFahrzeugeAusstattungen
Entfernen eines Ausstattungsmerkmals Zum Entfernen eines Ausstattungsmerkmals eines Fahrzeugs gibt es ebenfalls zwei Möglichkeiten. Entweder Sie markieren den zu entfernenden Datensatz und klicken auf die Schaltfläche zum Entfernen eines Datensatzes oder Sie klicken doppelt auf den zu entfernenden Eintrag im Listenfeld. Der Aufbau der dadurch ausgelösten Prozeduren ist identisch mit dem der Prozeduren zum Hinzufügen eines Ausstattungsmerkmals und die Prozedur Entfernen ist das Pendant zur Prozedur Hinzufuegen. Lediglich die verwendete SQL-Anweisung hat ein anderes Aussehen: Private Sub Entfernen(lngFahrzeugID As Long, lngAusstattungID As Long) Dim db As DAO.Database Dim strSQL As String
Abbildung verschiedener Beziehungsarten
213
Set db = CurrentDb strSQL = "DELETE FROM tblFahrzeugeAusstattungen " _ & "WHERE FahrzeugID = " & lngFahrzeugID _ & " AND AusstattungID = " & lngAusstattungID
db.Execute strSQL Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End Sub Listing 4.19: Prozedur zum Entfernen eines Ausstattungsmerkmals eines Fahrzeugs
Hinzufügen oder Entfernen aller Ausstattungsmerkmale Die beiden Schaltflächen mit dem doppelten Kleiner- beziehungsweise Größer-Zeichen bewegen jeweils alle Ausstattungen eines Fahrzeugs von einem Listenfeld ins andere. Aus Datensicht bedeutet das, dass entweder für jedes Ausstattungsmerkmal in Kombination mit dem aktuell angezeigten Fahrzeug ein Datensatz in der Tabelle tblFahrzeugeAusstattungen angelegt wird oder dass alle vorhandenen Einträge für ein Fahrzeug entfernt werden. Das Hinzufügen aller Ausstattungsmerkmale erledigt die Prozedur, die durch das Ereignis Beim Klicken der Schaltfläche cmdAlleHinzufuegen ausgelöst wird. Im Gegensatz zu den Prozeduren zum Hinzufügen eines einzelnen Datensatzes braucht hier nicht geprüft zu werden, ob ein Eintrag des Listenfeldes markiert ist: Private Sub cmdAlleHinzufuegen_Click() Dim db As DAO.Database Dim strSQL As String If Not IsNull(Me!FahrzeugID) Then Set db = CurrentDb strSQL = "INSERT INTO tblFahrzeugeAusstattungen " _ & "SELECT " & Me!FahrzeugID & " AS FahrzeugID, " _ & "AusstattungID FROM tblAusstattungen" db.Execute strSQL Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery
214
4
Formulare
Set db = Nothing End If End Sub Listing 4.20: Hinzufügen aller vorhandenen Ausstattungsmerkmale zu einem Fahrzeug
Das Entfernen aller Ausstattungen eines Fahrzeugs geschieht in der folgenden Prozedur. Der wesentliche Unterschied zur vorherigen Prozedur liegt in der SQL-Anweisung: Private Sub cmdAlleEntfernen_Click() Dim db As DAO.Database Dim strSQL As String If Not IsNull(Me!FahrzeugID) Then Set db = CurrentDb strSQL = "DELETE FROM tblFahrzeugeAusstattungen " _ & "WHERE FahrzeugID = " & Me!FahrzeugID
db.Execute strSQL Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End If End Sub Listing 4.21: Entfernen aller Ausstattungsmerkmale eines Fahrzeugs
4.4.12 Reflexive Beziehungen Für die Darstellung der Daten aus reflexiven Beziehungen bietet sich das TreeviewSteuerelement an. Mit ein wenig Fantasie lassen sich hierarchische Daten zwar auch in Listenfeldern oder gar in Textfeldern anzeigen, aber ihre Anwendung macht nicht wirklich Spaß. Daher finden Sie nachfolgend eine Anleitung zum Erstellen eines Formulars mit einen Treeview-Steuerelement zur Darstellung der Hierarchie von Mitarbeitern und ihren Vorgesetzten.
Abbildung verschiedener Beziehungsarten
215
Eine ausführliche Vorstellung der Funktionen und Möglichkeiten des TreeviewSteuerelements würde den Rahmen dieses Kapitels bei weitem sprengen. Daher soll hier nur exemplarisch gezeigt werden, wie Sie ein Formular zur Anzeige von reflexiven verknüpften Daten erstellen können. Das Treeview-Steuerelement ist übrigens Bestandteil der Datei msocomctl.ocx, die mit Office 2003 in der Professional-Version installiert wird. Das Treeview-Steuerelement legen Sie über den Dialog ActiveX-Steuerelement einfügen an, den Sie mit dem Menüeintrag Einfügen/ActiveX-Steuerelement… aufrufen (siehe Abbildung 4.39).
Abbildung 4.39: Einfügen des Treeview-Steuerelements
Benennen Sie das neu hinzugefügte Steuerelement ctlTreeview. Anschließend können Sie direkt den für das Füllen des Treeview-Steuerelements benötigten Code schreiben.
Füllen des Treeview-Steuerelements Zum Speisen des Treeview-Steuerelements mit Daten aus einer Tabelle mit einer reflexiven Beziehung benötigen Sie eine rekursive Prozedur. Das bedeutet, dass sich die Prozedur immer wieder selbst aufruft, solange die Datenherkunft tiefer verschachtelte Elemente enthält, und erst dann die folgenden Elemente der übergeordneten Ebenen abarbeitet.
216
4
Formulare
Den Start macht allerdings eine herkömmliche Routine. Sie legt eine modulweit gültige Variable namens objTreeview mit einem Verweis auf das Steuerelement an. Anschließend durchläuft die Routine alle Datensätze eines Recordsets mit allen Mitarbeitern, die keinen Vorgesetzten haben, und fügt je einen Knoten zum Treeview-Steuerelement hinzu. Dabei legt sie als Beschriftung den Nachnamen und den Vornamen des aktuellen Mitarbeiters an. Außerdem verwendet sie die Key-Eigenschaft des neuen Elements, um den Wert des Feldes MitarbeiterID des aktuellen Datensatzes hinzuzufügen. Da Werte der Eigenschaft Key mit einem Buchstaben beginnen müssen, stellt die Routine ein »x« voran. Nach dem Anlegen des Knotens ruft sie die Routine zum Anlegen der untergeordneten Mitarbeiter auf – dazu weiter unten mehr. Im Anschluss daran werden weitere Datensätze – soweit vorhanden – auf die gleiche Art abgearbeitet. Dim objTreeview As TreeView Dim db As DAO.Database Private Sub Form_Load() Dim rst As DAO.Recordset Dim objNode As Node Dim objListItem As ListItem Set objTreeview = Me!ctlTreeview.Object objTreeview.Nodes.Clear Set db = CurrentDb Set rst = db.OpenRecordset( _ "SELECT * FROM tblMitarbeiterMitVorgesetzten " _ & "WHERE VorgesetzterID IS NULL", dbOpenDynaset) Do While Not rst.EOF Set objNode = objTreeview.Nodes.Add With objNode .Text = rst!Nachname & ", " & rst!Vorname .Key = "x" & rst!MitarbeiterID End With AddChilds rst!MitarbeiterID rst.MoveNext Loop End Sub Listing 4.22: Diese Routine fügt die Elemente der ersten Ebene in das Treeview-Steuerelement ein.
Die Routine AddChilds erwartet die MitarbeiterID des übergeordneten Mitarbeiters als Parameter. Per OpenRecordset legt die Routine eine Datensatzgruppe an, die alle Mitarbeiter-Datensätze enthält, deren Vorgesetzter der Mitarbeiter mit der übergebenen MitarbeiterID ist. Anschließend wird für jeden »Untergebenen« ein Element unterhalb des Vorgesetzten angelegt. Um das Element des Vorgesetzten zu ermitteln, setzt die Routine den Buch-
Abbildung verschiedener Beziehungsarten
217
staben »x« und die MitarbeiterID zu dem Wert zusammen, den die aufrufende Routine als Key für den übergeordneten Mitarbeiter angelegt hat, und findet so das passende Element. Für jeden »Untergebenen« wird diese Routine rekursiv aufgerufen, um auch die »Untergebenen« der »Untergebenen« zu finden und entsprechende Elemente anzulegen. Private Dim Dim Set
Sub AddChilds(lngMitarbeiterID As Long) rst As DAO.Recordset objNode As Node rst = db.OpenRecordset( _ "SELECT * FROM tblMitarbeiterMitVorgesetzten " _ & "WHERE VorgesetzterID = " _ & lngMitarbeiterID, dbOpenDynaset) Do While Not rst.EOF Set objNode = objTreeview.Nodes.Add(Relative:="x" _ & lngMitarbeiterID, Relationship:=tvwChild) With objNode .Text = rst!Nachname & ", " & rst!Vorname .Key = "x" & rst!MitarbeiterID End With AddChilds rst!MitarbeiterID rst.MoveNext Loop End Sub Listing 4.23: Rekursiver Teil der Prozeduren zum Füllen des Treeview-Steuerelements
Auf diese Weise werden alle Datensätze durchlaufen und dem Treeview-Steuerelement hinzugefügt.
Anzeigen des Detailformulars zu einem Element Natürlich sollen Sie auch etwas mit dem gefüllten Treeview anfangen können. Daher finden Sie nachfolgend eine kleine Routine, die nach einem Klick auf eine dafür vorgesehene Schaltfläche ein Detailformular mit den Daten des aktuell im Treeview-Steuerelement markierten Eintrags anzeigt: Private Sub cmdDetails_Click() Dim lngMitarbeiterID As Long lngMitarbeiterID = CLng(Mid(objTreeview.SelectedItem.Key, 2)) DoCmd.OpenForm "frmMitarbeiterDetail", _ WhereCondition:="MitarbeiterID = " & lngMitarbeiterID End Sub Listing 4.24: Code zum Öffnen eines Detailformulars zum aktuellen Element des Treeview-Steuerelements
218
4
Formulare
Das gefüllte Treeview-Steuerelement mit Detailformular für einen der Einträge sieht wie in Abbildung 4.40 aus.
Abbildung 4.40: Treeview-Steuerelement mit Detailformular (»frmMitarbeiterMitVorgesetzten«, »frmMitarbeiterDetail«)
4.5 Von Formular zu Formular Weiter oben haben Sie bereits einige Möglichkeiten kennen gelernt, mit denen Sie von einem Formular aus ein weiteres Formular aufrufen und diesem bestimmte Informationen übergeben können. Bei der Anwendung dieser Möglichkeiten sind einige Voraussetzungen zu beachten, damit Sie länger Spaß an den auf diese Weise »verknüpften« Formularen haben. Die wichtigste Regel ist: Sorgen Sie für möglichst wenig Abhängigkeiten zwischen aufrufendem und aufgerufenem Formular. Das bedeutet in diesem Fall, dass Sie die Abhängigkeit erstens unidirektional auslegen und zweitens an bestimmten Punkten konzentrieren sollten – etwa auf das Öffnen und das Schließen des aufgerufenen Formulars. Zur besseren Verständlichkeit heißt das aufrufende Formular in den nächsten Abschnitten »Parent« und das aufgerufene Formular »Child«.
Von Formular zu Formular
219
Unidirektionale Abhängigkeit bedeutet, dass zwar das Parent-Formular das Child-Formular kennen muss, aber nicht umgekehrt. In der Praxis sieht das folgendermaßen aus: Das Parent-Formular kennt den Namen des Child-Formulars und ruft dieses darüber auf. Das Parent-Formular kennt die Datenherkunft des Child-Formulars und weiß, wie eine Bedingung zur Einschränkung der Datenherkunft aussehen muss. Das Parent-Formular weiß, welche Werte es dem Child-Formular mit dem Öffnungsargument übergeben kann. Umgekehrt weiß das Child-Formular nichts vom Parent-Formular – es wertet lediglich die vom Parent-Formular übergebenen Informationen aus, ist aber nicht von der Lieferung dieser Informationen abhängig. Andersherum soll das Parent-Formular neben den beim Aufruf übergebenen Informationen nach dem Schließen des Child-Formulars Werte von dort auslesen können. Daher darf das Child-Formular nicht geschlossen, sondern nur unsichtbar gemacht werden. Außer diesen zwei Kontakten – Parameterübergabe beim Aufruf und Auslesen des Child-Formulars vor dem Schließen – finden im Optimalfall keine Kontakte statt. Das heißt insbesondere, dass das Child-Formular nicht auf das Parent-Formular zugreift. Es gibt natürlich die Möglichkeit, Variablen, die zwischen Parent- und Child-Formular hin- und hergereicht werden sollen, in globalen Variablen zu speichern; man könnte auch vom Child-Formular aus lesend und schreibend auf das Parent-Formular zugreifen. Ersteres bedingt, dass beide Formulare die globalen Variablen kennen müssen, und Letzteres, dass das Parent-Formular das Child-Formular kennen muss und umgekehrt und diese damit voneinander abhängig sind.
Formulare aufrufen und vor dem Schließen auslesen VBA-technisch sieht das wie folgt aus. Der Aufruf des Child-Formulars erfolgt mit der DoCmd.OpenForm-Methode. Damit das Parent-Formular das Child-Formular nach dem Ausblenden und vor dem Schließen auslesen kann, muss das Child-Formular als modaler Dialog geöffnet werden. Das bedeutet, dass keine anderen Aktionen in der Access-Anwendung möglich sind, solange das Child-Formular geöffnet ist. Auf diese Weise verhindern Sie Wechselwirkungen zwischen Child-Formular und anderen Elementen der Benutzungsoberfläche. Um ein Formular mit der OpenForm-Methode als modalen Dialog zu öffnen, verwendet man den Parameter WindowMode mit dem Wert acDialog: DoCmd.OpenForm "frmChild", WindowMode:=acDialog
220
4
Formulare
Wichtigster Nebeneffekt des Öffnens als modaler Dialog ist, dass auch die aufrufende Prozedur angehalten wird. Diese läuft erst dann weiter, wenn das Child-Formular durch Setzen der Eigenschaft Visible auf den Wert True oder durch Schließen den Fokus verliert. Die aufrufende Routine weiß dann bei der nächsten Zeile zumindest sicher, dass das Child-Formular entweder geschlossen oder ausgeblendet ist. Erstrebenswert ist natürlich Letzteres, denn sonst könnten Sie von dort keine Informationen mehr erlangen. Also sorgen Sie dafür, dass das Formular nicht auf herkömmlichem Wege geschlossen, sondern nur über eine spezielle Schaltfläche ausgeblendet werden kann. Die passende Ereignisprozedur einer Schaltfläche mit dem Namen cmdOK sieht dann folgendermaßen aus: Private Sub cmdOK_Click() Me.Visible = True End Sub Listing 4.25: Ausblenden eines Formulars
In der aufrufenden Prozedur sollten Sie auf jeden Fall prüfen, ob das Formular noch geöffnet ist. Dazu können Sie die Eigenschaft IsLoaded des entsprechenden Elements der AllForms-Auflistung verwenden: If CurrentProject.AllForms("").IsLoaded = True Then …
Aber auch das verschafft noch keine Sicherheit, denn das Formular könnte ja auch in der Entwurfsansicht geöffnet sein. Also folgt eine weitere Prüfung: If CurrentProject.AllForms("").CurrentView = _ acCurViewFormBrowse Then …
Anschließend kann das Child-Formular in Ruhe ausgelesen werden, bevor es geschlossen wird.
Beispiel für das Aufrufen eines weiteren Formulars Wenn Sie ein Kombinationsfeld etwa für die Auswahl der Kategorie eines Artikels verwenden, sollten Sie die Möglichkeit bieten, leicht weitere Kategorien anzulegen. Im folgenden Beispiel wird bei der Eingabe eines Eintrags, der in der Datensatzherkunft eines Kombinationsfeldes noch nicht vorhanden ist (siehe Abbildung 4.41), ein weiteres Formular zum Anlegen eines neuen Datensatzes in der Kategorien-Tabelle angezeigt (siehe Abbildung 4.42).
Von Formular zu Formular
221
Abbildung 4.41: Anlegen einer neuen Kategorie (»frmArtikel«) …
Abbildung 4.42: … in einem eigenen Formular (»frmKategorien«)
Ob der Benutzer einen noch nicht in der Datensatzherkunft eines Kombinationsfeldes vorhandenen Datensatz eingefügt hat, stellen Sie daran fest, dass das Ereignis Bei nicht in Liste ausgelöst wird. Dieses Ereignis machen Sie sich zu Nutze und fragen darin den Benutzer, ob er den neuen Eintrag tatsächlich anlegen möchte – vielleicht hat er sich ja auch nur vertippt. Anderenfalls öffnet die Routine das Formular frmKategorien (siehe Abbildung 4.42) mit einem neuen Datensatz, der automatisch mit der neuen Kategorie gefüllt wird – dazu später mehr. Nach der Eingabe und dem Ausblenden des Formulars prüft die Routine, ob das Formular noch in der Formularansicht geöffnet ist, und liest gegebenenfalls die KategorieID des neuen Eintrags ein. Nach dem Schließen des aufgerufenen Formulars, das zu diesem Zeitpunkt nur ausgeblendet, aber nicht geschlossen ist, stellt die Routine das Kombinationsfeld auf den neuen Wert ein. Private Sub KategorieID_NotInList(NewData As String, Response As Integer) Dim lngKategorieID As Long If MsgBox("Möchten Sie diese Kategorie anlegen?", _
222
4
Formulare
vbYesNo + vbExclamation, "Neue Kategorie") = vbYes Then DoCmd.OpenForm "frmKategorien", DataMode:=acFormAdd, _ OpenArgs:=NewData, WindowMode:=acDialog If CurrentProject.AllForms("frmKategorien").IsLoaded = True Then If CurrentProject.AllForms("frmKategorien").CurrentView = _ acCurViewFormBrowse Then lngKategorieID = Forms!frmKategorien!KategorieID DoCmd.Close acForm, "frmKategorien" Me!KategorieID = lngKategorieID Me!KategorieID.Requery End If End If End If Response = acDataErrContinue End Sub Listing 4.26: Aufrufen eines weiteren Formulars zum Eingeben verknüpfter Daten
4.6 Besonderheiten von Unterformularen Unterformulare können auf verschiedene Arten eingesetzt werden. Meist gibt es hier und da Probleme – etwa, wenn Daten im Unterformular eingegeben werden, ohne dass ein Datensatz im Hauptformular existiert, oder wenn Änderungen rückgängig gemacht werden sollen, obwohl diese Änderungen im Unterformular bereits gespeichert sind.
4.6.1 Eingabe von Daten ohne Detaildatensatz In Abschnitt 4.4.10 haben Sie ein Formular mit Unterformular zur Eingabe von Projekten und Projektzeiten kennen gelernt. Wenn Sie dort im Hauptformular auf einen neuen Datensatz springen und im Unterformular Daten anlegen, bekommen Sie folgendes Problem: Die Datensätze im Unterformular sind dann nicht mit einem Datensatz im Hauptformular verknüpft und verschwinden in die ewigen Jagdgründe – zumindest aus Sicht dieses Formulars, denn es zeigt im Unterformular nur Datensätze an, die mit dem im Hauptformular befindlichen Datensatz verknüpft sind (siehe Abbildung 4.43). Um diese Probleme zu vermeiden, sollten Sie verhindern, dass der Benutzer mit der Bearbeitung von Daten im Unterformular beginnt, bevor er nicht mindestens ein Feld im Hauptdatensatz ausgefüllt hat.
Besonderheiten von Unterformularen
223
Abbildung 4.43: Eingabe von Daten in ein Unterformular ohne verknüpften Datensatz im Hauptformular (»frmProjektzeiten«)
Dazu legen Sie für die Ereigniseigenschaft Beim Hingehen die folgende Prozedur an: Private Sub sfmProjektzeiten_Enter() If Me.NewRecord Then If Not Me.Dirty Then MsgBox "Bitte legen Sie zuerst einen Projektnamen an." Me!Projekt.SetFocus End If End If End Sub Listing 4.27: Kein Datensatz im Unterformular ohne Pendant im Hauptformular
Die Prozedur prüft, ob das Hauptformular einen neuen Datensatz enthält und ob dieser gegebenenfalls bereits bearbeitet wurde – womit der benötigte Primärschlüsselwert vorhanden wäre. Falls nicht, fordert die Routine den Benutzer auf, zunächst ein Projekt im Hauptformular anzulegen.
4.6.2 Undo in Haupt- und Unterformular Viele Formulare zeigen nicht nur selbst Daten an, sondern enthalten auch noch ein Unterformular zur Bearbeitung von Daten aus verknüpften Tabellen. Wie in gebundenen Formularen üblich, gilt auch hier: Wechselt man den Datensatz, wird er auch in der zugrunde liegenden Tabelle gespeichert. Bis dahin lassen sich Änderungen am aktuell bearbeiteten Datensatz noch relativ leicht durch Auslösen des Undo-Ereignisses rückgängig machen, etwa durch Betätigen der (Esc)-Taste.
224
4
Formulare
Beispieldatenbank: Die Formulare frmProjektzeiten_Undo und sfmProjektzeiten_Undo finden Sie unter Kap_04\Formulare.mdb. Viele Formulare enthalten eine Implementierung einer Undo-Funktion in Form einer Abbrechen-Schaltfläche. Den unbedarften Benutzer täuscht man freilich damit, denn diese Schaltfläche macht lediglich die Änderungen an dem im Hauptformular angezeigten Datensatz rückgängig. Die Änderungen im Unterformular sind und bleiben gespeichert. Abbildung 4.44 zeigt, wie solch ein Formular aussieht. Es handelt sich um das bereits weiter oben hergeleitete Formular mit zunächst zwei (sichtbaren) Erweiterungen: einer OK- und einer Abbrechen-Schaltfläche.
Abbildung 4.44: Formular mit Unterformular und Abbrechen-Funktion (»frmProjektzeiten_Undo«)
Der Code, der durch das Beim Klicken-Ereignis der jeweiligen Schaltfläche ausgelöst wird, sieht etwa wie folgt aus: Private Sub cmdOK_Click() 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Private Sub cmdAbbrechen_Click() 'Prüfen, ob Datensatz seit letzter Speicherung geändert wurde
Besonderheiten von Unterformularen
225
If Me.Dirty = True Then 'Falls ja, Änderungen rückgängig machen Me.Undo End If 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Listing 4.28: Ereignisprozeduren der OK- und der Abbrechen-Schaltfläche
Eine Änderung im Unterformular wird auf jeden Fall gespeichert, da das Verlassen des Unterformulars das Speichern des aktuellen Datensatzes zur Folge hat. Wenn Sie nun einmal eine Korrektur an den Projektzeiten eines Projekts vornehmen und dabei aus Versehen einige Datensätze löschen, weil Sie sich in einem anderen Projekt wähnten, gibt es keine Undo-Funktion: Solche Änderungen führen Sie ohne Netz und doppelten Boden aus. In den folgenden Abschnitten erfahren Sie, wie Sie den Benutzern Ihrer Datenbank (und vielleicht auch sich selbst) das Leben in dieser Hinsicht erleichtern können. Dabei machen Sie Gebrauch von einer Transaktion, die bei der ersten Änderung an einem Datensatz des Hauptformulars oder der verknüpften Datensätze im Unterformular gestartet und mit Speichern des Datensatzes im Hauptformular (etwa durch Betätigen der OK-Schaltfläche oder den Wechsel zu einem anderen Datensatz) beendet wird – was zum endgültigen Speichern der Änderungen im Haupt- und Unterformular führt. Auch die Abbrechen-Schaltfläche kann die Transaktion beenden – allerdings sollen dabei alle seit Beginn der Transaktion durchgeführten Änderungen verworfen werden. Normalerweise ist eine Transaktion ein gängiges Mittel, um mehrere Datenbankoperationen zu bündeln und je nach Verlauf der Operationen komplett durchzuführen oder zu verwerfen. Das kann doch bei den Aktionen innerhalb eines Formulars nicht viel anders sein: Also startet man die Transaktion einfach beim Öffnen des Formulars und beendet sie mit Betätigen der OK- oder der Abbrechen-Schaltfläche. Oder doch nicht? Nein, ganz so einfach ist es nicht. Access führt nämlich die in Zusammenhang mit gebundenen Formularen anfallenden Datenbankzugriffe nicht im Kontext des aktuellen Workspace aus. Wenn Sie also einen Datensatz ändern und speichern, bekommt die Transaktion davon überhaupt nichts mit. Abhilfe schafft überhaupt erst die mit Access 2000 eingeführte Recordset-Eigenschaft von Formularen. Damit lässt sich ein Formular an ein zuvor erstelltes Recordset-Objekt binden, das wiederum sehr wohl unter die Kontrolle einer Transaktion fallen kann.
226
4
Formulare
Es sind also folgende Maßnahmen für die Verwendung einer Transaktion zur Zusammenfassung von Datenbankoperationen in einem Formular mit Unterformular notwendig: Binden des Haupt- und des Unterformulars an Recordset-Objekte Starten der Transaktion beim Ändern des Datensatzes im Hauptformular oder in einem der verknüpften Datensätze im Unterformular Durchführen der Transaktion beim Klick auf die OK-Schaltfläche oder beim Wechseln des Datensatzes im Hauptformular Verwerfen der Operationen der Transaktion beim Betätigen der Abbrechen-Schaltfläche Die letzten beiden Punkte setzen voraus, dass die entsprechenden Prozeduren Informationen über den aktuellen Status haben – ist bereits eine Transaktion gestartet worden und wurde diese bereits beendet? Diese Information muss vom Haupt- und vom Unterformular aus schreib- und lesbar sein, da Datenbankoperationen in beiden Formularen eine Transaktion in Gang setzen können. Das ist deshalb wichtig, weil das Beenden einer Transaktion, die noch nicht gestartet wurde, zu einem Fehler führt. Genauso wichtig ist es, keine zweite Transaktion zu starten, wenn bereits eine Transaktion gestartet wurde. Dies würde als »verschachtelte« Transaktion gewertet werden; die Betätigung der OK- oder Abbrechen-Schaltfläche würde möglicherweise nur die innere von mehreren verschachtelten Transaktionen beenden und zu einem unvorhersehbaren Ergebnis führen.
Öffentliche Variablen für den Transaktionsstatus Es gibt mehrere Möglichkeiten, den Status bezüglich der Transaktion zu speichern und in Haupt- und Unterformular zugänglich zu machen. Globale Variablen verwendet man besser nicht, da diese allzu leicht von einer anderen Stelle aus geändert werden können. Besser ist die Verwendung von Membervariablen in einem der beiden Formulare und ihre Veröffentlichung über entsprechende Property-Prozeduren. Der folgende Quellcode zeigt, wie der Kopf des Klassenmoduls des Hauptformulars aussieht: Option Compare Database Option Explicit Dim mDirtyForm As Boolean Dim mDeletedForm As Boolean Dim db As DAO.Database Dim wrk As DAO.Workspace Public Property Get DirtyForm() As Boolean
Besonderheiten von Unterformularen
227
DirtyForm = mDirtyForm End Property Public Property Let DirtyForm(bolDirtyForm As Boolean) mDirtyForm = bolDirtyForm End Property Listing 4.29: Kopf des Klassenmoduls des Formulars frmProjektzeiten_Undo
Die Eigenschaft mDirtyForm machen Sie durch die nachfolgenden Property-Prozeduren von außerhalb schreib- und lesbar. Auf diese Weise können Sie auf die wichtigen Informationen über den Zustand der Transaktion vom Haupt- und auch vom Unterformular aus zugreifen. Die Verwendung von Property Get- und Property Let-Prozeduren ist ein kleiner Vorgriff auf Kapitel 13, »Objektorientierte Programmierung«. Weitere Informationen über die Verwendung dieser Prozeduren finden Sie in diesem Kapitel.
Recordset-Objekt als Datenherkunft Weiter oben wurde bereits erwähnt, dass Transaktionen von den Vorgängen im Formular und im Unterformular nur etwas mitbekommen, wenn die Datenherkunft ein Recordset-Objekt ist. Für das Hauptformular weisen Sie diese Eigenschaft am besten direkt beim Öffnen zu. Die Prozedur, die durch die Ereigniseigenschaft Beim Öffnen ausgelöst wird, hat folgendes Aussehen: Private Sub Form_Open(Cancel As Integer) 'Recordset-, Database- und Workspace-Objekt deklarieren Dim rst As DAO.Recordset Set db = DBEngine(0)(0) Set wrk = DBEngine.Workspaces(0) 'Recordset-Objekt öffnen... Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset) '...und der entsprechenden Eigenschaft des Formulars zuweisen Set Me.Recordset = rst End Sub Listing 4.30: Ereignisprozedur Form_Open des Hauptformulars frmProjektzeiten_Undo
228
4
Formulare
Da Sie dem Formular nun dynamisch die Datenherkunft zuweisen, können Sie die Eigenschaft Datenherkunft leeren. Wichtig ist, dass die Felder weiterhin an die entsprechenden Felder der Datenherkunft gebunden sind. Anderenfalls können diese natürlich nicht die gewünschten Daten anzeigen. Neben dem Zuweisen der Datenherkunft per Recordset-Eigenschaft ist die Deklaration des Workspace-Objekts wrk die zweite Besonderheit in dieser Prozedur. Ein WorkspaceObjekt können Sie anschaulich als Arbeitsbereich betrachten, in dem die aktuelle Access-Sitzung abläuft.
Kein neues Formular-Recordset während einer Transaktion Wenn eine Transaktion einmal gestartet ist, können Sie dem Formular kein neues Recordset-Objekt zuweisen. Diese Regel bedeutet kein besonderes Problem, da Formular und Unterformular durchaus vor der ersten Änderung der Daten und damit dem Starten der Transaktion mit den benötigten Recordsets versehen werden können. Es ergibt sich nur eine Einschränkung: Sie können während einer Transaktion nicht den Datensatz im Hauptformular wechseln. Warum nicht? Weil bei der Anzeige eines neuen Datensatzes im Hauptformular auch die im Unterformular angezeigten Daten aktualisiert werden müssen. Und das geht wiederum nur durch das Zuweisen eines neuen Recordsets, das die mit dem Datensatz im Hauptformular korrespondierenden Daten enthält. Das ist aber nicht schlimm, denn dass beim Wechseln des Datensatzes die Daten des vorherigen Datensatzes gespeichert werden, sollte dem Benutzer klar sein. Die technische Umsetzung indes ist nicht ganz einfach. Fest steht nur, dass beim Wechsel des Datensatzes, der das Ereignis Beim Anzeigen des Hauptformulars auslöst, dem Unterformular ein neues Recordset zugewiesen werden muss. Um den Rest kümmern Sie sich später. Die Prozedur Form_Current sieht nun folgendermaßen aus: Private Sub Form_Current() Dim strSQL As String Dim rst As DAO.Recordset 'Zusammenstellen des SQL-Strings für das Recordset strSQL = "SELECT tblProjektzeiten.ProjektID, " _ & "tblProjektzeiten.ProjektzeitID, " _ & "tblProjektzeiten.MitarbeiterID, " _ & "tblProjektzeiten.Datum, " _ & "tblProjektzeiten.Zeit, " _ & "tblMitarbeiter.Telefon " _ & "FROM tblProjektzeiten INNER JOIN tblMitarbeiter " _
Besonderheiten von Unterformularen
229
& "ON tblProjektzeiten.MitarbeiterID = " _ & " tblMitarbeiter.MitarbeiterID " _ & "WHERE ProjektID = " & Nz(Me!ProjektID, 0) 'Öffnen des Recordset Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) 'Zuweisen des Recordset an das Unterformular Set Me!sfmProjektzeiten_Undo.Form.Recordset = rst End Sub Listing 4.31: Quellcode der Prozedur Form_Current des Hauptformulars
Alternativ können Sie auch die gespeicherte Abfrage qryProjektzeitenMitarbeiter verwenden (siehe Abbildung 4.28). Die Prozedur sähe dann folgendermaßen aus: Private Sub Form_Current() Dim rst As DAO.Recordset 'Öffnen des Recordset Set rst = db.OpenRecordset("SELECT * FROM qryProjektzeitenMitarbeiter " _ & "WHERE ProjektID = " & Nz(Me!ProjektID, 0), dbOpenDynaset) 'Zuweisen des Recordset an das Unterformular Set Me!sfmProjektzeiten_Undo.Form.Recordset = rst End Sub Listing 4.32: Vereinfachte und performantere Version der Prozedur Form_Current
Dadurch, dass die im Unterformular angezeigten Datensätze ohnehin nach dem im Hauptformular angezeigten Projekt gefiltert werden, sind die beiden Eigenschaften Verknüpfen von und Verknüpfen nach im Unterformularsteuerelement nicht mehr notwendig. Leeren Sie also diese beiden Eigenschaften. Dadurch müssen Sie beim Anlegen neuer Datensätze im Unterformular dafür sorgen, dass diese Datensätze irgendwie mit der ProjektID des Datensatzes im Hauptformular versehen werden. Dazu jedoch später mehr. Sie kommen trotzdem nicht umhin, dafür zu sorgen, dass keine Datensätze im Unterformular angelegt werden, ohne dass sich ein Datensatz im Hauptformular befindet. Dazu können Sie die Routine aus Listing 4.27 verwenden.
230
4
Formulare
Transaktionen in Formularen Nun geht es an das eigentliche Problem: Das Einfassen von Änderungen im Hauptund Unterformular in eine Transaktion, um getätigte Änderungen für Haupt- und Unterformular komplett revidieren zu können. Dazu veranschaulichen Sie sich zunächst, welche Datensatzänderungen überhaupt berücksichtigt werden müssen, und leiten davon ab, in welchen Ereignisprozeduren die Transaktion gestartet, durchgeführt oder abgebrochen wird. Hinzufügen eines Datensatzes im Hauptformular: löst Form_Dirty im Hauptformular aus. Ändern eines Datensatzes im Hauptformular: löst Form_Dirty im Hauptformular aus. Löschen eines Datensatzes im Hauptformular: kann nicht rückgängig gemacht werden, da das Formular unmittelbar nach dem Löschen auf einen anderen Datensatz springt. Hinzufügen eines Datensatzes im Unterformular: löst Form_Dirty im Unterformular aus. Ändern eines Datensatzes im Unterformular: löst Form_Dirty im Unterformular aus. Löschen eines Datensatzes im Unterformular: löst Form_Delete im Unterformular aus. Damit stehen bereits die Ereignisse fest, die eine Transaktion starten sollen. Schauen Sie sich die dadurch ausgelösten Prozeduren an. Den Anfang macht die Prozedur Form_Dirty des Hauptformulars: Private Sub Form_Dirty(Cancel As Integer) 'Wenn aktueller Datensatz noch nicht geändert... If Me.DirtyForm = False Then 'Eigenschaft DirtyForm auf True setzen Me.DirtyForm = True 'Transaktion starten wrk.BeginTrans
End If End Sub Listing 4.33: Start einer Transaktion beim Ändern oder Hinzufügen eines Datensatzes im Hauptformular
Im Unterformular muss die Prozedur Form_Dirty noch ein wenig mehr leisten. Sie sorgt nicht nur dafür, dass im Falle der ersten Änderung die Transaktion gestartet und die
Besonderheiten von Unterformularen
231
Eigenschaft DirtyForm des Hauptformulars auf True gesetzt wird. Zusätzlich prüft sie, ob es sich bei der Änderung um einen neuen Datensatz handelt. Wie weiter oben erwähnt, sind Haupt- und Unterformular nicht über die entsprechenden Werte für die Eigenschaften Verknüpfen von und Verknüpfen nach miteinander verknüpft. Der Wert des Verknüpfungsfeldes ProjektID wird nicht automatisch gefüllt. Die Prozedur übernimmt daher auch diese Aufgabe und weist dem Feld ProjektID des Unterformulars den entsprechenden Wert des Hauptformulars zu. Damit im Unterformular überhaupt eine Transaktion gestartet werden kann, sind noch einige Zeilen Code notwendig. So befindet sich im Modulkopf neben den obligatorischen Zeilen noch die Deklaration des Workspace-Objekts für das Unterformular: Option Compare Database Option Explicit Dim wrk As DAO.Workspace
Dieses stellt das Form_Open-Ereignis des Unterformulars auf den gleichen Workspace wie im Hauptformular ein: Private Sub Form_Open(Cancel As Integer) Set wrk = DBEngine.Workspaces(0) End Sub Listing 4.34: Zuweisen des Workspace-Objekts
Die Prozedur Form_Dirty ist eine von zwei Prozeduren, die vom Unterformular aus eine Transaktion starten können: Private Sub Form_Dirty(Cancel As Integer) 'Wenn es sich um einen neuen Datensatz handelt, 'Wert für die Herstellung der Verknüpfung über das 'Feld ProjektID zuweisen If Me.NewRecord Then Me!ProjektID = Me.Parent!ProjektID End If 'Wenn noch keine Änderung im Haupt- oder Unterformular 'vorliegt, DirtyForm auf True setzen und Transaktion starten If Me.Parent.DirtyForm = False Then Me.Parent.DirtyForm = True wrk.BeginTrans
End If End Sub Listing 4.35: Form_Dirty-Prozedur des Unterformulars sfmProjektzeiten_Undo
232
4
Formulare
Die Prozedur, die durch die Beim Löschen-Eigenschaft des Unterformulars ausgelöst wird, muss sich nicht um die ProjektID scheren. Sie startet lediglich die Transaktion, falls dies noch nicht geschehen ist: Private Sub Form_Delete(Cancel As Integer) 'Wenn noch keine Änderung im Haupt- oder Unterformular... If Me.Parent.DirtyForm = False Then Me.Parent.DirtyForm = True wrk.BeginTrans
End If End Sub Listing 4.36: Die Prozedur Form_Delete startet gegebenenfalls eine Transaktion.
Abbrechen der Transaktion Das Abbrechen der Transaktion erreichen Sie durch Betätigen der Abbrechen-Schaltfläche. Die dadurch ausgelöste Prozedur prüft, ob Änderungen im Haupt- oder Unterformular durchgeführt wurden und damit eine Transaktion gestartet wurde. Falls ja, werden die Änderungen durch die Rollback-Methode des Workspace-Objekts wieder verworfen. Private Sub cmdAbbrechen_Click() 'Prüfen, ob Daten geändert wurden If Me.DirtyForm = True Then 'Transaktion abbrechen wrk.Rollback
End If 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Listing 4.37: Die Abbrechen-Schaltfläche verwirft alle Änderungen der aktuellen Daten des Haupt- und Unterformulars.
Durchführen der Transaktion Die Durchführung der Transaktion erfolgt durch Speichern des Datensatzes im Hauptformular. Dazu gibt es zum Beispiel folgende Möglichkeiten:
Besonderheiten von Unterformularen
233
Schließen des Formulars mit der OK-Schaltfläche Wechseln zu einem anderen Datensatz im Hauptformular Schließen des Formulars auf anderem Wege (beispielsweise Betätigen des SchließenButtons oben rechts im Formular) In diesem Beispiel werden nur die beiden ersten Möglichkeiten zum Speichern eines Datensatzes beschrieben. Zum Schließen des Formulars mit der OK-Schaltfläche ist die Prozedur verantwortlich, die durch das Ereignis Beim Klicken der Schaltfläche ausgelöst wird. Die Prozedur prüft anhand der Eigenschaft DirtyForm, ob überhaupt Änderungen im Haupt- oder Unterformular durchgeführt wurden und dementsprechend eine Transaktion gestartet wurde. Falls ja, schließt sie die Transaktion mit der Methode CommitTrans ab. Private Sub cmdOK_Click() 'Speichern der Änderungen DoCmd.RunCommand acCmdSaveRecord 'Wenn Änderungen vorliegen und damit eine 'Transaktion gestartet wurde: If Me.DirtyForm = True Then 'Transaktion durchführen wrk.CommitTrans
End If 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Listing 4.38: Das Schließen des Formulars mit der OK-Schaltfläche bewirkt die Durchführung der Transaktion.
Der Wechsel zu einem anderen Datensatz im Formular löst das Ereignis Beim Anzeigen aus. Die erste Version dieser Prozedur (siehe Listing 4.31) sieht noch keine Funktion zum Abschließen einer Transaktion vor. Die folgende Variante holt dies nach: Private Sub Form_Current() 'Wenn Daten geändert und Transaktion gestartet... If Me.DirtyForm = True Then 'aktuellen Datensatz speichern DoCmd.RunCommand acCmdSaveRecord 'Transaktion durchführen wrk.CommitTrans
234
4
Formulare
'Formular als "gespeichert" kennzeichnen Me.DirtyForm = False End If If mDeletedForm = False Then 'Prozedur zum Aktualisieren des Unterformulars aufrufen UnterformularAktualisieren Else mDeletedForm = False End If End Sub Listing 4.39: Abschließen der Transaktion beim Datensatzwechsel im Hauptformular
Die eigentliche Funktionalität zum Aktualisieren der Datenherkunft des Unterformulars wird dabei in eine eigene Prozedur ausgegliedert, weil diese noch von einer weiteren Ereignisprozedur aufgerufen werden soll: Private Sub UnterformularAktualisieren() Dim strSQL As String Dim rst As DAO.Recordset 'Zusammenstellen des SQL-Strings strSQL = "SELECT * FROM qryProjektzeitenMitarbeiter " _ & "WHERE ProjektID = " & Nz(Me!ProjektID, 0) 'Öffnen des Recordset Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) 'Zuweisen des Recordset an das Unterformular Set Me!sfmProjektzeiten_Undo.Form.Recordset = rst End Sub Listing 4.40: Prozedur zum Aktualisieren des Unterformulars
Löschen des aktuellen Datensatzes im Hauptformular Bleibt noch ein Problem: Wenn Sie den aktuell im Hauptformular angezeigten Datensatz löschen möchten, erscheint der Fehler aus Abbildung 4.45. Das Merkwürdige ist, dass beim Löschen gar keine Transaktion per VBA gestartet wurde – woher kommt nun diese Transaktion? Offensichtlich schließt Access den Löschvorgang automatisch in eine Transaktion ein. Normalerweise würden Sie davon gar nichts mitbekommen, da innerhalb dieser Transaktion keine unerlaubten Aktionen erfolgen wie das hier bemängelte Zuweisen eines Recordset-Objekts an die entsprechende Eigenschaft.
Besonderheiten von Unterformularen
235
Abbildung 4.45: Fehler beim Löschen des Datensatzes im Hauptformular
Hier liegt der Fall anders und bei genauer Betrachtung des Ablaufs der Ereignisse beim Löschen eines Datensatzes wird einiges klarer. Die Ereignisse haben folgende Reihenfolge: Form_Delete Form_Current Form_BeforeDelConfirm Form_AfterDelConfirm Dabei werden die beiden letzten Ereignisse nur ausgelöst, wenn die Einstellung Datensatzänderungen im Dialog Optionen (zu öffnen über den Menüeintrag Extras/Optionen) aktiviert ist (siehe Abbildung 4.46). Wichtig ist zunächst, dass nach dem Delete-Ereignis der zu löschende Datensatz beziehungsweise die zu löschenden Datensätze zunächst temporär gespeichert werden, bis feststeht, dass der Löschvorgang nicht durch das Ereignis Vor Löschbestätigung des Formulars abgebrochen wird. Und genau dieses temporäre Speichern erledigt die Transaktion, die sich nicht mit dem Zuweisen des Recordset-Objekts zum Unterformular verträgt. Das Problem lösen Sie, indem Sie den Zeitpunkt der Zuweisung des Recordset-Objekts einfach nach hinten verschieben – vom Form_Current- ins Form_AfterDelConfirm-Ereignis. Dazu verwenden Sie die folgenden Prozeduren. Die Prozedur Form_Delete stellt die Eigenschaft mDeletedForm auf den Wert True ein. Diese Eigenschaft wird in der Prozedur Form_Current ausgewertet; wenn sie den Wert True enthält, wird das Aktualisieren des Unterformulars unterbunden (siehe auch Listing 4.39).
236
4
Formulare
Abbildung 4.46: Notwendige Einstellung für das Auftreten der Ereignisse Form_BeforeDelConfirm und Form_AfterDelConfirm
Private Sub Form_Delete(Cancel As Integer) mDeletedForm = True End Sub Listing 4.41: Setzen einer Eigenschaft, an der andere Prozeduren den laufenden Löschvorgang erkennen können
Die Prozedur Form_BeforeDelConfirm können Sie theoretisch auch weglassen. In diesem Fall unterbindet sie die Standardmeldung von Access beim Löschen des Datensatzes (siehe Abbildung 4.47). Private Sub Form_BeforeDelConfirm(Cancel As Integer, Response As Integer) Response = acDataErrContinue End Sub Listing 4.42: Verhindern der Rückfrage beim Löschen
Abbildung 4.47: Standardmeldung von Access beim Löschen von Daten
Besonderheiten von Unterformularen
237
Die Prozedur Form_AfterDelConfirm ist wiederum sehr wichtig: Sie wird nur ausgelöst, wenn Sie wie in Listing 4.42 die Access-Meldung unterbinden oder Sie den Löschvorgang im Fall der Anzeige bestätigen. Die Prozedur ruft die Routine auf, die das Unterformular aktualisiert, und holt damit genau den Vorgang nach, der durch Setzen der Variablen mDeletedForm im Form_Current-Ereignis unterbunden wurde. Private Sub Form_AfterDelConfirm(Status As Integer) UnterformularAktualisieren End Sub Listing 4.43: Aktualisieren des Unterformulars nach vollendetem Löschvorgang
Restarbeiten Damit das Beispiel wie beschrieben funktioniert, brauchen Sie lediglich die Eigenschaften der Beziehungen zwischen den im Haupt- und Unterformular angezeigten Tabellen so anzupassen, dass beim Löschen des Datensatzes im Hauptformular direkt die entsprechenden Unterformulareinträge mitgelöscht werden. Dazu reicht das Setzen der Eigenschaft Löschweitergabe an verwandte Datensätze im Eigenschaftsfenster der passenden Beziehung (siehe Abbildung 4.48).
Abbildung 4.48: Aktivieren der Löschweitergabe zwischen verknüpften Tabellen
238
4
Formulare
Einsatz in eigenen Formularen Dem Einsatz dieser Technik in Ihren eigenen Formularen steht prinzipiell nichts im Wege. Sie müssen lediglich alle in den beiden Formularmodulen enthaltenen Deklarationen und Prozeduren in Ihre Formulare übertragen und einige Zeilen Code den Gegebenheiten Ihrer eigenen Datenbank anpassen – etwa die Datenherkünfte für Haupt- und Unterformular und die Zeile, die beim Anlegen eines neuen Datensatzes im Unterformular den Wert für den Fremdschlüssel festlegt (siehe Listing 4.35).
4.7 Eingabevalidierung Zur defensiven Programmierung gehört, dass Sie keine ungültigen Daten zulassen. Das fängt bei der Eingabe von Daten in Formularen an. Dieser Teil beschreibt die unterschiedlichen Möglichkeiten zur Validierung der Eingabe. Die wichtigste Frage bei der Validierung ist nicht, wie diese erfolgen soll, sondern zu welchem Zeitpunkt. Die folgenden drei Regeln sind praxiserprobt: Fehlerhafte Eingaben wie Zeichenketten in Datumsfelder werden sofort geahndet. Fehlende Eingaben werden erst beim Bestätigen des Datensatzes bemängelt. Die Handhabung von abhängigen Feldern ist Geschmackssache – Beispiel Startdatum und Enddatum. Am einfachsten ist eine Prüfung beim Bestätigen des Datensatzes.
4.7.1 Validieren direkt bei der Eingabe Das Validieren unmittelbar nach der Eingabe erfolgt in einer Prozedur, die durch die Ereigniseigenschaft Vor Aktualisierung des jeweiligen Steuerelements ausgeführt wird. Eine solche Routine könnte beispielsweise wie folgt aussehen: Private Sub Projekt_BeforeUpdate(Cancel As Integer) If IsNumeric(Left(Me!Projekt, 1)) Then MsgBox "Der Projektname muss mit einem Buchstaben beginnen.", _ vbOKOnly + vbExclamation, "Eingabefehler" Cancel = True Exit Sub End If End Sub Listing 4.44: Validierung bei der Eingabe
Eingabevalidierung
239
Die Routine prüft, ob die in das Feld Projekt eingegebenen Werte nicht mit einer Zahl anfangen. Sollte dies doch der Fall sein, erscheint eine entsprechende Meldung (siehe Abbildung 4.49).
Abbildung 4.49: Projektbezeichnungen, die mit einer Zahl beginnen, sind nicht erlaubt (»frmProjekteDetailansicht«).
4.7.2 Validieren vor dem Speichern Vor dem Speichern werden vor allem Pflichtfelder geprüft. Falls im obigen Formular die Felder Projekt und Startdatum Pflichtfelder wären, würde die Prüfung wie folgt aussehen: Private Function Validierung() As Boolean If IsNull(Me!Projekt) Then MsgBox "Bitte geben Sie einen Projektnamen ein.", _ vbOKOnly + vbExclamation, "Fehlende Eingabe" Me!Projekt.SetFocus Exit Function End If If IsNull(Me!Startdatum) Then MsgBox "Bitte geben Sie das Startdatum ein.", _ vbOKOnly + vbExclamation, "Fehlende Eingabe" Me!Startdatum.SetFocus Exit Function End If If Not IsNull(Me!Startdatum) And Not IsNull(Me!Enddatum) Then If Me!Startdatum > Me!Enddatum Then MsgBox "Das Enddatum darf nicht hinter dem Startdatum liegen.", _ vbOKOnly + vbExclamation, "Falsche Datumsangabe" Me!Enddatum.SetFocus Exit Function End If
240
4
Formulare
End If Validierung = True End Function Listing 4.45: Validierung vor dem Speichern des Datensatzes
Die Prozedur prüft von oben nach unten alle Pflichtfelder. Sobald sie auf eines trifft, das leer ist, gibt sie eine Fehlermeldung aus, setzt den Fokus auf das Feld mit dem fehlenden Inhalt und bricht die Prozedur ab. Außerdem wird der Funktionswert nur auf True gesetzt, wenn alle Validierungen erfolgreich verlaufen sind.
Abhängige Felder prüfen Im letzten Teil der Routine aus Listing 4.45 finden Sie ein Beispiel, wie sich abhängige Felder prüfen lassen. In diesem Beispiel wird sichergestellt, dass das Startdatum nicht hinter dem Enddatum liegt.
Aufruf der Validierung vor dem Speichern Nun müssen Sie diese Funktion nur noch von geeigneter Stelle aus aufrufen. Leider reicht es nicht, dies mit der Ereigniseigenschaft Vor Aktualisierung abzudecken. Die dazugehörende Ereignisprozedur enthält auch den passenden Cancel-Parameter, der das Speichern des Datensatzes bei falschen oder fehlenden Daten abbrechen könnte. Es gibt allerdings eine Schwachstelle: Theoretisch wird diese Routine zwar durch alle relevanten Vorgänge wie Wechsel des Datensatzes oder Schließen des Formulars aufgerufen – aber wenn das Schließen einmal ausgelöst wurde, hält auch das Setzen des Cancel-Parameters der Form_BeforeUpdate-Prozedur das Schließen nicht mehr auf. Somit würde in diesem Fall ein Datensatz mit falschen oder fehlenden Daten gespeichert. Daher rufen Sie die Funktion Validierung von zwei Prozeduren aus auf: Von der Form_BeforeUpdate-Prozedur sowie von der cmdOK_Click-Prozedur aus. Um auszuschließen, dass das Formular anders als mit der OK-Schaltfläche geschlossen wird, stellen Sie noch die Eigenschaft Schließen-Schaltfläche auf den Wert Nein ein. Die beiden Ereignisprozeduren sehen schließlich wie folgt aus: Private Sub cmdOK_Click() If Validierung = True Then DoCmd.Close acForm, Me.Name End If End Sub Private Sub Form_BeforeUpdate(Cancel As Integer) If Validierung = False Then
Eingabevalidierung
241
Cancel = True End If End Sub Listing 4.46: Diese beiden Routinen rufen die Funktion Validierung auf.
Alternativ können versierte Benutzer das Formular natürlich noch mit (Strg) + (F4) schließen. Um dem vorzubeugen, verwenden Sie eine Prozedur, die beim Ereignis Bei Taste ausgelöst wird und das Betätigen dieser Tastenkombination unterbindet. Gleichzeitig müssen Sie die Eigenschaft Tastenvorschau auf den Wert Ja einstellen: Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer) If KeyCode = 115 And Shift = 2 Then KeyCode = 0 End If End Sub Listing 4.47: Unterbinden der Tastenkombination zum Schließen eines Formulars
4.7.3 Sonderfälle beim Validieren Mit der oben genannten Vorgehensweise können Sie nicht alle möglichen Eingabefehler abfangen. Wenn im Tabellenentwurf festgelegte Einschränkungen (siehe Kapitel 2, Abschnitt 2.3, »Integritätsregeln«) bei der Eingabe oder beim Speichern des Datensatzes nicht berücksichtigt werden, werden die dadurch hervorgerufenen Meldungen vor dem Auslösen der Vor Aktualisierung-Ereignisprozeduren von Steuerelementen und Formularen angezeigt. Das ist zum Beispiel der Fall, wenn Sie einen Text in ein Datumsfeld eingeben (siehe Abbildung 4.50).
Abbildung 4.50: Access-eigene Validierungsmeldung
Wenn Sie diese Meldungen umgehen möchten, verwenden Sie die Bei Fehler-Ereigniseigenschaft des Formulars. Hier müssen Sie zwar ein wenig mehr Aufwand betreiben als bei den oben beschriebenen Validierungsfunktionen, aber dafür ersetzen Sie damit auch die Access-eigenen Validierungsmeldungen. Im Formular frmProjekteDetailansicht kann es etwa passieren, dass eines der Felder Startdatum oder Enddatum mit einem Wert bestückt wird, der nicht den Datentyp Datum/Uhrzeit enthält, oder dass kein Kunde für das Projekt ausgewählt wird. Beide Fehler würden die Integritätsregeln verletzen und eine Access-eigene Meldung heraufbeschwören.
242
4
Formulare
Diese Meldungen können Sie nur im Bei Fehler-Ereignis abfangen. Die folgende Routine wird durch dieses Ereignis ausgelöst und prüft, ob eines der beiden Datumsfelder einen ungültigen Wert enthält. Falls ja, gibt es eine entsprechende Meldung aus; der Fokus wird dann automatisch auf das betroffene Steuerelement gesetzt. Damit Sie auf den richtigen Fehler reagieren, prüfen Sie in einer Select Case-Verzweigung die in DataErr enthaltene Fehlernummer und verzweigen entsprechend. Mit der Anweisung Screen.ActiveControl ermitteln Sie, bei welchem Steuerelement der Fehler ausgelöst wurde, und behandeln den Fehler dem betroffenen Feld entsprechend. Ähnlich ist es beim Speichern des Datensatzes ohne Füllen des Pflichtfeldes KundeID. Die Fehlernummer lautet 3201 und Sie ermitteln ganz einfach, ob das Feld KundeID für diesen Fehler verantwortlich ist, indem Sie dessen Inhalt überprüfen. Ist dieser Null, muss der Benutzer den entsprechenden Kunden noch nachreichen. Wenn Sie nicht sicher sind, welche Fehlernummer sich hinter den eingebauten Fehlermeldungen verbirgt, lassen Sie sich in dieser Routine einfach den Wert DataErr mit der Debug.Print-Methode ausgeben. Private Sub Form_Error(DataErr As Integer, Response As Integer) Dim ctl As Control Select Case DataErr Case 2113 Set ctl = Screen.ActiveControl Select Case ctl.Name Case "Startdatum", "Enddatum" MsgBox "Bitte geben Sie ein Datum im Format " _ "'dd.mm.jjjj' ein.", vbOKOnly + vbExclamation, _ "Eingabefehler" Response = acDataErrContinue Exit Sub End Select Case 3201 If Me!KundeID = 0 Then MsgBox "Bitte wählen Sie einen Kunden für dieses " _ & "Projekt aus.", vbOKOnly + vbExclamation, _ "Fehlende Eingabe" Response = acDataErrContinue Me!KundeID.SetFocus Exit Sub End If End Select End Sub Listing 4.48: Behandlung von Verletzungen der Integrität bei der Dateneingabe
Suchen in Formularen
243
4.8 Suchen in Formularen Die Suche nach bestimmten Daten erfordert je nach Anwendungsfall unterschiedliche Maßnahmen. In den folgenden Abschnitten finden Sie zwei Möglichkeiten, die sich für viele Anwendungen eignen – eine Schnellauswahl per Kombinationsfeld und ein Textfeld zum schnellen Filtern von Listenfeldern.
4.8.1 Schnellauswahl per Kombinationsfeld Eine der einfachsten Möglichkeiten zum Einbau einer Suche ist ein Kombinationsfeld zur Auswahl von Werten aus einem oder aus mehreren kombinierten Feldern. Das folgende Beispiel basiert auf einem Formular zum Bearbeiten von Kontakten. Beispieldatenbank: Die Tabelle tblKontakte und das Formular frmSchnellsuchePerKombifeld finden Sie unter Kap04/Formulare.mdb. Die Schnellauswahl per Kombinationsfeld ist nur dann sinnvoll, wenn die gewünschten Datensätze nach einem einzigen Kriterium durchsucht werden – beispielsweise nach dem Nachnamen eines Kontaktes. Diese Variante finden Sie im folgenden Beispiel: Das Formular aus Abbildung 4.51 enthält alle Felder der Tabelle tblKontakte, zwei Schaltflächen mit der Beschriftung OK und Abbrechen sowie ein Kombinationsfeld namens cboSchnellsuche.
Abbildung 4.51: Formular mit Schnellauswahl in der Entwurfsansicht
244
4
Formulare
Das Kombinationsfeld verwendet die Abfrage aus Abbildung 4.52 als Datensatzherkunft. Die Abfrage enthält ein Feld mit dem Primärschlüsselfeld der Tabelle tblKontakte sowie ein aus den beiden Feldern Nachname und Vorname zusammengesetztes Feld, wobei die beiden Werte durch ein Komma getrennt werden.
Abbildung 4.52: Datensatzherkunft des Kombinationsfeldes zur Schnellauswahl von Kontakten
Damit lassen sich mit dem Kombinationsfeld alle Einträge der Tabelle tblKontakte in der Form , anzeigen. Damit das Formular auch den ausgewählten Datensatz anzeigt, fügen Sie der Ereigniseigenschaft Nach Aktualisierung des Kombinationsfeldes die folgende Prozedur hinzu: Private Sub cboSchnellauswahl_AfterUpdate() Dim rst As DAO.Recordset 'Kopie der aktuellen Formulardaten im Recordset speichern Set rst = Me.RecordsetClone 'Im Recordset nach dem im Kombinationsfeld ausgewählten 'Kontakt suchen rst.FindFirst "KontaktID = " & Me!cboSchnellauswahl 'Im Formular zum gefundenen Recordset springen Me.Bookmark = rst.Bookmark Set rst = Nothing End Sub Listing 4.49: Code für die Schnellsuche per Kombinationsfeld
Suchen in Formularen
245
Diese Variante ist auch für ältere Access-Versionen universell einsetzbar; in AccessVersionen, die für Formulare die Recordset-Eigenschaft bereitstellen, ist folgende Variante deutlich kürzer: Private Sub cboSchnellauswahl_AfterUpdate() Me.Recordset.FindFirst "KontaktID = " & Me!cboSchnellauswahl End Sub Listing 4.50: Datensatz suchen für Formulare mit Recordset-Eigenschaft
Das Auswählen eines Eintrags des Kombinationsfeldes führt nun zur Anzeige des gewünschten Datensatzes im Formular (siehe Abbildung 4.53). Es fehlt nur noch die Aktualisierung der Datensatzherkunft des Kombinationsfeldes beim Ändern, Hinzufügen oder Löschen von Datensätzen im Formular. Diese Aufgabe übernimmt die folgende Prozedur, die durch die Ereigniseigenschaft Beim Anzeigen des Formulars ausgelöst wird: Private Sub Form_Current() 'Aktualisieren des Inhalts des Kombinationsfeldes Me!cboSchnellauswahl.Requery End Sub Listing 4.51: Aktualisieren des Kombinationsfeldes
Abbildung 4.53: Die Schnellsuche im Einsatz (»frmSchnellsuchePerKombinationsfeld«)
246
4
Formulare
4.8.2 Schnelles Filtern von Listenfeldern Listenfelder kommen oft zum Einsatz, wenn Datensätze in einer Übersicht zur Auswahl angeboten werden sollen. Wenn viele Datensätze zur Auswahl bereitstehen, macht eine Möglichkeit zum Filtern der Datensätze Sinn. Die üblichen Filter-Funktionen bestehen aus verschiedenen Textfeldern zur Eingabe der Filterkriterien und einer Schaltfläche zum Auslösen des Filtervorgangs mit den neuen Parametern. Je mehr Felder man mit einer solchen Filter-Funktion abdeckt, desto mächtiger ist diese – dafür benötigt man aber auch entsprechend viele Steuerelemente, durch die sich der Benutzer erst einmal hindurchkämpfen muss. In vielen Fällen ist das gar nicht notwendig; dort lässt sich eine viel handlichere Lösung einsetzen: Stellen Sie dem Benutzer einfach ein Textfeld bereit, das in beliebig vielen Feldern des Listenfeldes sucht und unmittelbar nach der Eingabe eines jeden Buchstabens die angezeigten Datensätze filtert. Dazu ist neben ein wenig Routinearbeit noch ein kleiner Trick notwendig – sehen Sie einfach selbst. Abbildung 4.54 zeigt das fertige Formular in der Entwurfsansicht. Es enthält ein Listenfeld zur Anzeige der Datensätze, eine Schaltfläche zum Anzeigen der Detailansicht des markierten Datensatzes und ein Textfeld zur Eingabe des Suchbegriffs.
Abbildung 4.54: Listenfeld mit Schnellfilter
Das erste Textfeld namens txtSuche dient der Eingabe des Suchbegriffs. Der Inhalt des Listenfeldes lstKontakte soll nach jeder Änderung des im Textfeld txtSuche enthaltenen Suchbegriffs aktualisiert werden.
Suchen in Formularen
247
Das Listenfeld bezieht seine Daten aus der Abfrage qryKontakteListenfeld (siehe Abbildung 4.55). Die Abfrage enthält die anzuzeigenden Felder in der gewünschten Reihenfolge und Sortierung.
Abbildung 4.55: Datensatzherkunft des Listenfeldes zur Anzeige der Kontakte
Damit das Listenfeld nach jeder Änderung des Inhalts des Textfeldes txtSuche aktualisiert wird, verwenden Sie eine sonst recht stiefmütterlich behandelte Ereigniseigenschaft. Die Ereigniseigenschaft Bei Änderung des Feldes zur Eingabe des Suchbegriffs wird bei jeder Änderung des Inhalts ausgelöst. Für die entsprechende Ereignisprozedur fügen Sie den folgenden Code ein: Private Sub txtSuche_Change() Dim strSuchbegriff As String 'Suchbegriff in Variable speichern strSuchbegriff = Me!txtSuche.Text 'Neue Datensatzherkunft zuweisen Me!lstKontakte.RowSource = "SELECT * FROM qryKontakteListenfeld " _ "WHERE Nachname LIKE '" & strSuchbegriff & "*'" 'Inhalt des Listenfeldes aktualisieren Me!lstKontakte.Requery End Sub Listing 4.52: Prozedur zum Filtern des Listenfeldes
248
4
Formulare
Der Trick ist, dass Sie nicht den Inhalt des Textfeldes über die Eigenschaft Value auswerten, sondern die Text-Eigenschaft verwenden. Die Value-Eigenschaft wird nämlich erst nach dem Aktualisieren des Textfeldes auf den tatsächlichen Inhalt eingestellt. Der Abfrageausdruck ermittelt alle Datensätze der Abfrage qryKontakteListenfeld, in denen der Nachname mit dem im Textfeld angegebenen Ausdruck beginnt. Der Vollständigkeit halber finden Sie hier noch den Code zum Anzeigen des im Listenfeld ausgewählten Kontaktes. Der Kontakt soll im Formular aus dem vorherigen Beispiel angezeigt werden. Zwei Aktionen sollen dazu führen: ein Doppelklick auf das Listenfeld und ein Klick auf die Schaltfläche cmdAnzeigen. Die entsprechenden Ereignisse sollen prinzipiell die gleiche Aktion auslösen – daher legen Sie dafür eine neue Prozedur an und rufen diese über die beiden Ereignisprozeduren auf: Private Sub cmdAnzeigen_Click() Anzeigen End Sub Private Sub lstKontakte_DblClick(Cancel As Integer) Anzeigen End Sub
Private Sub Anzeigen() If Not IsNull(Me!lstKontakte) Then DoCmd.OpenForm "frmSchnellsuchePerKombifeld", _ WhereCondition:="KontaktID = " & Me!lstKontakte, _ WindowMode:=acDialog End If End Sub Listing 4.53: Prozeduren zum Anzeigen der Details eines Kontaktdatensatzes
5 Berichte Mit Berichten lassen sich Daten in nahezu beliebiger Weise darstellen. Voraussetzung ist, dass Sie genau wissen, wie ein solcher Bericht arbeitet – also wie Sie Gruppierungen und Sortierungen verwenden, wie Sie Unterberichte einsetzen, in welcher Reihenfolge der Bericht die einzelnen Ereignisse abarbeitet, wo innerhalb dieser Ereignisse Sie Aktionen etwa zum Ein- oder Ausblenden von Bereichen oder Steuerelementen unterbringen und so weiter. Dieses Kapitel hilft Ihnen, die Funktion von Berichten zu verstehen, und zeigt außerdem, wie sich Daten der gängigen Beziehungsarten darstellen lassen. Die Beispiele dieses Kapitels finden Sie auf der Buch-CD in der Datenbankdatei Kap_05\Berichte.mdb.
5.1 Berichte anzeigen Die Anzeige von Berichten erfolgt meist über die DoCmd.OpenReport-Methode. Die Methode hat folgende Parameter (hier nur mit den gebräuchlichsten Konstanten): ReportName: Name des anzuzeigenden Berichts View: Ansicht beim Öffnen (acPreview: Vorschau, acNormal: Drucken, acDesign: Entwurfsansicht) FilterName: Name einer Abfrage auf Basis der Datenherkunft WhereCondition: Teil einer Abfrage hinter der WHERE-Klausel zum Einschränken der enthaltenen Daten WindowMode: Fenstereigenschaften (acDialog: modales Fenster, acWindowNormal: Standard) OpenArgs: Öffnungsargument, mit dem zusätzliche Informationen übergeben werden können (Datentyp Variant)
250
5
Berichte
Die Parameter können Sie entweder in der angegebenen Reihenfolge durch Kommata getrennt eingeben. Wenn Sie einen Parameter nicht benötigen, lassen Sie diesen einfach weg, ohne aber die Anzahl der Kommata zu verändern. Hinter dem letzten verwendeten Parameter brauchen Sie allerdings keine Kommata mehr anzuhängen. Alternativ können Sie mit »benannten Argumenten« arbeiten. Dabei geben Sie den Parameternamen gefolgt von Doppelpunkt und Gleichheitszeichen sowie dem Wert an: DoCmd.OpenReport "rptKalenderdaten", View:=acViewPreview, FilterName:="qryKalenderdaten"
Zwischen Parameter und Wert dürfen sich keine Leerzeichen befinden. Die Reihenfolge bei der Verwendung benannter Parameter ist beliebig.
Besonderheit beim Öffnen von Berichten aus modal geöffneten Formularen Wenn ein Formular, von dem aus Sie etwa per Schaltfläche einen Bericht anzeigen möchten, mit dem Parameter WindowMode:=acDialog, also als modaler Dialog geöffnet wurde, müssen Sie auch den Bericht als modales Fenster öffnen. Anderenfalls wird der Bericht direkt im Hintergrund des Formulars angezeigt und Sie können nicht auf diesen zugreifen.
5.2 Filtern und sortieren Berichte enthalten vier Eigenschaften, mit denen sich Filter- und Sortierkriterien einstellen lassen. Diese können auf unterschiedliche Weise festgelegt werden – beispielsweise auf Basis der zugrunde liegenden Datenherkunft oder auch per Zuweisung der entsprechenden Eigenschaften mit VBA. Wenn Sie etwa eine Abfrage als Datenherkunft festlegen, die eine Sortierung und ein Kriterium enthält, werden diese Angaben einfach in den Bericht übernommen, ohne dass Sie dafür eine Eigenschaft einstellen müssen. Wenn Sie beim Öffnen des Berichts eine Abfrage mit dem Parameter FilterName übergeben, die eine Sortierung und ein Kriterium enthält, wird das Kriterium dieser Abfrage in die Eigenschaft Filter übernommen, aber die Eigenschaft Filter aktiv nicht gesetzt. Dafür bleibt die Eigenschaft Sortiert nach leer, aber die Eigenschaft Sortierung aktiv wird auf Ja gesetzt (siehe Abbildung 5.1). Trotzdem wird nach dem angegebenen Artikelnamen gefiltert: DoCmd.Openreport "rptArtikelUebersicht", acPreview, "qryArtikel"
Filtern und sortieren
251
Abbildung 5.1: Einstellungen der Filter- und Sortiereigenschaften nach Zuweisen einer Abfrage mit dem Parameter FilterName der DoCmd.OpenForm-Methode
Die Eigenschaften in dieser Abbildung lassen sich nebenher auch noch manuell oder per VBA zur Laufzeit einstellen. Wenn Sie eine der gleich vorgestellten Gruppierungen oder Sortierungen im Dialog Sortieren und Gruppieren festlegen, steht die in der Eigenschaft Sortiert nach festgelegte Sortierung ganz hinten an. Das bedeutet, wenn Sie in Sortiert nach etwa absteigend nach dem Artikelnamen sortieren und im Dialog Sortieren und Gruppieren eine aufsteigende Sortierung nach dem Artikelnamen festgelegt ist, dass dann die Datensätze aufsteigend nach dem Artikelnamen sortiert werden. Die Einstellung der Eigenschaft Sortiert nach macht sich erst bemerkbar, wenn diese Sortierung noch nicht im Dialog Sortieren und Gruppieren verwendet wird.
Filtern und Sortieren zur Laufzeit Interessant werden die Eigenschaften Filter, Filter aktiv, Sortierung und Sortierung aktiv, wenn die diesbezüglichen Möglichkeiten im Bericht noch nicht durch sonstige Sortierungen und Gruppierungen erschöpft sind. Ein Beispiel dafür sind einfache Übersichtslisten wie der Bericht rptArtikelUebersicht (siehe Beispieldatenbank). Wenn Sie diesen in der Vorschauansicht öffnen, können Sie zur Laufzeit per VBA die Sortierung und den Filter anpassen. Um die Datensätze beispielsweise in absteigender Reihenfolge nach dem Artikelnamen zu sortieren, verwenden Sie die folgende Anweisung: Reports!rptArtikelUebersicht.OrderBy = "Artikelname DESC"
Falls das nicht wirkt, ist die Sortierung noch nicht aktiv. Schieben Sie in diesem Fall noch folgende Anweisung hinterher: Reports!rptArtikelUebersicht.OrderByOn = True
Genauso können Sie auch den Filter einsetzen. Alle Artikel mit dem Anfangsbuchstaben A liefert die folgende Anweisung (wiederum bei aktivierter Vorschauansicht): Reports!rptArtikelUebersicht.Filter = "Artikelname LIKE 'A*'"
252
5
Berichte
Falls der Filter nicht greift, müssen Sie auch diesen anschalten: Reports!rptArtikelUebersicht.FilterOn = True
5.3 Berichtsbereiche und Ereignisse Von elementarer Bedeutung für die Arbeit mit Berichten – vor allem mit VBA – ist das Verständnis der einzelnen Bereiche eines Berichts und der Ereignisse dieser Berichtsbereiche.
5.3.1 Berichtsbereiche Standardmäßig zeigt ein Bericht einen Detailbereich sowie einen Seitenkopf und einen Seitenfuß an. Optional lassen sich noch ein Berichtskopf- und ein Berichtsfuß anzeigen. Letztere werden nur je einmal angezeigt – am Anfang und am Ende des Berichts – und können beispielsweise für die Gestaltung einer Titelseite verwendet werden. Seitenkopf und -fuß erscheinen am oberen und unteren Ende jeder Seite und legen somit fest, wie viel Platz noch für den Detailbereich und andere Bereiche übrig bleibt. Alle Bereiche lassen sich jedoch nach Bedarf ein- und ausblenden. Dazu später mehr. Eines der wichtigsten Elemente zur Strukturierung von Daten – und hier speziell von verknüpften Daten – liegt in der Möglichkeit, Daten nach bestimmten Kriterien zu gruppieren und zu sortieren. Sortierungen und Gruppierungen legen Sie in einem speziellen Dialog fest. Mit diesem wählen Sie einerseits die zu sortierenden und gruppierenden Felder aus, andererseits die Eigenschaften für die Felder, nach denen die Daten gruppiert werden sollen. Und hier liegt auch der Unterschied zwischen gruppierten und sortierten Daten: Sobald Sie eine der Eigenschaften Gruppenkopf oder Gruppenfuß auf den Wert Ja setzen, zeigt Access im grauen Kästchen vor dem betroffenen Feld das Gruppierungs-Symbol an (siehe Abbildung 5.2). Die Einstellungen aus diesem Dialog sorgen übrigens dafür, dass die zugrunde liegenden Artikel zunächst nach Kategorien und dann innerhalb der Kategorien nach Lieferanten gruppiert werden. Die Daten jeder Lieferanten-Gruppe werden dann noch einer aufsteigenden Sortierung unterzogen. Die Eigenschaften Gruppieren nach und Intervall lernen Sie weiter unten in Abschnitt 5.5 unter »Gruppieren nach und Intervall« kennen, die Eigenschaft Zusammenhalten im gleichen Abschnitt unter »Zusammenhalten von Daten«.
Berichtsbereiche und Ereignisse
253
Abbildung 5.2: Dialog zum Festlegen von Sortierungen und Gruppierungen
5.3.2 Ereignisse in Berichten Berichte bieten längst nicht so viele Ereignisse wie Formulare. Das liegt daran, dass es erstens nicht so viele verschiedene Ansichten gibt, andererseits bieten Berichte kaum Möglichkeiten zur Interaktion – diese beschränken sich nach dem Öffnen des Berichts auf das Wechseln der Seite oder das Anzeigen eines Kontextmenüs. Bis ein Bericht angezeigt ist, geschieht allerdings schon eine Menge und in vielen Fällen können Sie den Ablauf per Ereignisprozedur beeinflussen. Nachfolgend finden Sie zunächst eine Auflistung der Ereignisse eines Berichts und der Bereiche eines Berichts; im Anschluss lernen Sie einige Anwendungsfälle der Ereignisse kennen.
Zusammenfassung der Berichtsereignisse Der Bericht löst folgende Ereignisse aus: Beim Öffnen: Wird beim Öffnen, aber vor dem Erstellen des Berichts ausgelöst. Dient beispielsweise der Übergabe von Parametern oder zum Abbrechen der Ausgabe des Berichts. Bei Aktivierung: Bericht wird aktiviert oder gedruckt. Bei Seite: Wird nach Abschluss der Bei Formatierung- und der Beim Drucken-Ereignisse der Berichtsbereiche, aber vor dem Anzeigen der Seite ausgelöst. Beim Schließen: Wird beim Schließen des Berichts ausgelöst – etwa durch Betätigen der Schließen-Schaltfläche. Bei Deaktivierung: Bericht wird deaktiviert, da ein anderes Objekt den Fokus erhält oder der Bericht geschlossen wird. Bei Ohne Daten: Wird ausgelöst, wenn die Datenherkunft des Berichts keine Daten enthält. Bei Fehler: Wird beim Auftreten eines Fehlers ausgelöst.
254
5
Berichte
Zusammenfassung der Bereichsereignisse Die Bereiche Berichtskopf, Berichtsfuß, Seitenkopf, Seitenfuß, Kopf und Fuß der einzelnen Gruppierungen und der Detailbereiche lösen die folgenden Ereignisse aus. Dabei werden die Ereignisse Beim Formatieren und Beim Drucken je Element eines jeden Bereichs mindestens einmal aufgerufen: Beim Formatieren: Wird ausgelöst, wenn die Daten ermittelt sind, bevor der Bereich formatiert wird. Dieses Ereignis wird gegebenenfalls mehrere Male ausgelöst. Wenn es sich beim aktuell formatierten Bereich um einen Gruppenkopf handelt, haben Sie Zugriff auf die im Gruppenkopf angezeigten Daten und auf die Daten des ersten Datensatzes der Gruppierung im Detailbereich. Beim Gruppenfuß stehen die Daten des Gruppenfußbereichs und des letzten Datensatzes des Bereichs zur Verfügung. Im Detailbereich bietet dieses Ereignis Zugriff auf die Daten des aktuellen Datensatzes. Beim Drucken: Wird ausgelöst, wenn der Bereich formatiert, aber noch nicht gedruckt ist. In diesem Ereignis können Sie je Bereich auf die gleichen Daten zugreifen wie im Beim Formatieren-Ereignis. Mit »Drucken« ist hier nicht die Ausgabe auf Papier gemeint, sondern die grafische Ausgabe in das »Dokument«, das dann auf dem Bildschirm oder dem Drucker ausgegeben werden kann. Bei Rückname: Wird jedes Mal ausgelöst, wenn Access einen Bereich infolge Positionierungsberechnungen neu formatieren muss (steht nicht für den Seitenkopf zur Verfügung). Die genaue Abfolge der Ereignisse eines Berichts ist relativ komplex. Mehr Gruppierungen oder spezielle Bedingungen wie etwa das Zusammenhalten von Gruppierungen führen zu einer unüberschaubaren Menge von Ereignissen. Allein der Aufruf eines Berichts mit einer Gruppierung, der nur einen Datensatz anzeigt, löst die folgenden Ereignisse in der angegebenen Reihenfolge aus: Bericht Beim Öffnen Bericht Bei Aktivierung Seitenkopfbereich Beim Formatieren Gruppenfuß0 Beim Formatieren Detailbereich Beim Formatieren Gruppenfuß0 Beim Formatieren Seitenfußbereich Beim Formatieren Seitenkopfbereich Beim Formatieren Seitenkopfbereich Beim Drucken
Berichtsbereiche und Ereignisse
255
Gruppenfuß0 Beim Formatieren Gruppenfuß0 Beim Drucken Detailbereich Beim Formatieren Detailbereich Beim Drucken Gruppenfuß0 Beim Formatieren Gruppenfuß0 Beim Drucken Seitenfußbereich Beim Formatieren Seitenfußbereich Beim Drucken Bericht Bei Seite Bericht Beim Schließen Bericht Bei Deaktivierung
5.3.3 Zugriff auf die Berichtsbereiche Der Zugriff auf die Steuerelemente eines Berichts erfolgt genau wie in Formularen. Interessanter sind da die Elemente, die in Formularen nicht vorhanden sind – die einzelnen Bereiche der Berichte. Warum muss man eigentlich wissen, wie man per VBA auf diese Bereiche zugreift? Weil es Situationen gibt, in denen Sie beispielsweise einen Bereich ein- oder ausblenden oder die Eigenschaften eines Bereichs anpassen müssen. Auf einen Berichtsbereich greifen Sie über die Auflistung Section zu. Als Argument geben Sie entweder den Namen, eine Zahl oder – bei den eingebauten Bereichen – eine VBA-Konstante an. Auf den Detailbereich können Sie beispielsweise mit folgenden Anweisungen zugreifen: Debug.Print Debug.Print Debug.Print Debug.print
Reports!.Section(0).Name Reports!.Section(acDetail).Name Reports!.Section("Detailbereich").Name Reports!.Detailbereich.Name
Alle vier Anweisungen geben in der deutschen Version von Access 2003 den Namen »Detailbereich« aus. Dies ist der voreingestellte Name für den Detailbereich. Sie können den Namen der eingebauten Bereiche wie auch der zusätzlichen Gruppierungen leicht im Eigenschaftsfenster des jeweiligen Bereichs anpassen (siehe Abbildung 5.3). Damit wird deutlich, dass Access für jeden Bereich dynamisch eine Berichts-Eigenschaft mit dem Namen des jeweiligen Bereichs bereitstellt.
256
5
Berichte
Abbildung 5.3: Anpassen des Namens eines Berichtsbereichs
Die Namen, Zahlenwerte und Konstanten der einzelnen Berichtsbereiche finden Sie in Tabelle 5.1: Die Kopf- und Fußbereiche der bis zu zehn Gruppierungsebenen werden von 5 bis 24 durchnummeriert, wobei die Kopfbereiche jeweils ungerade und die Fußbereiche gerade Zahlen erhalten. Access legt automatisch Namen für die Bereiche an, die jeweils mit Gruppenkopf und Gruppenfuß beginnen und genau wie die anderen Steuerelemente eine eigene Nummer erhalten, die nicht zwangsläufig fortlaufend sein muss. Name
Zahl
Konstante
Detailbereich
0
acDetail
Berichtskopf
1
acHeader
Berichtsfuß
2
acFooter
Seitenkopfbereich
3
acPageHeader
Seitenfußbereich
4
acPageFooter
Tabelle 5.1: Name, Zahlenwert und Konstanten von Berichtsbereichen
5.4 Beispiele für den Einsatz der Berichts- und Bereichsereignisse Die Berichtsereignisse lassen sich teilweise für recht spezielle Aktionen einsetzen. In den folgenden Abschnitten finden Sie einige Beispiele, damit Sie ein Gefühl für das richtige Einsetzen der Berichtsereignisse erhalten.
5.4.1 Beim Öffnen: Auswertung von Öffnungsargumenten Der richtige Zeitpunkt zum Auswerten eines Öffnungsarguments ist das Ereignis Beim Öffnen. Mit dem Öffnungsargument der DoCmd.OpenReport-Anweisung lässt sich beispielsweise ein Parameter zum Filtern der Datenherkunft übergeben. Da dies aber relativ langweilig ist, finden Sie nachfolgend ein Beispiel, wie Sie die Gruppierung eines Berichts beim Öffnen verändern können. Voraussetzung ist der Bericht rptGruppierungenTauschen aus Abbildung 5.4.
Beispiele für den Einsatz der Berichts- und Bereichsereignisse
257
Abbildung 5.4: Bericht mit zwei Gruppierungsebenen
Der Clou an diesem Bericht ist, dass Sie mit wenigen Zeilen Code die Gruppierungsebenen vertauschen können. Dazu sind folgende, in der Abbildung nicht sichtbare Eigenschaften einzustellen: Name des Beschriftungsfeldes im Berichtskopf: lblUeberschrift Name des [Lieferanten-Nr]-Kopfbereichs: Gruppenkopf0 Name des Beschriftungsfeldes im [Lieferanten-Nr]-Kopfbereich: lblUeberschrift Gruppierung0 Name des Textfeldes im [Lieferanten-Nr]-Kopfbereich: txtUeberschriftGruppierung0 Name des [Kategorie-Nr]-Kopfbereichs: Gruppenkopf1 Name des Beschriftungsfeldes im [Kategorien-Nr]-Kopfbereich: lblUeberschrift Gruppierung0 Name des Textfeldes im [Kategorie-Nr]-Kopfbereich: txtUeberschriftGruppierung1 Wie bringen Sie nun Dynamik ins Spiel? Betrachten Sie den reinen Ablauf, so rufen Sie den Bericht auf und übergeben mit dem Öffnungsargument Informationen über die im Bericht anzuzeigenden Daten. Das sieht etwa folgendermaßen aus (in einer Zeile): DoCmd.OpenReport "rptGruppierungenTauschen", View:=acViewPreview, OpenArgs:="Artikel nach Kategorien und Lieferanten;[KategorieNr];[Lieferanten-Nr];Kategorie;Lieferant"
Das Öffnungsargument enthält sogar mehrere Argumente, die durch Semikola voneinander getrennt sind. Das erste enthält den Text, der im Berichtskopf als Überschrift angezeigt werden soll, das zweite und dritte enthalten die Beschriftungen der Bezeich-
258
5
Berichte
nungsfelder in den beiden Gruppenköpfen und das vierte und fünfte die Felder der Datenherkunft, nach denen gruppiert werden soll. Fehlt noch eine Routine, die diese Informationen auseinander nimmt und den entsprechenden Eigenschaften zuweist. Diese wird – wer hätte es gedacht – durch das Beim Öffnen-Ereignis des Berichts ausgelöst: Private Sub Report_Open(Cancel As Integer) Dim strOpenArgs As String Dim strGruppierungen() As String Dim i As Integer If IsNull(Me.OpenArgs) Then Exit Sub End If strGruppierungen() = Split(Me.OpenArgs, ";") Me!lblUeberschrift.Caption = strGruppierungen(0) For i = 1 To 2 Me("lblUeberschriftGruppierung" & i - 1).Caption = _ strGruppierungen(i + 2) Me.GroupLevel(i - 1).ControlSource = strGruppierungen(i) Next i End Sub Listing 5.1: Diese Prozedur sortiert die Gruppierungen nach den Vorgaben im Öffnungsargument.
Die Prozedur Report_Open zerlegt zunächst die im Öffnungsargument übergebene Liste mit der Split-Funktion und speichert die einzelnen Elemente in einem Array namens strGruppierungen. Warum so umständlich und gleich fünf Parameter mit OpenArgs übergeben? Die Reihenfolge der Gruppierungen wird doch in einem Formular festgelegt und da könnte man beim Öffnen des Berichts die Einstellungen des Formulars auslesen und entsprechend reagieren. Dagegen spricht allerdings ein Grundsatz, den Sie bereits in Kapitel 4, Abschnitt 4.4, »Von Formular zu Formular«, kennen gelernt haben: Je weniger Abhängigkeiten zwischen zwei Objekten bestehen, desto besser. Und weniger Abhängigkeit, als alle notwendigen Informationen beim Öffnen zu übergeben, ist fast nicht möglich. Die folgende For Next-Schleife wird genau zweimal durchlaufen – für jede Gruppierung einmal. Die erste Anweisung weist zunächst dem Beschriftungsfeld der äußeren Gruppierung die erste Gruppierungsbezeichnung zu – in diesem Fall »Kategorie«. Die zweite Anweisung legt das Feld fest, nach der die äußere Gruppierung erfolgen soll – hier »[Kategorie-Nr]«. Das gleiche Spiel wiederholt die Schleife noch für die innere Gruppierung – fertig! Mit dem obigen DoCmd.OpenReport-Befehl erhalten Sie eine nach Kategorien und Lieferanten gruppierte Artikelliste.
Beispiele für den Einsatz der Berichts- und Bereichsereignisse
259
Lediglich die Angabe der aktuellen Kategorie beziehungsweise des aktuellen Lieferanten im Kopfbereich der Gruppierungen fehlt noch. Dazu verwenden Sie die folgenden Prozeduren und erhalten gleichzeitig ein gutes Anwendungsbeispiel für den Einsatz des Ereignisses Beim Formatieren: Private Sub Gruppenkopf1_Format(Cancel As Integer, FormatCount As Integer) Me!txtUeberschriftGruppierung1 = Me(Me.GroupLevel(1).ControlSource).Text End Sub Private Sub Gruppenkopf0_Format(Cancel As Integer, FormatCount As Integer) Me!txtUeberschriftGruppierung0 = Me(Me.GroupLevel(0).ControlSource).Text End Sub Listing 5.2: Einstellen der Überschriften in den Gruppierungsköpfen
Wie oben erwähnt, können Sie von der Beim Formatieren-Ereignisprozedur aus auf den Inhalt des ersten Detaildatensatzes der Gruppierung zugreifen, was hier sehr nützlich ist: Auf diese Weise lässt sich leicht der angezeigte Text der Felder [Kategorie-Nr] und [Lieferanten-Nr] auslesen. Wenn Sie die Priorität der Gruppierung ändern möchten, verwenden Sie einfach die folgende Anweisung zum Anzeigen des Berichts: DoCmd.OpenReport "rptGruppierungenTauschen", View:=acViewPreview, OpenArgs:="Artikel nach Lieferanten und Kategorien;[LieferantenNr];[Kategorie-Nr];Lieferant;Kategorie"
Wenn Sie den Bericht mit geänderter Sortierung aufrufen möchten, müssen Sie ihn zuvor schließen. Ein Wechsel in die Entwurfsansicht reicht nicht aus. Den Aufruf können Sie übrigens in einem Formular wie dem aus Abbildung 5.5 unterbringen. Weitere Details finden Sie in der Beispieldatenbank zu diesem Kapitel (Kap_05\Berichte.mdb).
Abbildung 5.5: Formular zum Anzeigen von Berichten mit flexibler Gruppierungsreihenfolge (»frmGruppierungenTauschen«)
260
5
Berichte
5.4.2 Bei Aktivierung und Bei Deaktivierung: Berichtsabhängige Funktionen ein- und ausschalten Wenn Sie einen Bericht geöffnet haben und diesen dann schließen oder den Fokus auf ein anderes Objekt setzen, möchten Sie möglicherweise Elemente von benutzerdefinierten Symbolleisten aktivieren oder deaktivieren. Eine Drucken-Schaltfläche macht beispielsweise am meisten Sinn, wenn gerade ein Bericht angezeigt wird. Ein Beispiel finden Sie in Kapitel 10, Abschnitt 10.4.5, »Schaltflächen dynamisch aktivieren und deaktivieren«.
5.4.3 Bei Ohne Daten: Öffnen leerer Berichte vermeiden Das Ereignis Bei Ohne Daten wird ausgelöst, wenn die Anzahl Datensätze in der Datenherkunft des Berichts 0 ist. Damit ist die entsprechende Ereignisprozedur prädestiniert, Aufrufe der Vorschau- und der Berichtsansicht zu unterbinden, wenn gar keine Daten vorhanden sind. Die Benutzer werden es Ihnen danken, wenn nicht hin und wieder Ausdrucke von eigentlich leeren Berichten vorkommen. Zum Simulieren eines leeren Berichts setzen Sie einfach eine DoCmd.OpenReport-Anweisung mit einem entsprechenden Kriterium ein: DoCmd.OpenReport "rptArtikel", View:=acViewPreview, WhereCondition:="1=0"
Der Aufruf führt zur Anzeige eines bis auf die Bezeichnungsfelder leeren Berichts. Das ist noch zu verschmerzen, unangenehmer wird es, wenn diese »Blanko«-Variante des Berichts unnötig ausgedruckt wird. Den Druck eines leeren Berichts und die Anzeige einer leeren Vorschau können Sie abfangen, indem Sie eine Prozedur für das Bei Ohne Daten-Ereignis anlegen. Diese soll eine Meldung anzeigen und die Ausgabe abbrechen. Private Sub Report_NoData(Cancel As Integer) MsgBox "Der Bericht enthält keine Daten.", vbExclamation Or vbOKOnly, _ "Keine Daten" Cancel = True End Sub Listing 5.3: Unterbinden der Ausgabe eines leeren Berichts
Leider liefert ein abgebrochener Aufruf der DoCmd.OpenReport-Methode einen Laufzeitfehler. Diesen müssen Sie auch noch behandeln, indem Sie den Aufruf in eine geeignete Fehlerbehandlung einbetten.
Beispiele für den Einsatz der Berichts- und Bereichsereignisse
261
5.4.4 Bei Fehler: Fehler abfangen Das Ereignis Bei Fehler hat genau die gleiche Funktion wie bei Formularen. Damit lassen sich Laufzeitfehler behandeln, die nicht durch den VBA-Code im Klassenmodul des Berichts entstehen. Für weitere Informationen siehe Kapitel 11, Abschnitt 11.5, »Fehlerbehandlung in Formularen«.
5.4.5 Bei Seite: Seiten verschönern Wenn das Ereignis Bei Seite eintritt, ist der Großteil der Arbeiten bereits erledigt: Die Seite ist mit Inhalten gefüllt und fertig formatiert. Manchmal bereiten gewisse Feinheiten allerdings Probleme, beispielsweise Linien zum Trennen der einzelnen Bereiche oder – und hier wird es spannend – zum Trennen einzelner Spalten beziehungsweise Setzen von vertikalen Rändern. Wenn manch einer wüsste, wie leicht sich das mit VBA erledigen lässt, würde er die »Pixelpopelei« schnell sein lassen … VBA bietet mit der Line-Methode eine Anweisung zum genauen Anlegen von Rahmen und Trennlinien. In den folgenden Abschnitten finden Sie im Schnelldurchlauf einige interessante Beispiele für die Verwendung der Line-Methode, die Sie allesamt in der Ereignisprozedur Report_Page unterbringen können. Die folgenden Elemente der LineAnweisung sind für die nachfolgenden Beispiele ausreichend: Objekt.Line (x1, y1)-(x2, y2)[, [Farbe], [B[F]]]
Für Objekt geben Sie eine Referenz auf den Bericht an, in dem die Linie oder das Rechteck gezeichnet werden soll. Die Koordinaten x1, y1, x2, y2 geben die Position der Eckpunkte der Linie beziehungsweise des Rechtecks an. Der optionale Parameter Farbe enthält einen Wert für die Farbe des zu zeichnenden Elements. Standardmäßig wird hier die Farbe Schwarz verwendet. Verwenden Sie als letzten Parameter das B, wird ein Rahmen gezeichnet, BF liefert einen ausgefüllten Rahmen und wenn Sie den letzten Parameter weglassen, wird statt eines Rahmens eine Linie zwischen den beiden angegebenen Punkten gezeichnet.
Seite einrahmen Einen Rahmen um die komplette Seite legen Sie mit folgender Prozedur an: Private Sub Report_Page() Me.ScaleMode = 6 'Millimeter Me.Line (Me.ScaleLeft, Me.ScaleTop)-(Me.ScaleWidth - 0.3, _ Me.ScaleHeight - 0.3), &H0, B End Sub Listing 5.4: Kompletten Bericht einrahmen
262
5
Berichte
Möglicherweise wundern Sie sich um den »Abschlag« von 0.3mm bei den Koordinaten des zweiten Punktes. Die Linien haben eine bestimmte Dicke, um die der eingerahmte Bereich vergrößert wird. Dadurch rutschen mitunter die rechte und die untere Linie aus dem Bericht heraus. Daher müssen Sie den rechten unteren Punkt ein wenig nach innen beziehungsweise nach oben rücken. Berücksichtigen Sie dies auch beim Anlegen der Steuerelemente. Die verwendete Einheit legt man mit der Eigenschaft ScaleMode fest. Der Wert 6 steht für Millimeter, 7 für Zentimeter.
5.4.6 Beim Formatieren: Layout anpassen Da das Ereignis Beim Formatieren gegebenenfalls mehr als einmal aufgerufen wird, sollten Sie in der entsprechenden Ereignisprozedur nur Code unterbringen, der tatsächlich bei jedem Aufruf dieses Ereignisses ausgeführt werden muss. Um nicht unnötig Rechenkapazität zu vergeuden, können Sie im Beim FormatierenEreignis mit dem Parameter FormatCount prüfen, das wievielte Mal die entsprechende Prozedur aufgerufen wird. Damit Anweisungen nur beim ersten Durchlauf ausgeführt werden, fassen Sie diese etwa in folgendes If Then-Konstrukt ein: If FormatCount = 1 Then 'Code beim Formatieren End If
Während Sie theoretisch viele Aktionen wie etwa das sichtbar/unsichtbar machen von Steuerelementen, Einstellen von Hintergrundfarben oder Ähnliches sowohl im Ereignis Beim Formatieren als auch im Ereignis Beim Drucken durchführen können, sind Aktionen, die das Layout und insbesondere die Größe von Steuerelementen betreffen, nur im Beim Formatieren-Ereignis möglich.
Höhe von Steuerelementen einstellen Wenn Sie beispielsweise die Höhe von Steuerelementen oder Bereichen einstellen möchten, verwenden Sie die Beim Formatieren-Eigenschaft des jeweiligen Bereichs. Wenn Sie etwa einen Wochenkalender ausdrucken möchten, sollen vermutlich Samstage und Sonntage etwas weniger Platz einnehmen (außer Sie sind selbstständig und/ oder Autor). Ein Kalender wie in Abbildung 5.6 benötigt neben einer Tabelle mit allen Tagen des Jahres (in der Beispieldatenbank unter tblKalenderdaten zu finden) noch die folgende Prozedur. Diese prüft, ob es sich beim aktuellen Datum um einen Samstag oder Sonntag handelt, und passt davon abhängig die Höhe des Steuerelements und des Detailbereichs an. Letzterer ist ein wenig größer, um einen schicken Zwischenraum zwischen den einzelnen Kalendertagen einzuschieben:
Beispiele für den Einsatz der Berichts- und Bereichsereignisse
263
Private Sub Detailbereich_Format(Cancel As Integer, FormatCount As Integer) If Weekday(Me!Kalenderdatum) = 7 Or Weekday(Me!Kalenderdatum) = 1 Then Me!txtKalenderdatum.Height = 1100 Me.Detailbereich.Height = 1150 Else Me!txtKalenderdatum.Height = 2100 Me.Detailbereich.Height = 2150 End If End Sub Listing 5.5: Unterschiedliche Höhe für Werktage und Wochenenden
Abbildung 5.6: Kalender mit unterschiedlich hohen Detailbereichen
5.4.7 Beim Drucken Das Beim Drucken-Ereignis wird für jeden Bereich mindestens einmal, und zwar nach dem Beim Formatieren-Ereignis, ausgelöst. Es kann also auf fertig formatierte Elemente zugreifen, was sich die folgenden Beispielprozeduren zu Nutze machen.
Tabelle mit Gitternetzlinien Wenn Sie eine Tabelle im Excel-Stil mit Gitternetzlinien drucken möchten, müssen Sie nicht jedes Strichlein einzeln ziehen, sondern können sich der bereits weiter oben vorgestellten Line-Methode bedienen. Bauen Sie den Detailbereich des zu bearbeitenden Berichts wie in Abbildung 5.7 auf. Es ist empfehlenswert, zwischen oberem und unterem Rand sowie zwischen den Steuerelementen ein Pixel Platz zu lassen.
264
5
Berichte
Abbildung 5.7: Dieser Bericht soll hinter Gitter.
Die folgende Ereignisprozedur durchläuft in der For Each-Schleife alle Steuerelemente des Detailbereichs mit Ausnahme des letzten Steuerelements. Es legt rechts neben jedem Steuerelement eine Linie an, die am rechten oberen Eckpunkt des Steuerelements beginnt und am linken unteren Eckpunkt endet. Damit sind bereits Trennstriche zwischen den einzelnen Steuerelementen vorhanden. Mit der folgenden Line-Anweisung zieht die Routine einen Rahmen um den kompletten Detailbereich. Das Ergebnis sieht schließlich wie in Abbildung 5.8 aus. Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) Dim ctl As Control For Each ctl In Me.Section(acDetail).Controls If Not ctl.Name = "Auslaufartikel" Then With ctl Me.Line (.Left + .Width, 0)-(.Left + .Width, Me.Height),0,B End With End If Next With Me Me.Line (0, 0)-(.Width, .Height), 0, B End With End Sub Listing 5.6: Bericht mit Gitternetzlinien versehen
Beispiele für den Einsatz der Berichts- und Bereichsereignisse
265
Abbildung 5.8: Bericht mit Gitternetzlinien
Datensätze durchstreichen Wo Sie schon gerade Linien zeichnen: Wie wäre es, gelöschte Datensätze einmal als durchgestrichene Datensätze im Bericht anzuzeigen (siehe Abbildung 5.9)? Verwenden Sie einfach folgende Prozedur, die ebenfalls durch das Ereignis Beim Drucken des Detailbereichs ausgelöst wird: Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) If Me!Auslaufartikel And Lagerbestand = 0 Then Me.Line (0, Me.Height / 2)-(Me.Width, Me.Height / 2) End If End Sub Listing 5.7: Durchstreichen von ausgegangenen Auslaufartikeln
Abbildung 5.9: Nicht mehr verfügbare Artikel werden durchgestrichen.
266
5
Berichte
5.5 Wichtige Eigenschaften von Berichten und Berichtsbereichen Berichte und ihre Bereiche besitzen einige erwähnenswerte Eigenschaften, die Ihnen in der Praxis viel Arbeit ersparen können.
Gruppenkopf und Gruppenfuß Mit diesen beiden Eigenschaften legen Sie fest, ob zu einer Gruppierung ein Kopfoder ein Fußbereich eingeblendet werden soll.
Gruppieren nach und Intervall Standardmäßig wird nach jedem Wert des unter Feld/Ausdruck angegebenen Feldes gruppiert. Das lässt sich aber auch grober einstellen. Im einfachsten Fall gruppieren Sie etwa nach dem Primärschlüsselfeld, stellen die Eigenschaft Gruppieren nach auf den Wert Intervall ein und setzen für die Eigenschaft Intervall den Wert 10 fest. Warum das Ganze? Beispielsweise, um alle paar Zeilen eine zusätzliche Leerzeile einzufügen, um eine Liste besser lesbar zu machen. Ein Beispiel finden Sie im Bericht rptArtikelUebersichtInZehnergruppen (siehe Abbildung 5.10). In der Vorschau sieht der Bericht wie in Abbildung 5.11 aus.
Abbildung 5.10: Einstellung zur Anzeige von Daten in Zehnerpäckchen
Richtig interessant werden diese Eigenschaften, wenn Sie mit Datumsangaben arbeiten – hier können Sie dann nach Jahren, Quartalen, Monaten, Wochen und Tagen oder auch feinkörniger nach Stunden und Minuten gruppieren.
Wichtige Eigenschaften von Berichten und Berichtsbereichen
267
Abbildung 5.11: Vorschauansicht der gebündelten Artikeldatensätze
Zusammenhalten von Daten Innerhalb von Gruppierungen sorgen Sie mit der Eigenschaft Zusammenhalten (VBA: KeepTogether) des Dialogs Sortieren und Gruppieren dafür, dass entweder die komplette Gruppe (also Gruppenkopf, die enthaltenen Detaildatensätze und Gruppenfuß) oder zumindest der Gruppenkopf und der erste Detaildatensatz der Gruppierung auf einer Seite gedruckt werden. Die gleiche Eigenschaft gibt es auch noch einmal im Eigenschaftsfenster; dort kann diese Eigenschaft auch für den Detailbereich festgelegt werden.
Neue Seite, Zeile oder Spalte Gruppierungen und auch der Detailbereich bieten die Möglichkeit, vor, nach oder vor und nach einem Bereich eine neue Seite, Zeile oder Spalte zu beginnen. Mit der Eigenschaft Neue Seite (VBA: ForceNewPage) geben Sie an, ob vor oder nach dem Bereich oder auch in beiden Fällen eine neue Seite erzeugt werden soll. Ein Beispiel finden Sie in Abschnitt 5.8.8, »Bereiche auf neuer Seite anzeigen«.
268
5
Berichte
Die Eigenschaft Neue Zeile oder Spalte (VBA: NewRowOrCol) legt fest, ob Bereiche in mehrspaltigen Berichten in einer neuen Zeile oder einer neuen Spalte angelegt werden sollen. Ob es sich nun um eine neue Zeile oder Spalte handelt, hängt vom Spaltenlayout ab, das Sie im Dialog Seite einrichten festlegen (siehe Abbildung 5.12). Diesen Dialog öffnen Sie über den Menüeintrag Datei/Seite einrichten…
Abbildung 5.12: Optionen für mehrspaltige Berichte
Vergrößerbar und Verkleinerbar Manchmal enthalten Felder keine Daten – das kann beispielsweise bei Adressdaten passieren. Da fehlt mal hier ein Ansprechpartner und mal dort eine Straße. Es wäre doch traurig, wenn Access-Berichte hier keine Abhilfe schaffen könnten. Abbildung 5.13 zeigt, wie Sie die Eigenschaft Verkleinerbar der Textfelder eines Adressetiketts auf Ja einstellen. Damit sorgen Sie dafür, dass die Felder, wenn diese keinen Inhalt haben, auf die Höhe 0 verkleinert werden. Der Clou ist, dass die anderen Felder dafür nach oben rücken. Abbildung 5.14 enthält die Vorschauansicht des Berichts rptAdressetiketten: Bei Alfreds Futterkiste fehlt der Ansprechpartner, bei Ana Trujillo Emparedadosy helados die Straße. In beiden Fällen wurden die anderen Felder so nach oben verschoben, dass kein Freiraum mehr bleibt.
Wichtige Eigenschaften von Berichten und Berichtsbereichen
269
Einschränkung Das Verkleinern funktioniert allerdings nur, wenn sich die Felder nicht mit anderen Feldern überlappen und auch keine anderen Feldern auf der gleichen Höhe liegen. Im Bericht aus Abbildung 5.34 etwa ist das Verkleinern eines Feldes und das Verschieben der darunter liegenden Felder nach oben nicht möglich. Wenn Sie diese Technik verwenden möchten, müssen Sie außerdem bedenken, dass eventuelle Zwischenräume nicht verkleinert werden. Wenn Sie also zwischen dem ersten und zweiten Steuerelement 10 Pixel Zwischenraum haben und zwischen dem zweiten und dritten auch, dann bleiben beim Verkleinern des zweiten Steuerelements immer noch insgesamt 20 Pixel Zwischenraum übrig – wenn alle anderen Zwischenräume 10 Pixel betragen, ergibt das ein unschönes Bild. Abhilfe schaffen Sie, indem Sie einfach die Steuerelemente ein wenig größer ziehen (bei Textfeldern geht das problemlos) und statt dessen die Zwischenräume weglassen.
Abbildung 5.13: Verkleinerbare Textfelder im Bericht
Bereich wiederholen Wenn sich gruppierte Daten mit Feldüberschriften oder ähnlichen Informationen im Gruppenkopf über mehr als eine Seite erstrecken, sollten die Überschriften auch auf den Folgeseiten angezeigt werden. Dies legen Sie fest, indem Sie die Eigenschaft Bereich wiederholen der jeweiligen Gruppierung auf den Wert Ja einstellen. Ein Beispiel dafür finden Sie in Abschnitt 5.7.2, »Unterberichte über mehrere Seiten«.
270
5
Berichte
Abbildung 5.14: Adressetiketten mit geschrumpften Textfeldern
5.6 Darstellung von Daten Noch im vorherigen Kapitel haben Sie erfahren, wie Sie mit Formularen die unterschiedlichen Beziehungsarten anzeigen. Dort gab es Varianten wie Listenfelder, Unterformulare, Detailformulare und sogar das Treeview-Steuerelement wurde zu Hilfe genommen. Und das war nur die Spitze des Eisbergs; auf mehrere Unterformulare im gleichen Formular, Unterformulare auf verschiedenen Registerblättern und Ähnliches wurde dort gar nicht eingegangen. Berichte sind da wesentlich anspruchsloser. Die meisten Darstellungen lassen sich mit nur zwei unterschiedlichen Methoden erreichen: Sie fassen alle notwendigen Daten in der als Datenherkunft dienenden Abfrage zusammen. Alles Weitere erledigen Sie im Bericht durch entsprechende Gruppierungen. Das funktioniert fast immer, außer wenn … … die Haupttabelle des Berichts mit mehr als einer Tabelle per 1:n-Beziehung verknüpft ist und Daten aus diesen drei (oder mehr) Tabellen im Bericht angezeigt werden sollen. Dann helfen auch keine Gruppierungen mehr, hier müssen Unterberichte her. Diese beiden Arten von Berichten werden Sie in den nächsten Abschnitten kennen lernen.
Darstellung von Daten
271
5.6.1 Einzelne Tabellen Daten aus einzelnen Tabellen lassen sich in Berichten wie auch in Formularen als Detailansicht mit je einem Datensatz oder in Listenform anzeigen. Beides ist nicht besonders kompliziert.
Daten einzelner Tabellen in der Detailansicht Als Datenherkunft für das folgende Beispiel dient die Artikel-Tabelle der NordwindDatenbank. Auf je einer Seite sollen die Eigenschaften eines Artikels angezeigt werden. Um eine Entwurfsansicht wie in Abbildung 5.15 zu erzeugen, sind folgende Schritte notwendig: Stellen Sie die Eigenschaft Datenherkunft des Berichts auf die Tabelle Artikel ein. Ziehen Sie alle Felder aus der Feldliste in den Detailbereich des Berichtsentwurfs. Ordnen Sie die Felder sauber an und stellen Sie gegebenenfalls Schriftgröße- und/ oder Breite für eine bessere Lesbarkeit ein. Fügen Sie den Eintrag Artikelname der Feldliste ein zweites Mal in den Bericht ein – diesmal allerdings in den Bericht Seitenkopf. Vergrößern Sie Schriftgröße und -breite so, dass dieses Feld Überschriftscharakter erhält. Legen Sie im Bereich Seitenfuß zwei Textfelder an, die folgenden Namen und Steuerelementinhalt haben: txtSeite: ="Seite " & [Seite] & "/" & [Seiten] txtDatum: =Datum() Sorgen Sie dafür, dass nur ein Datensatz je Seite angezeigt wird. Dazu vergrößern Sie entweder den Detailbereich so, dass kein zweiter kompletter Detailbereich auf die Seite passt oder besser, Sie stellen die Eigenschaft Neue Seite des Detailbereichs auf den Wert Nach Bereich ein. Legen Sie außerdem eine Sortierung nach dem Artikelnamen fest. Dazu blenden Sie den Dialog Sortieren und Gruppieren ein (Menüeintrag Ansicht/Sortieren und Gruppieren) und legen dort in der ersten Zeile als Feld/Ausdruck das Feld Artikelname fest.
272
5
Berichte
Abbildung 5.15: Entwurfsansicht des Berichts zur Anzeige der Artikeldetails
Beschriftungen von Steuerelementen verschieben Beim Positionieren der Steuerelemente stört möglicherweise, dass Beschriftungsfelder und Steuerelemente standardmäßig zusammen verschoben werden. Wenn Sie lieber alle Elemente einzeln positionieren möchten, schneiden Sie jeweils eines der verbundenen Elemente aus und fügen Sie es direkt wieder ein (geht am schnellsten mit der Tastenkombination (Strg) + (C) und (Strg) + (V). Anschließend können Sie die Steuerelemente separat verschieben. Das einzige, was stört, ist das Dreieck in der linken oberen Ecke des neu eingefügten Bezeichnungsfeldes. Beim Anklicken des betroffenen Steuerelements erscheint das aufklappbare Element aus Abbildung 5.16 und offenbart einige Möglichkeiten zum Umgang mit dem Bezeichnungsfeld. Den vermeintlichen Fehler können Sie entweder jedes Mal mit dem Menüeintrag Fehler ignorieren unsichtbar machen oder, wenn Sie dauerhaft von Hinweisen bezüglich allein stehender Steuerelemente verschont bleiben möchten, die Optionen Nicht dazugehöriges Bezeichnungsfeld und Steuerelement und Neues nicht dazugehöriges Bezeichnungsfeld deaktivieren. Dazu verwenden Sie den Optionen-Dialog aus Abbildung 5.17, den Sie mit dem Menüeintrag Extras/Optionen öffnen.
Darstellung von Daten
273
Abbildung 5.16: Ein Smarttag weist auf »ungebundene« Bezeichnungsfelder hin.
Abbildung 5.17: Dauerhaftes Deaktivieren der Anzeige allein stehender Steuerelemente
Daten einzelner Tabellen in der Übersicht Eine Übersicht der Artikel aus dem oben beschriebenen Bericht lässt sich ebenso leicht anlegen. Legen Sie in einem neuen Bericht wiederum die Tabelle Artikel als Datenherkunft fest. Ziehen Sie auch hier alle Felder aus der Feldliste in den Detailbereich des Berichts. Anschließend stellen Sie zunächst die Orientierung des Berichts auf Querformat ein. Den passenden Dialog öffnen Sie mit dem Menüeintrag Datei/Seite einrichten… (siehe Abbildung 5.18). Auf der Registerseite Ränder können Sie – falls notwendig – auch die Ränder direkt einstellen.
274
5
Berichte
Abbildung 5.18: Vorbereitungen zum Anzeigen eines Berichts im Querformat
Die Artikel sollen als Tabelle angezeigt werden, also benötigen Sie eine Tabellenüberschrift je Seite. Dazu schneiden Sie die Beschriftungen der eingefügten Felder komplett aus und fügen diese im Bereich Seitenkopf wieder ein. Ordnen Sie dann die Felder nebeneinander an, sodass der Entwurf etwa wie in Abbildung 5.19 und Abbildung 5.20 aussieht – aus Platzgründen musste der Entwurf leider auf zwei Abbildungen aufgeteilt werden. Die Feldüberschriften erscheinen in der Vorschauansicht auf jeder Seite – genau wie die im Seitenfuß befindlichen Angaben zu Seitenzahl und Datum. Für eine bessere Optik empfiehlt sich auch die Verwendung von horizontalen Strichen zur Abgrenzung von Seitenkopf und -fuß und den eigentlichen Daten (siehe Abbildung 5.21).
Abbildung 5.19: Entwurfsansicht der Artikel-Übersicht, aufgeteilt in den linken Teil …
Darstellung von Daten
Abbildung 5.20: … und den rechten Teil
Abbildung 5.21: Artikelübersicht in der Vorschauansicht
275
276
5
Berichte
Wenn Sie eine Anwendung entwickeln, stehen Ihnen in manchen Fällen bereits umfangreiche Daten des Auftraggebers zur Verfügung. Diese werden Sie vermutlich ohnehin im Rahmen der Erstellung des Datenmodells importiert haben. Wenn Sie nicht über solche Daten verfügen, werden Sie wohl oder übel einige hundert Beispieldatensätze anlegen müssen. Viele nachträgliche Änderungen an Datenbankanwendungen resultieren nämlich daraus, dass die einzelnen Formulare und vor allem Berichte nicht mit realistischen Daten getestet wurden. Es zeigt sich dann in der Praxis, dass die Steuerelemente in Formularen und Berichten zu klein für die tatsächlichen Daten dimensioniert wurden oder Berichte ab bestimmten Seitenzahlen ein unerwartetes Verhalten aufweisen. Mit den richtigen Beispieldaten schaffen Sie solche Probleme bereits bei der Entwicklung aus der Welt.
5.6.2 1:n-Beziehungen Zur Darstellung von 1:n-Beziehungen müssen Sie nicht unbedingt, wie bereits weiter oben angedeutet, auf Unterformulare oder Steuerelemente wie Listenfelder zugreifen. In einem Bericht reicht es aus, die verknüpften Tabellen zu einer Datenherkunft zusammenzufassen und eine entsprechende Gruppierung anzulegen. Natürlich gibt es hier Ausnahmen wie etwa die Anzeige mehrerer mit der Mastertabelle verknüpfter Detailtabellen. Dazu jedoch später mehr. Die Darstellung einer 1:n-Beziehung lässt sich anschaulich an den Artikeln und Kategorien der Nordwind-Datenbank erläutern. Dazu soll einfach der Bericht aus dem vorherigen Beispiel so umgestaltet werden, dass jede Kategorie mit den enthaltenen Artikeln auf je einer Seite erscheint.
Datenherkunft des 1:n-Berichts Dazu erweitern Sie zunächst die Datenherkunft des Berichts um die Tabelle Kategorien und ziehen alle Felder der beiden Tabellen in den Abfrageentwurf (siehe Abbildung 5.22).
Gruppierung statt Unterformular Statt des bei der Verwendung von Formularen notwendigen Unterformulars oder Listenfeldes zur Darstellung von verknüpften Daten wenden Sie bei Berichten vorzugsweise Gruppierungen an. Im vorliegenden Beispiel sollen eine Kategorie und die dazugehörenden Artikel auf einer Seite angezeigt werden. Also gruppieren Sie die Datenherkunft nach Kategorien. Jede Gruppe erhält einen speziellen Kopfbereich, den Gruppenkopf. In diesem werden Daten wie Name der Kategorie, Beschreibung oder das in der Tabelle enthaltene Bild abgelegt.
Darstellung von Daten
277
Abbildung 5.22: Datenherkunft eines Berichts zur Anzeige der Kategorien und der enthaltenen Artikel
Um eine Gruppierung anzulegen, gehen Sie genauso wie beim Anlegen einer Sortierung vor – stellen Sie einfach im Dialog Sortieren und Gruppieren das Feld, nach dem gruppiert werden soll, unter Feld/Ausdruck ein. Außerdem legen Sie hier fest, ob die Gruppe einen Gruppenkopf und/oder -fuß erhalten soll. In diesem Fall reicht ein Gruppenkopf aus (siehe Abbildung 5.23). Fügen Sie außerdem eine Sortierung nach dem Artikelnamen hinzu. Achten Sie beim Anlegen von Sortierungen und Gruppierungen darauf, dass diese von oben nach unten abgearbeitet werden. In diesem Fall gilt: Gruppierungen müssen nach oben, Sortierungen innerhalb der gruppierten Daten nach unten. Der Detailbereich soll wie im Bericht rptArtikelUebersicht die Artikel der jeweiligen Kategorie in tabellarischer Form anzeigen. Natürlich benötigen Sie auch hier Spaltenüberschriften. Wie Abbildung 5.23 zeigt, befindet sich der Seitenkopf-Bereich über dem Gruppenkopf – dort würden die Feldüberschriften nicht gut aussehen. Also fügen Sie diese einfach im unteren Bereich des Gruppenkopfes der Kategorien ein.
Eine Gruppe je Seite Wenn Sie nun in die Vorschauansicht wechseln, werden die Artikel zwar nach Kategorien gruppiert, aber die Kategorien folgen ohne Seitenumbruch direkt aufeinander und stehen nicht auf je einer Seite. Dabei fällt gleichzeitig auf, dass Kategorien, die mehr Artikel enthalten, als auf eine Seite passen, zwar auf der nächsten Seite fortgesetzt werden, aber ohne Feldüberschriften. Das ist auch logisch, denn diese werden ja bisher nur im Gruppenkopf angezeigt.
278
5
Berichte
Abbildung 5.23: Entwurf des Berichts zur Anzeige von Artikeln nach Kategorie
Damit für jede Gruppierung eine neue Seite erzeugt wird, stellen Sie die Eigenschaft Neue Seite des Bereichs auf den Wert Vor Bereich ein (siehe Abbildung 5.24). Damit kennen Sie nun auch alle Möglichkeiten, Eigenschaften der Kopf- und Fußbereiche von Gruppierungen einzustellen – nämlich im Dialog Sortieren und Gruppieren und im entsprechenden Eigenschaftsfenster.
Abbildung 5.24: Eigenschaften des Gruppenkopfs eines Bereichs
Feldüberschriften auf Folgeseiten von Gruppierungen Fehlen noch die Feldüberschriften für Kategorien, die mehr als eine Seite füllen. Bisher dient der Kopfbereich der Gruppe Kategorie-Nr quasi als Seitenkopf; nun benötigen Sie den Seitenkopf selbst. Kopieren Sie hier die Feldüberschriften aus dem Kategorien-
Darstellung von Daten
279
Kopfbereich hinein und fügen Sie auch eine Kopie des Feldes Kategoriename aus diesem Bereich hinzu. Löschen Sie den Inhalt der Eigenschaft Steuerelementinhalt dieses Steuerelements; es wird später per VBA gefüllt (siehe Abbildung 5.25). Eine VBA-Prozedur ist es auch, die den Seitenkopf einblendet, wenn der Detailbereich eine Fortsetzung der vorherigen Seite ist. Die entsprechende Prozedur wird durch die Ereigniseigenschaft Beim Drucken ausgelöst. Sie fragt die Eigenschaft WillContinue ab, die genau die gesuchte Information liefert, die Sie hier benötigen. Ist der Wert False, handelt es sich nicht um eine Fortsetzung der Artikel einer Kategorie – hier wird dann der Kopfbereich der neuen Kategorie angezeigt. Interessant wird es, wenn WillContinue den Wert True zurückliefert – in diesem Fall wird der Seitenkopf eingeblendet und das Textfeld txtKategoriename mit dem Namen der aktuellen Kategorie und dem Text »(Fortsetzung)« gefüllt (siehe Abbildung 5.26). Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) If Me.Detailbereich.WillContinue = True Then Me.Seitenkopfbereich.Visible = True Me!txtKategoriename = Me!Kategoriename & " (Fortsetzung)" Else Me.Seitenkopfbereich.Visible = False End If End Sub Listing 5.8: Der Seitenkopf wird eingeblendet, wenn der Detailbereich eine Fortsetzung der vorherigen Seite ist.
Abbildung 5.25: Der neue Seitenkopf soll nur zum Einsatz kommen, wenn der Kategorien-Kopfbereich nicht angezeigt wird.
280
5
Berichte
Abbildung 5.26: Die Startseite einer Kategorie und ihre Fortsetzung
5.6.3 m:n-Beziehungen Die Darstellung von m:n-Beziehungen in Berichten funktioniert fast genauso wie im vorherigen Beispiel der 1:n-Beziehung. Der wichtigste Unterschied ist, dass die Datenherkunft nun mindestens drei Tabellen enthält (die beiden verknüpften Tabellen und die Verknüpfungstabelle). Davon abgesehen verwenden Sie genau wie bei der 1:n-Verknüpfung eine Gruppierung nach den gewünschten Daten der m-Seite der Tabelle und zeigen die anderen Daten entsprechend im Detailbereich an.
5.7 Berichte mit Unterberichten Einer der wenigen Gründe, der für den Einsatz von Unterberichten spricht, ist das Vorhandensein einer Mastertabelle, die mit mehreren Detailtabellen verknüpft ist. Wenn Sie also etwa eine Kunden-Tabelle haben, die mit einer Projekte-Tabelle und einer Ansprechpartner-Tabelle verknüpft ist und Sie diese alle in einem Bericht anzeigen möchten, kommen Sie um den Einsatz von Unterberichten nicht herum.
Berichte mit Unterberichten
281
5.7.1 Unterberichte Beginnen Sie mit den Unterberichten für die Projekte und die Mitarbeiter des Unternehmens. Der Unterbericht srpAnsprechpartner verwendet die Tabelle tblAnsprechpartner als Datenherkunft. Die Felder werden tabellarisch im Seitenkopf und im Detailbereich aufgebaut (siehe Abbildung 5.27). Der Unterbericht srpProjekte bezieht seine Daten aus der Tabelle tblProjekte und ist prinzipiell genauso aufgebaut wie der Bericht srpAnsprechpartner.
Abbildung 5.27: Unterbericht zur Darstellung der Mitarbeiter
Einbinden der Unterberichte in den Hauptbericht Der Bericht rptKunden dient als Hauptbericht. Er verwendet die Tabelle tblKunden als Datenherkunft und zeigt die dort enthaltenen Daten im oberen Teil des Detailbereichs an. Damit nur ein Kunde je Seite angezeigt wird, stellen Sie die Eigenschaft Neue Seite dieses Bereichs auf den Wert Vor Bereich ein. Die Unterformulare ziehen Sie genau wie beim Einfügen eines Unterformulars in ein Hauptformular einfach aus dem Datenbankfenster in den Detailbereich. UnterberichtSteuerelemente haben wie Unterformular-Steuerelemente zwei Eigenschaften namens Verknüpfen von und Verknüpfen nach, mit denen die Felder beziehungsweise Steuerelemente festgelegt werden, über die die in Haupt- und Unterbericht anzuzeigenden Daten verknüpft werden. Der wichtigste Unterschied zwischen dem Anlegen von Unterberichten im Vergleich zu Unterformularen ist, dass Sie Letztere bereits im Entwurf genau anpassen müssen. Unterberichte müssen nur die richtige Breite haben, die Höhe wird je nach Anzahl der enthaltenen Datensätze vergrößert – vorausgesetzt Sie stellen die Eigenschaft Vergrößerbar des Unterberichtsteuerelements auf Ja ein. Sie können auch die Eigenschaft Verkleinerbar auf Ja einstellen – in diesem Fall wird der Unterbericht gar nicht beziehungsweise mit der Höhe 0 angezeigt, wenn keine Datensätze enthalten sind.
282
5
Berichte
Abbildung 5.28: Hauptbericht mit zwei Unterberichten
Wenn Sie den Hauptbericht nun in der Vorschauansicht öffnen, fehlen allerdings die in den Unterberichten noch vorhandenen Kopfzeilen (siehe Abbildung 5.29). Das ist kein Fehler, denn ein Unterbericht enthält schlicht und einfach keine eigene Seite, auf der ein Seitenkopf untergebracht werden könnte. Also müssen Sie sich mit einem kleinen Trick behelfen. Wenn man keine Feldüberschriften mit einem Seitenkopf erzeugen kann, verwenden Sie einfach einen Kopfbereich einer Gruppierung. Oh, es gibt gar keine Möglichkeit für eine Gruppierung? Kein Problem: Dann verwenden Sie einfach eine fiktive Gruppierung ohne Bezug zu einem der Felder. Legen Sie dazu im Dialog Sortieren und gruppieren unter Feld/Ausdruck einen neuen Eintrag mit dem Inhalt –1 an und stellen Sie dessen Eigenschaft Gruppenkopf auf den Wert Ja ein. Verschieben Sie dann die Feldüberschriften aus dem Seitenkopfbereich in den Kopfbereich der neuen Gruppierung – fertig sind die Feldüberschriften (siehe Abbildung 5.30).
Berichte mit Unterberichten
Abbildung 5.29: Unterberichte ohne Kopfbereich
Abbildung 5.30: Erzeugen des Kopfbereichs einer fiktiven Gruppierung
283
284
5
Berichte
Abbildung 5.31 zeigt, dass die neuen Feldüberschriften auch im Hauptbericht angezeigt werden.
Abbildung 5.31: Unterberichte mit Feldüberschriften
5.7.2 Unterberichte über mehrere Seiten Die Unterberichte aus dem vorherigen Beispiel haben einen kleinen Fehler: Wenn ein Kunde einmal so viele Ansprechpartner oder Projekte enthält, dass der Unterbericht sich über zwei Seiten erstreckt, erscheinen wiederum keine Feldüberschriften. Dies zu ändern ist allerdings leicht: Stellen Sie einfach die Eigenschaft Bereich wiederholen des Gruppenkopfes im Entwurf des Unterformulars auf den Wert Ja ein (siehe Abbildung 5.32).
5.8 Rechnungserstellung mit Berichten Das Ausgeben von Rechnungen ist vermutlich einer der am meisten genutzten Anwendungsfälle von Berichten in Access. Deshalb und auch weil Rechnungsberichte eine große Menge Möglichkeiten zum Vorstellen verschiedener Techniken bieten, finden Sie nachfolgend ein ausführliches Beispiel zum Erstellen von Rechnungen mit Access-Berichten.
Rechnungserstellung mit Berichten
285
Abbildung 5.32: Mit dieser Einstellung erscheint der Kopfbereich der Gruppierung auch auf den Folgeseiten.
Als Grundlage verwenden Sie die Bestelldaten aus der Nordwind-Datenbank. Welche Tabellen Sie für die dem Bericht zugrunde liegende Abfrage benötigen, lässt sich mit einem Blick auf eine herkömmliche Rechnung herausfinden. Für die Anschrift des Kunden benötigen Sie die Kunden-Tabelle, den Rest erledigen die in einer m:n-Beziehung stehenden Tabellen Bestellungen, Bestelldetails und Artikel. Damit der Kunde weiß, an wen er sich bei Fragen wenden kann, nehmen Sie noch die Tabelle Personal hinzu, die ebenfalls mit der Tabelle Bestellungen verknüpft ist. Die Datenherkunft sieht schließlich wie in Abbildung 5.33 aus. Den aktuellen Steuergesetzen entsprechend wurde der Tabelle Bestellung noch ein Feld namens Rechnungsnummer hinzugefügt. Das Feld hat den Datentyp Text, um auch Zeichen wie Bindestriche oder Schrägstriche zu ermöglichen. Wie Sie die Rechnungsnummer ermitteln, bleibt Ihnen überlassen. Eine Rechnungsnummer muss aber auf jeden Fall eindeutig sein. Manch einer verwendet eine bei 1 startende Nummerierung in Zusammenhang mit der Jahreszahl (zum Beispiel »200501«); andere lassen die Kundennummer in die Rechnungsnummer einfließen. Wer wichtig wirken möchte, verwendet eine Rechnungsnummer wie »950 752 0642«. In der Beispieltabelle Bestellungen ist die Rechnungsnummer schlicht eine Kombination aus dem Datum der Rechnungsstellung und dem Primärschlüssel der Tabelle (etwa »20051009-11077«). So können Sie auch einmal nur einfach in den Ordner mit Rechnungen schauen, wenn ein Kunde (oder der Steuerprüfer) dazu eine Frage hat – vorausgesetzt dort liegen die Rechnungen in chronologischer Reihenfolge vor ... Außerdem wurde die Tabelle Artikel um ein Feld namens Mehrwertsteuer erweitert, das durchgängig den Wert 7% enthält. Die Abfrage selbst wurde um ein berechnetes Feld namens Bruttopreis ergänzt, das den Bruttopreis einer jeden Position enthält (siehe Auflistung).
286
5
Berichte
Sie verwenden in dem Bericht folgende Felder: Tabelle Artikel: Artikelname, Liefereinheit, Mehrwertsteuer Tabelle Bestelldetails: Einzelpreis, Anzahl, Rabatt Tabelle Bestellungen: Bestell-Nr, Bestelldatum, Versanddatum, VersandÜber, Frachtkosten, Empfänger, Straße, Ort, PLZ, Bestimmungsland, Rechnungsnummer Tabelle Kunden: Firma, Kontaktperson, Straße, Ort, PLZ, Land Tabelle Personal: Nachname, Vorname, Anrede, Durchwahl Büro Berechnetes Feld Bruttopreis: [Bestelldetails].[Einzelpreis]*[Bestelldetails].[Anzahl]* ([Bestelldetails].[Rabatt]+1)*([Artikel].[Mehrwertsteuer]+1) Berechnetes Feld Netto: [Bestelldetails.Einzelpreis]*[Anzahl]*(1+[Rabatt]) Berechnetes Feld MwStBetrag: [Bestelldetails.Einzelpreis]*[Anzahl]*(1+[Rabatt])*[Mehrwertsteuer] Die letzten beiden Felder werden nicht in der Auflistung der Positionen angezeigt, sondern dienen nur der Ermittlung der Summe der Nettopreise und der Mehrwertsteuer am Ende der Rechnung.
Abbildung 5.33: Datenherkunft für den Rechnungsbericht
Rechnungserstellung mit Berichten
287
5.8.1 Konzept für die Erstellung des Berichts Bevor Sie sich an die Erstellung eines Berichts machen, sollten Sie die Daten in Gedanken (oder auch auf dem Papier) kurz auf die einzelnen Bereiche eines Berichts aufteilen und sich überlegen, welche Daten etwa nur auf der ersten Seite angezeigt werden, was passiert, wenn so viele Datensätze vorhanden sind, dass der Bericht über mehrere Seiten geht, und so weiter. Im vorliegenden Fall haben Sie es mit einem ziemlich großen Berichtskopf zu tun: Dieser enthält den kompletten Briefkopf, die Anschrift des Kunden, Lieferdaten und so weiter. Moment: Ist das wirklich so? Wenn der Berichtskopf bereits kundenspezifische und damit rechnungsspezifische Daten enthält, dann können Sie nur eine Rechnung je Bericht ausgeben. Warum? Weil der Berichtskopf nur einmal je Bericht angezeigt wird. Wenn Sie aber mal einen ganzen Schwung Rechnungen ausdrucken möchten, haben Sie ein Problem: Sie müssen dann schon den gleichen Bericht mehrere Male aufrufen. Natürlich geht das auch, aber in diesem Fall sollen Sie einen Bericht erstellen, der mehrere Rechnungen auf einen Rutsch anfertigen kann. Also benötigen Sie eine Gruppierung über die einzelnen Bestellungen – als Gruppierungsfeld bietet sich das Feld Bestell-Nr an. Im Gruppenkopf bringen Sie nun alles unter, was Sie sonst in den Berichtskopf packen wollten – Briefkopf, Anschrift, Bestelldaten wie Bestellnummer und so weiter. Die einzelnen Positionen gehören – ganz klar – in den Detailbereich. Darüber benötigen Sie Feldüberschriften: Diese können Sie auf der ersten Seite direkt im Gruppenkopf unterbringen, auf den folgenden Seiten im Seitenkopf. Dieser darf dann wiederum nicht auf der ersten Seite angezeigt werden – dazu später mehr. Nach der letzten Rechnungsposition folgt noch die Rechnungssumme. Dafür bietet sich der Berichtsfuß an. Auf der Seite mit dem Berichtsfuß soll wiederum kein Seitenfußbereich angezeigt werden. Wenn Sie dem Kunden ein wenig mehr Komfort bieten möchten, sorgen Sie auch noch für eine Zwischensumme und einen Übertrag. Die Zwischensumme gehört mit in den Seitenfuß, der Übertrag in den Seitenkopf – dieser erscheint aber nur auf den Seiten, die keinen Gruppierungskopf anzeigen. Sie sehen – eine Rechnung ist nicht gerade der trivialste Bericht, der sich mit Access erstellen lässt, und er enthält noch nicht einmal Gruppierungen.
5.8.2 Erstellen des Gruppenkopfs Die Erstellung des Gruppenkopfs einer Rechnung ist im Wesentlichen Design-Arbeit und weniger Denksport. Hier bringen Sie den Briefkopf unter, den Block mit der Empfängeradresse und den Block mit den allgemeinen Rechnungsdaten (siehe Abbildung 5.34).
288
5
Berichte
Hier gibt es folgende Besonderheiten: Das Feld txtPLZUndOrt fasst die Felder PLZ und Ort der Tabelle Kunden zusammen. Wichtig ist hier wie bei anderen Steuerelementen, dass Sie Tabellennamen und Feldnamen von in mehreren Tabellen vorkommenden Feldern durch Punkt getrennt schreiben und in eckige Klammern einfassen. Dies geschieht noch einmal im Gruppenkopf, und zwar im Textfeld txtAnsprechpartner. Dessen Inhalt lautet: =[Personal.Anrede] & " " & [Personal.Vorname] & " " & [Personal.Nachname]
Die Feldüberschriften werden Sie normalerweise erst im folgenden Schritt beim Füllen des Detailbereichs anlegen, in der Abbildung sehen Sie aber bereits ihre Anordnung.
Abbildung 5.34: Gruppenkopf einer Rechnung in der Detailansicht
5.8.3 Anlegen des Detailbereichs Der Detailbereich ist reine Fleißarbeit – abgesehen von einem Feature, das aber erst später hinzukommt, und der Angabe der Rechnungspositionen. Der Detailbereich enthält die Felder Artikelname, Liefereinheit, Bruttopreis, Einzelpreis, Anzahl, Rabatt, Mehrwertsteuer und Bruttopreis, wobei Letzteres das berechnete Feld ist, das Sie der Abfrage weiter oben hinzugefügt haben.
Rechnungserstellung mit Berichten
289
Es fehlt noch das ganz linke Feld in der Entwurfsansicht (siehe Abbildung 5.35). Es heißt txtPosition und besitzt als Steuerelementinhalt den Wert =1. Damit dieses Feld die Position des Artikels für die aktuelle Rechnung, also innerhalb der aktuellen Gruppierung, anzeigt, stellen Sie die Eigenschaft Laufende Summe dieses Feldes auf Über Gruppe ein.
Abbildung 5.35: Entwurfsansicht des Detailbereichs mit den Rechnungspositionen
5.8.4 Berechnungen in Berichten oder Berechnungen in Formularen Möglicherweise fragen Sie sich, ob man die Berechnung des Bruttopreises nicht auch innerhalb des Berichts hätte durchführen können. Die Frage ist berechtigt, da das sogar funktionieren würde. Das Problem ist nur, dass Sie keine datensatzübergreifenden Berechnungen über den Bezug auf das Feld mit dem Berechnungsergebnis durchführen könnten. Ein in der Abfrage berechnetes Feld behandelt Access im Bericht aber wie ein normales Tabellenfeld; hier sind Berechnungen wie etwa das Bilden der Summe über alle Datensätze einer Gruppierung möglich. Streng genommen würde man im Bericht auch ohne das in der Abfrage berechnete Feld auskommen, aber dann müsste man im Feld zur Berechnung der Summe über mehrere Datensätze Bezug auf die Berechnungsformel nehmen und nicht auf das Feld mit dem Berechnungsergebnis.
5.8.5 Summenbildung im Fußbereich der Gruppierung Im Fußbereich der Gruppierung heißt es: Abrechnen! Hier wird die Summe über das Feld Bruttopreis des Detailbereichs gebildet und schließlich noch der Frachtkostenanteil hinzuaddiert. Unterhalb der Rechnungssumme ist ein guter Ort, um die Bankverbindung unterzubringen. Diese könnte man zwar auch auf der ersten Seite zu den allgemeinen Rechnungsdaten hinzufügen, aber der Rechnungsempfänger wird sich freuen, wenn er zum Begleichen der Rechnung per Online-Banking nicht noch hinund herblättern muss (siehe Abbildung 5.36). Der Fußbereich greift auf zwei Felder zurück, die sich zwar in der Abfrage, aber nicht im Detailbereich des Berichts befinden: Netto und MwStBetrag. Diese werden aus Gründen der Übersicht nicht in jedem Datensatz angezeigt, sondern nur aufsummiert am Ende der Rechnung.
290
5
Berichte
Abbildung 5.36: Der Fußbereich der Gruppierung in der Entwurfsansicht
5.8.6 Feinheiten: Zwischensumme und Übertrag Ist abzusehen, dass sich einige Rechnungen über mehrere Seiten erstrecken, macht die Angabe von Zwischensumme und Übertrag Sinn. Die Zwischensumme platzieren Sie im Bereich Seitenfuß des Berichts (siehe Abbildung 5.37). Zur Ermittlung der laufenden Summe müssen Sie zunächst ein zusätzliches unsichtbares Feld im Detailbereich anlegen. Dieses erhält den Namen txtLaufendeSumme und hat als Steuerelementinhalt den Wert =BruttoPreis. Damit die laufende Summe nur im Rahmen der aktuellen Rechnung ermittelt wird, stellen Sie die Eigenschaft Laufende Summe auf Über Gruppe ein. Dieses Feld enthält jeweils den Inhalt des gleichen Feldes aus dem vorherigen Datensatz zuzüglich des Inhalts des Feldes Bruttopreis des aktuellen Datensatzes. Das Feld txtZwischensumme im Seitenfuß des Berichts enthält den Inhalt des Feldes txtLaufendeSumme, welcher am Seitenende dem Wert des untersten angezeigten Datensatzes entspricht.
Abbildung 5.37: Entwurfsansicht des Seitenfußes mit der laufenden Summe
5.8.7 Überschriften für Folgeseiten und Rechnungsübertrag Fehlen noch die Feldüberschriften für die Folgeseiten in Rechnungen, die über mehr als eine Seite gehen. Diese platzieren Sie genau wie das Feld zur Anzeige des Rechnungsübertrags im Seitenkopf des Berichts (siehe Abbildung 5.38). Das Feld zur
Rechnungserstellung mit Berichten
291
Anzeige des Übertrags heißt txtUebertrag und ist ungebunden. Damit es immer den Wert anzeigt, den das Feld txtZwischensumme auf der vorherigen Seite hatte, setzen Sie eine Ereigniseigenschaft ein, die direkt nach der Ermittlung der Zwischensumme ausgelöst wird und das Feld txtUebertrag mit dem aktuellen Wert des Feldes txtZwischensumme füllt. Die entsprechende Routine sieht folgendermaßen aus: Private Sub Seitenfußbereich_Print(Cancel As Integer, PrintCount As Integer) Me!txtUebertrag = Me!txtZwischensumme End Sub Listing 5.9: Routine zum Aktualisieren des Übertrags
Abbildung 5.38: Der Seitenkopf in der Entwurfsansicht
5.8.8 Rechnungsentwurf im Zusammenhang und Restarbeiten Wenn Sie den Rechnungsbericht, dessen Entwurf Sie in Abbildung 5.39 im Überblick sehen, nun öffnen, werden Sie noch kleinere Schönheitsfehler entdecken. Es beginnt nicht jede Rechnung auf einer neuen Seite, der Seitenkopfbereich erscheint auch auf der ersten Seite einer Bestellung mit dem Gruppenkopf und der Seitenfuß erscheint auch auf der letzten Seite, wo er eigentlich nicht sichtbar sein sollte. Diese Ungereimtheiten räumen Sie jetzt nacheinander aus.
Bereiche auf neuer Seite anzeigen In der aktuellen Fassung beginnt nicht jede Rechnung auf einer neuen Seite, die Rechnungen werden einfach nacheinander weggedruckt. Um die Anzeige jeder Rechnung auf einer neuen Seite zu erreichen, ist nur eine kleine Änderung vonnöten: Stellen Sie die Eigenschaft Neue Seite des Kopfbereichs der Gruppierung Bestell-Nr auf den Wert Vor Bereich ein. Dadurch wird vor jedem Gruppenkopf ein Seitenumbruch eingefügt.
292
5
Berichte
Abbildung 5.39: Entwurf der Rechnung im Überblick
Seitenkopf und Seitenfuß nur auf bestimmten Seiten anzeigen Nun wenden Sie sich dem Seitenkopf und dem Seitenfuß zu. Der Seitenkopf erscheint dummerweise auch auf der ersten Seite über dem Briefkopf und der Seitenfuß soll nicht auf Seiten angezeigt werden, die den Gruppenfuß enthalten. Würden Sie hier die weiter oben angesprochene Variante des Rechnungsberichts verwenden, bei der jede Rechnung in einem einzelnen Bericht angezeigt wird und sich der Briefkopf mit den allgemeinen Rechnungsdaten im Berichtskopf statt im hier verwendeten Gruppenkopf befindet, wäre das Problem sehr einfach zu lösen: Sie würden dann einfach die Eigenschaft Seitenkopf des Berichts auf den Wert Außer Berichtskopf einstellen (siehe Abbildung 5.40).
Rechnungserstellung mit Berichten
293
Abbildung 5.40: Seitenkopf nicht mit dem Berichtskopf auf einer Seite anzeigen
Nachdem Sie nun wissen, wie dies normalerweise funktionieren würde, kommen Sie nun zur anspruchsvolleren Variante: Und da ist schon ein wenig Gehirnschmalz notwendig. Die wichtigste Information, die Sie zum Ein- beziehungsweise Ausblenden von Berichtsbereichen haben müssen, ist folgende: Es funktioniert nicht zuverlässig, wenn Sie den Bereich mit der Visible-Eigenschaft sichtbar oder unsichtbar machen. Auf der sicheren Seite sind Sie, wenn Sie die Cancel-Eigenschaft der Beim Formatieren-Eigenschaft des jeweiligen Bereichs auf True setzen, um die Anzeige des Bereichs zu unterbinden. Ob einer der beiden Bereiche Seitenkopf oder Seitenfuß angezeigt werden soll, entscheidet sich freilich nicht in der jeweiligen Beim Formatieren-Eigenschaft, sondern in anderen Ereignisprozeduren. Nun der Reihe nach: Das folgende Listing enthält das komplette Klassenmodul des Berichts. Die Ereignisprozeduren sind nach der Reihenfolge ihres Auftretens geordnet, wobei das Beim Öffnen-Ereignis des Berichts nur einmal ausgelöst wird. Um festzulegen, ob die Anzeige von Seitenfuss oder Seitenkopf unterbunden werden soll, verwenden Sie zwei Boolean-Variablen namens bolCancelSeitenfuss und bolCancelSeitenkopf. Beim Öffnen des Berichts wird das Ereignis Beim Öffnen ausgelöst. Die erste Seite des Berichts enthält logischerweise die erste Seite einer Rechnung. Daher wird hier auf jeden Fall der Gruppenkopf der Rechnung angezeigt; der Seitenkopf soll dann nicht erscheinen – die Variable bolCancelSeitenkopf wird auf True eingestellt. Vor dem Anzeigen des Gruppenkopfs wird dessen Beim Drucken-Ereignis ausgelöst. Hier wird die Variable bolCancelSeitenfuss prophylaktisch auf False eingestellt. Das kann sich allerdings schnell ändern, wenn die Gruppierung so wenige Datensätze enthält, dass der Gruppenkopf noch auf der gleichen Seite angezeigt wird. Das dann ausgelöste Ereignis Beim Drucken des Gruppenfußes stellt die Variable bolCancelSeitenfuss dann auf True ein. Damit steht auch fest, dass auf der nächsten Seite eine neue Rechnung
294
5
Berichte
beginnt – der Seitenkopf soll also wieder dem Gruppenkopf weichen: Dazu erhält die Variable bolCancelSeitenkopf ebenfalls den Wert True. Geht es dann an das Formatieren des Seitenfußes, prüft das Ereignis Beim Formatieren den Wert bolCancelSeitenfuss und weist diesen dem Cancel-Parameter zu. Ist dieser True, wird der Bereich nicht angezeigt. Soll der Bereich doch angezeigt werden, wird nach dem Beim Formatieren-Ereignis auch noch das Beim Drucken-Ereignis ausgelöst. In diesem stellen Sie dann direkt die Variable bolCancelSeitenkopf auf den Wert False ein, denn wenn noch nicht die letzte Seite der Rechnung erreicht ist, soll auf der Folgeseite auf jeden Fall der Seitenkopf angezeigt werden. Dim bolCancelSeitenfuss As Boolean Dim bolCancelSeitenkopf As Boolean Private Sub Report_Open(Cancel As Integer) 'auf erster Seite wird auf jeden Fall der Gruppenkopf sichtbar, 'also Seitenkopf ausblenden bolCancelSeitenkopf = True End Sub Private Sub Gruppenkopf0_Print(Cancel As Integer, PrintCount As Integer) 'Seitenfuß soll erstmal nicht ausgeblendet werden... bolCancelSeitenfuss = False End Sub Private Sub Gruppenfuß1_Print(Cancel As Integer, PrintCount As Integer) '... außer, der Gruppenfuß wird angezeigt. Dann gibt es keinen Seitenfuß. bolCancelSeitenfuss = True 'Und wenn der Gruppenfuß angezeigt wird, kommt auf der nächsten Seite 'eine neue Rechnung, also soll auch der Seitenkopf nicht angezeigt 'werden. bolCancelSeitenkopf = True End Sub Private Sub Seitenfußbereich_Format(Cancel As Integer, _ FormatCount As Integer) 'Abbrechen, wenn bolCancelSeitenfuss True ist Cancel = bolCancelSeitenfuss
Rechnungserstellung mit Berichten
295
Me!txtUebertrag = Me!txtZwischensumme End Sub Private Sub Seitenfußbereich_Print(Cancel As Integer, PrintCount As Integer) 'Wenn Seitenfuß, dann auf jeden Fall Seitenkopf auf nächster Seite bolCancelSeitenkopf = False End Sub Private Sub Seitenkopfbereich_Format(Cancel As Integer, _ FormatCount As Integer) 'Abbrechen, wenn bolCancelSeitenkopf True ist Cancel = bolCancelSeitenkopf End Sub Listing 5.10: Inhalt des Klassenmoduls des Berichts rptRechnungen
Auf diese Weise zeigt der Bericht auch Rechnungen über zwei oder mehr Seiten mit Zwischensumme, Übertrag und Gesamtsumme an (siehe Abbildung 5.41).
Abbildung 5.41: Mehrseitiger Rechnungsbericht
296
5
Berichte
Rechnungen stellen ist mehr als Berichte drucken … Zum Schluss darf der Hinweis nicht fehlen, dass Sie Rechnungen unbedingt dokumentieren sollten – am besten, indem Sie Rechnungen und die enthaltenen Positionen in separaten Tabellen sichern. Das ist unumgänglich, wenn Sie die Rechnung später noch einmal ausdrucken möchten – ansonsten laufen Sie Gefahr, dass die Rechnung einen völlig anderen Betrag enthält, weil sich beispielsweise in der Zwischenzeit die Mehrwertsteuer erhöht hat.
6 VBA VBA ist als Basic-Dialekt eine strukturierte und sehr leicht lesbare sowie gut verständliche Programmiersprache. Das hat Vor- und Nachteile: Es ist sehr einfach, etwas in Basic zu programmieren – selbst Programmiereinsteiger bringen hier schnell erste Resultate zustande. Das verleitet natürlich dazu, einfach drauflos zu programmieren – man benötigt nur eine Prozedur und schreibt dort alles hinein, was die aktuelle Aufgabe erfordert. Auf diese Weise entstehen schnell endlos lange Prozeduren (so genannter Spaghetti-Code), bei denen man in den letzten Zeilen schon nicht mehr weiß, welche Variablen man zu Beginn der Prozedur deklariert hat, und in denen es vielleicht sogar noch Sprungmarken gibt, zwischen denen munter hin- und hergesprungen wird. Wer seine Aufgaben mit solchen oder ähnlichen Prozeduren löst, hat aber eines möglicherweise bereits geschafft: Er kennt sich ein wenig mit der Sprache aus. Das ist auch die Voraussetzung für dieses Kapitel: Es bietet keine grundlegende Einführung in die Sprache VBA, sondern setzt grundlegende Kenntnisse voraus. Sie sollten also schon wissen, wie eine Prozedur aufgebaut ist, wie Sie Variablen deklarieren, wie Sie diesen Variablen Werte zuweisen und wie die Konstrukte zum Verzweigen und zur Realisierung von Schleifen aussehen. Dieses Kapitel soll vielmehr dabei helfen, die Grundkenntnisse ein wenig auszubauen, und vermitteln, wie Sie Code so strukturiert aufbauen, dass dieser leicht verständlich und damit leicht zu pflegen und zu erweitern ist. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_06\ VBA.mdb.
6.1 Namenskonventionen in VBA Routinen und Variablen sind die Elemente in VBA, denen der Programmierer nach eigenem Ermessen Namen zuteilen darf. Stopp: Ganz so beliebig geht es hier doch nicht zu. Immerhin gibt es ein paar Konventionen, die bei VBA zu berücksichtigen sind – man darf bestimmte Zeichen nicht verwenden und es dürfen keine Zahlen am
298
6
VBA
Anfang eines Routinen- oder Variablennamens stehen. Um nicht alle Ausnahmen aufzuzählen: Verwenden Sie nur Zahlen, Buchstaben und den Unterstrich (_) und beginnen Sie den Variablennamen mit einem Buchstaben, dann sind Sie auf der sicheren Seite. Auch bei der Wahl der Namen sollten Sie bestimmte Regelmäßigkeiten einhalten. Das hilft Ihnen zum einen, wenn Sie mal aus dem Kopf ein paar weiter oben in der Prozedur deklarierte Variablennamen abrufen möchten, zum anderen ist es nützlich, wenn Sie aus dem Namen einer Variable oder einer Routine ableiten können, was diese enthält beziehungsweise bewirkt. Bei Variablen wäre es zudem interessant, nicht nur die Art des Inhalts, sondern auch den Variablentyp aus dem Namen ableiten zu können – spätestens hier stoßen Sie dann auf die ungarische Notation. Diese stammt von Gregory Reddick und liefert einen Vorschlag für eine Konvention zur Benennung von Variablen. Die komplette Konvention finden Sie im Internet unter der Adresse http://www.xoc.net/standards/rvbanc.asp. Nach dieser Konvention wird ein Variablenname wie folgt aufgebaut: [prefixes]tag[BaseName[Suffixes]]
Dabei legt Basename den eigentlichen Inhalt der Variablen fest und tag ein Präfix, das den Typ der Variablen festsetzt (beispielsweise str für String, obj für Objekte, frm für Formulare). Der erste Teil prefixes enthält spezielle Informationen wie m für private Variablen (member) oder g für öffentliche Variablen (global). Der Teil Suffixes enthält nähere Informationen zum Inhalt der Variablen wie Min oder Max für Extremwerte. Die ungarische Notation ist quasi Standard bei der Benennung von Variablen- und Objektnamen. Fast jeder verwendet mehr oder weniger bewusst die hier festgelegten Regeln – manch einer übernimmt vermutlich intuitiv diese Konvention aus Codebeispielen. Hier und da gibt es sicher Variationen (auch in diesem Buch), aber im Großen und Ganzen hilft diese Notation ganzen Heerscharen von Programmierern, zumindest die Variablen- und Objektnamen ihrer Kollegen zu verstehen.
6.2 Layout von Code VBA-Code muss gut lesbar und verständlich sein, damit Sie oder andere ihn leicht warten oder erweitern können. Dazu gehört nicht nur die eigentliche Struktur des Codes, sondern auch seine Darstellung oder das Layout. Man könnte hier nun einige Negativbeispiele anführen, aber dafür ist der Platz zu schade. Daher direkt einige Hinweise, die eine Grundlage für die gute Lesbarkeit des Codes liefern.
Layout von Code
299
6.2.1 Funktionalität vor Schönheit? Manch ein Programmierer mag nun denken: Eine Routine muss funktionieren und nicht schön aussehen. Tatsache ist aber: Eine Routine muss nicht nur funktionieren, sondern sie soll auch mal gelesen, geändert oder debugged werden, und wenn das dann ein anderer als der Urheber mit seinem geringen ästhetischen Empfinden machen muss, hat dieser sicher nicht gerade helle Freude daran. Tatsache ist, dass schon die Einrückungen bestimmter Teile des Codes diesen sehr viel besser lesbar machen. Allein das Einrücken des eigentlichen Inhalts von Routinen gegenüber der Routinendeklaration und der End-Zeile hilft beim schnellen Durchscrollen, das Ende der einen und den Beginn der nächsten Routine zu finden. Genauso verhält es sich mit Kontrollstrukturen wie Verzweigungen, Schleifen oder auch nur dem With-Statement.
6.2.2 Code einrücken zur Verdeutlichung der logischen Struktur Wenn Sie Code einrücken, verwenden Sie dazu am besten die Tabulator-Taste. Im Optionen-Dialog der VBA-Entwicklungsumgebung können Sie Ihrem Geschmack entsprechend festlegen, wie viele Leerzeichen die Schrittweite des Tabulators umfassen soll (Abbildung 6.1).
Abbildung 6.1: Optionen-Dialog der VBA-Entwicklungsumgebung
300
6
VBA
Typische Beispiele für die Einrückung sehen wie folgt aus: Public Sub DatenLesen() Dim db As DAO.Database Dim rst As DAO.Recordset On Error GoTo DatenLesen_Err Set db = CurrentDb Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Projekt rst.MoveNext Loop DatenLesen_Exit: On Error Resume Next rst.Close Set rst = Nothing Set db = Nothing DatenLesen_Err: MsgBox "Es ist ein Fehler aufgetreten!" Resume DatenLesen_Exit End Sub Listing 6.1: Beispiele für Einrückungen
Zunächst einmal sind alle Anweisungen außer der ersten und der letzten Zeile der Routine um einen Tabulatorschritt eingerückt. Innerhalb der Do While-Schleife erfolgt eine weitere Einrückung. Die Sprungpunkte DatenLesen_Exit und DatenLesen_Err wiederum werden automatisch an den linken Rand gesetzt, damit man diese leicht identifizieren kann. Der geübte VBA-Programmierer sieht hier auf einen Blick: Der eigentliche Kern dieser Routine befindet sich zwischen der ersten Zeile und der ersten Sprungmarke DatenLesen_Exit. Andere Beispiele für Einrückungen sind (neben vielen anderen): For Next-Schleife: For i = 1 to 10 'Inhalt der Schleife Next i
If Then…Else-Verzeigung If a=b then 'Inhalt der Verzweigung End If
Layout von Code
301
Select Case-Verzweigung Select Case str Case "a" 'Inhalt des Zweiges Case "b" 'Inhalt des Zweiges End Select
Umbrochene Zeilen MsgBox "Es ist schön, wenn man nicht scrollen muss, " _ & "um eine Zeile komplett zu lesen.", _ vbOkOnly + vbExclamation, "Sinnfreie Meldung"
Wenn Sie einmal mit dem Code anderer Entwickler oder selbst geschriebenem Code arbeiten müssen, der nicht sauber formatiert ist, sollten Sie dies nachholen – aber nicht von Hand. Es gibt praktische Tools, mit denen sich VBA-Code automatisch in eine ansprechende Form bringen lässt. Ein Beispiel ist die Freeware SmartIndenter, die Sie unter http://www.bmsltd.ie/indenter/default.htm finden. Diese formatiert nicht stur nach Schema, sondern bietet auch einige Einstellungsmöglichkeiten zum Formatieren des Quellcodes.
6.2.3 Leerzeilen für bessere Lesbarkeit Neben Einrückungen sind Leerzeilen ein sinnvolles Mittel zur Verbesserung der Lesbarkeit des Codes. Das Beispiel aus Listing 6.1 wäre beispielsweise viel leichter zu lesen, wenn Sie durch Leerzeichen für eine Gruppierung zusammenhängender Codezeilen sorgen. Dies könnte so aussehen: Public Sub DatenLesen() Dim db As DAO.Database Dim rst As DAO.Recordset On Error GoTo DatenLesen_Err Set db = CurrentDb Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Projekt rst.MoveNext Loop DatenLesen_Exit:
302
6
VBA
On Error Resume Next rst.Close Set rst = Nothing Set db = Nothing DatenLesen_Err: MsgBox "Es ist ein Fehler aufgetreten!" Resume DatenLesen_Exit End Sub Listing 6.2: Optisches Strukturieren des Codes mit Leerzeilen
Mit den hier eingefügten Leerzeilen wächst die Lesbarkeit deutlich. Die Deklarationszeilen werden mit dem Prozedurkopf zusammengefasst und einige weitere Blöcke werden ebenfalls sinnvoll gruppiert. Wichtig ist bei der Verwendung von Leerzeilen, dass Sie diese nicht einsetzen, um Zeilen voneinander zu trennen, sondern um zusammenhängende Anweisungen zu gruppieren.
6.2.4 Zeilenumbrüche Extrem lange Codezeilen sind im Quellcode eher selten, aber wenn sie dennoch auftauchen, sollten Sie diese umbrechen. So ersparen Sie dem Leser des Codes unnötiges Scrollen. Paradebeispiel für lange Zeilen sind die Deklarationen von API-Funktionen. Ob man diese nun im Detail lesen muss, ist eine andere Frage, das Umbrechen schadet jedenfalls nicht. Wie weiter oben bereits erwähnt, sollten Sie Fortsetzungen umbrochener Zeilen einrücken, um diese als solche kenntlich zu machen. In einem Fall ist sogar eine folgende Leerzeile angezeigt: Wenn der Prozedurkopf einen Zeilenumbruch erfordert, sollten Sie vor der folgenden Zeile eine Leerzeile einfügen. Die Fortsetzungszeile und die eingerückte Zeile lassen sich optisch sonst nur schwer auseinander halten. Es gibt zwei Varianten zum Umbrechen von Zeilen: innerhalb und außerhalb von Zeichenketten. Außerhalb einer Zeichenkette können Sie eine Zeile fast überall umbrechen, außer mitten in Schlüsselwörtern. Dazu fügen Sie an der Stelle des gewünschten Umbruchs ein Leerzeichen, einen Unterstrich und einen Zeilenumbruch ein – und vergessen Sie das Einrücken nicht. Die folgenden zwei Zeilen enthalten ein Beispiel für eine Zeile mit allen möglichen Umbrüchen: Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset)
Layout von Code
303
Die extrem umbrochene Variante sieht so aus – lesbar ist der Code so natürlich nicht mehr, aber er zeigt die Möglichkeiten auf: Set _ rst _ = _ db _ . _ OpenRecordset _ ( _ "tblProjekte" _ , _ dbOpenDynaset _ )
Die zweite Variante von Zeilenumbrüchen betrifft Zeichenketten innerhalb von Anführungszeichen. Diese lassen sich an mehr Stellen umbrechen, nämlich nach jedem Buchstaben. Allerdings sind die Regeln geringfügig umfangreicher. Nehmen Sie die folgende Zeile als Ausgangspunkt: MsgBox "Es ist ein Fehler aufgetreten!"
Der Zeilenumbruch erfolgt zum besseren Verständnis in zwei Schritten. Erst wird die Zeichenkette in zwei Teilzeichenketten aufgeteilt: MsgBox "Es ist ein " & "Fehler aufgetreten!"
Dann fügen Sie wie oben einfach den Umbruch ein: MsgBox "Es ist ein " _ & "Fehler aufgetreten!"
Ob Sie den Umbruch vor oder nach dem Und-Zeichen durchführen, bleibt Ihnen überlassen.
6.2.5 Anweisungen zusammenfassen Es gibt in VBA verschiedene Möglichkeiten, Anweisungen in einer Zeile zusammenzufassen. So können Sie beispielsweise die If Then-Anweisung ohne Else-Teil in eine Zeile schreiben. Ausgangspunkt ist die folgende Zeile: If a = 1 Then Debug.Print "A ist gleich 1" End If
304
6
VBA
Daraus wird diese Variante: If a = 1 Then Debug.Print "A ist gleich 1"
Und mehrere einzelne Anweisungen lassen sich durch einen Doppelpunkt getrennt in einer einzigen Zeile eingeben: rst.Close: Set rst = Nothing
Diese Varianten sparen zwar Zeilen ein, übersichtlicher und lesbarer machen sie den Code aber nicht unbedingt. Außerdem gibt es Probleme, wenn Sie beim Dokumentieren von Fehlern die Zeilennummer mit einbeziehen – Sie können dann nicht erkennen, welche Anweisung den Fehler ausgelöst hat (weitere Informationen zum Ermitteln der Zeilennummer fehlerhafter Zeilen finden Sie in Kapitel 11, Abschnitt 11.4.1, »Wichtige Fehlerinformationen«).
6.3 Kommentare Mit dem Hochkomma (') leiten Sie einen Kommentar ein. Kommentare können als komplette Zeilen oder als Anhängsel an bestehende Codezeilen eingesetzt werden. Wenn Sie Kommentare in einer Routine verwenden, beziehen sich diese meist auf eine einzelne Zeile oder eine Gruppe von Zeilen. Setzen Sie den Kommentar direkt über die betroffenen Zeilen und rücken Sie den Kommentar genauso weit wie die kommentierte Zeile ein. Auf diese Weise machen Sie die Zugehörigkeit gut kenntlich: Public Sub DatenLesen() … 'Alle Datensätze durchlaufen und Projekte ausgeben Do While Not rst.EOF Debug.Print rst!Projekt rst.MoveNext Loop … End Sub Listing 6.3: Kommentare rücken Sie am besten genauso wie die kommentierte Zeile ein.
Kommentare wie im vorherigen Beispiel benötigen Sie normalerweise nicht. Die kommentierte Do While-Schleife verwendet jeder Access-Entwickler vermutlich täglich. Setzen Sie Kommentare nur dort ein, wo diese wirklich benötigt werden – etwa wenn Sie nicht triviale Methoden oder einen Trick verwenden, um zu einem bestimmten Ergebnis zu gelangen.
Konstanten
305
Ein Beispiel für Kommentare im Anschluss an Zeilen ist der Deklarationsbereich. In vielen Fällen würde das Unterbringen aller benötigten Informationen zu lange Variablennamen erfordern – etwa wenn der Variableninhalt eine bestimmte Einheit hat: Dim sngLaenge As Single 'Länge in Meter [m]
Weitere sinnvolle Einsatzmöglichkeiten sind folgende: Hinweise auf noch zu bearbeitende Code-Bereiche: 'ToDo: …
Auskommentieren von Zeilen, etwa um eine alte Version nicht endgültig zu löschen, während man eine neue Variante ausprobiert Kommentierter Bereich im Kopf einer Routine, um deren Eingangs- und Ausgangsparameter, die enthaltene Funktionalität und weitere Informationen anzugeben Das Kommentieren und Auskommentieren von Code brauchen Sie übrigens nicht zeilenweise von Hand vorzunehmen. Die Menüleiste Bearbeiten (Menüeintrag Ansicht/ Symbolleisten/Bearbeiten) liefert zwei Schaltflächen, mit denen Sie mehrere Zeilen gleichzeitig ein- und auskommentieren können (siehe Abbildung 6.2).
Abbildung 6.2: Menüleiste mit zwei Befehlen zum Kommentieren und Entkommentieren von VBA-Code
6.4 Konstanten Konstanten bieten eine Möglichkeit, Werte, die zur Laufzeit nicht verändert werden, zentral zu speichern und durch einen aussagekräftigen Ausdruck zu ersetzen. VBA und die in Access üblicherweise verwendeten Bibliotheken enthalten Hunderte, wenn nicht Tausende Konstanten. Schauen Sie sich allein die MsgBox-Anweisung an: MsgBox "Meldungstext", vbOKCancel Or vbCritical Or vbDefaultButton1
Hinter den dort verwendeten Konstanten verbergen sich völlig harmlose Zahlenwerte. Ein gutes Einsatzgebiet für Konstanten in Ihren eigenen Anwendungen sind beispielsweise Optionsgruppen, deren Wert Sie nach der Auswahl in einer Select Case-Verzweigung auswerten (siehe Abbildung 6.3).
306
6
VBA
Abbildung 6.3: Optionsgruppe zur Anzeige von Detailinformationen
Die folgende Prozedur legt für die drei Optionen aussagekräftige Konstanten fest, die in der Routine cmdAuswaehlen_Click eingesetzt werden können. Const pizKlein = 1 Const pizMittel = 2 Const pizGross = 3 Private Sub cmdAuswaehlen_Click() Select Case Me!ogrPizzagroesse Case pizKlein MsgBox "Für den kleinen Hunger zwischendurch." Case pizMittel MsgBox "Normale Größe." Case pizGross MsgBox "Nur für Access-Entwickler und Buchautoren." End Select End Sub Listing 6.4: Einsatz benutzerdefinierter Konstanten
Ein anderes sinnvolles Einsatzgebiet von Konstanten ergibt sich, wenn Sie bestimmte Kennzahlen im Code einsetzen – das gilt erst recht, wenn diese Kennzahlen an mehr als einer Stelle vorkommen. Ein gutes Beispiel für D-Mark-Liebhaber wäre der Umrechnungsfaktor zwischen DM und Euro: Public Const EUROFAKTOR = 1.95583
Andere Beispiele, die den Code lesbarer machen, sind folgende: Public Const ANZAHL_MONATE = 12 Public Const ANZAHL_STUNDEN_PRO_TAG = 24
Variablen
307
Auch oft verwendete Zeichenketten sollten Sie in Konstanten packen. Wenn Sie beispielsweise eine Anwendung entwickeln und sich über ihren Namen noch nicht im Klaren sind, diesen aber an mehreren Stellen ausgeben wollen, legen Sie einfach eine Konstante mit dieser Information an und verwenden Sie diese an den entsprechenden Stellen statt des Variablennamens: Public Const ANWENDUNGSNAME = "Beispieldatenbank VBA"
Sie können diese Konstante dann im Begrüßungsformular, Meldungsfenster und an anderen Orten anstatt des hart codierten Anwendungsnamens zuweisen.
Konvention für Konstanten Die ungarische Notation sieht für Konstanten die gleichen Regeln vor wie für Variablen. An den obigen Beispielen haben Sie schon erkennen können, dass diese Konvention hier keine Beachtung findet. Das ist wiederum Geschmackssache, aber aufgrund der sehr unterschiedlichen Anwendungszwecke verdienen Konstanten eine wesentlich auffälligere Notation als ein zwischengeschobenes »c«, wie in der ungarischen Notation vorgeschlagen.
6.5 Variablen In diesem Abschnitt erfahren Sie, wie Sie Variablen benennen, wie Sie diese optimal einsetzen und welche speziellen Variablentypen es gibt.
6.5.1 Variablennamen Variablennamen sollten zunächst einmal den in Abschnitt 6.1, »Namenskonventionen in VBA« genannten Konventionen entsprechen. Noch nicht besprochen wurde dort der Teil zwischen Präfix und Suffix, also der eigentlich wichtigste Teil. Dieser Teil kann aus einem Wort oder mehreren Wörtern bestehen. Schreiben Sie jedes neue Wort groß, damit man es als neues Wort erkennen kann, etwa intAnzahlMitarbeiter. Gestalten Sie den Variablennamen so lang wie nötig und so kurz wie möglich, allerdings ohne mit wilden und nicht nachvollziehbaren Abkürzungen zu arbeiten. So ist sngMwStSatz statt sngMehrwertsteuersatz sicher sinnvoll, aber sngMS ist ein wenig kurz und selbst im richtigen Zusammenhang schwer zu deuten. Gleichzeitig sollten Sie eine Variable so benennen, dass man unmittelbar erkennen kann, was diese Variable für einen Wert enthält. Beispiele: strSQL datAktuellesDatum
308
6
VBA
curBetrag rstMitarbeiter Diese Variablen weisen noch einen weiteren Vorteil auf: Sie lassen sich allesamt leicht einprägen. Das ist besonders wichtig, denn Sie wollen sicher nicht bei jeder Anwendung einer Variablen zum Deklarationsbereich scrollen, um die genaue Schreibweise dieser Variablen zu ermitteln.
Zahlen in Variablennamen Wenn Sie in einer Routine Variablennamen wie strMitarbeiter1, strMitarbeiter2 oder ähnliche finden, sollten Sie das Design des Codes überprüfen. Entweder ließe sich hier besser ein Array verwenden oder es handelt sich tatsächlich um zwei verschiedene Variablen, die aber mit wesentlich aussagekräftigeren Namen ausgestattet werden sollten.
6.5.2 Spezielle Variablennamen Es haben sich einige Variablennamen eingebürgert, die der weiter oben erwähnten Konvention widersprechen. Dennoch werden sie immer wieder benutzt – eben weil sie gängig sind.
Lauf- oder Zählervariablen Das beste Beispiel ist sicher die Variable i als Zählervariable. Wer nicht mit der Konvention brechen möchte, mag vielleicht eine Variante wie intZaehler verwenden – das ist letzten Endes Geschmackssache. Ein alternativer Name für eine Laufvariable ist definitiv angezeigt, wenn dieser Wert beispielsweise anschließend weiter verwendet wird. Auch wenn es sich um Laufvariablen in verschachtelten Schleifen handelt, sollte man über eine andere Benennung als i und j nachdenken.
Temporäre Variablen Temporäre Variablen verdienen meist einen aussagekräftigeren Namen als temp oder tmp. Man könnte zumindest die Bezeichnung dessen, was sie beinhalten, vorne anfügen – dann hieße eine temporäre Variable beispielsweise rstMitarbeiterTemp.
Statusvariablen Wenn Sie eine Variable verwenden, um einen Status zu speichern, sollten Sie auch dieser einen brauchbaren Namen geben. Viele Entwickler nennen solche Variablen lieblos »Flag«. Spätestens, wenn Sie mal zwei »Flags« in einer Routine oder in einem Modul
Variablen
309
benötigen, müssen Sie sich zwei unterschiedliche Namen ausdenken – und dann tun Sie sich selbst den Gefallen und nennen diese nicht »Flag1« und »Flag2« …
6.5.3 Aufzählungstypen Aufzählungstypen sind ein probates Mittel, Konstanten für eine Variable zur Verfügung zu stellen, deren Wertebereich bekannt und begrenzt ist. Dies ist vor allem für solche Werte interessant, die in oft genutzten Routinen als Parameter zum Einsatz kommen – die möglichen Werte werden dann durch IntelliSense angezeigt (siehe Abbildung 6.4).
Abbildung 6.4: Einsatz einer Enumeration
Damit eine Routine eine solche Liste zur Verfügung stellt, verwenden Sie eine Enumeration und einen Funktionskopf wie in folgendem Listing: Public Enum ePizzagroesse ePizzagroesse_Klein = 1 ePizzagroesse_Mittel = 2 ePizzagroesse_Gross = 3 End Enum Public Function Teigmenge(intPizzagroesse As ePizzagroesse) As Integer Select Case intPizzagroesse Case ePizzagroesse_Klein Teigmenge = 500 Case ePizzagroesse_Mittel Teigmenge = 750 Case ePizzagroesse_Gross Teigmenge = 1250 End Select End Function Listing 6.5: Bereitstellen einer Enumeration als Parameter einer Routine
Die Mitglieder einer Enumeration werden, wenn Sie keine expliziten Werte angeben, mit dem Wert 1 beginnend durchnummeriert.
310
6
VBA
6.5.4 Arrays Arrays sind Datenfelder zum Speichern mehrerer Daten gleichen Datentyps. Diese Datenfelder können auch mehrdimensional ausgelegt werden. Sie deklarieren ein Datenfeld entweder direkt mit der gewünschten Anzahl möglicher Werte oder lassen diesen Parameter offen: Dim strVornamen() As String
oder Dim strVornamen(10) As String
In beiden Fällen können Sie die Anzahl später noch mit der ReDim-Anweisung ändern. Diese gibt es in zwei Ausführungen: Ohne das Schlüsselwort Preserve werden alle enthaltenen Daten gelöscht, mit diesem Schlüsselwort behält das Array die vorhandenen Daten bei, was normalerweise gewünscht sein dürfte: ReDim Preserve strVornamen(15) As String
Arrays beginnen standardmäßig mit dem Index 0. Wenn ein anderer Index gewünscht ist, wobei als einzige Alternative 1 erlaubt ist, verwenden Sie folgende Anweisung im Kopf des Moduls: Option Base1
Viele Fehler treten dadurch auf, dass auf Array-Elemente zugegriffen wird, die nicht vorhanden sind. Das können Sie – etwa in einer Schleife – mit den beiden Funktionen UBound und LBound verhindern. Diese Funktionen liefern den obersten und den untersten Index eines Arrays zurück. Folgendes Beispiel fasst die wichtigsten Funktionen von Arrays zusammen: Option Base 1 Public Sub BeispielArray() Dim strName() As String Dim intUntereGrenze As Integer Dim intObereGrenze As Integer Dim i As Integer 'Felddimensionen festlegen ReDim Preserve strName(3) strName(1) = "André" strName(2) = "Sascha"
Variablen
311 strName(3) = "Rita" 'Feld für Nachzügler erweitern ReDim Preserve strName(4) strName(4) = "Sylvia" intUntereGrenze = LBound(strName) intObereGrenze = UBound(strName) For i = intUntereGrenze To intObereGrenze Debug.Print strName(i) Next i
End Sub Listing 6.6: Beispiel für die Verwendung eines Arrays
6.5.5 Benutzerdefinierte Typen Benutzerdefinierte Typen fassen ähnlich wie Arrays Daten zusammen. Allerdings bestehen Sie aus einer festen Anzahl von Elementen, die dafür aber auch verschiedene Datentypen besitzen können. Sinnvoll ist die Verwendung solcher Typen, wenn Sie bestimmte Variablen immer gemeinsam etwa beim Aufruf anderer Routinen weitergeben. Im folgenden Beispiel werden Informationen zu einem Artikel beispielsweise zum Typ TArtikel zusammengefasst. Die Funktion TypFuellen füllt die einzelnen Elemente der Type-Variablen und gibt diese an eine andere Funktion weiter, die die enthaltenen Informationen ausliest. Public Type TArtikel ArtikelNr As Long Artikelname As String Einzelpreis As Currency End Type Public Function TypFuellen() Dim typArtikel As TArtikel With typArtikel .ArtikelNr = 1 .Artikelname = "Das Access 2003 Entwicklerbuch" .Einzelpreis = 49.9 End With TypAusgeben typArtikel End Function Public Function TypAusgeben(typArtikel As TArtikel) With typArtikel
312
6
VBA
Debug.Print .ArtikelNr, .Artikelname, Format(.Einzelpreis, "0.00 _") End With End Function Listing 6.7: Beispiel für die Verwendung benutzerdefinierter Typen
6.5.6 Alle Variablen verwenden Sorgen Sie dafür, dass alle deklarierten Variablen auch verwendet werden. Wenn Sie nicht sicher sind, etwa weil die Routine zu umfangreich ist, um dies auf einen Blick zu erkennen, können Sie wie folgt vorgehen: Kommentieren Sie alle Variablen aus und kompilieren Sie den Code so lange, bis Sie durch die entsprechenden Fehlermeldungen alle wirklich benötigten Variablen identifiziert haben.
6.5.7 Globale Variablen Globale Variablen sind unbestritten in einigen Fällen nützlich, weil man darin Daten speichern kann, die von überall zugreifbar sind. Sie bergen allerdings auch Gefahren. Oft wird ihr Inhalt unbewusst von mehreren Stellen aus geändert. Bei nicht behandelten Fehlern irgendwo im VBA-Projekt verlieren globale Objektvariablen außerdem in der Regel ihren Inhalt und haben dann den Wert Nothing. Um Risiken bei der Anwendung globaler Variablen zu vermeiden, sollten Sie diese besonders kennzeichnen – die oben beschriebene Namenskonvention schlägt vor, ein »g« voranzustellen – das scheint ein guter Vorschlag zu sein. Genau wie die Rückgabewerte von Funktionen (dazu später mehr) sollten Sie globale Variablen nicht für Zwischenergebnisse verwenden, sondern nur für das Endergebnis. Eine alternative Technik zur Verwendung globaler Variablen finden Sie in Kapitel 13, Abschnitt 13.8.1, »Auflistungen selbst gemacht«. Grundsätzlich gilt jedoch: Verwenden Sie globale Variablen nur, wenn es keine andere Möglichkeit gibt.
6.6 Kontrollstrukturen Die Kontrollstrukturen unter VBA bieten eine Menge Möglichkeiten, um unsauber, fehlerhaft oder performancehemmend eingesetzt zu werden. In den folgenden Abschnitten lernen Sie die Strukturen kennen und erfahren, wie Sie diese sinnvoll verwenden.
Kontrollstrukturen
313
6.6.1 If Then-Anweisung Die If Then-Anweisung dient dem Verzweigen in verschiedene Unterabschnitte aufgrund einer Bedingung. Die Erweiterung in Form einer If Then…Else-Anweisung erlaubt auch die Verwendung mehrerer Kriterien. Die einfachste Variante prüft nur eine Bedingung und führt in Abhängigkeit davon die zwischen If Then und End If liegenden Anweisungen aus. Befindet sich dort nur eine Anweisung, können Sie die End If-Anweisung weglassen und den If Then-Teil und die auszuführende Anweisung in eine Zeile schreiben (siehe weiter oben Abschnitt 6.2.5, »Anweisungen zusammenfassen«).
Wahrscheinliche Fälle nach oben Wenn die If Then-Anweisung nicht nur eine Bedingung, sondern mehrere und damit entsprechende Else- beziehungsweise ElseIf-Abschnitte enthält, sollten Sie den am wahrscheinlichsten eintretenden Fall nach oben setzen: If bolWahrscheinlicherFall Then 'Wahrscheinlicher Fall Else 'Weniger wahrscheinlicher Fall End If
Das Gleiche gilt für If Then-Anweisungen mit mehr als einer Else-Bedingung.
Keine leeren Zweige Oft formulieren Entwickler eine Bedingung, die so nur selten oder gar nicht eintritt. Im letzteren Fall bleibt dann die erste Verzweigung völlig leer: If bolSehrUnwahrscheinlich = True Then 'leere Verzweigung Else 'sehr wahrscheinlicher Fall End If
Wenn Sie einmal eine derartige If Then-Anweisung erstellen und erst später bemerken, dass die genannte Bedingung selten oder nie eintritt, formulieren Sie die Bedingung um oder verneinen Sie diese einfach und vertauschen Sie auch die einzelnen Zweige. If bolSehrUnwahrscheinlich = False Then 'sehr wahrscheinlicher Fall Else
314
6
VBA
'leere Verzweigung End If
oder If Not bolSehrUnwahrscheinlich Then 'sehr wahrscheinlicher Fall Else 'leere Verzweigung End If
Oder-Verknüpfungen in If- oder ElseIf-Bedingungen Im Falle von Oder-Verknüpfungen im If- oder im ElseIf-Abschnitt sollten Sie die Verwendung einer Select Case-Anweisung in Erwägung ziehen. Diese Anweisung ist wesentlich flexibler bei der Verarbeitung von Oder-Verknüpfungen.
Alle erwarteten Fälle behandeln Bevor Sie eine If Then-Abfrage programmieren, führen Sie sich alle denkbaren Fälle vor Augen. Legen Sie für all diese Fälle konkrete If/ElseIf-Zweige an. Die schlechtere Alternative wäre, nur die Fälle exakt zu erfassen, die Sie interessieren, und alles andere durch den Else-Teil abzufangen. Der Else-Teil behandelt dann zwar alle nicht definierten Fälle, aber dabei kann es sich durchaus um erwartete und nicht erwartete Ergebnisse handeln. Beispiel: Public Function Sachbearbeiter(strAnfangsbuchstabe As String) As String If strAnfangsbuchstabe Like "[a-k]" Then Sachbearbeiter = "a-k: Herr Müller" Else Sachbearbeiter = "l-z: Frau Meier" End If End Function Listing 6.8: Schlechte Auswertung mit If…Then…Else
Die Routine behandelt alle Fälle richtig, in denen ein Buchstabe als Parameter übergeben wird. Wenn Sie aber aus Versehen eine Zahl eingeben, wird auch die zweite Verzweigung ausgeführt, was ja in diesem Falle falsch ist. Besser ist folgende Variante: Public Function Sachbearbeiter(strAnfangsbuchstabe As String) As String If strAnfangsbuchstabe Like "[a-k]" Then Sachbearbeiter = "a-k: Herr Müller" ElseIf strAnfangsbuchstabe Like "[l-z]" Then
Kontrollstrukturen
315
Sachbearbeiter = "l-z: Frau Meier" Else Sachbearbeiter = "Kein Sachbearbeiter gefunden" End If End Function Listing 6.9: Zuverlässige Auswertung des Anfangsbuchstabens
Hier werden die Buchstaben von a bis z ebenfalls ordnungsgemäß zugewiesen. Zufällig falsch eingegebene Zeichen werden aber ebenso richtig mit einem entsprechenden Rückgabewert beantwortet.
6.6.2 Select Case Die Select Case-Anweisung können Sie oft verwenden, wenn If Then-Konstrukte zu kompliziert werden – entweder wenn viele If/ElseIf-Anweisungen hintereinander abgearbeitet werden oder wenn die Bedingung in einzelnen Verzweigungen beispielsweise aus vielen mit Or verknüpften Ausdrücken besteht. Voraussetzung für die Verwendung einer Select Case-Anweisung ist, dass alle Verzweigungen sich auf einen Ausdruck und dessen unterschiedliche Werte beziehen. Hier gilt das Gleiche wie für If Then-Verzweigungen: Ordnen Sie die einzelnen CaseZweige so an, dass die wahrscheinlichsten zuerst abgearbeitet werden – das kommt der Performance zu Gute. Wenn keine Bedingung eine höhere Wahrscheinlichkeit als andere erwarten lässt, verwenden Sie eine Sortierung, die gut lesbar ist – beispielsweise nach dem Alphabet. Und die für die If Then-Konstrukte beschriebene Regel, alle erwarteten Ergebnisse zu berücksichtigen und den Else-Zweig für eine Behandlung unerwarteter Ergebnisse zu verwenden, gilt auch für Select Case-Anweisungen. Hier heißt der für Ausnahmen verantwortliche Zweig allerdings Case Else: Public Function SachbearbeiterMitSelectCase(strAnfangsbuchstabe As String) _ As String Select Case strAnfangsbuchstabe Case "a" To "k" SachbearbeiterMitSelectCase = "a-k: Herr Müller" Case "l" To "z" SachbearbeiterMitSelectCase = "l-z: Frau Meier" Case Else SachbearbeiterMitSelectCase = "Kein Sachbearbeiter gefunden" End Select End Function Listing 6.10: Ausnahmefälle behandeln mit Select Case
316
6
VBA
6.6.3 For Next-Schleifen Die For Next-Schleife wird eingesetzt, wenn Sie vor dem ersten Durchlauf wissen, wie oft die Schleife durchlaufen werden soll. Beispiel: Sie möchten für jeden Monat eines Jahres irgendetwas veranlassen – in diesem Fall die Ausgabe des Monatsnamens: Public Sub MonateAusgeben() Dim intMonat As Integer For intMonat = 1 To 12 Debug.Print Format("1." & intMonat & ".2004", "mmmm") Next intMonat End Sub Listing 6.11: Durchlaufen der Monate eines Jahres per For Next-Schleife
Künstlicher Ausgang For Next-Schleifen müssen nicht grundsätzlich bis zum Ende durchlaufen werden. Mit der Exit-Anweisung können Sie diese an jeder beliebigen Stelle innerhalb der Schleife
stoppen. Sie müssen nur die gewünschte Bedingung festlegen: If bolAbbruchbedingung Then Exit For
Sie könnten eine For Next-Schleife auch beenden, indem Sie innerhalb der Schleife den Wert der Zählervariablen so verändern, dass er größer als der höchste im Schleifenkopf angegebene Wert ist – das ist allerdings kein guter Programmierstil. Verwenden Sie statt dessen die Exit-Anweisung.
Name der Zählervariablen Versuchen Sie, einen möglichst aussagekräftigen Namen für eine Zählervariable zu verwenden. In einfachen Schleifen reicht i zwar oft aus, ein richtiger Variablenname wie intMonat in diesem Beispiel (noch besser wäre intAktuellerMonat) erhöht aber auf jeden Fall die Lesbarkeit. Richtig interessant wird es, wenn Sie verschachtelte Schleifen verwenden. Vermutlich hat jeder von Ihnen schon einmal i und j bei der Verwendung verschachtelter Schleifen vertauscht.
Kontrollstrukturen
317
6.6.4 For Each-Schleifen For Each-Schleifen durchlaufen alle Elemente von Auflistungen. Auch hier ist die Anzahl der Durchläufe vor dem ersten Durchlauf bekannt. Damit können Sie beispielsweise die in den unterschiedlichen Objektmodellen enthaltenen Auflistungen durchlaufen.
Das folgende Beispiel gibt alle Formulare des aktuellen Projekts aus: Public Sub FormulareAusgeben() Dim frm As AccessObject For Each frm In CurrentProject.AllForms Debug.Print frm.Name Next frm End Sub Listing 6.12: Ausgabe von Auflistungselementen per For Each-Schleife
Damit sind For Each-Schleifen praktisch eine Vereinfachung von For Next-Schleifen. Die obige Funktionalität ließe sich auch mit einer For Next-Schleife nachbilden. Der Programmieraufwand ist aber auf jeden Fall höher: Public Sub FormulareAusgebenMitForNext() Dim intAnzahlFormulare As Integer Dim i As Integer Dim frm As AccessObject intAnzahlFormulare = CurrentProject.AllForms.Count For i = 0 To intAnzahlFormulare - 1 Debug.Print CurrentProject.AllForms(i).Name Next i End Sub Listing 6.13: Auflistung mit einer For Next-Schleife durchlaufen
6.6.5 Do While…Loop-Schleifen und Varianten Bei Do While-Schleifen wissen Sie bis zum letzten Durchlauf nicht, wie viele Durchläufe die Schleife machen wird. Ob die Schleife weiterläuft oder nicht, entscheidet allein das Abbruchkriterium. Das bekannteste Beispiel ist vermutlich folgendes:
318
6
VBA
Public Sub AlleDatensaetzeAusgeben() Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("Artikel", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Artikelname rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 6.14: Durchlaufen einer Schleife mit Abbruchkriterium
Es gibt zwei Varianten der Do While-Schleife: Do While mit Abbruchbedingung in der ersten Zeile Do While mit Abbruchbedingung in der letzten Zeile Der wesentliche Unterschied zwischen diesen beiden Varianten ist, dass Letztere mindestens einmal durchlaufen wird. Daneben gibt es noch die While Wend-Schleife, die nur in der ersten Zeile abgebrochen werden kann. Auch mit Do While-Schleifen lassen sich andere Schleifentypen nachbilden. Das folgende Beispiel gibt wiederum alle Formularnamen der aktuellen Datenbank aus. Auch hier ist der Programmieraufwand höher als bei der Lösung mit For Each – auch hier sollten Sie immer die For Each-Variante verwenden. Public Sub FormulareAusgebenMitDoWhile() Dim intAnzahlFormulare As Integer Dim i As Integer Dim frm As AccessObject intAnzahlFormulare = CurrentProject.AllForms.Count Do While i < intAnzahlFormulare Debug.Print CurrentProject.AllForms(i).Name
Kontrollstrukturen
319
i = i + 1 Loop End Sub Listing 6.15: Schleife mit fester Anzahl Durchläufe mit Do While abarbeiten
6.6.6 Exit Die Exit-Anweisung bietet nicht nur die Möglichkeit, vorzeitig aus den unterschiedlichen Schleifen auszusteigen. Sie können damit auch vorzeitig Routinen verlassen. Ein gutes Beispiel für einen solchen vorzeitigen Ausstieg sind Validierungen im BeforeUpdate-Ereignis von Formularen. Wenn eines der vorderen geprüften Steuerelemente nicht ordnungsgemäß ausgefüllt ist, nutzen Sie die Gelegenheit, die Prüfung der anderen Steuerelemente abzubrechen. Anderenfalls würde der Benutzer bei mehreren falsch ausgefüllten Feldern erstmal mehrere Meldungsfenster erhalten – das ist keinesfalls ergonomisch. Beispiel: … If IsNull(Me!txtVorname) Then MsgBox "Bitte geben Sie einen Vornamen ein." Me!txtVorname.SetFocus Exit Sub End If …
Ein anderes Beispiel ist die Fehlerbehandlung einer Routine: Auf diese soll nur gesprungen werden, wenn in der Routine ein Fehler auftritt. Ansonsten soll die Anwendung die am Ende stehenden Anweisungen zur Behandlung von Fehlern niemals erreichen – und dafür sorgt eine Exit-Anweisung. Private Sub ProzedurMitFehlerbehandlung() On Error GoTo ProzedurMitFehlerbehandlung_Err 'Prozedurinhalt ProzedurMitFehlerbehandlung_Exit: 'Restarbeiten Exit Sub
ProzedurMitFehlerbehandlung_Err: 'Fehlerbehandlung GoTo ProzedurMitFehlerbehandlung_Exit End Sub Listing 6.16: Die Exit-Anweisung für den Ausstieg vor dem Erreichen der Fehlerbehandlung
320
6
VBA
Die Exit-Anweisung erfordert immer einen zweiten Teil, der angibt, von wo der Abschied erfolgen soll – beispielsweise Exit For, Exit Sub oder Exit Funktion.
6.6.7 Die GoTo-Anweisung und Sprungmarken In der Fehlerbehandlung des obigen Beispiels haben Sie noch zwei wichtige Elemente von VBA kennen gelernt: Mit der GoTo-Anweisung springen Sie zu einer bestimmten Sprungmarke. Sprungmarken wie in der obigen Fehlerbehandlung bestehen immer aus einer Zeichenkette, die den Konventionen für die Benennung von Variablen und Routinennamen entspricht, und einem angehängten Doppelpunkt (:). Solche Zeilen werden automatisch an den linken Rand gesetzt und können so leicht identifiziert werden.
6.7 Routinen Der Grund, warum die Überarbeitung vieler Access-Anwendungen zur Neverending Story gerät, ist die fehlerhafte Verwendung von Routinen. Sie erhalten keinen aussagekräftigen Namen, haben einen schwachen Zusammenhalt, weil sie mehr Aufgaben erledigen, als der Routinenname verrät, und sind mitunter zu stark aneinander gekoppelt. Dabei sind Routinen das vermutlich wichtigste Element bei der Programmierung von Anwendungen. Ohne Routinen könnten Sie den gleichen Code immer und immer wieder an den verschiedenen Stellen einer riesigen Prozedur einfügen. Die Wartung wäre ein Lebenswerk, Änderungen mit riesigem Aufwand verbunden. Routinen bieten so viele Erleichterungen beim Schreiben von Code, dass man ein Narr wäre, würde man nicht alle ihre Vorteile nutzen. Die folgenden Abschnitte sollen einige wichtige Informationen zur Erstellung guter Routinen liefern und erläutern, welche Vorteile es bringt, einige Regeln beim Erstellen von Routinen zu beachten. Führen Sie sich zunächst einmal vor Augen, welche Vorteile Routinen überhaupt bringen: Routinen kapseln Funktionalität. Eine Routine erhält einen Namen, mit dem diese aufgerufen werden kann, und führt im Optimalfall eine Aktion aus, die mit dem Routinennamen komplett beschrieben wird. Routinen vereinfachen den Code. Wenn Sie eine große Prozedur, die verschiedene Funktionalitäten enthält, in mehrere kleine Routinen aufteilen, die nur noch von der ehemals großen Prozedur aufgerufen werden, machen Sie aus einem unverdaulichen Brocken viele kleine leicht verdauliche Häppchen.
Routinen
321
Routinen sind meist wieder verwendbar. Sie werden viele Beispiele von Funktionen kennen, die immer wiederkehrende Funktionen ausführen. Wenn Sie eigene Routinen so gestalten, dass diese genau die im Routinennamen beschriebene Funktion ausführen, werden Sie diese zu einem großen Teil früher oder später wieder verwenden können. Die Wiederverwendbarkeit erleichtert die Wartung des Codes. Änderungen, die Sie sonst an vielen Stellen durchführen müssten, können Sie nach dem Erzeugen einer Routine, die von vielen Stellen aufgerufen wird, an einer Stelle erledigen.
6.7.1 Routinenarten VBA enthält die Routinentypen Function und Sub. Der Unterschied ist, dass FunctionRoutinen einen Rückgabewert haben. Der Unterschied ist aber nicht so gewichtig, weil Sie auch die Parameter von Sub-Routinen verwenden können, um Ergebnisse der Routine zurückzuliefern.
6.7.2 Routinennamen Die Wahl des richtigen Routinennamens ist entscheidend für die Güte einer Routine. Sie dient nicht nur dazu, die richtige Routine für eine bestimmte Aufgabe ausfindig zu machen, sondern zwingt im Optimalfall den Entwickler dazu, die Routine auch nur mit der Funktionalität zu füllen, die der Routinenname vermuten lässt. Meist führt eine Routine eine Aktion auf einem Objekt aus – beispielsweise Erzeugen eines Datensatzes, Ermitteln eines Wertes, Einlesen einer Textdatei, Daten exportieren oder Ähnliches. Dementsprechend setzen sich Routinennamen meist aus einem Verb und einem Substantiv zusammen. Wenn Sie mehr als ein Verb und ein Substantiv für die Beschreibung der Funktion einer Routine benötigen, sollten Sie überlegen, ob die Routine nicht zu viele nicht zusammenhängende Aufgaben erledigt und vielleicht aufgeteilt werden sollte. AdressenEinlesenUndDrucken wäre beispielsweise ein wenig viel für eine einzige Routine. Versuchen Sie, eine feste Reihenfolge von Verb und Substantiv einzuhalten – also entweder BenutzerErmitteln oder ErmittleBenutzer, wobei die erste Variante durchweg besser klingt. Wichtig ist, dass Sie aus der Funktion einer Routine auf ihren Namen schließen können und nicht erst nachsehen müssen, wie die Routine nun heißt.
Englisch oder deutsch? Ob man Variablen- und Routinennamen in englischer oder deutscher Sprache verfasst, ist leider nicht nur Geschmackssache. Wenn die Gefahr besteht, dass Ihr Werk irgendwann einmal internationale Sphären erreichen sollte, liegen Sie mit der englischen Sprache sicher nicht verkehrt. In diesem Buch wird meist die deutsche Variante ver-
322
6
VBA
wendet – allerdings überwiegend aus Gründen der Lesbarkeit. Normalerweise sollte jeder Google-gestählte Leser zumindest mit Variablen- und Routinennamen von ein oder zwei englischen Wörtern klarkommen, aber warum die Sache unnötig verkomplizieren …
Länge von Routinennamen Hier gilt das Gleiche wie für Variablennamen: So lang wie nötig, aber so kurz wie möglich. Kombinationen aus Verb und Substantiv werden sicher nicht so lang, dass diese unlesbar sind, und Abkürzungen wie zu Zeiten, als Variablen und Routinen noch mit acht Buchstaben abgehandelt werden mussten, sollten ebenfalls vermieden werden.
Keine Zahlen! Wie bei Variablennamen sind Zahlen zur Unterscheidung von Routinen tabu. Wenn Sie mal eine Version einer Routine temporär von Routinenname in Routinenname1 umbenennen, um eine Variation einer Routine zu testen – kein Problem. Aber wer programmieren kann, sollte in der Lage sein, Routinen mit unterschiedlichen Funktionen auch unterschiedlich zu benennen.
6.7.3 Starker Zusammenhalt von Routinen Wenn es Ihnen durchweg gelingt, Routinen mit zwei Wörtern zu benennen und damit die darin enthaltene Funktionalität zu beschreiben, haben Sie vermutlich bereits das erreicht, was man als »starken Zusammenhalt« bezeichnet. Starker Zusammenhalt bezieht sich immer auf eine einzelne Routine und ist gegeben, wenn alle enthaltenen Anweisungen nur dem Zweck dienen, der im Routinennamen beschrieben wird. Starken Zusammenhalt in dieser Form zu erreichen ist ein hohes Ziel. Wenn man sich gängigen Code ansieht, stellt man oft fest, dass es bis zum starken Zusammenhalt noch ein gutes Stück Arbeit ist. In vielen Fällen wird ein starker Zusammenhalt nicht oder nur schwer zu erreichen sein. Wenn Sie sich beispielsweise die Ereignisprozeduren eines Formulars ansehen – etwa das Ereignis Beim Öffnen – erkennen Sie, dass dort mitunter zahlreiche Aktionen stattfinden, die nicht unbedingt miteinander zusammenhängen. Die Stärke des Zusammenhalts liegt hier eher darin, dass alle Aktionen zu einem bestimmten Zeitpunkt ausgeführt werden müssen.
6.7.4 Lose Kopplung zwischen Routinen Der Begriff lose Kopplung kommt eigentlich aus der objektorientierten Welt, lässt sich aber auch leicht auf Routinen anwenden. Routinen sind lose gekoppelt, wenn die aufrufende Routine keine Implementierungsdetails der aufgerufenen Routine kennen
Routinen
323
muss, um diese perfekt verwenden zu können. Die einzigen Kenntnisse sind der Routinenname und die Parameter der aufgerufenen Routine. Je weniger Parameter es gibt, desto loser die Kopplung, das heißt desto besser. Schlecht ist es, wenn die Kopplung zwischen zwei Routinen über mehr als den Aufruf und die Übergabe von Parametern und gegebenenfalls das Zurückliefern hergestellt wird. Ein Beispiel ist eine globale Variable, die von der aufrufenden Prozedur mit einem Wert gefüllt und von der aufgerufenen Prozedur ausgelesen wird. In diesem Fall reicht die Kenntnis des Routinennamens und der Parameter (zusammengefasst der Schnittstelle) der Routine nicht mehr aus – man muss sich die aufgerufene Routine zuvor ansehen, um zu erfahren, dass hier noch eine globale Variable eine Rolle spielt.
6.7.5 Parameter und Rückgabewerte einer Routine Sie können für jede VBA-Routine Parameter festlegen. Diese dienen einerseits dazu, Informationen an die aufgerufene Routine zu übergeben. Andererseits können Sie diese auch verwenden, um die Ergebnisse der Routine zurückzuliefern. Parameter werden in Pflicht- und optionale Parameter unterteilt. Optionale Parameter befinden sich immer am Ende der Parameterliste und werden mit dem Schlüsselwort Optional gekennzeichnet. Sie können Parameter einfach in der Reihenfolge angeben, in der sie in der Parameterliste aufgeführt werden, oder benannte Parameter verwenden. Dazu übergeben Sie der Routine beim Aufruf eine durch Kommata getrennte Liste von Wertepaaren nach dem Schema <Parameter>:=<Wert>. In der Routine können Sie das Vorhandensein von Werten bei optionalen Parametern mit der IsMissing-Funktion überprüfen. Optionale Parameter müssen den Datentyp Variant besitzen, damit die IsMissing-Funktion funktioniert. Ein andere Alternative, bei der der optionale Parameter auch einen anderen Datentyp haben kann, ist das Zuweisen eines Vorgabewertes mit dem Gleichheitszeichen: Sub BeispielParameter2(str1 As String, Optional lngWert As Long = -1)
In der Prozedur ist dann zu prüfen, ob der Parameter lngWert einen anderen Wert als –1 hat. Letzteres würde ihn als nicht übergeben kennzeichnen.
Namen von Parametern Für Parameter von Routinen gelten im Allgemeinen die gleichen Regeln wie für sonstige Variablen.
324
6
VBA
Parameterwerte nur mit Vorsatz ändern In manchen Fällen bieten die Parameter einer Routine die einzige Möglichkeit, Informationen an die aufrufende Routine zurückzugeben. Gut, man könnte ein Array als Funktionswert oder einen benutzerdefinierten Typen verwenden. Parameter lassen sich aber wesentlich leichter handhaben. Beispiel: Die Routine BeispielRueckgabewert deklariert die beiden Variablen strVorname und strNachname und übergibt diese einschließlich der Personalnummer der gesuchten Person an die Funktion NameErmitteln. Diese liest den entsprechenden Datensatz ein und füllt die Parameter strVorname und strNachname mit den Daten aus der Tabelle. Diese lassen sich in der aufrufenden Routine anschließend ganz normal weiterverwenden. Public Sub BeispielRueckgabeparameter() Dim strVorname As String Dim strNachname As String NameErmitteln 1, strVorname, strNachname Debug.Print strVorname Debug.Print strNachname End Sub Public Function NameErmitteln(lngPersonalID As Long, strVorname As String, _ strNachname As String) Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("SELECT Vorname, Nachname FROM Personal " _ & "WHERE [Personal-Nr] = " & lngPersonalID) strVorname = rst!Vorname strNachname = rst!Nachname rst.Close Set rst = Nothing Set db = Nothing End Function Listing 6.17: Parameter als Rückgabewert
Dies funktioniert, weil unter VBA alle Parameter standardmäßig als Referenztyp übergeben werden. Das bedeutet, dass nicht der eigentliche Wert, sondern eine Referenz
Routinen
325
auf die Adresse, an der der Wert gespeichert ist, übergeben wird. Ändert die aufgerufene Routine diesen Wert, findet die aufrufende Prozedur in der verwendeten Variablen den geänderten Wert vor. Wenn Sie den Wert selbst statt der Referenz übergeben möchten, verwenden Sie das Schlüsselwort ByVal. Wenn Sie die genannte Technik für die Rückgabe von Ergebnissen verwenden, ordnen Sie die Parameter so an, dass die Rückgabeparameter am Ende der Parameterliste stehen.
Parameterwerte nicht ohne Vorsatz ändern Umgekehrt gilt: Wenn Sie nicht jede Variable explizit als Werttyp deklarieren möchten, dürfen Sie Parameter nicht als Variablen verwenden und diese in der aufgerufenen Routine ändern. Daher weisen Sie Parameter zunächst prozedurinternen Variablen zu und verwenden diese für die weiteren Aktionen.
Rückgabewerte erst am Ende zuweisen Viele VBA-Entwickler neigen dazu, den Rückgabewert einer Routine, also die Variable mit dem Namen der Routine, innerhalb der Routine als Variable zu verwenden und ihren Wert dort nach Belieben zu ändern. Das kann zu Problemen führen, wenn die Routine etwa einmal vorzeitig beendet wird – der Rückgabewert liefert dann unter Umständen ein falsches Ergebnis. Besser ist es, wenn Sie den Rückgabewert einer Routine erst kurz vor dem Ende der Routine zuweisen. Tritt dann vorher ein Fehler auf, lässt sich dieser durch den leeren Rückgabewert leicht identifizieren.
Beliebige Anzahl Parameter übergeben Mit dem Schlüsselwort Optional wird die Übergabe von Parametern an eine Prozedur zwar variabler, weil nicht alle Parameter mit einem Wert gefüllt sein müssen. Die Anzahl Parameter ist dennoch festgelegt. Mit dem Schlüsselwort ParamArray ist es jedoch möglich, einer Prozedur eine beliebige nicht festgelegte Zahl Parameter zu übergeben. Dabei werden die einzelnen übergebenen Parameter in einem VariantArray gespeichert, das in der Prozedur ausgelesen werden kann: Public Sub BeispielParameterArray(str1 As String, ParamArray arrParamMulti()) Dim i As Long Debug.Print "Pflichtparameter: " & str1 For i = 0 To UBound(arrParamMulti) Debug.Print "Optionaler Parameter " & i & ": " & arrParamMulti(i) Next i End Sub
326
6
VBA
Ein Beispielaufruf dieser Prozedur sieht so aus: BeispielParameterArray "Minhorst", "Alter", 34, "Gewicht", 86.5
6.7.6 Gleichzeitige Rückgabe von Statuswert und Ergebnissen In vielen Fällen dürfte eine Funktion für die Ermittlung der gewünschten Information reichen. Sobald Sie aber neben einem Ergebnis auch noch eine Statusmeldung erwarten, sollten Sie die oben genannte Vorgehensweise zur Rückgabe von Ergebnissen per Parameter mit der Rückgabe eines Funktionswerts verknüpfen. Der Rückgabewert der Funktion gibt dann den Statuswert zurück und die Parameter sind für das Ergebnis zuständig. Eine Erweiterung der Routinen aus Listing 6.17 zeigt, wie dies funktioniert: Public Function NameErmittelnMitStatus(lngPersonalID As Long, _ strVorname As String, strNachname As String) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("SELECT Vorname, Nachname FROM Personal " _ & "WHERE [Personal-Nr] = " & lngPersonalID) If Not rst.EOF Then strVorname = rst!Vorname strNachname = rst!Nachname NameErmittelnMitStatus = True Else NameErmittelnMitStatus = False End If rst.Close Set rst = Nothing Set db = Nothing End Function
Public Sub BeispielRueckgabeparameterMitStatus() Dim strVorname As String Dim strNachname As String If NameErmittelnMitStatus(1, strVorname, strNachname) = True Then Debug.Print strVorname Debug.Print strNachname Else
Zugriff auf andere Bibliotheken und Objekte
327
Debug.Print "Der Mitarbeiter konnte nicht eingelesen werden." End If End Sub Listing 6.18: Gleichzeitige Übergabe von Statuswert und Funktionsergebnis
6.7.7 Alle Routinen verwenden Um Karteileichen zu vermeiden, sollten Sie spätestens vor der Abnahme eines Projekts dafür sorgen, dass alle nicht benötigten Routinen aus den Modulen entfernt werden. Ob eine Routine noch verwendet wird, erfahren Sie ohne Hilfsmittel, wenn Sie im kompletten Projekt nach Aufrufen mit dem Namen dieser Routine suchen. Mit den im Anhang vorgestellten MZTools können Sie per Kontextmenüeintrag für die aktuell markierte Routine alle Aufrufe anzeigen lassen.
6.8 Zugriff auf andere Bibliotheken und Objekte Der Fokus dieses Buchs soll sich allein auf Access richten. Es gibt zwar eine Menge Themen im Umfeld von Access, die in Zusammenhang mit der Entwicklung von Access-Anwendungen interessant sind, aber der Umfang dieses Buchs ist begrenzt und eine zu weite Themenstreuung würde dazu führen, dass als wichtig erachtete Themen nicht mehr in der notwendigen Tiefe bearbeitet werden können. Ein spezielles Thema soll jedoch zumindest angerissen werden: Der Zugriff auf die anderen Office-Anwendungen und sonstige Objekte. Zwar finden Sie nachfolgend keine detaillierten Beispiele dafür, wie man Daten aus Excel-Tabellen einliest und zurückschreibt, automatisiert Serienbriefe mit Word erstellt oder die kompletten Termin-, Mail- und Adressdaten aus Outlook in die Datenbank zieht, aber zumindest den Einstieg dazu sollen Sie hier erhalten.
6.8.1 Type Libraries: Zugriff per Bibliothek Jede Office-Anwendung und auch Office selbst halten eine oder mehrere Type Libraries (Schnittstellenbibliotheken) bereit, über die Sie auf die Objekte, Methoden, Eigenschaften und Ereignisse der jeweiligen Anwendung zugreifen können. Wenn Sie per VBA eine der anderen Office-Anwendungen komfortabel fernsteuern möchten, müssen Sie zunächst die passende Objektbibliothek einbinden. Dies erledigen Sie bequem über den Dialog Verweise der VBA-Entwicklungsumgebung (Menüeintrag Extras/Verweise, siehe Abbildung 6.5).
328
6
VBA
Abbildung 6.5: Der Verweise-Dialog mit (fast) allem, was Office zu bieten hat
Die Einbindung einer Bibliothek per Verweis ist nicht zwingend notwendig. Sie können auch mit dem so genannten Late Binding arbeiten – die Objektbibliothek wird dann erst bei der Instanzierung des jeweiligen Objekts aus diesem selbst ausgelesen – ein Objekt führt immer automatisch eine Type Library mit sich. Wenn Sie den Verweis im Vorhinein erstellen, heißt dies Early Binding – die Objektbibliothek wird dann bereits beim Öffnen der Anwendung eingelesen. Der Vorteil von Early Binding ist, dass Sie über den Objektkatalog auf die entsprechenden Objekte zugreifen können und dass IntelliSense die Programmierung dieser Objekte unterstützt. Außerdem ist Early Binding erheblich schneller, weil das Auslesen der Type Library aus dem Objekt zur Laufzeit entfällt.
6.8.2 Der Objektkatalog Jeder Entwickler ist froh, wenn er die Objektmodelle, mit denen er täglich arbeitet, halbwegs kennt. Wenn er sich dann mit anderen Objektbibliotheken wie der von Word, Excel oder Outlook auseinander setzen soll, kann er sich darüber freuen oder nicht – der Objektkatalog wird auf jeden Fall eine gute Hilfe sein. Der Objektkatalog bietet die Möglichkeit, sich die Hierarchie der Objekte eines Objektmodells anzusehen, nach Objekten, Methoden und Eigenschaften zu suchen oder einfach die Konstanten für den Parameter einer bestimmten Methode zu durchforsten (siehe Abbildung 6.6).
Zugriff auf andere Bibliotheken und Objekte
329
Das Kombinationsfeld oben links dient der Auswahl einer oder aller aktuell im Projekt verknüpften Objektmodelle. Das Feld darunter kann zur Suche verwendet werden, unter Suchergebnisse finden Sie … ach, raten Sie einfach. Wenn Sie im Listenfeld Klassen einen Eintrag auswählen, zeigt das daneben befindliche Feld alle Objekte, Methoden, Eigenschaften und Ereignisse des jeweiligen Objekts an. Das Kontextmenü der beiden Listenfelder offenbart weitere Möglichkeiten, wobei für Einsteiger in eines der Objektmodelle vor allem die Online-Hilfe interessant sein dürfte.
Abbildung 6.6: Der Objektkatalog der VBA-Entwicklungsumgebung
6.8.3 Zugriff per Early Binding Das Instanzieren einer Instanz einer Office-Anwendung per Early Binding erfolgt mit zwei Anweisungen: der Deklaration der Objektvariablen und der eigentlichen Instanzierung: Public Sub OutlookVerwenden() 'Outlook-Objekt deklarieren Dim objOutlook As Outlook.Application
330
6
VBA
'Outlook-Objekt instanzieren Set objOutlook = New Outlook.Application 'etwas mit Outlook machen With objOutlook Debug.Print .Name .Quit End With Set objOutlook = Nothing End Sub Listing 6.19: Instanzieren und Verwenden von Outlook mit Early Binding
6.8.4 Zugriff per Late Binding Mit Late Binding ist das Ganze ein wenig komplizierter. Die folgende Prozedur versucht zunächst, eine bestehende Instanz von Outlook zu referenzieren und erzeugt erst nach dem Scheitern dieses Vorhabens eine neue Instanz. Sollte auch diese scheitern, ist das ein Indiz dafür, dass Outlook auf dem Rechner nicht installiert ist. Public Sub OutlookVerwendenLateBinding() 'Outlook-Objekt deklarieren Dim objOutlook As Object On Error Resume Next 'Versuch, auf bestehende Instanz zu verweisen Set objOutlook = GetObject(, "Outlook.Application") 'Fehler, falls keine Instanz vorhanden If Err.Number = 429 Then 'Err-Objekt zurücksetzen Err.Clear 'Neue Instanz erstellen Set objOutlook = CreateObject("Outlook.Application") 'Wenn kein Outlook vorhanden If Err.Number = 429 Then MsgBox "Outlook ist auf diesem Rechner nicht installiert", _ vbCritical End If End If
Zugriff auf andere Bibliotheken und Objekte
331
On Error GoTo 0 'etwas mit Outlook machen With objOutlook Debug.Print .Name .Quit End With Set ObjOutlook = Nothing End Sub Listing 6.20: Instanzieren und Verwenden von Outlook mit Late Binding
Die obige Prozedur schert sich nicht darum, ob schon eine Outlook-Instanz geöffnet war oder nicht – die Quit-Anweisung schließt die vorhandene Instanz einfach. Bei Word oder Excel sieht das anders aus: Wenn Sie die obige Routine für den Zugriff auf eine dieser Anwendungen umschreiben, werden Sie auch bei geöffneter Instanz von Word oder Excel feststellen, dass diese nicht durch die Routine geschlossen werden. Der Unterschied zwischen Outlook auf der einen und Word und Excel auf der anderen Seite ist, dass Outlook eine Multi-Use-Instanz ist und es sich bei den anderen beiden um Single-Use-Instanzen handelt. Von einer Multi-Use-Instanz gibt es nur jeweils eine Instanz, auf die verschiedene Anwendungen zugreifen können, während bei SingleUse-Instanzen jede Instanz tatsächlich neu erzeugt wird. Wichtig ist dieses Wissen dann, wenn Sie eine Anwendung weitergeben, die ein Objekt einer Multi-Use-Instanz verwendet. Sie müssen dann Maßnahmen treffen, die verhindern, dass Ihre Anwendung eine eventuell geöffnete Outlook-Instanz nach der Benutzung abschießt, obwohl der Benutzer vielleicht gerne noch die eine oder andere E-Mail empfangen hätte.
6.8.5 Weitere Informationen zur Verwendung der Office-Anwendungen per VBA Hier endet der kleine Exkurs in die Welt der Office-Fernsteuerung per VBA – fast. Sie finden noch ein kleines Beispiel für den Zugriff auf Outlook-Objekte – hier auf den E-Mail-Ordner – und einen Hinweis für die Fernsteuerung von Word und Excel.
Zugriff auf Outlook-Mails Wenn Sie auf Outlook zugreifen möchten, hier noch ein kleines Beispiel für den prinzipiellen Zugriff mit einem interessanten Tool, mit dem Sie die Sicherheitssperre beim Ausspähen der E-Mail-Adressen von Outlook umgehen können.
332
6
VBA
Die folgende Routine soll beispielsweise alle Vor- und Nachnamen sowie die E-MailAdressen aller Kontakte in einem noch auszuwählenden Ordner von Outlook ausgeben (die Routine enthält keine Behandlung von Fehlern, die etwa durch das Auswählen eines Nicht-Kontakte-Ordners entstehen). Public Sub OutlookadressenLesen() Dim Dim Dim Dim
objOutlook As Outlook.Application objNamespace As Outlook.Namespace objFolder As Outlook.MAPIFolder objKontakt As Outlook.Object
On Error Resume Next Set objOutlook = GetObject(, "Outlook.Application") If Err.Number = 429 Then On Error Resume Next Set objOutlook = CreateObject("Outlook.Application") If Err.Number = 429 Then MsgBox "Kein Outlook vorhanden." Exit Sub End If End If On Error GoTo 0 Set objNamespace = objOutlook.GetNamespace("MAPI") Set objFolder = objNamespace.PickFolder For Each objKontakt In objFolder.Items Debug.Print objKontakt.FirstName & " " & objKontakt.LastName, _ objKontakt.Email1Address Next objKontakt Set objFolder = Nothing Set objNamespace = Nothing Set objOutlook = Nothing End Sub Listing 6.21: Ausgeben von Namen und E-Mail-Adressen aller Kontakte in einem Outlook-Ordner
Die Routine tut auch ihren Dienst, allerdings erscheint bei jedem Aufruf die Meldung aus Abbildung 6.7. Diese Meldung lässt sich mit herkömmlichen Mitteln kaum umgehen. Es gibt jedoch ein Freeware-Tool, das neben der Registrierung einer .dll-Datei und wenigen Zeilen Zusatzcode keinen weiteren Aufwand erfordert und auf elegante Weise verhindert, dass die Sicherheitsmeldung erscheint.
Zugriff auf andere Bibliotheken und Objekte
333
Abbildung 6.7: Sicherheitsmeldung beim Zugriff auf die Adressdaten von Outlook
Sie finden das Tool unter folgender Internetadresse: http://www.moss-soft.de/ public/olconnector/. Wenn Sie automatisiert ohne die lästige Fehlermeldung auf die Kontakte in Outlook zugreifen möchten, fügen Sie nach der Installation des OLConnectors die fett gedruckten Zeilen in obigen Code ein. Das Tool profitiert davon, dass das Sicherheitssystem, das für die Anzeige der Meldung verantwortlich ist, nicht ausgelöst wird, wenn das Outlook-Objekt intern instanziert wird. Das Tool wird beim Öffnen von Outlook als Add-In geladen und arbeitet damit im Prozessraum von Outlook. Damit kann es auf alle Objekte zugreifen, ohne Sicherheitsmeldungen auszulösen. Und hier kommt der Clou: Das Add-In liegt nicht nur im Prozessraum von Outlook, sondern kann auch von außen instanziert werden und ein innerhalb von Outlook instanziertes Outlook.Application-Objekt nach außen »durchschleifen«. Public Sub OutlookadressenLesenOhneMeldung() Dim Dim Dim Dim
objOutlook As Outlook.Application objNamespace As Outlook.Namespace objFolder As Outlook.MAPIFolder objKontakt As Object
Dim objConnect As Object
On Error Resume Next Set objOutlook = GetObject(, "Outlook.Application") If Err.Number = 429 Then On Error Resume Next Set objOutlook = CreateObject("Outlook.Application") If Err.Number = 429 Then MsgBox "Kein Outlook vorhanden." Exit Sub End If End If On Error GoTo 0 'Versuch, die OLConnector-Instanz zu erhalten '(Outlook ist an dieser Stelle bereits offen!)
334
6
VBA
Set objConnect = GetObject(, "OLConnector.Connect")
'Falls fehlgeschlagen: Ausstieg mit angepasster Fehlermeldung If objConnect Is Nothing Then Set objOutlook = Nothing MsgBox "Konnte keine Verbindung zum OutlookConnector" & "herstellen." & vbCrLf & "Ist das COM-Add-In" & "OLConnector registriert und " & "wird es von Outlook geladen?", vbExclamation Exit Sub End If
'Hier wird das Outlook.Application-Objekt aus der Eigenschaft 'olApplication des Connectors ermittelt und kann für die 'weiteren Automationsschritte verwendet werden. 'Durch diese Zuweisung wird der direkte Zugriff auf das 'Outlook.Application-Objekt umgangen, wodurch die Sicherheitsmeldung 'ausbleibt. Set objOutlook = objConnect.olApplication
Set objNamespace = objOutlook.GetNamespace("MAPI") Set objFolder = objNamespace.PickFolder For Each objKontakt In objFolder.Items Debug.Print objKontakt.FirstName & " " & objKontakt.LastName, _ objKontakt.Email1Address Next objKontakt Set objFolder = Nothing Set objNamespace = Nothing Set objOutlook = Nothing End Sub Listing 6.22: Zugriff auf Outlook ohne Sicherheitsmeldung
Zugriff auf Word und Excel Ein Spruch unter Access-Entwicklern lautet: »Wenn Du Access-Anwendungen nicht mit VBA programmieren kannst, dann wirst Du halt VBA-Programmierer für Word oder Excel.« Das ist natürlich nicht böse gemeint, spielt aber darauf an, dass man mit der praktischen Funktion zum Aufzeichnen von Makros unter Word und Excel in vielen Fällen nur noch ein wenig Anpassungsarbeit leisten muss, um Vorgänge in Word und Excel zu automatisieren. Daher endet dieses Kapitel ohne ein praktisches Beispiel für die Fernsteuerung von Word oder Excel – die Wahrscheinlichkeit, dass es genau der Codeschnippsel gewesen wäre, den Sie suchen, ist ohnehin gering. Mit der Analyse des vom Makro-Rekorder erstellten Codes kommen Sie vermutlich schon relativ weit und beim Rest helfen Intuition und Google weiter.
7 Access-SQL SQL (Structured Query Language, strukturierte Abfragesprache) ist eine weitgehend standardisierte Abfragesprache für relationale Datenbanken. SQL dient der Auswahl von Daten aus den Tabellen einer relationalen Datenbank und ihrer Manipulation. Der Sprachumfang von Access-SQL ist in zwei Bereiche unterteilt: Die Data Manipulation Language (DML) liefert die Befehle zum Auswählen und Manipulieren von Daten, die Data Definition Language (DDL) die Anweisungen zum Erstellen, Bearbeiten und Löschen des Datenmodells selbst. Die Befehle der DML finden Sie in den Abschnitten 7.2, »Daten auswählen«, und 7.3, »Daten manipulieren«, und die der DDL in Abschnitt 7.4, »Datenmodell erstellen und manipulieren«. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_07/AccessSQL.mdb.
SQL-Versionen Access-SQL bietet eine Mischung aus den Standards SQL-89, SQL-92 und einigen Access-spezifischen Erweiterungen – beispielsweise lassen sich unter Access VBAAusdrücke in SQL-Anweisungen integrieren. Damit ist etwa die Verwendung von Standardfunktionen oder Bezügen auf Formulare und Steuerelemente möglich. Die Bestandteile aus SQL-92 werden nur an bestimmten Stellen unterstützt. In den folgenden Abschnitten finden Sie hauptsächlich die Möglichkeiten von Access-SQL, die sowohl in der SQL- und Abfrage-Entwurfsansicht als auch unter DAO und ADO ohne Einschränkung verwendet werden können. Soweit die Erweiterungen die hier beschriebenen Funktionen von SQL betreffen, finden Sie ihre Erläuterung und einen entsprechenden Hinweis auf die Version. Um die erst unter SQL-92 enthaltenen Funktionen einsetzen zu können, müssen Sie eine Einstellung in den Optionen der Datenbank vornehmen (siehe Abbildung 7.1). Diese Änderung hat weit reichende Folgen: Unter anderem ändert sich die Syntax in einigen Punkten – zum Beispiel werden die unter Access verwendeten Platzhalter
336
7
Access-SQL
Sternchen (*) und Fragezeichen (?) nicht mehr unterstützt, sondern das Prozentzeichen (%) und der Unterstrich (_) verwendet.
Abbildung 7.1: Aktivieren der SQL-92-Erweiterungen
Sie können die Erweiterungen auch ohne Ändern dieser Einstellung verwenden, allerdings nicht an allen Stellen. Das Einsatzgebiet beschränkt sich auf die Anwendung in VBA-Code und dort auf die ADO-Objektbibliothek. Sie finden an entsprechender Stelle eine Beispielprozedur für den Aufruf von SQL-Anweisungen mit SQL-92-spezifischen Sprachelementen.
7.1 SQL und Access SQL wird in Access an den verschiedensten Stellen eingesetzt – teilweise völlig unbemerkt. Wer mit einem Grundlagenbuch in Access einsteigt, wird unter Umständen erst in den hinteren Kapiteln – wenn überhaupt – mit SQL in Berührung kommen. Und wenn man es darauf anlegt, kann man sich eine ganze Weile um die erste selbst geschriebene SQL-Anweisung herumdrücken. Schließlich bietet Access mit der Abfrage-Entwurfsansicht ein ausgezeichnetes Hilfsmittel für die Erstellung von Abfragen (siehe Abbildung 7.2).
SQL und Access
337
Abbildung 7.2: Die Entwurfsansicht zur Erstellung von Abfragen
Dieses Werkzeug ermöglicht es, die in einer Abfrage anzuzeigenden Tabellen und Felder auszuwählen, Kriterien direkt oder als Parameter anzugeben, Sortierungen und Gruppierungen festzulegen und Funktionen zur Summierung, Mittelwertbildung, Ermittlung von Extremwerten oder Anzahlen anzuwenden. Wer bis dato noch keinen Kontakt mit SQL hatte und sich darum vielleicht auch keine größeren Gedanken gemacht hat, ist möglicherweise überrascht, dass alles, was man in der Abfrage-Entwurfsansicht anstellt, in die Erstellung einer SQL-Anweisung mündet. Beweis gefällig? Dann wählen Sie doch einfach aus dem Kontextmenü des Abfrageentwurfs den Eintrag SQL-Ansicht aus – das nun erscheinende Fenster offenbart das wahre Aussehen der Abfrage (siehe Abbildung 7.3). Die Schriftgröße dieses Fensters lässt sich übrigens nicht ändern.
Abbildung 7.3: Abfrage in der SQL-Ansicht
338
7
Access-SQL
Wozu trotz Abfrage-Entwurfsansicht SQL lernen? Warum sollten Sie sich eigentlich mit SQL beschäftigen, obwohl die Entwurfsansicht für Abfragen so eine Erleichterung beim Erstellen von Abfragen ist? Dafür gibt es mehrere Gründe: Nicht jede Abfrage lässt sich in der Entwurfsansicht darstellen, darunter UNIONAbfragen, Datendefinitions-Abfragen und PassThrough-Abfragen. Die Erstellung einer Abfrage per SQL-Code geht manchmal einfach schneller. Die Eingabe der folgenden Zeile im Direktfenster bekommt manch einer fixer hin, als die entsprechende Tabelle zu öffnen, alle Datensätze zu markieren, den Menüeintrag Bearbeiten/Löschen auszuwählen und die Tabelle wieder zu schließen: CurrentDB.Execute "DELETE FROM tblKunden"
Gelegentlich ist es aus Performancegründen sinnvoll, eine SQL-Abfrage im VBACode einzuarbeiten und von dort auszuführen – beispielsweise, wenn sich die in den betroffenen Tabellen enthaltenen Daten oft ändern und der Vorteil einer komprimierten und optimierten gespeicherten Abfrage nicht mehr vorhanden ist (siehe Kapitel 12, Abschnitt 12.2.3, »Gespeicherte Abfragen versus Ad Hoc-Abfragen«). Manchmal ist es auch unabwendbar, eine SQL-Anweisung im VBA-Code zusammenzusetzen – beispielsweise, weil die Abfrage das Ergebnis einer Suche liefern soll, deren Kriterien die Anzahl der beteiligten Tabellen variabel machen.
Wo lässt sich SQL überall einsetzen? Von Hand erstellte SQL-Ausdrücke lassen sich praktisch an allen Stellen einsetzen, an denen auch Tabellen oder Abfragen als Datenquelle angegeben werden können. Aber auch »normale« Abfragen lassen sich statt über die Entwurfsansicht über die SQLAnsicht eingeben und können anschließend wie gewohnt über die Entwurfsansicht angepasst werden. Diese Vorgehensweise bietet sich an, wenn Sie im VBA-Code eine SQL-Abfrage zusammensetzen, die ihren Dienst verweigert. Sie können sich dann die entsprechende Zeichenkette im Direktfenster ausgeben lassen, den SQL-Ausdruck in die SQL-Ansicht einer Abfrage eingeben und dann nach Fehlern suchen. SQL-Ausdrücke lassen sich an folgenden Stellen verwenden: SQL-Ansicht von Abfragen Datenherkunft von Formularen Datenherkunft von Berichten Datensatzherkunft von Kombinationsfeldern Datensatzherkunft von Listenfeldern
Daten auswählen
339
Datensatzherkunft von Nachschlagefeldern in Tabellen Makro AusführenSQL RunSQL-Anweisung des DoCmd-Objekts in VBA Execute-Anweisung des Database-Objekts (ADO) in VBA Execute-Anweisung des Connection-Objekts (ADO) in VBA
7.2 Daten auswählen SQL-Abfragen dienen der Auswahl von Daten aus einer oder mehreren Tabellen. Dabei können Sie sowohl die Anzahl auszugebender Felder als auch der auszugebenden Datensätze einschränken. Die einfachste Form einer Auswahlabfrage liefert den kompletten Inhalt einer Tabelle wie in folgendem Beispiel: SELECT * FROM tblKunden
Das Ergebnis dieser Abfrage entspricht genau dem Bild, das Sie auch beim Öffnen einer Tabelle erhalten. Es enthält alle Felder und alle Datensätze des Originals. Um die Datenblatt-Ansicht dieser und der folgenden SQL-Beispiele anzusehen, gehen Sie folgendermaßen vor: 1. Klicken Sie auf der Registerseite Abfragen des Datenbankfensters auf den Eintrag Neu. 2. Übernehmen Sie die Auswahl Entwurfsansicht. 3. Schließen Sie den Dialog Tabelle anzeigen, ohne eine Tabelle auszuwählen. 4. Wählen Sie aus der Symbolleiste den nun angezeigten Eintrag SQL aus (siehe Abbildung 7.4). 5. Geben Sie im nächsten Fenster den SQL-Ausdruck ein und wählen Sie anschließend statt der SQL-Ansicht die Datenblatt-Ansicht aus. Wie Sie bereits gesehen haben, besteht die einfachste Form einer SQL-Abfrage aus mindestens zwei Teilen: Die SELECT-Klausel leitet den ersten Teil ein und enthält die Auflistung der auszuwählenden Felder. Die FROM-Klausel legt fest, aus welcher beziehungsweise welchen Tabellen die Daten stammen. Die weiteren Teile einer Abfrage dienen dem Einschränken (WHERE-Klausel) und Sortieren des Ergebnisses (ORDER BY-Klausel). Diese beiden Klauseln sind optional.
340
7
Access-SQL
Abbildung 7.4: Anzeigen der SQL-Ansicht einer Abfrage
7.2.1 Festlegen der anzuzeigenden Felder Der erste Teil einer Abfrage, der durch die SELECT-Klausel eingeleitet wird, legt fest, welche Felder die Ergebnismenge enthält. Die Felder werden im Anschluss an das SELECT-Schlüsselwort aufgelistet und durch Kommata voneinander getrennt. Unter Umständen schiebt man noch einige Elemente zwischen SELECT und Feldliste – dazu später mehr. Die folgende Abfrage gibt die drei Felder KundeID und Firma der Tabelle tblKunden aus: SELECT KundeID, Firma FROM tblKunden;
Eine etwas ausführlichere Variante würde lauten: SELECT tblKunden.KundeID, tblKunden.Firma FROM tblKunden;
Die Angabe des Tabellennamens vor dem jeweiligen Feld ist sinnvoll, wenn die Abfrage ihre Daten aus mehreren Tabellen bezieht, und Pflicht, wenn sich in den zugrunde liegenden Tabellen mehrere Felder gleichen Namens befinden. Wenn Sie einen SQL-Ausdruck ohne Angabe der Tabelle zusammen mit den Feldnamen in der SQL-Ansicht einer Abfrage eingeben, anschließend zur Entwurfsansicht wechseln und von dort aus die Abfrage speichern, enthält die SQL-Ansicht beim nächsten Anzeigen jeweils den zu den Feldern gehörenden Tabellennamen. Wenn Sie andersherum die Entwurfsansicht zum Erstellen der Abfrage verwenden, enthält die SQL-Ansicht immer die vollständigen Feldnamen – also mit Tabellennamen.
Alle Felder einer Tabelle ausgeben Um die Inhalte aller Felder einer Tabelle zu ermitteln, geben Sie entweder alle Felder explizit an oder verwenden das Sternchen (*) anstelle der Auflistung der Felder: SELECT * FROM tblKunden;
Daten auswählen
341
oder ausführlicher: SELECT tblKunden.* FROM tblKunden;
Sonderzeichen in Feldnamen Vom Gebrauch von Sonderzeichen in Feldnamen ist grundsätzlich abzuraten. Manchmal lässt sich das aber nicht verhindern, weil man etwa eine bestehende Datenbank weiterentwickelt, bei der das Kind schon in den Brunnen gefallen ist und eine nachträgliche Änderung zu aufwändig wäre. In diesem Fall fassen Sie den Feldnamen in eckige Klammern ein: SELECT [Kunde-ID], Firma FROM tblKunden;
Auch hier die ausführliche Variante mit Tabellennamen: SELECT tblKunden.[Kunde-ID], tblKunden.Firma FROM tblKunden;
Feldnamen ersetzen Wenn Sie für einen bestimmten Zweck einen anderen Feldnamen in der Ausgabe der Abfrage wünschen, verwenden Sie das AS-Schlüsselwort, um einem Feld einen alternativen Namen zuzuweisen: SELECT [Kunde-ID] AS KundeID, Firma AS Kunde FROM tblKunden;
Wie Sie sehen, lassen sich die unbequemen Feldnamen mit Sonderzeichen auf diese Weise zumindest für den weiteren Gebrauch auf Basis des SQL-Ausdrucks umbenennen. Auch für Felder, die Sie aus anderen Feldern berechnen oder zusammensetzen, verwenden Sie das AS-Schlüsselwort: SELECT MitarbeiterID, Nachname & ", " & Vorname AS Mitarbeiter FROM tblMitarbeiter;
Das Ergebnis dieser Abfrage sieht wie in Abbildung 7.5 aus.
7.2.2 Festlegen der enthaltenen Tabellen Neben den Feldern, die ein SQL-Ausdruck enthält, müssen Sie natürlich auch noch die Tabellen angeben, aus denen die Felder stammen. Das gilt übrigens nicht nur für die angezeigten Felder, sondern auch für die Felder, die als Kriterien- oder Sortierfeld dienen.
342
7
Access-SQL
Abbildung 7.5: Abfrage mit zusammengesetztem Feld
Die Tabellen listet man unmittelbar hinter der FROM-Klausel auf. Bei einer einzigen Tabelle ist der Ausdruck noch recht übersichtlich: SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter;
Wenn die Daten aber aus mehreren Tabellen stammen, reicht es unter Umständen nicht aus, einfach nur die beteiligten Tabellen aufzulisten. Der einzige Fall, in dem Sie so verfahren, liefert alle Kombinationen der Felder aus den angegebenen Tabellen: SELECT tblMitarbeiter.MitarbeiterID, tblMitarbeiter.Vorname, tblMitarbeiter.Nachname, tblProjekte.ProjektID, tblProjekte.Projekt FROM tblMitarbeiter, tblProjekte;
Dieser SQL-Ausdruck würde alle Kombinationen der Datensätze der Tabelle tblMitarbeiter und der Tabelle tblProjekte ausgeben. Normalerweise wird man nur bestimmte Kombinationen aus Projekten und Mitarbeitern anzeigen wollen – etwa einen Mitarbeiter, der in der Projekt-Tabelle als Projektleiter eingetragen ist. Um einen derartigen Zusammenhang festzulegen, verwendet man entweder eine JOIN-Klausel, die Informationen über eine Verknüpfung zwischen den beteiligten Tabellen angibt, oder eine WHERE-Klausel, die festlegt, welche Felder der beiden Tabellen übereinstimmen müssen, damit eine Kombination ausgegeben wird.
Vereinfachen der Schreibweise Um die Schreibweise und Übersichtlichkeit von SQL-Ausdrücken mit mehreren Tabellen zu vereinfachen, können Sie den Tabellennamen auch alternative Bezeichnungen zuweisen. Diese werden dann im Rest des SQL-Ausdrucks verwendet. Im folgenden Beispiel soll die Tabelle tblMitarbeiter die Bezeichnung t1 erhalten und die Tabelle tblProjekte die Bezeichnung t2: SELECT t1.MitarbeiterID, t1.Vorname, t1.Nachname, t2.ProjektID, t2.Projekt FROM tblMitarbeiter AS t1, tblProjekte AS t2;
In der Entwurfsansicht stellen Sie einen alternativen Namen übrigens über die Eigenschaft Alias ein (siehe Abbildung 7.6).
Daten auswählen
343
Abbildung 7.6: Eingeben eines Alias-Namens für eine Tabelle
7.2.3 Festlegen von Bedingungen Bisher haben Sie nur die Felder ausgewählt, die Sie im Abfrageergebnis der SQLAbfrage anzeigen möchten. Nun legen Sie fest, welche Datensätze angezeigt werden sollen. Dazu verwenden Sie die WHERE-Klausel von SQL. Die WHERE-Klausel enthält einen oder mehrere Ausdrücke, die durch AND oder OR voneinander getrennt sind. Damit können Sie etwa alle Datensätze der Tabelle tblMitarbeiter herausfinden, bei denen das Feld MitarbeiterID den Wert 1 enthält: SELECT * FROM tblMitarbeiter WHERE MitarbeiterID = 1;
Mit Hilfe der OR-Verknüpfung können Sie Datensätze zurückgeben, die mindestens eine von mehreren Bedingungen erfüllen: SELECT * FROM tblMitarbeiter WHERE MitarbeiterID = 1 OR MitarbeiterID = 2;
Mit der AND-Verknüpfung schränken Sie die zurückgegebenen Datensätze so ein, dass diese alle aufgeführten Bedingungen erfüllen: SELECT * FROM tblMitarbeiter WHERE Vorname = 'Bernd' AND Nachname = 'Held';
Wenn der SQL-Ausdruck mehr als einen Verknüpfungsoperator enthält, können Sie die Reihenfolge der Abarbeitung durch das Setzen von Klammern beeinflussen.
344
7
Access-SQL
7.2.4 Vergleichsausdrücke Bei der Zusammenstellung der Vergleichsausdrücke sind der Fantasie praktisch keine Grenzen gesetzt. In den folgenden Abschnitten finden Sie die Grundlagen und die wichtigsten Möglichkeiten für Vergleichsausdrücke.
Operatoren Für Vergleiche stellt SQL die folgenden Operatoren zur Verfügung: gleich (=) größer als (>) größer oder gleich (>=) kleiner als (<) kleiner als oder gleich (<=) ungleich (<>) Vergleichsumkehr (NOT ) zwischen zwei Werten (BETWEEN … AND) Vergleich mit Platzhaltern wie Sternchen (*, beliebig viele Zeichen) oder Fragezeichen (?, ein Zeichen): LIKE Vergleich mit Null-Werten: IS NULL Vergleich mit den Werten einer Menge: IN
Vergleiche mit Zahlen Wenn Sie einen der obigen Vergleichsoperatoren für den Vergleich mit Zahlen verwenden, können Sie alle Varianten außer Vergleichen mit Platzhaltern einsetzen. Beispiele: SELECT * FROM tblArtikel WHERE Preis > 10; SELECT * FROM tblArtikel WHERE Preis BETWEEN 5 AND 15;
Geben Sie Zahlen immer ohne Formatierungen wie Prozentzeichen (%), Währungsangaben (€) oder dergleichen ein. Verwenden Sie nur die nackten Zahlen als Vergleichsausdrücke und setzen Sie den Punkt als Dezimaltrennzeichen: SELECT * FROM tblArtikel WHERE Mehrwertsteuer IN (0.7, 0.16);
Daten auswählen
345
Vergleiche mit Zeichenketten Wichtig für die Verwendung von Zeichenketten ist vor allem, dass Sie den Vergleichswert in einfache (') oder doppelte (") Anführungszeichen einschließen. Weiterhin sind alle Vergleichsoperationen möglich. Am sinnvollsten ist jedoch der LIKE-Vergleich, da er die Verwendung von Platzhaltern wie Sternchen (*), das stellvertretend für beliebig viele Zeichen steht, und Fragezeichen (?), das stellvertretend für je ein Zeichen steht, unterstützt. Beispiele: Alle Mitarbeiter, deren Vorname mit A beginnt: SELECT * FROM tblMitarbeiter WHERE Vorname LIKE "A*";
Alle Mitarbeiter, deren Nachname die Zeichenfolge EI enthält: SELECT * FROM tblMitarbeiter WHERE Nachname LIKE "*EI*";
Alle Mitarbeiter, deren Nachname mit einem Buchstaben von A–J beginnt: SELECT * FROM tblMitarbeiter WHERE Vorname >= "A" AND Vorname < "K";
Vergleiche mit Datumsangaben Datumsangaben sind ein fehlerträchtiges Gebiet – vor allem in Abfragen. Auf Nummer Sicher gehen Sie mit den folgenden beiden Datumsformaten: Amerikanisches Datumsformat: #mm/dd/yyyy# ISO-Datumsformat: #yyyy/mm/dd# Verwenden Sie eines dieser Formate als Grundlage für den Vergleichswert. Gehen Sie kein Risiko ein. Lassen Sie sich auch nicht davon irritieren, dass das Datum in der Tabelle ganz anders eingegeben und angezeigt wird. Von dieser Feinheit abgesehen können Sie für Vergleiche mit dem Datum alle Vergleichsvarianten mit Ausnahme des Vergleichs mit Platzhaltern heranziehen. Beispiele: SELECT * FROM tblBestellungen WHERE Bestelldatum = #2005/05/15#;
346
7
Access-SQL
SELECT * FROM tblBestellungen WHERE Bestelldatum In (#5/5/2005#, #5/7/2005#); SELECT * FROM tblBestellungen WHERE Lieferdatum BETWEEN #2005/5/10# AND #2005/5/15#;
Vergleiche mit dem Null-Wert Der NULL-Wert besitzt eine besondere Funktion: Liefert ein Vergleich eines Feldes mit dem Wert NULL den Wert True, ist das Feld leer. Das folgende Beispiel liefert alle Mitarbeiter, die keine E-Mail-Adresse haben: SELECT * FROM tblMitarbeiter WHERE Email IS NULL;
Umgekehrt ergibt das nächste Beispiel alle Mitarbeiter, die eine E-Mail-Adresse besitzen: SELECT * FROM tblMitarbeiter WHERE NOT EMail IS NULL;
Vergleiche mit Funktionen Access-SQL bietet die Möglichkeit, auch Funktionen als Vergleichswert zu verwenden. Eine oft als Vergleichswert verwendete Funktion ist Date(). Die folgende Abfrage ermittelt alle Bestellungen, deren Lieferdatum mit dem aktuellen Datum übereinstimmt: SELECT * FROM tblBestellungen WHERE Lieferdatum = DATE();
Auch berechnete Ausdrücke sind erlaubt. Alle Bestellungen der letzten 30 Tage ermitteln Sie folgendermaßen: SELECT * FROM tblBestellungen WHERE Bestelldatum = DATE() – 30;
Standard-SQL bietet einige Funktionen wie etwa die Aggregatfunktionen Sum, Count, Max oder Min (siehe weiter unten in Abschnitt 7.2.6, »Aggregatfunktionen«). Unter Access-SQL lassen sich wie im Beispiel auch eingebaute VBA-Funktionen, benutzerdefinierte Funktionen und Verweise auf Objekte verwenden. Letzteres ist vor allem im Zusammenhang mit der Einbindung der Steuerelementinhalte von Formularen interessant.
7.2.5 Sortieren von Daten Die ORDER BY-Klausel dient der Angabe der Sortierreihenfolge für beliebig viele Felder. Nach der ORDER BY-Klausel, die sich übrigens immer am Ende der Abfrage befindet, geben Sie den Namen eines oder mehrerer Felder an, nach denen das Abfrageergebnis
Daten auswählen
347
sortiert werden soll. Mehrere Feldnamen trennt man durch Kommata. Um anzugeben, ob aufsteigende oder absteigende Sortierung verwendet werden soll, ergänzen Sie den Feldnamen um einen der beiden Ausdrücke ASC (aufsteigend) oder DESC (absteigend). Ohne Angabe von ASC oder DESC wird automatisch ASC angenommen. Wenn Sie die Datensätze der Mitarbeitertabelle aufsteigend nach den Nachnamen sortieren möchten, verwenden Sie folgende Abfrage: SELECT * FROM tblMitarbeiter ORDER BY Nachname;
Die nächste Variante legt die Sortierreihenfolge explizit fest: SELECT * FROM tblMitarbeiter ORDER BY Nachname ASC;
Da Nachnamen gegebenenfalls mehrfach vorkommen, macht eine zusätzliche Sortierung nach dem Vornamen Sinn: SELECT * FROM tblMitarbeiter ORDER BY Nachname, Vorname;
7.2.6 Aggregatfunktionen Sie können SQL Aggregatfunktionen verwenden, um Datensätze zu zählen, Mittelwerte zu bilden oder Summen zu ermitteln. Diese Aggregatfunktionen beziehen sich auf das komplette Abfrageergebnis oder auf einzelne Gruppierungen. Mehr über den Einsatz von Aggregatfunktionen auf das komplette Abfrageergebnis erfahren Sie im Anschluss an die Auflistung der Funktionen; der Einsatz von Aggregatfunktionen mit gruppierten Daten wird in Abschnitt 7.2.7, »Gruppieren von Daten« vorgestellt. SQL bietet die folgenden Aggregatfunktionen: Avg(): Berechnet den arithmetischen Mittelwert des in enthaltenen Wertes. Count: Ermittelt die Anzahl von Datensätzen. Es gibt zwei Varianten: Count(*) ermittelt die Anzahl aller enthaltenen Datensätze. Count() ermittelt die Anzahl aller Datensätze, in denen der nicht den Wert Null besitzt.
First(): Ermittelt den Wert von des ersten Datensatzes des Abfrageergebnisses. Last(): Ermittelt den Wert von des letzten Datensatzes des Abfrageergebnisses.
348
7
Access-SQL
Max(): Ermittelt den größten Wert von des Abfrageergebnisses. Min(): Ermittelt den kleinsten Wert von des Abfrageergebnisses. StDev(), StDevP(): Ermittelt die Standardabweichung von bezogen auf die Grundgesamtheit (StDevP) oder eine Stichprobe (StDev). Sum(): Summiert die Werte von , Null-Werte werden ignoriert. Var(), VarP(): Berechnet die Varianz von bezogen auf die Grundgesamtheit (VarP) oder eine Stichprobe (Var).
Aggregatfunktionen ohne Gruppierung Die Aggregatfunktionen lassen sich ohne Gruppierung auf die komplette Ergebnismenge der Abfrage anwenden. Wenn Sie beispielsweise den Durchschnittspreis von Produkten ermitteln möchten, verwenden Sie etwa folgende SQL-Abfrage: SELECT Avg(Preis) AS Durchschnittspreis FROM tblProdukte;
Das Ergebnis der Abfrage sieht wie in Abbildung 7.7 aus.
Abbildung 7.7: Ergebnis der Abfrage eines Durchschnittswertes
Wichtig ist, dass Sie für den durch die Aggregatfunktion entstandenen Ausdruck einen Alias-Namen angeben – in diesem Fall mit AS Durchschnittspreis –, sonst vergibt Access automatisch einen Namen der Form »Ausdr1«, »Ausdr2«, ...
7.2.7 Gruppieren von Daten Daten lassen sich mit SQL innerhalb einer einzigen Abfrage gruppieren. Dadurch erhalten Sie die Möglichkeit, Aggregatfunktionen auf gruppierte Datensätze mit bestimmten Eigenschaften auszuführen. Bei der Erstellung von Abfragen mit Gruppierungen sind zwei Regeln zu beachten: Gruppierungen können entweder Tabellenfelder, berechnete Felder oder Konstanten enthalten.
Daten auswählen
349
Jedes Feld, das der SELECT-Bereich der Abfrage enthält, muss entweder mit einer Aggregatfunktion versehen oder ein Element des GROUP BY-Abschnitts sein. Das fällt auch beim Erstellen von Gruppierungen mit der Abfrage-Entwurfsansicht auf: Dort wird für jedes hinzugefügte Feld standardmäßig die Funktion Gruppierung festgelegt. Es ist nicht möglich, in der Spalte Funktion weder eine Gruppierung noch eine Aggregatfunktion auszuwählen.
Einfache Gruppierungen Das folgende Beispiel zeigt eine Abfrage, die Produkte nach Kategorien gruppiert und die Anzahl der Produkte je Kategorie ausgibt: SELECT KategorieID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte GROUP BY KategorieID;
Das Ergebnis der Abfrage sieht wie in Abbildung 7.8 aus.
Abbildung 7.8: Abfrageergebnis einer Gruppierung von Datensätzen mit der Anzahl Datensätze jeder Gruppe
Sie können auch nach mehreren Feldern gruppieren. Ist die Abfrage aus dem vorherigen Beispiel nicht aussagekräftig genug, können Sie auch noch den Hersteller in die Gruppierung einbeziehen: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte GROUP BY KategorieID, HerstellerID;
Diese Variante gruppiert nach allen vorhandenen Kombinationen aus Kategorie und Hersteller, wie das Ergebnis in Abbildung 7.9 zeigt.
350
7
Access-SQL
Abbildung 7.9: Gruppierung nach einer Kombination aus mehreren Feldern
Gruppierungen einschränken Es gibt zwei verschiedene Möglichkeiten, das Ergebnis einer gruppierten Abfrage einzuschränken: vor dem Gruppieren der enthaltenen Datensätze und nach dem Gruppieren der enthaltenen Datensätze. Die erste Möglichkeit kennen Sie bereits: Mit der WHERE-Klausel leiten Sie einen Bereich ein, der Kriterien für die anzuzeigenden Datensätze enthält. Den WHERE-Abschnitt fügen Sie dabei hinter dem FROM-Bereich, aber vor dem GROUP BY-, HAVING- oder ORDER BY-Bereich ein. Das folgende Beispiel ermittelt die Anzahl der Produkte teurer als EUR 50,– je Kategorie. Dabei werden durch die WHERE-Bedingung zunächst alle Produkte ermittelt, die mehr als EUR 50,– kosten, und anschließend wird die Gruppierung durchgeführt: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte WHERE Preis > 50 GROUP BY KategorieID, HerstellerID;
Die zweite Möglichkeit zum Einschränken einer gruppierten Abfrage führt zunächst die Gruppierung durch und wertet dann das Ergebnis einer Aggregatfunktion als Kriterium aus. Dazu wird – analog zur WHERE-Klausel – die HAVING-Klausel zum Bereitstellen des Kriteriums beziehungsweise der Kriterien verwendet.
Daten auswählen
351
Im folgenden Beispiel sollen die Produkte nach Kategorie und Hersteller gruppiert und die Anzahl der Produkte je Gruppierung ermittelt werden, bevor die Abfrage diejenigen Kombinationen herausfiltert, die mit weniger als zwei Produkten vertreten sind. Abbildung 7.10 zeigt das Ergebnis des folgenden SQL-Ausdrucks an: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte GROUP BY KategorieID, HerstellerID HAVING Count(ProduktID) > 1;
Abbildung 7.10: Abfrageergebnis eines SQL-Ausdrucks mit HAVING-Klausel
7.2.8 WHERE, GROUP BY, HAVING und ORDER BY im Überblick Die vielfältigen Möglichkeiten zum Einschränken, Gruppieren und Sortieren der Ergebnismenge eines SQL-Ausdrucks bergen ein Problem: Wie soll man sich die Reihenfolge der unterschiedlichen Bereiche merken? Folgendes hilft vielleicht: WHERE schränkt die Daten ein und kommt daher zuerst. GROUP BY gruppiert die übrig gebliebenen Datensätze. HAVING schränkt ebenfalls Daten ein, aber nur auf Basis von Gruppierungen. ORDER BY kommt zum Schluss, weil alles andere vergeudete Rechenleistung wäre. Diese Reihenfolge scheint intuitiv richtig zu sein – das reicht als Eselsbrücke für den Aufbau von SQL-Anweisungen aus. Tatsächlich werden Abfragen mitunter auch anders abgearbeitet – mehr dazu erfahren Sie in Kapitel 12 im Abschnitt 12.2.1, »Abfragen und die Jet-Engine«).
352
7
Access-SQL
7.2.9 Verknüpfen von Tabellen in Abfragen Wenn Sie mehrere Tabellen im FROM-Bereich als Datenherkunft eines SQL-Ausdrucks angeben, werden diese von der Jet-Engine als völlig losgelöst von jeglichen zuvor angelegten Beziehungen zwischen den Tabellen betrachtet.
Manuelles Hinzufügen einer Verknüpfung Sie können durchaus eine 1:n-Beziehung mit referentieller Integrität zwischen Tabellen wie tblProjekte und tblMitarbeiter im Beziehungsfenster angelegt haben – sobald Sie diese beiden Tabellen in der folgenden Form in einem SQL-Ausdruck angeben, ist alles vergessen: SELECT tblProjekte.ProjektID, tblMitarbeiter.MitarbeiterID FROM tblProjekte, tblMitarbeiter;
Die Abfrage-Entwurfsansicht hat da ein etwas besseres Gedächtnis. Sie erkennt direkt bestehende Beziehungen und zeigt diese auch an, wie in Abbildung 7.11. Allerdings können Sie auch diese Möglichkeit ausschalten: Dazu deaktivieren Sie auf der Registerseite Tabellen/Abfragen des Optionen-Dialogs (Menüeintrag Extras/Optionen) die Eigenschaft Autoverknüpfung aktivieren.
Abbildung 7.11: Die Abfrage-Entwurfsansicht übernimmt bestehende Beziehungen nach dem Hinzufügen von verknüpften Tabellen.
Daten auswählen
353
Ein Wechsel von hier aus in die SQL-Ansicht zeigt, welchen zusätzlichen Code die Festlegung einer Beziehung zwischen zwei Tabellen mit sich bringt: SELECT tblMitarbeiter.MitarbeiterID, tblProjekte.ProjektID FROM tblMitarbeiter INNER JOIN tblProjekte ON tblMitarbeiter.MitarbeiterID = tblProjekte.MitarbeiterID;
Es gibt noch eine weitere Möglichkeit, zwei Tabellen innerhalb eines SQL-Ausdrucks zu verknüpfen. Dabei erfolgt die Verknüpfung im WHERE-Teil der Abfrage: SELECT tblMitarbeiter.MitarbeiterID, tblProjekte.ProjektID FROM tblMitarbeiter, tblProjekte WHERE tblMitarbeiter.MitarbeiterID = tblProjekte.MitarbeiterID;
Diese Variante schreibt sich zwar etwas kürzer, kann aber keine OUTER JOIN-Verknüpfungen abbilden – mehr zu dieser Verknüpfungsart später. Noch gravierender ist der Nachteil, dass das Abfrageergebnis nicht aktualisiert werden kann – das zeigt ein Blick auf die Datenblatt-Ansicht einer solchen Abfrage (siehe Abbildung 7.12). Dort findet sich keine Möglichkeit, einen neuen Datensatz anzulegen, und auch eine Änderung der enthaltenen Daten ist nicht möglich.
Abbildung 7.12: WHERE-Verknüpfungen sind nicht aktualisierbar.
Im Entwurfsfenster einer solchen Abfrage erscheint übrigens kein Verknüpfungspfeil, die WHERE-Bedingung wird wie üblich im Entwurfsraster angezeigt (siehe Abbildung 7.13).
Aufbau von Verknüpfungen (INNER JOIN) Einfache Verknüpfungen zwischen zwei Tabellen definiert man in SQL folgendermaßen: INNER JOIN ON . = .;
354
7
Access-SQL
Abbildung 7.13: Beziehung per Kriterium
Kommen weitere Tabellen hinzu, setzt man den Ausdruck für die erste Verknüpfung in Klammern und behandelt ihn für die Erstellung der zweiten Verknüpfung wie eine einzige Tabelle. Die neue Verknüpfung baut man einfach um den bestehenden Ausdruck herum wie im folgenden Beispiel (neue Teile fett gedruckt): INNER JOIN ( INNER JOIN
ON . = .) ON .;
Beispiel: Die klassische m:n-Beziehung zwischen Bestellungen, Bestelldetails und Artikeln enthält zwei INNER JOIN-Verknüpfungen (siehe Abbildung 7.14). Der entsprechende SQL-Ausdruck sieht folgendermaßen aus: SELECT tblBestelldetails.ArtikelID, tblArtikel.Artikel, tblBestelldetails.Anzahl, tblBestellungen.Lieferdatum FROM tblBestellungen INNER JOIN ( tblArtikel INNER JOIN tblBestelldetails ON tblArtikel.ArtikelID = tblBestelldetails.ArtikelID ) ON tblBestellungen.BestellungID = tblBestelldetails.BestellungID;
Weitere Verknüpfungsarten Neben den INNER JOINS, die alle Datensätze ausgeben, bei denen die Inhalte des Verknüpfungsfeldes gleich sind, gibt es noch weitere Verknüpfungsarten.
Daten auswählen
355
Abbildung 7.14: Abfrage mit zwei Verknüpfungen
Ein OUTER JOIN liefert alle Datensätze der ersten Tabelle zurück und nur die Datensätze der zweiten Tabelle, die mit einem Datensatz der ersten Tabelle verknüpft sind. OUTER JOIN-Abfragen treten als LEFT OUTER JOIN und RIGHT OUTER JOIN auf. LEFT beziehungsweise RIGHT legt dabei fest, ob alle Datensätze von der links vom Schlüsselwort JOIN stehenden Tabelle ausgegeben werden oder von der rechts davon stehenden Tabelle. Beispiel: Der folgende SQL-Ausdruck soll alle Mitarbeiter und deren Projekte ausgeben, aber auch die Mitarbeiter berücksichtigen, denen kein Projekt zugeordnet ist. SELECT [Nachname] & ", " & [Vorname] AS Mitarbeiter, tblProjekte.Projekt FROM tblMitarbeiter LEFT JOIN tblProjekte ON tblMitarbeiter.MitarbeiterID=tblProjekte.MitarbeiterID;
Abbildung 7.15 zeigt das Ergebnis dieses SQL-Ausdrucks. Für die Verwendung von OUTER JOIN-Verknüpfungen gibt es zwei wichtige Regeln: Die rechte Tabelle eines LEFT OUTER JOIN kann nicht mit anderen Tabellen per INNER JOIN verknüpft werden und kann nicht die linke Tabelle eines anderen RIGHT OUTER JOIN oder die rechte Tabelle eines LEFT OUTER JOIN sein. Rekursive Verknüpfungen lassen sich ebenfalls mit SQL realisieren. Allerdings gibt es dafür kein spezielles Sprachkonstrukt. Statt dessen verwenden Sie einfach einen kleinen Trick. Zum Herstellen einer Beziehung kommen Sie definitiv nicht daran vorbei, zwei Tabellen miteinander zu verknüpfen – es ist aber nicht verboten, zweimal die gleiche Tabelle zu nehmen. Sie müssen nur eine der Tabellen – wie weiter oben erläutert – mit dem AS-Schlüsselwort umbenennen.
356
7
Access-SQL
Abbildung 7.15: Ergebnis einer Abfrage mit LEFT OUTER JOIN-Verknüpfung
Das folgende Beispiel zeigt, wie es funktioniert. Das Abfrageergebnis finden Sie in Abbildung 7.16. SELECT tblMitarbeiter.MitarbeiterID, [tblMitarbeiter].[Nachname] & ", " & [tblMitarbeiter].[Vorname] AS Mitarbeiter, [tblVorgesetzte].[Nachname] & ", " & [tblVorgesetzte].[Vorname] AS Vorgesetzter FROM tblMitarbeiter INNER JOIN tblMitarbeiter AS tblVorgesetzte ON tblMitarbeiter.VorgesetzterID = tblVorgesetzte.MitarbeiterID;
Abbildung 7.16: Beispiel für die Ausgabe rekursiv verknüpfter Daten
In der Bedingung für die Herstellung der Verknüpfung (den ON-Abschnitt) haben die bisherigen Beispiele immer das Gleichheitszeichen als Vergleichsoperator verwendet.
Daten auswählen
357
Sie können auch andere Vergleichsoperatoren wie >, >=, <, <=, <> oder BETWEEN einsetzen. Der Anteil von SQL-Ausdrücken mit anderen Vergleichsoperatoren als dem Gleichheitszeichen ist aber relativ gering.
Unterabfragen Weiter oben in Abschnitt 7.2.4 unter »Vergleiche mit Zahlen« haben Sie bereits erfahren, dass man mit dem IN-Operator Vergleiche von Feldinhalten mit einer Gruppe von Werten vornehmen kann. Es wäre sehr unpraktisch, wenn man die Vergleichswerte immer manuell eintragen müsste. Unterabfragen lösen dieses und andere Probleme auf elegante Weise: Sie treten als Abfrage in der Abfrage auf und liefern so die Vergleichswerte für Kriterienausdrücke. Dabei können diese entweder nur einen einzelnen Wert als Vergleichswert zurückliefern oder eine Gruppe von Werten eines Feldes, das mit dem IN-Operator untersucht wird. Beispiel für eine Unterabfrage, die einen Vergleichswert liefert: Der folgende SQL-Ausdruck liefert alle Produkte zurück, deren Preis über dem durchschnittlichen Preis aller Produkte liegt. SELECT ProduktID, Produkt, Preis FROM tblProdukte WHERE Preis > (SELECT Avg(Preis) FROM tblProdukte);
Behandlung doppelter Datensätze in Ergebnissen von SQL-Ausdrücken SQL-Ausdrücke, die mehrere Abfragen enthalten, liefern oft doppelte Daten zurück. Das ist zum Beispiel der Fall, wenn Sie eine Projekt- und eine Kundentabelle verknüpfen und alle Kunden ausgeben möchten, denen ein aktuelles Projekt zugewiesen ist: SELECT tblKunden.KundeID, tblKunden.Firma FROM tblKunden INNER JOIN tblProjekte ON tblKunden.KundeID = tblProjekte.KundeID;
Wenn es zwei oder mehr Projekte im Auftrag eines Kunden gibt, wird dieser Kunde im Ergebnis auch mehrfach angezeigt, wie Abbildung 7.17 zeigt. In dieser Abfrage wird implizit das Prädikat ALL für den SELECT-Teil der Abfrage verwendet. Das bedeutet, dass alle Datensätze des Ergebnisses ausgegeben werden. Der folgende SQL-Ausdruck mit dem ALL-Prädikat erfüllt die gleiche Funktion: SELECT ALL tblKunden.KundeID, tblKunden.Firma FROM tblKunden INNER JOIN tblProjekte ON tblKunden.KundeID = tblProjekte.KundeID;
358
7
Access-SQL
Abbildung 7.17: Mehrfache Anzeige von Kunden
Wenn Sie gleiche Datensätze nicht doppelt anzeigen möchten, können Sie eines der Prädikate DISTINCT oder DISTINCTROW verwenden. Jedes dieser Prädikate geben Sie unmittelbar hinter dem SELECT-Schlüsselwort an. Mit dem DISTINCT-Prädikat sorgen Sie dafür, dass das Abfrageergebnis jede Kombination der ausgegebenen Daten nur einmal anzeigt. Dieses Prädikat hat die gleiche Wirkung wie das Einstellen der Eigenschaft Keine Duplikate (in Visual Basic: UniqueValues) auf den Wert Ja (True). Im folgenden Beispiel werden also tatsächlich nur die Firmen angezeigt, die mit einem Projekt verknüpft sind: SELECT DISTINCT tblKunden.KundeID, tblKunden.Firma FROM tblKunden INNER JOIN tblProjekte ON tblKunden.KundeID = tblProjekte.KundeID;
Das DISTINCTROW-Prädikat bringt in vielen Fällen das gleiche Ergebnis wie das DISTINCTPrädikat. Es bezieht sich nicht nur auf die im Abfrageergebnis angezeigten Felder, sondern auf die gesamte Datenherkunft der Abfrage. Das Prädikat hat die gleiche Wirkung wie das Einstellen der Eigenschaft Eindeutige Datensätze (UniqueRecords) auf den Wert Ja (True). Ein weiterer großer Unterschied zwischen DISTINCT und DISTINCTROW ist, dass das Ergebnis einer Abfrage mit dem DISTINCTROW-Prädikat aktualisierbar ist. Außerdem ist DISTINCTROW eine Spezialität von Access-SQL, die nicht dem SQL-Standard entspricht. Ein gutes Verständnis für den Unterschied liefert folgendes Beispiel. Die Tabelle aus Abbildung 7.18 enthält Mitarbeiterdaten. Die folgenden beiden Abfragen sollen nur die Vornamen der Mitarbeiter ausgeben und damit zeigen, wie sich die beiden Prädikate auswirken.
Daten auswählen
359
Abbildung 7.18: Beispieltabelle für den Einsatz von DISTINCT und DISTINCTROW
Die erste Abfrage verwendet das DISTINCT-Prädikat. Sie liefert das Ergebnis des linken Fensters in Abbildung 7.19 – der Vorname Bernd wird nur einfach ausgegeben, obwohl er zweimal in der zugrunde liegenden Tabelle enthalten ist. Der Grund ist, dass DISTINCT nur die ausgegebenen Felder auf Duplikate untersucht. SELECT DISTINCT Vorname FROM tblMitarbeiter;
Die zweite Abfrage mit dem DISTINCTROW-Prädikat gibt beide Mitarbeiter mit dem Vornamen Bernd aus. Diese Abfrage untersucht die ganze Tabelle tblMitarbeiter. Deren Datensätze sind natürlich alle verschieden, da zumindest der Primärschlüssel einen eindeutigen Wert enthält. SELECT DISTINCTROW Vorname FROM tblMitarbeiter;
Abbildung 7.19: Ergebnisse der DISTINCT- und der DISTINCTROW-Version einer Abfrage
360
7
Access-SQL
Die ersten x oder die ersten x Prozent der Datensätze ausgeben Wenn Sie das Ergebnis einer Abfrage dahingehend einschränken möchten, dass nur die ersten x Datensätze oder die ersten x Prozent der Datensätze ausgegeben werden sollen, verwenden Sie das TOP-Prädikat. Die folgende Beispielabfrage liefert die drei teuersten Produkte einer Tabelle zurück: SELECT TOP 3 ProduktID, Produkt, Preis FROM tblProdukte ORDER BY Preis;
Wichtig ist bei der Verwendung des TOP-Prädikats natürlich die Sortierung der Datensätze nach dem gewünschten Kriterium. Der nächste SQL-Ausdruck ermittelt die teuersten zehn Prozent der Produkte: SELECT TOP 10 PERCENT ProduktID, Produkt, Preis FROM tblProdukte ORDER BY Preis;
Die Prozent-Auswahl gibt »angebrochene« Datensätze mit aus – wenn Sie also zehn Prozent von 21 Produkten ausgeben möchten, erhalten Sie drei Datensätze als Ergebnis.
Parameter verwenden Wenn Sie eine Abfrage mit Parametern verwenden möchten, wie es auch in der Abfrage-Entwurfsansicht in Abbildung 7.20 möglich ist, reicht es aus, die Parameter wie in der Entwurfsansicht in eckigen Klammern als Kriterium anzugeben: SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Vorname=[Vorname eingeben];
Sie können zusätzlich die PARAMETERS-Klausel verwenden, um einen Datentyp für die Parameter festzulegen. Sie beginnt mit dem Schlüsselwort PARAMETERS und enthält eine durch Kommata getrennte Liste von Wertepaaren, die aus dem in eckigen Klammern gefassten Parameternamen und dem Datentyp bestehen. Das folgende Beispiel zeigt die vorherige Abfrage mit der PARAMETERS-Auflistung: PARAMETERS [Vorname eingeben] STRING; SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Vorname=[Vorname eingeben];
Daten auswählen
361
Abbildung 7.20: Abfrage mit Parameter in der Entwurfsansicht
Die Verwendung des PARAMETERS-Schlüsselworts macht nur Sinn, wenn Sie die Eingabe eines Wertes mit dem richtigen Datentyp erzwingen möchten. In der Regel sollten solche Validierungen aber bereits im Code erfolgen und nicht auf AbfrageEbene festgelegt werden.
Zusammenfassen von Abfrageergebnissen mit UNION Das UNION-Schlüsselwort von SQL ermöglicht das Zusammenfassen der Ergebnisse mehrerer SELECT-Anweisungen. Damit können Sie beispielsweise die Adressen aus einer Mitarbeiter- und einer Kundentabelle in einen Topf werfen, um eine Verteilerliste für Weihnachtskarten zu erstellen. Der Einsatz von UNION ist ganz einfach: Sie fassen damit zwei Abfragen zusammen, indem Sie zwei oder mehr Abfragen mit diesem Schlüsselwort verbinden: Abfrage1 UNION Abfrage2 [UNION Abfrage 3 […]]
Voraussetzung für den Einsatz einer UNION-Abfrage ist, dass alle beteiligten Abfragen die gleiche Anzahl Felder aufweisen. Der Vorteil von UNION-Abfragen ist, dass auch Daten zusammengeführt werden können, die in den einzelnen Tabellen in unterschiedlicher Form auftreten. Das folgende Beispiel zeigt, wie Sie Vorname und Nachname der Mitarbeitertabelle zusammensetzen und mit den in einem einzigen Feld gespeicherten Namen des Ansprechpartners von Kunden zusammenführen können: SELECT Vorname & " " & Nachname AS Empfaenger FROM tblMitarbeiter UNION SELECT Ansprechpartner FROM tblKunden;
362
7
Access-SQL
Wenn Sie für das Ergebnis einer UNION-Abfrage eine Sortierung vornehmen möchten, müssen Sie diese im Anschluss an die letzte Abfrage angeben. Sortierkriterien in den vorherigen Abfragen werden nicht berücksichtigt. Als Feldname des Abfrageergebnisses verwendet Access immer den in der ersten Abfrage angegebenen Feldnamen. UNION-Abfragen können nicht in der Entwurfsansicht für Abfragen angelegt werden.
Wenn Sie alle Felder einer Abfrage oder Tabelle in einer UNION-Abfrage verwenden möchten, müssen Sie diese nicht alle im Quellcode angeben. Es reicht die Sternchen (*)-Syntax: SELECT * FROM tblMitarbeiter. Aber auch diese Schreibweise können Sie noch abkürzen: TABLE tblMitarbeiter liefert genauso alle Felder der Tabelle zurück. Diese Syntax können Sie übrigens auch für Abfragen verwenden. Außerdem ist der Einsatzbereich dieser Abkürzung nicht auf UNION-Abfragen beschränkt.
Benutzern ohne Berechtigung den Zugriff auf Tabellen per Abfrage erlauben Das Sicherheitssystem von Access ermöglicht die Einschränkung des Zugriffs auf die in der Datenbank enthaltenen Daten. Dabei lassen sich zunächst der schreibende und der lesende Zugriff auf komplette Tabellen festlegen – aber auch nur für komplette Tabellen. Sie können nicht direkt angeben, welche Felder oder Datensätze der Benutzer lesen oder in welche er schreiben darf. Für diesen Fall gibt es die Option WITHOWNERACCESS OPTION, die Sie an das Ende einer Abfrage anhängen können. Eine Abfrage mit dieser Option ermöglicht den Zugriff auf eine Tabelle, die für den direkten lesenden und/oder schreibenden Zugriff gesperrt ist. Sie können also mit einer Abfrage genau festlegen, auf welche Felder und welche Datensätze der Tabelle der Benutzer lesend oder schreibend zugreifen kann. Ausschlaggebend für den Zugriff sind die Berechtigungen des Benutzers, der die Abfrage angelegt hat, der Eigentümer (OWNER). Wenn dieser Benutzer nur lesenden Zugriff auf die in der Abfrage enthaltenen Tabellen hat, kann auch der Anwender über die Abfrage nur lesend auf die Tabelle zugreifen. Weitere Informationen erhalten Sie in Kapitel 16, »Sicherheit von Access-Datenbanken«.
7.2.10 Zugriff auf externe Datenquellen Access-SQL bietet die Möglichkeit, per angehängter IN-Klausel im Anschluss an die Angabe der beteiligten Tabellen eine externe Datenbank oder Datei als Datenquelle festzulegen. Das sieht dann beispielsweise wie folgt aus: SELECT * FROM tblMitarbeiter IN '\<.mdb-Dateiname>'
Daten manipulieren
363
Die Technik funktioniert nicht nur mit Access-Datenbanken, sondern auch mit dBase, Excel, Exchange, HTML, Lotus, Outlook, Paradox und Textdateien in den verschiedenen Versionen. Dies ist allerdings nur eine Alternative dazu, die externen Daten als verknüpfte Tabellen in die aktuelle Access-Datenbank einzubinden. Das Einbinden bringt in den meisten Fällen weniger Probleme, da Sie damit die externen Daten an einer zentralen Stelle verwalten. So müssen Sie nicht in jeder Abfrage auf eine externe Datenquelle verweisen, sondern können auf die verknüpfte Tabelle zugreifen.
7.3 Daten manipulieren Mit SQL lassen sich natürlich nicht nur Daten abfragen, sondern auch anlegen, ändern und löschen. Dazu dienen im Wesentlichen vier verschiedene Befehle, deren Ausprägungen und Eigenschaften Sie in den folgenden Abschnitten kennen lernen.
7.3.1 Daten aktualisieren Das Aktualisieren von Daten mit SQL erfolgt über die UPDATE-Anweisung: UPDATE SET = <Wert1>[, = <Wert2>][, ...] [WHERE ]
Dabei können Sie als beliebige Tabellen oder aktualisierbare Abfragen angeben. Sie können auch verknüpfte Tabellen eingeben, was kein SQL-Standard ist. Dafür fällt die vom Standard vorgesehene Möglichkeit der Verwendung von Unterabfragen weg. , , … müssen in enthalten sein. <Wert1>, <Wert2>, … sind die Werte, die den Feldern , , … zugewiesen werden sollen. Mit legen Sie einen Ausdruck fest, der die von der Aktualisierung betroffenen Datensätze einschränkt. Die folgende Aktualisierungsabfrage erhöht die Preise aller Produkte, die teurer als EUR 50,– sind, um zehn Prozent: UPDATE tblProdukte SET Preis = [Preis]*1.1 WHERE Preis>50;
7.3.2 Daten löschen Löschabfragen lassen sich in SQL mit dem DELETE-Schlüsselwort ausführen. Sie haben die folgende Syntax: DELETE [.*] FROM [WHERE ]
364
7
Access-SQL
und sind identisch, wenn unter nur eine Tabelle angegeben wird. In diesem Fall kann .* weggelassen werden. .* müssen Sie nur angeben, wenn einen aus mehreren ver-
knüpften Tabellen bestehenden Ausdruck enthält. Die erste Löschabfrage bezieht sich auf eine einzige Tabelle: DELETE FROM tblKunden WHERE KundeID = 1;
7.3.3 Daten an bestehende Tabelle anfügen Die Anweisung zum Anfügen von Daten an eine bestehende Tabelle heißt INSERT INTO. Sie hat die folgende Syntax: INSERT INTO
Als geben Sie den Namen der Tabelle an, an die die Daten angefügt werden sollen. Für den Ausdruck gibt es zwei Möglichkeiten: Angabe einer SELECT-Anweisung Direkte Angabe der Felder und der einzufügenden Werte
Anzufügende Daten per SELECT-Anweisung angeben Wenn Sie eine SELECT-Anweisung als Datenherkunft für das Anfügen von Daten an eine Tabelle verwenden, müssen Sie darauf achten, dass die Datentypen der anzufügenden Felder mit den Zielfeldern übereinstimmen und dass alle Felder, die eine Eingabe erfordern, auch gefüllt werden. Die folgende Abfrage kopiert alle Datensätze der Produkte-Tabelle in die Tabelle tblProdukteArchiv, die auslaufen und deren Lagerbestand erschöpft ist: INSERT INTO tblProdukteArchiv(ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) SELECT ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand FROM tblProdukte WHERE LaeuftAus = True AND Lagerbestand = 0;
Eine kürzere Fassung dieses SQL-Ausdrucks wäre folgende: INSERT INTO tblProdukteArchiv SELECT ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand
Daten manipulieren
365
FROM tblProdukte WHERE LaeuftAus = True AND Lagerbestand = 0;
Und nun die kürzeste Version: INSERT INTO tblProdukteArchiv SELECT * FROM tblProdukte WHERE LaeuftAus = True AND Lagerbestand = 0;
Anzufügende Daten direkt angeben Die erste und längste Variante des oben genannten Beispiels ist auch Grundlage für das Anfügen eines Datensatzes, dessen Daten nicht aus einer Tabelle ermittelt werden: INSERT INTO tblProdukteArchiv(ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) VALUES(100, 'Testprodukt', 50, 2, 3, TRUE, 0)
Die einzufügenden Werte geben Sie in diesem Fall direkt in der VALUES-Liste an. Beachten Sie, dass Sie genau die Reihenfolge einhalten, die durch die Feldliste im INSERT INTO-Abschnitt angegeben ist. Diese Variante der INSERT INTO-Anweisung funktioniert nur, wenn das Feld ProduktID nicht als Autowert-Feld deklariert ist. In einer Tabelle zum Archivieren von Datensätzen kann es sinnvoll sein, keinen Autowert zu verwenden, sondern die ProduktID zu übernehmen – so können später Informationen über Bestellungen dieses Produkts wieder hergestellt werden. Wenn das Feld ProduktID auch in der Zieltabelle tblProdukteArchiv den Datentyp Autowert hat, können Sie diesem Feld keinen Wert mit der Anfügeabfrage übergeben. Die obige Variante müssten Sie entsprechend kürzen, damit Access die ProduktID selbst anlegen kann: INSERT INTO tblProdukteArchiv(Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) VALUES('Testprodukt', 50, 2, 3, TRUE, 0)
Sie können auch feste Werte mit dem SELECT-Schlüsselwort angeben: INSERT INTO tblProdukteArchiv(Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) SELECT 'Testprodukt', 50, 2, 3, TRUE, 0
366
7
Access-SQL
7.3.4 Neue Tabelle mit Daten erstellen Mit der SELECT INTO-Anweisung können Sie das Erstellen einer neuen Tabelle und das Anfügen von Daten in einem Schritt erledigen. Die Syntax dieses Abfragetyps ist fast identisch mit der für normale SELECT-Anweisungen – mit der Ausnahme, dass Sie mit dem INTO-Schlüsselwort noch den Namen der Tabelle angeben, die Access erstellen und mit den Daten des Abfrageergebnisses füllen soll. Den INTO-Abschnitt platzieren Sie einfach zwischen den SELECT- und den FROMAbschnitt einer Abfrage. Das folgende Beispiel zeigt, wie Sie die Abfrage zur Ermittlung der Anzahl Produkte je Kategorie und Hersteller mit einem Preis über EUR 50,– in eine neue Tabelle schreiben: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte INTO tblAnzahlProdukteJeKategorieTeurerAls50EUR
FROM tblProdukte WHERE Preis>50 GROUP BY KategorieID, HerstellerID;
Das Ergebnis bietet natürlich nicht den Komfort des eigentlichen Abfrageergebnisses – dort wurden dank der Festlegung von Nachschlagefeldern für die Felder KategorieID und HerstellerID noch die entsprechenden Informationen aus den verknüpften Tabellen angezeigt. Hier finden Sie nur nackte Zahlen, wie Abbildung 7.21 zeigt.
Abbildung 7.21: Ergebnis einer SELECT INTO-Abfrage
7.4 Datenmodell erstellen und manipulieren Neben dem Abfragen und Manipulieren der Inhalte von Tabellen können Sie mit SQL auch die Tabellen sowie die damit zusammenhängenden Elemente wie Felder und Indizes erstellen und wieder löschen. Die folgenden Abschnitte beschreiben die dazu notwendigen Anweisungen.
Datenmodell erstellen und manipulieren
367
7.4.1 Tabellen erstellen Das Erstellen einer Tabelle erfolgt über die CREATE TABLE-Anweisung. Die Anweisung hat folgende Syntax: CREATE [TEMPORARY] TABLE Tabelle (Feld1 Typ [(Größe)] [NOT NULL] [Index1] [, Feld2 Typ [(Größe)] [NOT NULL] [Index2] [, ...]] [, Mehrfelderindex [, ...]]) Tabelle gibt den Namen der zu erstellenden Tabelle an. Anschließend folgen in Klam-
mern die einzelnen Felder der Tabelle. Felder können Sie nachträglich hinzufügen, Sie brauchen lediglich ein Feld beim Erstellen der Tabelle anzugeben. Für jedes Feld geben Sie den Datentyp (Typ) sowie gegebenenfalls die Größe (Größe) an. Dafür stehen die Datentypen aus Tabelle 7.1 zur Verfügung. Den Parameter Größe verwenden Sie nur bei Feldern des Datentyps Text. Sie geben damit die maximale Anzahl Zeichen an, die das Feld enthalten darf. Wenn das Feld keine Null-Werte enthalten darf, geben Sie das Schlüsselwort NOT NULL ein. Hinweis: Ab Microsoft Jet 4.0 werden die Inhalte von Text- und Memofeldern im Unicode-Format gespeichert. Da dieses mit zwei Byte pro Zeichen doppelt so viel Speicherplatz wie üblich benötigt und die Größe vorhandener Access-Anwendungen somit stark wachsen könnte, gibt es den Parameter WITH COMPRESSION für Felder, die als CHARACTER und MEMO angelegt wurden. Die Daten werden dann bei Bedarf in einem komprimierten Format gespeichert und beim Lesen wieder dekomprimiert. Soll das Feld auf irgendeine Weise indiziert werden – etwa als Primärschlüssel, eindeutiger Index oder einfacher Index –, tragen Sie unter Index1, Index2, … einen entsprechenden CONSTRAINT-Abschnitt ein. Wenn die Tabelle einen Mehrfelderindex erhalten soll, verwenden Sie ebenfalls einen CONSTRAINT-Abschnitt, den Sie hinter dem letzten Feld, aber noch innerhalb der Klammer eingeben. Mehr zur CONSTRAINT-Klausel erfahren Sie in Abschnitt 7.4.2, »Primärschlüssel, Indizes und Einschränkungen mit CONSTRAINT«.
368
7
Access-SQL
SQL-Datentyp
Datentyp im Tabellenentwurf
Größe
Werte
TEXT oder CHAR
Text
2 Bytes pro Zeichen (siehe Anmerkungen)
Von 0 bis 255 Zeichen
LONGTEXT
Memo
2 Bytes pro Zeichen (siehe Anmerkungen)
Von 0 bis maximal 2,14 GB
BYTE
Zahl (Byte)
1 Byte
Eine Ganzzahl von 0 bis 255
SMALLINT
Zahl (Integer)
2 Bytes
Eine Ganzzahl von –32.768 bis 32.767
INTEGER
Zahl (Long Integer) 4 Bytes
Eine lange Ganzzahl von – 2.147.483.648 bis 2.147.483.647
REAL
Zahl (Single)
4 Bytes
Eine Gleitkommazahl einfacher Genauigkeit, die die folgenden Werte annehmen kann: –3,402823E38 bis –1,401298E-45 für negative Werte, 1,401298E-45 bis 3,402823E38 für positive Werte und 0
FLOAT
Zahl (Double)
8 Bytes
Eine Gleitkommazahl doppelter Genauigkeit, die die folgenden Werte annehmen kann: –1.79769313486232E308 bis – 4.94065645841247E-324 für negative Werte, 4,94065645841247E-324 bis 1,79769313486232E308 für positive Werte und 0
GUID
Zahl (ReplikationsID)
16 Bytes
Globally unique identifier (GUID)
DATETIME
Datum
8 Bytes
Eine Datums- oder Zeitangabe ab dem Jahr 100 bis zum Jahr 9999
MONEY
Währung
8 Bytes
Eine skalierte Ganzzahl von – 922.337.203.685.477,5808 bis 922.337.203.685.477,5807
COUNTER
Autowert (Long Integer)
4 Bytes
Eine lange Ganzzahl von – 2.147.483.648 bis 2.147.483.647
BIT
Ja/Nein
1 Byte
Ja/Nein-Werte (boolsche Werte) sowie Felder, die einen von zwei möglichen Werten enthalten
IMAGE
OLE-Objekt
Nach Bedarf
Von 0 bis maximal 2,14 GB. Wird für OLE-Objekte verwendet
BINARY
Binär
Maximal 510 Bytes
Byte-Array mit maximal 510 Werten
NUMERIC
Zahl (Double)
8 Bytes
Wie FLOAT
Tabelle 7.1: Datentypen unter Access-SQL
Datenmodell erstellen und manipulieren
369
Die folgende SQL-Anweisung erstellt eine Tabelle mit allen in der Tabellen-Entwurfsansicht auswählbaren Datentypen in der Reihenfolge des dortigen Vorkommens. Nicht enthalten ist der Datentyp Zahl (Dezimal): CREATE TABLE tblDatentypen( xText TEXT, xLongtext LONGTEXT, xByte BYTE, xSmallInt SMALLINT, xInteger INTEGER, xReal REAL, xFloat FLOAT, xUniqueIdentifier GUID, xDatetime DATETIME, xMoney MONEY, xCounter COUNTER, xBit BIT, xImage IMAGE xBinary BINARY, xNumeric NUMERIC); Listing 7.1: Erstellen einer Tabelle mit allen Datentypen
Die erstellte Tabelle sieht wie in Abbildung 7.22 aus.
Abbildung 7.22: Das Produkt einer Tabellenerstellungsabfrage
Standardwerte vorgeben Unter Jet 4 (SQL-92) können Sie wie in der Abfrage-Entwurfsansicht einen Standardwert für ein Feld angeben. Dazu fügen Sie die Klausel DEFAULT gefolgt von dem gewünschten Standardwert hinter dem Felddatentyp des jeweiligen Feldes ein: CREATE TABLE tblBeispiel(FeldMitStandardwert TEXT(50) DEFAULT Standardwert)
370
7
Access-SQL
Gültigkeitsregel festlegen Ebenfalls ab Jet 4 können Sie mit dem Schlüsselwort CHECK eine Gültigkeitsregel für ein Feld festlegen. CHECK ist ein zusätzliches CONSTRAINT und wird mit diesem Schlüsselwort eingeleitet. Beispiel: CREATE TABLE tblBeispiel(Beispielfeld TEXT(50), CONSTRAINT CHKBeispielfeld CHECK(Beispielfeld= 'A%'))
7.4.2 Primärschlüssel, Indizes und Einschränkungen mit CONSTRAINT Ohne einen Primärschlüssel läuft nichts in einem relationalen Datenbanksystem und auch die Verknüpfungen muss man irgendwo festlegen. Dazu dient die CONSTRAINTKlausel. Sie lässt sich an zwei Stellen einsetzen: für ein einzelnes Feld und als Mehrfeldeinschränkung für mehrere Felder. Die Syntax ist in beiden Fällen unterschiedlich. Die Einzelfeldvariante sieht folgendermaßen aus: CONSTRAINT Name {PRIMARY KEY | UNIQUE | NOT NULL | REFERENCES FremdTabelle [(FremdFeld1, FremdFeld2)] [ON UPDATE CASCADE | SET NULL] [ON DELETE CASCADE | SET NULL]
Die Mehrfeldeinschränkung mit dem CONSTRAINT-Schlüsselwort hat diese Syntax: CONSTRAINT Name {PRIMARY KEY (Primär1[,Primär2[, ...]]) | UNIQUE (Eindeutig1[,Eindeutig2[, ...]]) | NOT NULL (Nichtnull1[, Nichtnull2 [, ...]]) | FOREIGN KEY [NO INDEX] (Ref1[, Ref2 [, ...]]) REFERENCES FremdTabelle [(FremdFeld1 [, FremdFeld2 [, ...]])]} [ON UPDATE CASCADE | SET NULL] [ON DELETE CASCADE | SET NULL]
In den folgenden Abschnitten lernen Sie die Möglichkeiten dieses Elements genauer kennen.
Primärschlüssel anlegen Um eine Tabelle mit einem Primärschlüssel zu erstellen, können Sie beispielsweise folgende Anweisung verwenden: CREATE TABLE tblMitarbeiter (MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY, Vorname TEXT(50), Nachname TEXT(50))
Datenmodell erstellen und manipulieren
371
Abbildung 7.23 zeigt das Ergebnis dieser Abfrage in der Entwurfsansicht.
Abbildung 7.23: Ergebnis einer Tabellenerstellungsabfrage
Eindeutigen Index anlegen Zum Anlegen einer Tabelle mit einem eindeutigen Index verwenden Sie das UNIQUESchlüsselwort. Der erweiterte SQL-Ausdruck aus dem vorherigen Beispiel fügt der Tabelle nun ein zusätzliches Feld für eine Mitarbeiternummer hinzu: CREATE TABLE tblMitarbeiter( MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY, Mitarbeiternummer TEXT(10) CONSTRAINT UKMitarbeiternummer UNIQUE,
Vorname TEXT(50), Nachname TEXT (50))
Wenn Sie eine CREATE TABLE-Anweisung verwenden und die zu erstellende Tabelle schon existiert, zeigt Access eine entsprechende Meldung an. Sie müssen die bestehende Tabelle also zuerst löschen oder umbenennen. Die Verwendung von UNIQUE entspricht dem Einstellen der Eigenschaft Indiziert des angegebenen Feldes auf den Wert Ja (Ohne Duplikate).
Feld darf nicht Null sein Um zu verhindern, dass eines der beiden Felder Vorname und Nachname der Mitarbeitertabelle nicht gefüllt wird, können Sie die Tabelle so einstellen, dass die Eingabe erforderlich ist: CREATE TABLE tblMitarbeiter( MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY, Mitarbeiternummer TEXT(10) CONSTRAINT UKMitarbeiternummer UNIQUE, Vorname TEXT(50) NOT NULL, Nachname TEXT (50) NOT NULL)
Die Verwendung von NOT NULL entspricht dem Einstellen der Eigenschaft Eingabe erforderlich auf den Wert Ja.
372
7
Access-SQL
Das gleiche Schlüsselwort kann auch in Mehrfeldeinschränkungen verwendet werden. Beachten Sie, dass jedes Feld nur einmal mit der Einschränkung NOT NULL ausgestattet werden darf.
Fremdschlüsselfelder festlegen Ein ganz wichtiges Element in relationalen Datenbanksystemen sind Fremdschlüsselfelder. Dementsprechend bietet Access-SQL die Möglichkeit, eine Tabelle mit einem solchen Fremdschlüsselfeld anzulegen. Die Mitarbeitertabelle aus den vorherigen Beispielen soll auch Informationen über die Abteilung von Mitarbeitern enthalten, die in einer weiteren Tabelle namens tblAbteilungen enthalten sind. Zur Übung können Sie diese Tabelle kurz mit folgender Anweisung erstellen: CREATE TABLE tblAbteilungen( AbteilungID INTEGER CONSTRAINT PKAbteilungID PRIMARY KEY, Abteilung TEXT(50) CONSTRAINT UKAbteilung UNIQUE)
Nun passen Sie den SQL-Ausdruck zum Erstellen der Mitarbeitertabelle so an, dass sie direkt ein Fremdschlüsselfeld für die Verknüpfung mit der Tabelle tblAbteilungen anlegt: CREATE TABLE tblMitarbeiter_( MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY, Mitarbeiternummer TEXT(10) CONSTRAINT UniqueMitarbeiternummer UNIQUE, Vorname TEXT(50) NOT NULL, Nachname TEXT (50) NOT NULL, AbteilungID INTEGER, CONSTRAINT FKAbteilungID FOREIGN KEY (AbteilungID) REFERENCES tblAbteilungen)
In der Entwurfsansicht macht sich die besondere Eigenschaft des Feldes AbteilungID als Fremdschlüssel nicht direkt bemerkbar. Dafür wird die so erstellte Beziehung zwischen den beiden Tabellen tblMitarbeiter- und tblAbteilungen aber im Beziehungsfenster sichtbar (siehe Abbildung 7.24).
Abbildung 7.24: Eine per SQL-Anweisung erstellte Beziehung zwischen zwei Tabellen
Datenmodell erstellen und manipulieren
373
Lösch- und Aktualisierungsweitergabe Im Eigenschaftenfenster einer Beziehung können Sie angeben, ob die Beziehung mit referentieller Integrität festgelegt und dabei Aktualisierungsweitergabe und Löschweitergabe realisiert werden sollen (siehe Abbildung 7.25).
Abbildung 7.25: Optionen für Beziehungen mit referentieller Integrität
Das Einstellen dieser Eigenschaften funktioniert erst mit SQL-92. Wenn Sie nicht mit dem Datenbankformat Access 2002 oder höher arbeiten und die Datenbank auf SQL Server-kompatible Syntax eingestellt haben, können Sie diese nur per VBA/ADO verwenden. Die folgende Beispielprozedur, die eine Tabelle mit Aktualisierungsweitergabe und Löschweitergabe anlegt, verwendet ADO für die Ausführung der SQL-Anweisung: Public Function CREATETABLEMitANSI92() Dim cnn As ADODB.Connection Dim strSQL As String strSQL = "CREATE TABLE tblMitarbeiter(" _ & "MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY, " _ & "Mitarbeiternummer TEXT(10) " _ & "CONSTRAINT UniqueMitarbeiternummer UNIQUE, " _ & "Vorname TEXT(50) NOT NULL, " _ & "Nachname TEXT (50) NOT NULL, " _ & "AbteilungID INTEGER, " _ & "CONSTRAINT FKAbteilungID FOREIGN KEY (AbteilungID) " _
374
7
Access-SQL
& "REFERENCES tblAbteilungen ON UPDATE CASCADE ON DELETE CASCADE)" Set cnn = CurrentProject.Connection cnn.Execute strSQL Set cnn = Nothing End Function Listing 7.2: Verwenden von SQL-92 per VBA und ADO
Einfache Indizes anlegen Ein Blick in den Entwurf der Tabelle tblMitarbeiter und dort auf den Dialog Indizes offenbart, dass dieses Feld noch nicht indiziert ist, obwohl es als Fremdschlüsselfeld dient. Aus Gründen der Performance sollte dies allerdings noch nachgeholt werden – indizierte Felder auf beiden Seiten einer Beziehung machen sich hier sehr gut. Einen solchen Index erstellen Sie nicht direkt mit der Tabelle, sondern mit einer separaten Anweisung: CREATE INDEX IndexAbteilung ON tblMitarbeiter(Vorname)
Abbildung 7.26 zeigt den Dialog Indizes der auch den neu hinzugefügten Index IAbteilungID enthält. Diesen Dialog aktivieren Sie, indem Sie die betroffene Tabelle in der Entwurfsansicht und den Menüeintrag Ansicht/Indizes öffnen.
Abbildung 7.26: Auflistung der Indizes der Tabelle tblMitarbeiter
Einsatz zusammengesetzter CONSTRAINTS Die oben genannten CONSTRAINTS können Sie auch für mehrere Felder gleichzeitig verwenden. Das bedeutet etwa beim Beispiel des eindeutigen Index, dass die Kombination von zwei oder mehr Feldern nicht in mehr als einem Datensatz gleich sein darf. Es können also durchaus die Inhalte eines oder mehrerer Felder zweier Datensätze übereinstimmen, aber eben nicht alle.
Datenmodell erstellen und manipulieren
375
Zusammengesetzter Primärschlüssel Einen zusammengesetzten Primärschlüssel benötigen Sie beispielsweise in Fällen, in denen Sie eine Verknüpfungstabelle für die Herstellung einer m:n-Beziehung erstellen. Meist sollen die Kombinationen der Datensätze aus den beiden betroffenen Tabellen eindeutig sein – zum Beispiel soll einem Fahrzeug jedes Ausstattungsmerkmal nur einmal zugewiesen werden. Die folgende SQL-Anweisung illustriert die Vorgehensweise: CREATE TABLE tblFahrzeugeAusstattungsmerkmale( FahrzeugID INTEGER, AusstattungsmerkmalID INTEGER, CONSTRAINT PKFahrzeugIDAusstattungsmerkmalID PRIMARY KEY(FahrzeugID, AusstattungsmerkmalID))
Das Ergebnis sieht wie in Abbildung 7.27 aus.
Abbildung 7.27: Tabelle mit zusammengesetztem Primärschlüssel
Zusammengesetzter eindeutiger Schlüssel Die SQL-Anweisung zum Erstellen eines zusammengesetzten eindeutigen Schlüssels erfolgt genau wie beim zusammengesetzten Primärschlüssel – Sie müssen nur PRIMARY KEY durch UNIQUE KEY ersetzen.
7.4.3 Tabelle ändern Bei Bedarf können Sie eine Tabelle und ihre Felder auch nachträglich ändern. Die Syntax für die ALTER TABLE-Anweisung lautet folgendermaßen: ALTER TABLE Tabelle {ADD {COLUMN Feld Typ[(Größe)] [NOT NULL] [CONSTRAINT Index] | ALTER COLUMN Feld Typ[(Größe)] | CONSTRAINT Mehrfeldindex} | DROP {COLUMN Feld I CONSTRAINT Indexname} }
Daraus lassen sich die drei Funktionen aus den folgenden Abschnitten ableiten.
376
7
Access-SQL
Hinzufügen eines Feldes Um ein Feld einer bestehenden Tabelle hinzuzufügen, verwenden Sie beispielsweise folgende Anweisung, die das Feld Strasse an die Tabelle tblMitarbeiter anfügt: ALTER TABLE tblMitarbeiter ADD Strasse Text(50);
Auch die CONSTRAINT- und die NOT NULL-Klauseln lassen sich hiermit verwenden. Ein neues Fremdschlüsselfeld (etwa das Feld GeschlechtID in der Tabelle tblMitarbeiter) fügen Sie mit folgender Anweisung hinzu: ALTER TABLE tblMitarbeiter ADD GeschlechtID INTEGER, CONSTRAINT FKGeschlechtID FOREIGN KEY(GeschlechtID) REFERENCES tblGeschlecht;
Ändern eines Feldes Das Ändern bestehender Felder erfolgt prinzipiell auf dem gleichen Weg wie das Anlegen neuer Felder. Sie verwenden lediglich das Schlüsselwort ALTER COLUMN statt ADD COLUMN: ALTER TABLE tblMitarbeiter ALTER COLUMN Vorname TEXT(40);
Mit ALTER TABLE lassen sich nicht nur Feldnamen, sondern auch Datentypen und Feldgrößen ändern. Beachten Sie, dass nicht alle Quell- und Zielfeldtypen kompatibel sind.
Löschen eines Feldes Beim Löschen von Feldern müssen Sie beachten, dass Felder, die Teil eines Index sind, nur nach Entfernen des Index oder zusammen mit diesem gelöscht werden können. Die folgende Anweisung scheitert beispielsweise, wenn das Feld GeschlechtID mit der Tabelle tblGeschlecht verknüpft ist: ALTER TABLE tblMitarbeiter DROP GeschlechtID;
In diesem Fall entfernen Sie zunächst den Index: ALTER TABLE tblMitarbeiter DROP CONSTRAINT FKGeschlechtID;
Anschließend funktioniert die obige Anweisung zum Löschen des Feldes.
Datenmodell erstellen und manipulieren
377
7.4.4 Tabelle löschen Zum Löschen einer Tabelle verwenden Sie die DROP-Anweisung zusammen mit der Objektart TABLE und dem Namen der zu löschenden Tabelle: DROP TABLE Tabelle
Die Tabelle tblMitarbeiter entfernen Sie beispielsweise mit folgender Anweisung: DROP TABLE tblMitarbeiter
7.4.5 Index löschen Auch Indizes lassen sich mit der DROP-Anweisung löschen. Hier lautet die Syntax: DROP INDEX Index ON Tabelle
Beispiel: DROP INDEX PKMitarbeiterID ON tblMitarbeiter
8 DAO DAO (Data Access Objects) ist neben den ADO (Active Data Objects) eine der beiden Bibliotheken für den Zugriff auf die Daten in einer Access-Datenbank. In Access 97 war DAO noch Alleinunterhalter auf diesem Gebiet, ab Access 2000 kam dann ADO hinzu. ADO ist eigentlich als legitimer Nachfolger von DAO vorgesehen, denn es ermöglicht sowohl die Verwendung von herkömmlichen Access-Datenbanken als auch von Access-Projekten (*.adp), die ebenfalls mit Access 2000 eingeführt wurden. In Access-Projekten, die die MSDE oder den Microsoft SQL Server als DatenbankBackend verwenden, ist ADO definitiv die bessere Wahl, denn damit können Sie direkt via OLE DB auf die Daten zugreifen. Um von einer Access-Datenbank, also einer .mdbDatei, an die Daten in einer MSDE- oder SQL Server-Datenbank heranzukommen, müssen Sie die benötigten Tabellen zunächst verknüpfen und können dann per DAO oder ADO darauf zugreifen. Das ist in jedem Fall nur die zweitbeste Wahl. Warum also wird DAO in diesem Buch ein eigenes Kapitel gewidmet? Dafür gibt es eine Menge Gründe, die alle genauso die Frage beantworten könnten, warum DAO überhaupt noch verwendet wird: DAO gibt es bereits einige Zeit und es läuft mittlerweile stabil und fehlerfrei. DAO ist für den Einsatz mit der Jet-Engine optimiert. In Zusammenarbeit mit der Jet-Engine ist es in vielen Fallen schneller als ADO. Einige Features von Access, etwa die Verwendung der RecordsetClones in Formularen oder das Komprimieren und Reparieren per VBA, erfordern den Einsatz von DAO. Aus diesen Gründen lässt sich schon erkennen, wann die Anwendung von DAO Sinn macht und wann man eher ADO den Vorzug geben sollte. Wenn Sie eine Access-Anwendung mit einem überschaubaren Lebenszyklus planen, deren Anwendungszweck keine Wünsche nach einer Erweiterung in Richtung eines »größeren« Systems, also MSDE oder SQL Server, beinhaltet, sind Sie mit DAO gut bedient und entscheiden sich damit unter anderem für die meist bessere Performance. Wenn Sie eine bestehende Access-Anwendung weiterentwickeln, die mit DAO arbeitet, hängt es ebenfalls von den Zukunftsplänen mit dieser Anwendung ab – bei einer
380
8
DAO
geplanten Vergrößerung in Richtung SQL Server macht ein Umstieg auf ADO definitiv Sinn – je früher, desto besser. Je mehr hier noch in DAO hinzuprogrammiert wird, desto mehr ist anschließend nach ADO umzuwandeln. Sie können natürlich auch mit beiden Bibliotheken parallel arbeiten – und so bei Bedarf neue Bestandteile mit ADO entwickeln und Bestehendes Stück für Stück umwandeln. Wenn Sie DAO bereits kennen und sich noch nicht besonders mit ADO auseinander gesetzt haben, lohnt es unter Umständen vielleicht gar nicht mehr, sich noch mit ADO zu beschäftigen – ist doch ADO.NET bereits allgegenwärtig. Das ist allerdings hypothetisch, denn zum Zeitpunkt der Drucklegung dieses Buchs gab es noch keine verbindliche Aussage von Microsoft, ob und wann die .NET-Technologie (vielleicht in Form von VBA.NET?) Einzug in Access hält. Für den Fall, dass Sie aufgrund der oben genannten Gründe DAO für die richtige Datenzugriffstechnik unter VBA halten, finden Sie in den nächsten Abschnitten alles Wichtige zu diesem Thema. Anderenfalls überspringen Sie dieses Kapitel einfach und wenden sich direkt Kapitel 9, »ADO« zu. Beispiele auf CD: Sie finden alle Code-Beispiele dieses Kapitels auf der Buch-CD unter Kap_08/DAO.mdb im Modul mdlDAO. Die Datenbankdatei enthält auch die für die Beispiele verwendeten Tabellen.
8.1 DAO und ADO im Einsatz In der Access-Welt gibt es Datenbanken, die nur DAO, nur ADO oder auch beide Objektbibliotheken verwenden. Wenn Sie niemals Probleme bekommen möchten, weil Sie die (etwas anderen) Methoden etwa eines Recordset-Objekts von DAO unter ADO verwenden, sollten Sie direkt klarstellen, mit welchem Objektmodell Sie aktuell arbeiten. Im Verweise-Dialog (zu öffnen mit dem Menüeintrag Extras/Verweise der VBA-Entwicklungsumgebung) finden Sie gegebenenfalls beide Bibliotheken (siehe Abbildung 8.1). Wenn Sie beide Objektbibliotheken gleichzeitig eingebunden haben, kann es vorkommen, dass Sie eines der gleichnamigen Elemente einer der Objektbibliotheken verwenden und auf eine nicht vorhandene oder anders einzusetzende Methode oder Eigenschaft zugreifen. Falls Sie nämlich beide Objektmodelle verwenden und ein in beiden vorkommendes Objekt benutzen, greift Access das im Verweise-Fenster weiter oben angeordnete Objekt zu.
Das DAO-Objektmodell
381
Abbildung 8.1: Aktivierte DAO- und ADO-Verweise
Dem lässt sich vorbeugen: Deklarieren Sie die Objekte einfach direkt mit Bezug auf das richtige Objektmodell, indem Sie den VBA-Namen der entsprechenden Bibliothek voranstellen: 'Deklaration eines Recordset der DAO-Bibliothek Dim rst As DAO.Recordset 'Deklaration eines Recordset der ADO-Bibliothek Dim rst As ADODB.Recordset
Übrigens: Der Screenshot aus Abbildung 8.1 wurde mit Access 2003 erstellt. Der Verweis auf die (ältere) DAO-Bibliothek befindet sich über dem Verweis auf die ADOObjektbibliothek. Dazu mag sich jeder seinen Teil denken …
8.2 Das DAO-Objektmodell Der erste Teil dieses Kapitels verschafft Ihnen einen kleinen Überblick über die Objekte von DAO und zeigt Ihnen, wie Sie auf diese zugreifen. Sie finden hier ausdrücklich keine Referenz mit allen Objekten, Eigenschaften und Methoden, sondern erhalten eine Vorstellung von den wichtigsten Elementen dieser Objektbibliothek. Das Verstehen gerade der obersten Objekte in der DAO-Hierarchie ist in vielen Fällen sehr wichtig. Man kommt zwar lange Zeit ohne großartiges Hintergrundwissen damit aus, einfach ein Database-Objekt und eines oder mehrere Recordsets zu verwenden, aber irgendwann tauchen die ersten Fragen auf.
382
8
DAO
Im Anschluss an diesen Teil erfahren Sie anhand häufig vorkommender Anwendungen mehr über den Praxiseinsatz von DAO.
8.2.1 Zugriff auf die Elemente des Objektmodells Abbildung 8.2 zeigt das DAO-Objektmodell ohne Eigenschaften und Methoden. In der Hierarchie folgt immer eine Auflistung auf ein Objekt und umgekehrt, wobei Auflistungen durch kursive Schreibweise hervorgehoben sind. Alle Zugriffe auf die Objekte erfolgen theoretisch über das oberste Element der Hierarchie namens DBEngine. Es gibt allerdings Möglichkeiten für den Quereinstieg: Der oft benötigte Zugriff auf die aktuelle Datenbank erfordert beispielsweise nur die Instanzierung eines Database-Objekts durch Zuweisung eines Verweises per CurrentDBMethode. Regulär kann der Zugriff auf beliebige Objekte des Objektmodells auf verschiedenen Wegen erfolgen. Der Zugriff auf ein Database-Objekt lässt sich etwa auf folgende Arten realisieren: Über den Namen des Objekts in der Auflistung als Zeichenkette oder Variable (zu verwenden, wenn der Name variabel ist): DBEngine.Workspaces("<Workspacename>").Databases("") DBEngine.Workspaces(strWorkspacename).Databases(strDatenbankname)
Über den Objektnamen: DBEngine.Workspaces!<Workspacename>.Databases!
Über die Ordinalzahl: DBEngine.Workspaces(0).Databases(0)
Über die Ordinalzahl (abgekürzte Variante): DBEngine(0)(0)
Die Abkürzung DBEngine(0)(0)profitiert gleich von zwei Abkürzungen: Erstens ist Workspaces das Default-Element des DBEngine-Objekts und Databases das DefaultElement des Workspace-Elements, und zweitens liefern die jeweils ersten, mit der Zahl 0 versehenen Auflistungselemente den Verweis auf das hier benötigte aktuelle Database-Objekt.
Das DAO-Objektmodell
383
DBEngine Errors Error Workspaces Workspace Groups Group Users
Users User
User
Databases
Groups
Database
Group
Containers Container QueryDefs
Documents
QueryDef Recordsets
Document Fields
Recordset Relations
Field Fields
Relation TableDefs
Parameters Field
Parameter
Fields TableDef
Field Indexes Index Fields
Fields Field
Field
Abbildung 8.2: Das DAO-Objektmodell im Überblick
8.2.2 Deklarieren und Instanzieren Theoretisch kann man auf Objekte des DAO-Objektmodells zugreifen, ohne überhaupt eine Variable zu verwenden. Folgende Anweisung gibt etwa die Anzahl Datensätze einer Tabelle aus, wenn Sie im Direktfenster abgesetzt wird (in einer Zeile): Debug.Print DBEngine.Workspaces(0).Databases(0).TableDefs("tblMitarbeiter").RecordCount
In VBA-Routinen ist dies auch möglich, aber es ist kein guter Programmierstil und außerdem leidet unter Umständen die Performance darunter – nämlich dann, wenn Sie mehr als einmal auf ein Objekt zugreifen möchten. Dann macht es Sinn, eine Objektvariable anzulegen und mit dieser auf das gewünschte Objekt zu verweisen.
384
8
DAO
Die Routine aus Listing 8.1 deklariert zunächst die gewünschten Objekte und weist diese anschließend den Objektvariablen zu – dabei arbeitet sie sich in der Objekthierarchie von oben nach unten durch. Während die Objekte Workspace, Database und Recordset explizit zugewiesen werden, erfolgt der Zugriff auf die Field-Elemente des TableDef-Objekts per For Each-Schleife über dessen Fields-Auflistung. Public Sub Tabelleneigenschaften() Dim wrk As DAO.Workspace Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field Set wrk = DBEngine.Workspaces(0) Set db = wrk.Databases(0) Set tdf = db.TableDefs("tblMitarbeiter") For Each fld In tdf.Fields Debug.Print fld.Name Next fld Set tdf = Nothing Set db = Nothing Set wrk = Nothing End Sub Listing 8.1: Beispiel für das Deklarieren und Instanzieren von Objekten des DAO-Objektmodells
Kürzer – und in vielen Fällen sinnvoller, wie sich weiter unten zeigen wird – ist folgende Variante. Dabei wird die Referenz direkt über die Methode CurrentDB erzeugt. Diese Variante ist die am meisten verbreitete: Public Sub TabelleneigenschaftenKurz() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field Set db = CurrentDb Set tdf = db.TableDefs("tblMitarbeiter") For Each fld In tdf.Fields Debug.Print fld.Name Next fld Set tdf = Nothing Set db = Nothing End Sub Listing 8.2: Erstellen einer Objektreferenz auf ein Database-Objekt per CurrentDB
Das DAO-Objektmodell
385
8.2.3 Auf Auflistungen zugreifen In Listing 8.2 haben Sie bereits ein Beispiel für das Durchlaufen einer Auflistung des DAO-Objektmodells kennen gelernt. Es gibt zwei Möglichkeiten, die zahlreichen Auflistungen dieses Objektmodells zu durchlaufen: die For Next-Schleife und die For EachSchleife. Die For Next-Schleife erscheint als eines der in fast allen Programmiersprachen vertretenen Konstrukte möglicherweise intuitiver: Public Sub TabellenfelderMitForNext() … Dim i As Integer Set db = CurrentDb Set tdf = db.TableDefs("tblMitarbeiter") For i = 0 To tdf.Fields.Count - 1 Set fld = tdf.Fields(i) Debug.Print fld.Name Next i … End Sub Listing 8.3: Auflistung per For Next-Schleife durchlaufen
Alternativ dazu finden Sie hier die For Each-Variante. Diese Variante spart die Laufvariable i, die Ermittlung der Anzahl der Elemente der Auflistung und die explizite Zuweisung des Field-Objekts ein. Dafür ist die folgende Prozedur aber unmerklich langsamer als die vorherige: Public Sub TabellenfelderKurz() … Set db = CurrentDb Set tdf = db.TableDefs("tblMitarbeiter") For Each fld In tdf.Fields Debug.Print fld.Name Next fld … End Sub Listing 8.4: Zugriff auf eine Auflistung per For Each-Schleife
386
8
DAO
8.2.4 Punkte und Ausrufezeichen DAO und ADO haben eines mit den übrigen Objekten von Access gemeinsam: die gemischte Verwendung von Punkten (.) und Ausrufezeichen (!) als Trennzeichen zwischen zwei miteinander in Beziehung stehenden Objekten, zum Beispiel: Debug.Print rstMitarbeiter!MitarbeiterID.Value
Bezüge zu Steuerelementen in Formularen sehen beispielsweise so aus: Debug.Print Forms!frmMitarbeiter!MitarbeiterID
Dort würde aber auch Folgendes funktionieren: Debug.Print Forms!frmMitarbeiter!MitarbeiterID
Wo setzt man nun welches Zeichen ein und warum funktionieren manchmal beide? Diese Fragen sind relativ leicht zu beantworten. Den Punkt verwendet man immer, wenn das nachgestellte Element ein Access-internes Objekt ist, das Ausrufezeichen kommt bei nachgestellten benutzerdefinierten Elementen zum Zuge. Dabei gilt als benutzerdefiniert alles, was Sie selbst zur Datenbank hinzugefügt haben – Tabellen, Tabellenfelder, Indizes, Formulare und Berichte und die enthaltenen Steuerelemente. Alles andere wird von Access gestellt: eingebaute Objekte, Eigenschaften, Methoden und Ereignisse. Warum in manchen Fällen Punkt und Ausrufezeichen funktionieren? Das geschieht nur scheinbar. Erfahrungen zeigen, dass sporadisch Probleme auftauchen, wenn man die Punkt-Notation anstelle der Ausrufezeichen-Notation verwendet, obwohl Letztere angezeigt ist. Um allen Problemen aus dem Wege zu gehen, schreiben Sie einfach vor alle selbst erstellten Objekte ein Ausrufezeichen.
8.3 DBEngine Das DBEngine-Objekt ist der »Kopf« des DAO-Objektmodells. Beim Start von Access wird automatisch eine Instanz dieses Objekts erzeugt. Es enthält zwei Auflistungen: Die Workspaces-Auflistung und die Errors-Auflistung. Hinweis: In manchen Fällen benötigen Sie eine zusätzliche Instanz des DBEngineObjekts – etwa, wenn Sie auf eine geschützte Datei zugreifen möchten. In diesem Fall erzeugen Sie die neue Instanz mit den folgenden zwei Zeilen: Dim objDBEngine As DAO.DBEngine Set objDBEngine = New DAO.DBEngine
Mehr zu diesem Thema erfahren Sie in Kapitel 16, »Sicherheit«.
Workspace – Arbeitsbereich oder Sitzung?
387
DAO-Fehler verfolgen Die Errors-Auflistung ist ein DAO-eigenes Fehlerobjekt, das Fehler separat vom ErrObjekt von VBA speichert. Die Errors-Auflistung von DAO kann allerdings mehrere Fehler speichern. Der Grund hierfür ist, dass manche DAO-Operationen mehr als einen Fehler auslösen können. Um dennoch alle Fehler auszulesen, speichert DAO diese in der Errors-Auflistung. Der oder die Fehler einer Operation werden so lange in der Errors-Auflistung gespeichert, bis die nächste fehlerhafte Anweisung ausgeführt wird.
8.4 Workspace – Arbeitsbereich oder Sitzung? Die zweite Auflistung unterhalb des DBEngine-Objekts enthält die Workspaces einer Datenbank. Workspace ist eigentlich die Übersetzung für Arbeitsbereich, aber Benutzersitzung trifft in diesem Fall eher den Punkt. Ein Workspace wird mit dem Anmelden eines Benutzers an eine Datenbank erzeugt und mit dessen Abmeldung wieder zerstört. Sie können in einer einzigen Access-Instanz mehrere Workspace-Objekte erzeugen – das macht vor allem Sinn, wenn Sie automatisch die Wechselwirkungen von Sperrmechanismen oder Transaktionen im Mehrbenutzerbetrieb testen möchten. Um ein neues Workspace-Objekt zu erzeugen, reicht die CreateWorkspace-Methode aus, an die Workspaces-Auflistung wird es allerdings nicht automatisch angehängt. Das Erzeugen eines Workspace-Objekts und das Anhängen an die entsprechende Auflistung zeigt folgendes Beispiel: Public Sub WorkspacesSample() Dim wrk As DAO.Workspace Dim wrkExt As DAO.Workspace 'Neues Workspace-Objekt erzeugen... '(Arbeitsgruppe ist in diesem Fall die Standardsarbeitsgruppe) Set wrkExt = DBEngine.CreateWorkspace("#New Workspace#", "admin", "") '...und an die Workspaces-Auflistung anhängen Workspaces.Append wrkExt 'Namen der Workspaces ausgeben Debug.Print "Workspaces:" For Each wrk In DBEngine.Workspaces Debug.Print wrk.Name Next wrk End Sub Listing 8.5: Anlegen eines neuen Workspace-Objekts und Anhängen an die Workspaces-Auflistung
388
8
DAO
8.4.1 Auflistungen des Workspace-Objekts Das Workspace-Objekt enthält drei Auflistungen, die weiter unten ausführlicher beschrieben werden: Databases: Enthält alle Database-Objekte, die im Kontext der Sitzung geöffnet wurden. Users: Enthält eine Auflistung aller Benutzer der aktuellen Arbeitsgruppe. Groups: Enthält eine Auflistung aller Benutzergruppen der aktuellen Arbeitsgruppe.
8.4.2 Aufgaben des Workspace-Objekts Das Workspace-Objekt stellt eine ganze Reihe Funktionen zur Verfügung. Zwei davon sind besonders wichtig: das Verwalten von Transaktionen und das Verwalten von Benutzern und Benutzergruppen. Beide werden separat behandelt: die Transaktionen in Abschnitt 8.11 dieses Kapitels und die Benutzerverwaltung in Kapitel 16, »Sicherheit«. Darüber hinaus enthält es Funktionen zum Erzeugen neuer Datenbanken (CreateDatabase) oder zum Öffnen weiterer Datenbanken im gleichen Workspace (OpenDatabase). Informationen zu weiteren Elementen finden Sie in der Online-Hilfe.
8.4.3 Datenbanken erzeugen und öffnen Im Kontext eines Workspace-Objekts lassen sich neue Datenbanken erzeugen und bestehende Datenbanken öffnen. Eine neue Datenbank erzeugen Sie mit der CreateDatabase-Methode, eine bestehende Datenbank öffnen Sie mit OpenDatabase. Eine aus der aktuellen Datenbank heraus erzeugte neue Datenbank könnte beispielsweise dem Auslagern temporärer Tabellen dienen. Auf diese Weise würden temporäre Daten die Datenbank nicht unnötig aufblähen. Besonders interessant ist diese Vorgehensweise, wenn die Datenbankgröße ein kritisches Maß erreicht hat und die temporären Daten diese sprengen könnten.
8.5 Aktuelle Datenbank referenzieren Um per DAO auf die aktuelle Datenbank zuzugreifen, gibt es zwei Möglichkeiten: Entweder man arbeitet sich vom obersten Element der Objekthierarchie bis zum DatabaseObjekt vor und verweist darauf oder man verwendet direkt die CurrentDB-Methode, um einen Verweis zu erhalten: DBEngine.Workspaces(0).Databases(0)
Aktuelle Datenbank referenzieren
389
oder abgekürzt DBEngine(0)(0)
verweisen dabei auf die aktuelle Datenbank, während CurrentDB
einen neuen Verweis erstellt und diesen zurückliefert. Welche Vor- und Nachteile gibt es nun und welche Variante setzt man wann ein? Die DBEngine-Version ist die einzige der beiden Möglichkeiten, mit der man von außerhalb – also etwa von Excel oder Word aus – auf eine Access-Datenbank zugreifen kann. Intern sind beide Varianten möglich. CurrentDB hat dabei den Vorteil, dass es immer auf die aktuelle Datenstruktur zugreift, während dies bei DBEngine(0)(0) nicht immer der Fall ist. CurrentDB aktualisiert nämlich sämtliche im Database-Objekt enthaltenen Auflistungen, während bei der Verwendung von DBEngine ein zusätzlicher Aufruf der Refresh-Methode erforderlich ist. Den von CurrentDB zurückgegebenen Verweis muss man speichern, um länger als für die Dauer der aktuellen Anweisung auf das DatabaseObjekt und untergeordnete Objekte zugreifen zu können; greift man über CurrentDB auf untergeordnete Objekte zu, sind diese nur für die Dauer der Anweisung existent. Der Nachteil von CurrentDB ist, dass es minimal langsamer als DBEngine(0)(0) ist – was sich allerdings im normalen Betrieb kaum bemerkbar macht. Ein Beispiel für dieses Verhalten finden Sie in Abbildung 8.3. Ausnahmen für dieses Verhalten gibt es auch: Wenn Sie mit folgendem Quellcode eine Datensatzgruppe erzeugen, verschwindet das Objekt nicht von alleine wieder in die ewigen Jagdgründe: Public Sub CurrentDBPersistenzMitRecordset() Dim rst As Recordset Set rst = CurrentDb.OpenRecordset("tblMitarbeiter", dbOpenDynaset) Debug.Print rst.RecordCount Set rst = Nothing End Sub Listing 8.6: Erfolgreiches Erzeugen eines Objektverweises mit CurrentDB und untergeordnetem Recordset-Objekt
390
8
DAO
Abbildung 8.3: Fehler beim Versuch, einen Verweis auf ein untergeordnetes Objekt mit CurrentDB herzustellen
8.5.1 Users und Groups Die Auflistung Users und Groups sowie die enthaltenen User- und Group-Elemente dienen der Verwaltung von Benutzern und Benutzergruppen. Dies ist insbesondere in Zusammenhang mit der Sicherheit von Access-Datenbanken interessant. Die Verwaltung von Benutzern und Benutzergruppen sowie deren Zugriffsrechte wird aber vermutlich meist über die Verwendung der Benutzungsoberfläche realisiert und weniger über eine extra programmierte Schnittstelle. Weitere Informationen zum Thema Sicherheit finden Sie in Kapitel 16, »Sicherheit«.
8.6 Das Database-Objekt Das Database-Objekt enthält einige Auflistungen, Eigenschaften und Methoden, von denen vier Kategorien in den folgenden Abschnitten besonderes Augenmerk erhalten: Manipulation des Datenmodells Zugriff auf Auflistungen wie QueryDefs, Recordsets, Relations und TableDefs Anwenden der OpenRecordset-Methode Ausführen von Aktionsabfragen
8.6.1 Manipulation des Datenmodells Tabellen, Felder und Beziehungen lassen sich nicht nur mit den Data Definion Language (DML)-Anweisungen von SQL erzeugen, bearbeiten und löschen, sondern auch per DAO.
Das Database-Objekt
391
Dazu stehen die folgenden Methoden zur Verfügung: CreateDatabase (Workspace-Objekt): Erzeugen einer Datenbankdatei CreateTableDef (Database-Objekt): Erstellen einer Tabelle CreateProperty (Database-Objekt): Anlegen einer Eigenschaft CreateQueryDef (Database-Objekt): Erstellen einer Abfrage CreateRelation (Database-Objekt): Erstellen einer Tabellenbeziehung CreateField (TableDef-Objekt, Relations-Objekt, Index-Objekt): Erstellen eines Feldes CreateIndex (TableDef-Objekt): Erstellen eines Index CreateProperty (TableDef-Objekt, Index-Objekt): Erstellen einer Eigenschaft
Erstellen einer Tabelle Die folgende Routine zeigt, wie man eine neue Tabelle namens tblUnternehmen und die beiden Felder UnternehmenID und Unternehmen anlegt und in der Datenbank verfügbar macht. Zu beachten ist hier, dass Sie eine Menge Tabellen und Felder mit CreateTable und CreateField erzeugen können – aber wenn Sie diese nicht an die entsprechenden Auflistungen TableDefs beziehungsweise Fields anhängen, sind alle erzeugten Objekte mit Ablauf der Routine wieder verschwunden. Abbildung 8.4 zeigt die Abhängigkeiten der an der Erstellung einer Tabelle beteiligten Elemente der DAO-Objektbibliothek. Wenn Sie sich merken, dass Sie einfach zuerst die einzelnen Elemente erzeugen und diese dann an die Auflistung des übergeordneten Elements anhängen müssen, haben Sie das Objektmodell bezüglich der Erstellung von Objekten schon fast im Griff. Databases Database TableDefs TableDef Fields Field
Abbildung 8.4: Hierarchie der beim Erstellen einer Tabelle beteiligten Elemente der DAO-Objektbibliothek
392
8
DAO
Tabelle 8.1 enthält alle Konstanten, die Sie für die Konstante Type der CreateFieldAnweisung verwenden können, wobei die kursiv gedruckten in .mdb-Dateien nicht zur Verfügung stehen, sondern nur in ODBC-Tabellen (SQL-Server-Datenbanken). Public Sub TabelleAnlegen() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field 'Verweis auf aktuelle Datenbank Set db = CurrentDb 'Verweis auf neues TableDef-Objekt Set tdf = db.CreateTableDef 'Name der Tabelle zuweisen tdf.Name = "tblUnternehmen" 'Feld neu erstellen und per Objektvariable referenzieren Set fld = tdf.CreateField("UnternehmenID", dbDouble) 'Feld an die Feldliste der Tabelle anhängen tdf.Fields.Append fld 'Gleiches Spiel mit einem zweiten Feld Set fld = tdf.CreateField("Unternehmen", dbText, 255) tdf.Fields.Append fld 'Die bisher nicht vorhandene Tabelle zur TableDefs-Auflistung hinzufügen db.TableDefs.Append tdf 'Auflistung aktualisieren db.TableDefs.Refresh 'Datenbankfenster aktualisieren Application.RefreshDatabaseWindow Set db = Nothing End Sub Listing 8.7: Erstellen einer Tabelle mit DAO
Konstante
Datentyp
dbBigInt
Big Integer
dbBinary
Binary
Tabelle 8.1: Konstanten für den Datentyp in der CreateField-Anweisung
Das Database-Objekt
393
Konstante
Datentyp
dbBoolean
Boolean
dbByte
Byte
dbChar
Char
dbCurrency
Currency
dbDate
Date/Time
dbDecimal
Decimal
dbDouble
Double
dbFloat
Float
dbGUID
GUID
dbInteger
Integer
dbLong
Long
dbLongBinary
Long Binary (OLE Object)
dbMemo
Memo
dbNumeric
Numeric
dbSingle
Single
dbText
Text
dbTime
Time
dbTimeStamp
Time Stamp
dbVarBinary
VarBinary (OLE-Objekt)
Tabelle 8.1: Konstanten für den Datentyp in der CreateField-Anweisung (Fortsetzung)
Autowert anlegen Wenn Sie das Primärschlüsselfeld der Tabelle als Autowertfeld auslegen möchten, brauchen Sie lediglich ein einziges Attribut zu setzen. Dazu fügen Sie zwischen der CreateField-Anweisung und der Append-Anweisung zum Hinzufügen des Feldes die folgende Zeile ein: 'Attributes-Eigenschaft auf Autowert einstellen fld.Attributes = dbAutoIncrField
Löschen der Tabelle Das Löschen einer Tabelle erfolgt über die Delete-Methode der TableDefs-Auflistung. Diese Methode erwartet die Angabe des Namens der zu löschenden Tabelle als Parameter: Public Sub TabelleLoeschen() Dim db As DAO.Database
394
8
DAO
Set db = CurrentDb db.TableDefs.Delete "tblUnternehmen" db.TableDefs.Refresh Application.RefreshDatabaseWindow Set db = Nothing End Sub Listing 8.8: Löschen der soeben erzeugten Tabelle tblUnternehmen
Die vorhergehenden beiden Beispielroutinen und auch die folgenden enthalten keine Fehlerbehandlung. Für den Praxiseinsatz ist eine Fehlerbehandlung natürlich Pflicht, aber hier wird diese aus Gründen der Übersichtlichkeit ausgespart. Mehr zum Thema Fehlerbehandlung erfahren Sie in Kapitel 11, »Debugging, Fehlerbehandlung und Fehlerdokumentation«.
Erstellen eines Index Der oben erstellten Tabelle fehlt noch der Primärschlüssel auf dem Feld UnternehmenID. Die wichtigste Funktion in der folgenden Routine ist die Methode CreateIndex des TableDef-Objekts. Der Weg zum Ziel ist hier in drei Ebenen verschachtelt. Abbildung 8.5 zeigt die an der Indexerstellung beteiligten Objekte des DAO-Objektmodells und deren Anordnung. Beim Erstellen der am Index beteiligten Field-Objekte müssen Sie nur noch den Namen des jeweiligen Feldes angeben. Voraussetzung ist, dass dieses Feld bereits existiert. Databases Database TableDefs TableDef Indexes Index Fields Field
Abbildung 8.5: Elemente der DAO-Bibliothek zum Anlegen eines Index
Public Sub IndexErstellen() Dim db As DAO.Database Dim idx As DAO.Index
Das Database-Objekt
395
Dim fld As DAO.Field Set db = CurrentDb 'Index erstellen Set idx = db.TableDefs("tblUnternehmen").CreateIndex("PrimaryKey") 'Feld für den Index angeben Set fld = idx.CreateField("UnternehmenID") 'Feld an Fields-Auflistung des Index anhängen idx.Fields.Append fld 'Index als Primärschlüssel kennzeichnen idx.Primary = True 'Index an Indexes-Auflistung der Tabelle anhängen db.TableDefs("tblUnternehmen").Indexes.Append idx db.TableDefs.Refresh Application.RefreshDatabaseWindow Set idx = Nothing Set db = Nothing End Sub Listing 8.9: Anlegen eines Primärschlüssels für eine Tabelle
Löschen eines Index Das Löschen eines Index erfolgt über die Delete-Methode der Indexes-Auflistung des betroffenen TableDef-Objekts. Die Methode erwartet den Namen des zu löschenden Index als Parameter. Public Sub IndexLoeschen() Dim db As DAO.Database Set db = CurrentDb db.TableDefs("tblMitarbeiter").Indexes.Delete "PrimaryKey" Set db = Nothing End Sub Listing 8.10: Entfernen eines Index
Erstellen einer Beziehung Das letzte fehlende Element zur Erstellung kompletter Datenmodelle per DAO ist die Methode CreateRelation zum Erstellen von Beziehungen zwischen zwei Tabellen.
396
8
DAO
Das nachfolgende Beispiel erstellt eine Beziehung zwischen den beiden Tabellen tblMitarbeiter und tblUnternehmen. Die Beziehung soll über das Feld UnternehmenID hergestellt werden, das in der Tabelle tblUnternehmen als Primärschlüsselfeld mit dem Datentyp Long und in der Tabelle tblMitarbeiter als einfaches Long-Feld vorhanden ist. Neben den beteiligten Tabellen und Feldern ist die Art der Beziehung sehr wichtig. Tabelle 8.2 zeigt alle möglichen Beziehungsarten. Standardmäßig legt die CreateRelation-Methode eine Beziehung mit referentieller Integrität an – dies entspricht dem Wert 0 für die Eigenschaft Attributes des Relation-Objekts. Die eigentümlichen Zahlenwerte der Konstanten für diese Eigenschaft rühren daher, dass man die Konstanten miteinander kombinieren können soll. Wenn Sie also etwa eine Beziehung mit referentieller Integrität als LEFT JOIN mit Aktualisierungs- und Löschweitergabe festlegen möchten, addieren Sie die Werte der einzelnen Konstanten in einer boolschen Operation – also 0 Or 256 Or 4096 Or 16777216. Sie können diesen Ausdruck als Wert der Eigenschaft Attributes eintragen oder auch vorher die Summe berechnen. Die folgende Routine erstellt die Beziehung aus Abbildung 8.6. Public Sub BeziehungErstellen() Dim db As DAO.Database Dim rel As DAO.Relation Dim fld As DAO.Field Set db = CurrentDb 'Erstellen der Beziehung Set rel = db.CreateRelation() 'Zuweisen der benötigten Informationen: 'Name der Beziehung rel.Name = "relMitarbeiterUnternehmen" 'Name der Tabelle mit dem Fremdschlüsselfeld rel.ForeignTable = "tblMitarbeiter" 'Name der Tabelle mit dem Primärschlüssel rel.Table = "tblUnternehmen" 'Typ der Beziehung rel.Attributes = 0 'Feld, über das die Beziehung hergestellt werden soll Set fld = rel.CreateField("UnternehmenID", dbLong) 'Name des Feldes in der Detailtabelle fld.ForeignName = "UnternehmenID"
Das Database-Objekt
397
'Name des Feldes in der Mastertabelle fld.Name = "UnternehmenID" 'Anhängen des Feldes an die Fields-Auflistung der Beziehung rel.Fields.Append fld 'Anhängen der Beziehung an die Relations-Auflistung der Datenbank db.Relations.Append rel Set rel = Nothing Set db = Nothing End Sub Listing 8.11: Herstellen einer Beziehung zwischen zwei Tabellen
Konstante
Zahlenwert
Bedeutung
dbRelationUnique
1
1:1-Beziehung
dbRelationDontEnforce
2
Die Beziehung wird nicht erzwungen (keine referentielle Integrität).
dbRelationInherited
4
Die Beziehung besteht in Datenbanken, die zwei verknüpfte Tabellen enthalten.
dbRelationUpdateCascade
256
Aktualisierungsweitergabe
dbRelationDeleteCascade
4096
Löschweitergabe
dbRelationLeft
16777216
Nur in Microsoft Access. Erstellt ein LEFT JOIN zwischen den Tabellen.
dbRelationRight
33554432
Nur in Microsoft Access. Erstellt ein RIGHT JOIN zwischen den Tabellen.
Tabelle 8.2: Konstanten, Zahlenwerte und Bedeutung für die Eigenschaft Attributes beim Anlegen einer Beziehung
Abbildung 8.6: Diese Beziehung wurde mit der Prozedur aus Listing 8.11 angelegt.
398
8
DAO
Löschen einer Beziehung Das Löschen einer Beziehung erfolgt über die Methode Delete der Relations-Auflistung. Beispiel: Public Sub BeziehungLoeschen() … db.Relations.Delete "relMitarbeiterUnternehmen" … End Sub Listing 8.12: Löschen einer Beziehung
Erstellen von Eigenschaften Manche Eigenschaften von Objekten der Datenbank sind unter DAO nicht verfügbar – zumindest nicht als eingebaute Eigenschaften. Diese Eigenschaften heißen benutzerdefinierte Eigenschaften und müssen vor dem Verwenden zunächst erstellt werden. Die folgende Routine zeigt, wie Sie einem Währungsfeld einer Tabelle die Eigenschaft Format hinzufügen und diese direkt mit einem Wert füllen. Public Sub PropertyEinsetzen() … Set db = CurrentDb Set tdf = db.TableDefs("tblArtikel") Set fld = tdf.Fields("Preis") Set prp = fld.CreateProperty("Format", dbText, "#,##0.00 USD") fld.Properties.Append prp … End Sub Listing 8.13: Einstellen der Format-Eigenschaft eines Tabellenfeldes auf ein bestimmtes Währungsformat
8.6.2 Zugriff auf Auflistungen und Elemente In den vorhergehenden Abschnitten haben Sie Tabellen, Felder, Indizes und Relationen erstellt. Sie werden in vielen Fällen auf diese Objekte zugreifen müssen – normalerweise schon beim Erstellen solcher Objekte, etwa um zu überprüfen, ob ein Objekt bereits vorhanden ist.
Alle Tabellen ausgeben Die Prozedur TabellenAusgeben durchläuft die Auflistung TableDefs und gibt zu jedem enthaltenen Element den Wert der Name-Eigenschaft aus.
Das Database-Objekt
399
Public Sub TabellenAusgeben() Dim db As DAO.Database Dim tdf As TableDef Set db = CurrentDb For Each tdf In db.TableDefs Debug.Print tdf.Name Next tdf Set db = Nothing End Sub Listing 8.14: Ausgeben aller Tabellen einer Datenbank
Prüfen, ob eine Tabelle schon vorhanden ist Es gibt (mindestens) zwei Möglichkeiten, um das Vorhandensein einer Tabelle zu prüfen. Die erste arbeitet mit der gleichen Technik wie die Prozedur aus Listing 8.14: Sie durchläuft alle Tabellen und vergleicht deren Namen mit dem der gesuchten Tabelle. Die Funktion gibt den Wert True zurück, wenn die im Parameter strTabellenname angegebene Tabelle bereits vorhanden ist: Public Function IstTabelleVorhanden(strTabellenname As String) As Boolean Dim db As DAO.Database Dim tdf As TableDef Set db = CurrentDb For Each tdf In db.TableDefs If tdf.Name = strTabellenname Then IstTabelleVorhanden = True End If Next tdf Set db = Nothing End Function Listing 8.15: Prüfung auf Vorhandensein einer Tabelle per Durchlaufen der TableDefs-Auflistung
Die zweite Variante ist ein wenig brachialer: Sie versucht einfach, auf das gesuchte TableDef-Objekt zuzugreifen und löst einen Fehler aus, wenn die Tabelle nicht vorhanden ist. Diesen Fehler behandelt die Prozedur entsprechend und gibt ebenfalls den Wert True zurück, falls die gesuchte Tabelle existiert: Public Function IstTabelleVorhanden_Error(strTabellenname As String) _ As Boolean Dim db As DAO.Database
400
8
DAO
Dim tdf As DAO.TableDef Set db = CurrentDb On Error Resume Next Set tdf = db.TableDefs(strTabellenname) If Err.Number = 3265 Then IstTabelleVorhanden_Error = False Else IstTabelleVorhanden_Error = True End If Set db = Nothing End Function Listing 8.16: Prüfung auf Vorhandensein einer Tabelle durch direkten Zugriff und ausgelösten Fehler
8.6.3 Datensatzgruppen erstellen mit OpenRecordset Die Methode OpenRecordset ist eine der Möglichkeiten für den Zugriff auf die Daten einer Tabelle oder einer Abfrage. Damit ist sie eine der wichtigsten Methoden des Database-Objekts. Die Methode kommt in zwei Ausführungen. Die erste öffnet eine Datensatzgruppe auf Basis einer Tabelle, einer Auswahlabfrage oder einer SQL-Select-Anweisung: Set Recordset = .OpenRecordset (, , , <Sperren>)
Die zweite Variante öffnet eine Datensatzgruppe auf Basis eines bestehenden Recordset-Objekts. Als geben Sie hierbei das bestehende Recordset-Objekt an: Set Recordset = .OpenRecordset (, , <Sperren>)
Das Ergebnis der OpenRecordset-Methode wird immer einem Recordset-Objekt zugewiesen. Bei handelt es sich um ein Database-Objekt, das zuvor etwa mit CurrentDB erzeugt wird. Die ist entweder der Name einer Tabelle oder Auswahlabfrage oder eine SQL-Select-Anweisung. Der beschreibt die Art der zu öffnenden Datensatzgruppe. Tabelle 8.3 enthält die verschiedenen Möglichkeiten.
Das Database-Objekt
401
Performance Die Auswahl des richtigen Typs beim Öffnen eines Recordset-Objekts spielt eine entscheidende Rolle für die Performance bei der Arbeit mit diesem Objekt. Weitere Informationen hierzu finden Sie in Kapitel 12, »Performance«. Konstante
Beschreibung
dbOpenTable
Öffnet eine Tabelle in der lokalen Datenbank; Standard für TabellenObjekte. Die Daten in einem solchen Recordset können bearbeitet werden; außerdem kann die Seek-Methode für die Suche in indizierten Feldern verwendet werden.
dbOpenDynaset
Öffnet ein Recordset-Objekt vom Typ Dynaset. Standard für verknüpfte Tabellen, Abfragen und SQL-Select-Anweisungen. Die Daten solcher Recordsets können in den meisten Fällen bearbeitet werden. Ein DynasetRecordset enthält nur die Verweise auf die Datensätze der beteiligten Tabellen, der Inhalt eines Datensatzes wird bei Bedarf eingelesen.
dbOpenSnapshot
Öffnet ein Recordset-Objekt vom Typ Snapshot. Ein Snapshot ist ein Abbild des betroffenen Datenbestandes zu einem bestimmten Zeitpunkt. Die enthaltenen Daten können nicht geändert werden.
dbOpenForwardOnly
Öffnet ein Recordset-Objekt vom Typ Forward-Only. Entspricht weitgehend dem Snapshot, kann aber nur vorwärts (und damit nur einmal) durchlaufen werden.
Tabelle 8.3: Konstanten für den Typ einer per OpenRecordset geöffneten Datensatzgruppe
Der Parameter ist unter anderem für den Einsatz von Access in Mehrbenutzerumgebungen interessant; außerdem lassen sich weitere Optionen festlegen. Einen Überblick über die gängigsten Optionen bietet Tabelle 8.4. Die Parameter lassen sich auch kombinieren, indem mehrere Parameter mit einer boolschen Operation addieren – etwa dbConsistent Or dbSeeChanges . Konstante
Beschreibung
dbAppendOnly
Lässt nur das Anfügen von Daten zu (Typ: Dynaset).
dbConsistent
Lässt nur konsistente Aktualisierungen zu (Typ: Dynaset und Snapshot).
dbDenyWrite
Verhindert, dass andere Benutzer Datensätze ändern oder hinzufügen können.
dbDenyRead
Verhindert, dass andere Benutzer Daten in Tabellen lesen können (Typ: Table).
dbForwardOnly
Recordset kann nur vorwärts durchlaufen werden (Typ: Snapshot). Alternative: Direkt dbOpenForwardOnly als Typ wählen.
Tabelle 8.4: Zusätzliche Optionen zum Öffnen von Datensatzgruppen
402
8
DAO
Konstante
Beschreibung
dbInconsistent
Ermöglicht inkonsistente Aktualisierungen (Typ: Dynaset und Snapshot).
dbReadOnly
Verhindert Änderungen am Recordset-Objekt. Alternative: dbReadOnly für den Parameter Sperren angeben.
dbSeeChanges
Löst einen Laufzeitfehler aus, wenn ein Benutzer Daten ändert, die ein anderer Benutzer bearbeitet (Typ: Dynaset). Dies ist in Anwendungen hilfreich, in denen mehrere Benutzer gleichzeitig per Lese-/Schreib-Zugriff über die gleichen Daten verfügen.
dbSQLPassThrough
Gibt eine SQL-Anweisung an eine mit Microsoft Jet verbundene ODBCDatenquelle zur Bearbeitung weiter (Typ: Snapshot).
Tabelle 8.4: Zusätzliche Optionen zum Öffnen von Datensatzgruppen (Fortsetzung)
Sperren von Daten während der Bearbeitung Wenn mehrere Benutzer gleichzeitig auf die Daten einer Datenbank zugreifen können, müssen Sie festlegen, wie Jet darauf reagieren soll. Verwenden Sie den Parameter <Sperren>, um einen der in Tabelle 8.5 aufgeführten Werte zu verwenden. Konstante
Beschreibung
dbReadOnly
Öffnet ein schreibgeschütztes Recordset. Kann nur für einen der Parameter <Sperren> oder eingesetzt werden.
dbPessimistic
Sperrt die komplette Speicherseite, in der sich der von einer Änderung betroffene Datensatz befindet, sobald die Bearbeitung beginnt (EditMethode).
dbOptimistic
Sperrt die komplette Speicherseite, in der sich der von einer Änderung betroffene Datensatz befindet, sobald der Datensatz aktualisiert wird (Update-Methode).
Tabelle 8.5: Konstanten für den Parameter Sperren der OpenRecordset-Methode
Beispiel: Öffnen eines Recordsets auf Basis einer Tabelle Ein typisches Beispiel sieht folgendermaßen aus: Public Sub DatensatzgruppeOeffnen() Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst 'etwas mit dem Recordset machen End With
Das Database-Objekt
403
rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 8.17: Verwendung von OpenRecordset
Hier ist die aktuelle Datenbank das Objekt, die Tabelle tblMitarbeiter die Quelle und dbOpenDynaset der Typ. Weitere Optionen sind nicht festgelegt.
Beispiel: Öffnen eines Recordsets auf Basis eines anderen Recordsets Sie können die OpenRecordset-Methode auch auf bestehenden Recordset-Objekten ausführen. Im folgenden Beispiel werden die Datensätze des ersten Recordset gefiltert und das Ergebnis wird einem zweiten Recordset zugewiesen: Public Sub DatensatzgruppeOeffnen_Recordset() Dim db As DAO.Database Dim rstUngefiltert As DAO.Recordset Dim rstGefiltert As DAO.Recordset Set db = CurrentDb Set rstUngefiltert = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rstUngefiltert.Filter = "Nachname LIKE 'A*'" Set rstGefiltert = rstUngefiltert.OpenRecordset With rstGefiltert 'etwas mit dem Recordset machen End With rstGefiltert.Close rstUngefiltert.Close Set rstGefiltert = Nothing Set rstUngefiltert = Nothing Set db = Nothing End Sub Listing 8.18: OpenRecordset auf Basis eines Recordset-Objekts
Was Sie alles mit Recordset-Objekten anstellen können, erfahren Sie im Abschnitt 8.7: Daten bearbeiten mit dem Recordset-Objekt. Dort finden Sie auch weitere Praxisbeispiele für den Aufruf der OpenRecordset-Methode.
404
8
DAO
Beispiel: Öffnen eines Recordsets auf Basis eines QueryDef-Objekts Das Öffnen einer Datensatzgruppe auf Basis eines QueryDef (Objektbezeichnung für die in der Datenbank gespeicherten Abfragen) erfolgt prinzipiell wie bei Verwendung eines Recordsets als Basis: Public Sub DatensatzgruppeOeffnen_QueryDef() … Dim qdf As QueryDef … Set qdf = db.QueryDefs("qryMitarbeiter") Set rst = qdf.OpenRecordset(dbOpenDynaset) With rst 'etwas mit dem Recordset machen End With … Set qdf = Nothing End Sub Listing 8.19: Öffnen eines Recordsets auf Basis eines QueryDef-Objekts
Theoretisch hätten Sie hier auch direkt auf die Abfrage zugreifen können: Set rst = db.OpenRecordset("qryMitarbeiter", dbOpenDynaset)
Interessant wird es, wenn die Abfrage einen Parameter wie in Abbildung 8.7 enthält. Das Erstellen eines Recordsets auf Basis dieser Abfrage löst dann eine Fehlermeldung aus, die nach dem fehlenden Parameter verlangt.
Abbildung 8.7: Abfrage mit Parameter
Das Database-Objekt
405
Abhilfe schafft die Parameters-Auflistung des QueryDef-Objekts. Dieses enthält die in der Abfrage festgelegten Parameter. Weist man allen Parametern einen Wert zu, läuft die Routine ohne Fehler durch. Public Sub DatensatzgruppeOeffnen_QueryDef_Parameter() … Dim prm As DAO.Parameter … Set qdf = db.QueryDefs("qryMitarbeiter") Set prm = qdf.Parameters("Nachname eingeben") prm.Value = "Wurst" Set rst = qdf.OpenRecordset(dbOpenDynaset) With rst 'etwas mit dem Recordset machen End With … End Sub Listing 8.20: Erzeugen eines Recordsets aus einem QueryDef-Objekt mit Parametern
8.6.4 Ausführen von Aktionsabfragen Die Methode Execute und die damit in Verbindung stehende Eigenschaft RecordsAffected dienen dem Ausführen von Aktionsabfragen und der Rückgabe der von einer Aktionsabfrage betroffenen Zeilenzahl. Alles Wissenswerte zum Thema Aktionsabfragen erfahren Sie in Kapitel 7, »AccessSQL«. Die folgende Routine verwendet die Execute-Anweisung, um eine Aktionsabfrage auszuführen und gibt anschließend die Anzahl der betroffenen Datensätze aus: Public Sub Aktionsabfrage() Dim db As DAO.Database Set db = CurrentDb db.Execute "INSERT INTO tblMitarbeiter(Vorname, Nachname) " _ & "VALUES('André', 'Minhorst')"
406
8
DAO
Debug.Print db.RecordsAffected Set db = Nothing End Sub Listing 8.21: Ausführen einer Aktionsabfrage und Ausgabe der Anzahl der betroffenen Datensätze
8.7 Daten bearbeiten mit dem Recordset-Objekt Das Bearbeiten von Daten ist vermutlich das bedeutendste Feature der DAO-Bibliothek. Dreh- und Angelpunkt ist das Recordset-Objekt, das Sie bereits weiter oben in 8.6.3 kurz kennen gelernt haben – dort ging es um das Erzeugen eines solchen Objekts mit der OpenRecordset-Methode und den dabei verwendeten Parametern.
8.7.1 Methoden und Eigenschaften des Recordset-Objekts In den folgenden Abschnitten lernen Sie zunächst die folgenden Methoden und Eigenschaften des Recordset-Objekts kennen: EOF: Eigenschaft, die den Wert True annimmt, wenn sich der Datensatzzeiger hinter dem letzten Datensatz befindet. MoveNext: Methode, die den Datensatzzeiger um eine Position zum Ende der Datensatzgruppe hin verschiebt.
8.7.2 Datensätze durchlaufen Daten macht man aus mehreren Gründen als Recordset verfügbar, um per VBA darauf zuzugreifen: Datensätze sollen angelegt oder geändert werden und diese Änderung lässt sich nicht mit einer Aktionsabfrage erledigen. Sie möchten einen Datensatz anlegen und direkt danach auf seinen Primärschlüssel zugreifen. Sie müssen für einen oder mehrere Datensätze des Recordsets eine Aktion durchführen.
Alle Datensätze durchlaufen Das Durchlaufen aller Datensätze eines Recordset-Objekts erfolgt in einer Schleife. Im folgenden Listing wird dazu eine Do While-Schleife verwendet. Die Abbruchbedingung der Schleife heißt rst.EOF. Die Eigenschaft EOF gibt an, dass sich der Datensatzzeiger hinter dem letzten Datensatz des Recordset befindet.
Daten bearbeiten mit dem Recordset-Objekt
407
Das ist der Fall, wenn die MoveNext-Methode den Datensatzzeiger über den letzten Datensatz hinausgeschoben hat. Die MoveNext-Methode schiebt den Datensatzzeiger jeweils um einen Datensatz weiter. Public Sub DatensatzgruppeDurchlaufen() Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) Do While Not rst.EOF Debug.Print rst!Vorname & " " & rst!Nachname rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 8.22: Durchlaufen aller Datensätze eines Recordset-Objekts
Tipp: Wann immer Sie eine Do While-Schleife zum Durchlaufen einer Datensatzgruppe verwenden, legen Sie immer erst den Körper an und vergessen Sie nicht die MoveNext-Anweisung. Ansonsten werden Sie sich unter Umständen wundern, wie lange so ein Durchlauf durch eine Datensatzgruppe dauern kann …
Zu bestimmten Datensätzen springen Nicht immer möchte man die ganze Datensatzgruppe durchlaufen, sondern den Datensatzzeiger mal vor- und mal zurückbewegen oder gar auf ganz bestimmte Datensätze springen. Das Bewegen in kleinen Schritten lässt sich leicht mit den beiden Methoden MoveNext und MovePrevious bewerkstelligen, von denen Sie Erstere schon kennen gelernt haben. Zum Springen auf bestimmte Datensätze gibt es mehrere Möglichkeiten. Oft benötigt werden Sprünge auf den ersten oder den letzten Eintrag einer Datensatzgruppe. Dazu verwenden Sie die beiden Methoden MoveFirst und MoveLast. Beispiele dazu finden Sie weiter unten. Den Sprung auf einen beliebigen Datensatz ermöglicht die Methode Move. Sie erwartet zwei Parameter: die Anzahl der zu überspringenden Datensätze und den Ausgangspunkt. Letzterer wird in Form eines Bookmarks übergeben – das ist eine Markierung
408
8
DAO
eines Datensatzes. Wenn der zweite Parameter fehlt, bewegt die Move-Methode den Datensatzzeiger von der aktuellen Position aus.
Aktuelle Position des Datensatzzeigers ermitteln Die aktuelle Position des Datensatzzeigers lässt sich mit den beiden Eigenschaften AbsolutePosition und PercentPosition ermitteln. Die folgende Routine springt zunächst auf den ersten Datensatz und gibt die absolute und die prozentuale Position aus; dann folgt dieselbe Ausgabe für den letzten Datensatz: Public Sub Datensatzposition() Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveFirst Debug.Print "Absolut: & " Prozentual: " rst.MoveLast Debug.Print "Absolut: & " Prozentual: "
" & rst.AbsolutePosition _ & rst.PercentPosition " & rst.AbsolutePosition _ & rst.PercentPosition
rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 8.23: Ausgabe der absoluten und der prozentualen Position des Datensatzzeigers für den ersten und den letzten Datensatz
Andersherum lassen sich die beiden Eigenschaften auch dazu verwenden, die Position des Datensatzzeigers festzulegen – dazu weisen Sie einer der Eigenschaften einfach den gewünschten Wert zu. Im Falle des prozentualen Wertes landet der Datensatzzeiger auf dem nächstliegenden Datensatz.
Datensatzzeiger im Niemandsland Manchmal macht der Datensatzzeiger seinem Namen nicht gerade Ehre: Dann landet er vor dem ersten oder hinter dem letzten Datensatz. Eine der Eigenschaften, die diesen Zustand abfragt, haben Sie bereits weiter oben kennen gelernt: Dort prüfte die EOFEigenschaft, ob der Datensatzzeiger sich hinter dem letzten Zeiger befindet. Die Eigenschaft BOF prüft das Gegenteil, nämlich ob der Datensatzzeiger sich vor dem ersten Datensatz befindet.
Daten bearbeiten mit dem Recordset-Objekt
409
Zu beachten ist hier, dass man einen Fehler auslöst, wenn man den Datensatzzeiger um mehr als eine Position über den Rand des Recordsets hinausschiebt. Public Sub FehlerNachEOF() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveLast Debug.Print rst.EOF rst.MoveNext Debug.Print rst.EOF rst.MoveNext 'hier wird ein Fehler ausgelöst … End Sub Listing 8.24: Wenn EOF oder BOF den Wert True haben, darf der Datensatzzeiger nicht weiter vom Recordset wegbewegt werden.
Anzahl der Datensätze ermitteln Das Ermitteln der in einem Recordset-Objekt enthaltenen Anzahl an Datensätzen ist nicht so trivial, wie man es sich vielleicht vorstellt. Zwar gibt es eine Eigenschaft namens RecordCount, aber diese liefert nicht immer das richtige Ergebnis zurück. Die folgende Prozedur liefert beispielsweise den Wert 1 zurück, obwohl die zugrunde liegende Tabelle wesentlich mehr Datensätze enthält: Public Sub AnzahlDatensaetze_Falsch() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) Debug.Print rst.RecordCount … End Sub Listing 8.25: Diese Routine liefert die falsche Anzahl Datensätze zurück.
Der Grund für das falsche Ergebnis liegt darin, dass die RecordCount-Eigenschaft offenbar nur die Datensätze zählt, die bereits einmal mit dem Datensatzzeiger »überfahren« wurden. Das ist zumindest nachweisbar, wenn man per MoveLast-Methode auf den letzten Datensatz springt und dann die Datensätze zählt. Folgende Routine gibt die korrekte Anzahl aus: Public Sub AnzahlDatensaetze_Richtig() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveLast
410
8
DAO
Debug.Print rst.RecordCount … End Sub Listing 8.26: Erst ein Sprung auf den letzten Datensatz liefert die richtige Anzahl.
Zwischen zwei Zählungen sollten Sie in jedem Fall die Requery-Methode des Recordset-Objekts verwenden, um den Datenbestand zu aktualisieren. Anderenfalls laufen Sie Gefahr, beim zwischenzeitlichen Löschen oder Hinzufügen von Datensätzen die alte Anzahl auszugeben. Die Requery-Methode sorgt für ein erneutes Einlesen der zugrunde liegenden Daten. In der folgenden Routine wird die Anzahl in beiden Fällen korrekt ausgegeben. Beachten Sie, dass die Requery-Methode vor dem Sprung auf den letzten Datensatz ausgeführt werden muss! Public Sub AnzahlDatensaetze_MitAktualisierung() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveLast Debug.Print rst.RecordCount db.Execute "INSERT INTO tblMitarbeiter(Vorname, Nachname) " _ & "VALUES('Hans','Wurst')" rst.Requery rst.MoveLast Debug.Print rst.RecordCount … End Sub Listing 8.27: Zählen der Datensatzanzahl vor und nach dem Ändern des Datenbestands.
Die ersten beiden Beispiele beschäftigen sich mit Dynaset-Recordsets. Mit TableRecordsets müssen Sie nicht erst auf den letzten Datensatz springen, um die korrekte Anzahl der enthaltenen Datensätze zu bestimmen. Hier reicht ein einfaches RecordCount: Public Sub AnzahlDatensaetze_Table() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) Debug.Print rst.RecordCount … End Sub Listing 8.28: Beim Table-Recordset funktioniert RecordCount richtig.
Daten bearbeiten mit dem Recordset-Objekt
411
Zusätzlich wird hier deutlich, dass ein Table-Recordset direkt an die Tabelle gebunden ist: Wenn Sie die obige Prozedur zweimal die Anzahl der Datensätze ausgeben lassen und zwischendurch einen Datensatz entfernen, zeigt die zweite Ausgabe direkt die korrigierte Anzahl an. Bleibt noch das Snapshot-Recordset: Auch hier ist wieder der Sprung auf den letzten Datensatz nötig, um die korrekte Anzahl der Datensätze zu ermitteln. Da sich das Snapshot-Recordset nicht aktualisieren lässt, liefert RecordCount immer die Anzahl der Datensätze beim Anlegen der Datensatzgruppe zurück. Am schnellsten ermitteln Sie die Anzahl der Datensätze mit dem folgenden Listing: Public Sub AnzahlDatensaetze_Table_Schnell() Dim db As DAO.Database Set db = CurrentDb Debug.Print db.TableDefs("tblMitarbeiter").RecordCount Set db = Nothing End Sub Listing 8.29: Schnelle Ermittlung der Datensatzanzahl
8.7.3 Daten aus Datensätzen ausgeben Im Beispiel aus Listing 8.22 haben Sie bereits eine Methode zur Ausgabe des Inhalts von Feldern eines Datensatzes kennen gelernt. Dort wurde der Ausdruck rst!Vorname verwendet, um den Inhalt des Feldes Vorname der Datensatzgruppe auszugeben. Es gibt mehrere Möglichkeiten, um auf den Inhalt eines Feldes eines Datensatzes zuzugreifen. Die folgenden Ausdrücke geben alle den Inhalt des Feldes Vorname der Datensatzgruppe rst aus: Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print
rst.Fields("Vorname").Value rst.Fields("Vorname") rst.Fields(1).Value rst.Fields(1) rst(1).Value rst(1) rst("Vorname").Value rst("Vorname") rst!Vorname.Value rst!Vorname
Die Eigenschaft Value kann man in jedem dieser Fälle weglassen, das es sich dabei um die Standardeigenschaft des Field-Objekts handelt.
412
8
DAO
Alle Varianten haben ihre Berechtigung: rst!Vorname ist beispielsweise die kürzeste und kennzeichnet Vorname durch das Ausrufezeichen als Feldnamen und nicht als Eigenschaft oder Methode. Die Varianten, bei denen der Feldname in Klammern und Anführungszeichen eingebettet ist, können auch Variablen beinhalten: Dim strFeldname as String strFeldname = "Vorname" Debug.Print rst(strFeldname)
Auch Feldnamen mit Leerzeichen können dargestellt werden: rst.Fields("Feld mit Leerzeichen") rst![Feld mit Leerzeichen]
Das sollte Sie allerdings nicht dazu verleiten, Leerzeichen in Feldnamen unterzubringen – das ist sehr schlechter Stil und führt früher oder später zu Problemen. Und auch die Angabe der Ordinalposition eines Feldes innerhalb des RecordsetObjekts hat ihren Sinn: Auf diese Weise können Sie beispielsweise die Namen aller Felder ausgeben lassen. Außerdem ist diese Variante gegenüber denen mit Angabe einer Zeichenkette schneller.
8.7.4 Datensätze suchen Das Suchen von Datensätzen in Recordsets erfolgt in Abhängigkeit vom gewählten Recordset-Typ auf unterschiedliche Arten. In Table-Recordsets können Sie die in der Tabelle festgelegten Indizes direkt für die Suche einsetzen, was zu einer hohen Suchgeschwindigkeit führt. In Dynaset- und Snapshot-Recordsets kann die Suche nicht zwangsläufig auf Basis eines Index stattfinden.
Die Seek-Methode zum Suchen in Table-Recordsets Die Anwendung der Seek-Methode erfordert das Vorhandensein eines Index auf dem betroffenen Feld, dessen Name bekannt ist. Den Namen eines Index finden Sie im Dialog Indizes der jeweiligen Tabelle (siehe Abbildung 8.8). Sie können den Dialog über den Menüeintrag Ansicht/Indizes öffnen, während die betroffene Tabelle in der Entwurfsansicht angezeigt wird. Die Suche per Seek sieht wie im folgenden Listing aus. Vor der Anwendung der SeekMethode legen Sie mit der Index-Eigenschaft den Namen des Index fest. Die SeekMethode selbst erwartet zwei Parameter: einen Vergleichsoperator (<, <=, =, =>, >) und den Vergleichswert.
Daten bearbeiten mit dem Recordset-Objekt
413
Abbildung 8.8: Ermitteln des Indexnamens
Nach dem Suchen prüft man mit der NoMatch-Eigenschaft, ob die Seek-Methode einen Datensatz gefunden hat. Die Seek-Methode verschiebt den Datensatzzeiger auf den gefundenen Datensatz, sodass dieser bequem ausgegeben werden kann. Beachten Sie, dass der Datensatzzeiger auf dem aktuellen Datensatz stehen bleibt, wenn die SeekMethode keinen Datensatz gefunden hat – die Prüfung der NoMatch-Eigenschaft ist also unbedingt erforderlich, um auch tatsächlich den gesuchten Datensatz auszugeben. Public Sub SucheMitSeek() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) rst.Index = "idxNachname" rst.Seek "=", "Minhorst" If Not rst.NoMatch Then Debug.Print rst!Vorname & " " & rst!Nachname End If … End Sub Listing 8.30: Suche nach einem Datensatz mit der Seek-Methode
Die Find-Methoden zum Suchen in Dynaset- und Snapshot-Recordsets In Dynaset- und Snapshot-Recordsets lässt sich die Seek-Methode nicht einsetzen. Dafür gibt es dort mehrere Suchmethoden, die zwar nicht so performant sind, aber durch flexiblere Einsetzbarkeit glänzen.
414
8
DAO
Es gibt nämlich insgesamt vier Methoden zum Suchen in Recordsets: FindFirst, FindNext, FindPrevious und FindLast. Da diese Methoden sich nicht auf einen Index beziehen, geben Sie hier direkt den Namen des Feldes an, in dem gesucht werden soll. Der Feldname wird zusammen mit dem Vergleichsoperator und dem Vergleichswert in einem Ausdruck zusammengefasst und der entsprechenden Find…-Methode als Parameter mitgegeben. Die folgende Routine sucht nach dem ersten Datensatz, dessen Feld Nachname den Wert Minhorst enthält: Public Sub SucheMitFindNext() Dim db As DAO.Database Dim rst As DAO.Recordset Dim strKriterium As String Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) strKriterium = "Nachname = 'Minhorst'" rst.FindFirst strKriterium If Not rst.NoMatch Then Debug.Print rst!Vorname & " " & rst!Nachname Else Debug.Print "Kein passender Datensatz gefunden." End If rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 8.31: Suche nach einem Datensatz per FindNext-Methode
Auch hier müssen Sie unbedingt die NoMatch-Eigenschaft auswerten. Nur der Wert False weist auf eine erfolgreiche Suche hin, anderenfalls bleibt der Datensatzzeiger auf der aktuellen Position stehen.
Alle Datensätze mit einem bestimmten Kriterium finden Natürlich haben neben der FindFirst-Methode auch die anderen Methoden ihre Daseinsberechtigung. Die FindNext-Methode lässt sich gut mit der FindFirst-Methode kombinieren, wenn es um das Durchforsten der ganzen Datensatzgruppe nach dem gewünschten Kriterium geht. Dazu sucht man zunächst mit der FindFirst-Methode den ersten Treffer und steigt im Erfolgsfall in eine Do While-Schleife ein, in der die Informationen zum gesuchten Datensatz ausgegeben werden und mit der FindNextMethode der nächste Treffer gesucht wird: Public Sub SucheAlleMitFindNext() … strKriterium = "Nachname = 'Minhorst'"
Daten bearbeiten mit dem Recordset-Objekt
415
rst.FindFirst strKriterium Do While Not rst.NoMatch Debug.Print rst!Vorname & " " & rst!Nachname rst.FindNext strKriterium Loop … End Sub Listing 8.32: Suche aller Treffer in einem Recordset
Alternative: Vorheriges Filtern der Datensatzgruppe Die obigen Beispiele liefern keinen Grund, warum man nicht direkt die dem Recordset zugrunde liegende Tabelle oder Abfrage mit einem Kriterium so einschränken sollte, dass das Recordset nur noch die gesuchten Datensätze enthält. Auch im wirklichen Leben sollten Sie immer prüfen, ob datenrelevante Funktionalität nicht in eine Abfrage mit Parameter verlagert werden könnte. Diese ist wesentlich schneller als die entsprechende Suchmethode in einem Recordset-Objekt. Die Ausgabe nach allen Datensätzen mit einem bestimmten Nachnamen gestaltet sich dann wie folgt: Public Sub SucheMitSQLAbfrage() Dim db As DAO.Database Dim qdf As DAO.QueryDef Dim rst As DAO.Recordset Dim strName As String Dim prm As DAO.Parameter Set db = CurrentDb Set qdf = db.QueryDefs("qryMitarbeiter") Set prm = qdf.Parameters("Nachname eingeben") prm.Value = "Minhorst" Set rst = qdf.OpenRecordset(dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Vorname & " " & rst!Nachname rst.MoveNext Loop rst.Close Set qdf = Nothing Set prm = Nothing Set rst = Nothing Set db = Nothing End Sub Listing 8.33: Suche nach einem Datensatz per Parameterabfrage
416
8
DAO
8.7.5 Lesezeichen DAO bietet eine Eigenschaft namens Bookmark an, um einen Verweis auf den aktuellen Datensatz zu erzeugen oder mit diesem Verweis auf den betroffenen Datensatz zu springen. Diese Eigenschaft wird meistens im Zusammenhang mit Recordsets verwendet, die an Formulare gebunden sind. Einige Beispiele dazu finden Sie im Kapitel 4, »Formulare«. An dieser Stelle sei nur die grundlegende Handhabung erläutert: Die Bookmark-Eigenschaft liefert einen Wert zurück, den Sie in einer Variant-Variablen speichern können: Public Sub Lesezeichen() … Dim varLesezeichen As Variant … 'Lesezeichen speichern varLesezeichen = rst.Bookmark 'zum letzten Datensatz springen rst.MoveLast 'wieder zum markierten Datensatz zurück rst.Bookmark = varLesezeichen … End Sub Listing 8.34: Lesezeichen ermitteln und zum markierten Datensatz zurückspringen
8.8 Sortieren und Filtern von Datensätzen Zum Sortieren und Filtern von Datensätzen stellt die DAO-Bibliothek die Methoden Sort und Filter. Wie bereits beim Suchen von Datensätzen gibt es auch beim Filtern und Sortieren Unterschiede bezüglich der Handhabung von Table-Recordsets und Dynaset- beziehungsweise Snapshot-Recordsets.
8.8.1 Sortieren mit der Sort-Eigenschaft Das Sortieren von Dynasets und Snapshots erfolgt in drei Schritten: 1. Erzeugen des Ausgangsrecordsets 2. Festlegen des Sortierkriteriums mit der Sort-Eigenschaft 3. Erzeugen des sortierten Recordsets auf Basis des Ausgangsrecordsets und des Wertes der Sort-Eigenschaft
Sortieren und Filtern von Datensätzen
417
Für die Sort-Eigenschaft gibt man einen dem Parameter des ORDER BY-Kriteriums einer SQL-Abfrage entsprechenden Ausdruck an. In einer Prozedur sieht das folgendermaßen aus: Public Sub Sortieren_Dynaset() … 'Ausgangsrecordset erzeugen Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) 'Sortierung festlegen rst.Sort = "Nachname ASC" 'Sortierten Recordset erzeugen Set rstSortiert = rst.OpenRecordset(dbOpenDynaset) 'etwas mit dem sortierten Recordset machen … End Sub Listing 8.35: Sortiertes Recordset erzeugen
Alternative: Sortieren per ORDER BY-Klausel Auch hier gilt: Was sich per Abfrage erledigen lässt, sollte auch dort stattfinden. Sprich: Man verlegt den Sortiervorgang einfach in die Datenherkunft des Recordsets, indem man etwa folgenden Ausdruck verwendet: Set rst = db.OpenRecordset("SELECT * FROM tblMitarbeiter ORDER BY Nachname", dbOpenDynaset)
Theoretisch könnte man die SELECT-Anweisung auch direkt in einer Abfrage speichern, würde dadurch aber ein wenig an Flexibilität einbüßen. Das Sortierkriterium müsste man dann definitiv festlegen, während sich der SQL-Ausdruck in einer OpenRecordsetAnweisung in der Prozedur dynamisch zusammensetzen lässt.
8.8.2 Sortieren mit der Index-Eigenschaft Recordsets vom Typ Table lassen sich nur über die festgelegten Indizes sortieren. Wie diese Sortierung erfolgt, legen Sie im Dialog Indizes fest (siehe Abbildung 8.9). Wenn Sie sich die Möglichkeit offen halten möchten, in beiden Richtungen zu sortieren, legen Sie einfach zwei Indizes auf das gleiche Feld mit unterschiedlicher Sortierreihenfolge an. Beachten Sie, dass die Performance bei der Suche mit indizierten Feldern zwar meist verbessert wird, aber Anfüge- und Aktualisierungsabfragen mehr Rechenzeit benötigen, da auch die Indizes jeweils angepasst werden müssen.
418
8
DAO
Abbildung 8.9: Festlegen der Reihenfolge für einen Index
Public Sub Sortieren_Table() … 'Ausgangsrecordset erzeugen Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) 'Index für die Sortierung festlegen rst.Index = "idxNachname" 'Daten in sortierter Reihenfolge ausgeben Do While Not rst.EOF Debug.Print rst!Vorname & " " & rst!Nachname rst.MoveNext Loop … End Sub Listing 8.36: Sortieren per Index
8.8.3 Filtern mit der Filter-Eigenschaft Das Filtern von Recordsets und Snapshots erfolgt nach dem gleichen Schema wie das Sortieren: Recordset anlegen, Filter festlegen, gefiltertes Recordset auf Basis des bestehenden Recordsets erzeugen. Die folgende Prozedur gibt zusätzlich zum Filtervorgang noch die gefilterten Datensätze aus: Public Sub Filtern_Dynaset() … 'Ausgangsrecordset erzeugen Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) 'Filter angeben
Daten bearbeiten
419
rst.Filter = "Nachname = 'Minhorst'" 'Gefiltertes Recordset erzeugen Set rstGefiltert = rst.OpenRecordset(dbOpenDynaset) 'Gefilterte Datensätze ausgeben Do While Not rstGefiltert.EOF Debug.Print rstGefiltert!Nachname rstGefiltert.MoveNext Loop …. End Sub Listing 8.37: Erzeugen einer gefilterten Datensatzgruppe
8.9 Daten bearbeiten Bisher haben Sie erfahren, wie Sie mit DAO Recordsets erzeugen, darin navigieren oder Datensätze suchen. Die meisten dieser Aktionen haben weniger die Anzeige der Daten als vielmehr das Auswählen eines Datensatzes zur Bearbeitung zum Ziel. Die folgenden Abschnitte zeigen daher, wie Sie mit DAO Datensätze anlegen, bearbeiten und löschen können. Wie üblich gelten auch hier unterschiedliche Regeln für die Handhabung von Table-, Dynaset- und Snapshot-Recordsets. Das Ändern von Daten in Snapshots ist generell nicht möglich, da diese schreibgeschützt sind. Bei den anderen beiden RecordsetTypen kommt es zunächst darauf an, ob gerade ein anderer Benutzer darauf zugreift und/oder die Datenbank exklusiv geöffnet hat und/oder in den Optionsparametern beim Öffnen des Recordsets Beschränkungen auferlegt wurden (beispielsweise dbReadOnly; siehe Tabelle 8.4). Außerdem kann es möglich sein, dass Sie gar keine Rechte über den schreibenden Zugriff besitzen. Schließlich gibt es noch den feinen Unterschied zwischen Table- und Dynaset-Recordsets: Greift keiner der anderen Gründe für eine Sperrung des Zugriffs, lässt sich das Table-Recordset auf jeden Fall bearbeiten und das Dynaset-Recordset in den meisten Fällen. Einzige Ausnahme könnte eine ungünstige Zusammenstellung der zugrunde liegenden Tabellen und ihrer Beziehungen sein. Ob sich der schreibende Zugriff auf ein Dynaset-Recordset generell verbietet, lässt sich ganz einfach feststellen, indem Sie den SQL-Ausdruck in eine Abfrage packen und dort versuchen, einen neuen Datensatz anzufügen. Auch programmatisch ist der Nachweis schnell erbracht: Die Eigenschaft Updatable liefert den Wert True zurück, wenn die Daten bearbeitet werden können.
420
8
DAO
8.9.1 Anlegen eines Datensatzes Das Anlegen eines Datensatzes erfolgt mit der AddNew-Methode des Recordset-Objekts und durch anschließendes Speichern des neuen Datensatzes mit der Update-Methode. Dazwischen liegen die Anweisungen zum Eintragen der Feldwerte: Public Sub DatensatzHinzufuegen() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst .AddNew !Vorname = "Hans" !Nachname = "Wurst" .Update End With … End Sub Listing 8.38: Hinzufügen eines Datensatzes
Sofern das Primärschlüsselfeld als Autowert deklariert ist, müssen Sie sich im Übrigen genauso wenig Sorgen um dessen Anlage machen, als wenn Sie Daten direkt in die Tabelle eintragen – der Wert des Primärschlüsselfeldes wird automatisch hinzugefügt.
8.9.2 Bearbeiten eines Datensatzes Das Bearbeiten eines Datensatzes erfolgt ganz ähnlich. Hier müssen Sie allerdings zunächst zu einer der zuvor genannten Techniken greifen, um den zu bearbeitenden Datensatz zu finden. Anschließend starten Sie den Bearbeitungsvorgang mit der EditMethode und weisen den Feldern die gewünschten Werte zu. Das Speichern des geänderten Datensatzes erfolgt wie oben mit der Update-Methode. Public Sub DatensatzAendern() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst .FindFirst "Vorname = 'Hans' AND Nachname = 'Wurst'" If Not .NoMatch Then .Edit !Vorname = "Hans-Peter" .Update End If End With … End Sub Listing 8.39: Ändern eines Datensatzes
QueryDefs – Auswahl oder Aktion nach Wahl
421
8.9.3 Löschen eines Datensatzes Auch beim Löschen eines Datensatzes muss der zu löschende Datensatz zuvor ausgewählt werden. Ist der Datensatzzeiger auf dem gewünschten Datensatz positioniert, sorgt die Delete-Methode für die Entfernung dieses Datensatzes. Public Sub DatensatzLoeschen() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst .FindFirst "Vorname = 'Hans' AND Nachname = 'Wurst'" If Not .NoMatch Then .Delete End If End With … End Sub Listing 8.40: Löschen eines Datensatzes
Alternative zum Löschen eines Datensatzes Das Löschen von Datensätzen lässt sich mit einer SQL-Aktionsabfrage in den meisten Fällen schneller und in jedem Fall durch weniger Codezeilen bewerkstelligen. Die SQL-Variante des obigen Codes sieht beispielsweise folgendermaßen aus: Public Sub DatensatzLoeschenPerSQL() Dim db As DAO.Database Set db = CurrentDb db.Execute "DELETE FROM tblMitarbeiter " _ & "WHERE Vorname = 'Hans' AND Nachname = 'Wurst'" Set db = Nothing End Sub Listing 8.41: Löschen eines Datensatzes per SQL-Aktionsabfrage
8.10 QueryDefs – Auswahl oder Aktion nach Wahl Weiter oben haben Sie bereits erfahren, wie Sie QueryDefs verwenden, um Parameterabfragen mit Werten zu versehen und das Ergebnis per OpenRecordset verfügbar zu machen.
422
8
DAO
Ein QueryDef-Objekt kann allerdings nicht nur Auswahlabfragen als Basis für die OpenRecordset-Methode liefern, sondern auch Aktionsabfragen beinhalten und eine geeignete Methode zum Ausführen der Abfrage bereitstellen. Die folgende Routine füllt ein QueryDef-Objekt mit der Abfrage qryMitarbeiterLoeschen und führt diese mit der Execute-Methode aus. Über die RecordsAffected-Methode schreibt die Routine anschließend die Anzahl der betroffenen Datensätze in das Direktfenster. Public Sub QueryDefMitAktion() Dim db As DAO.Database Dim qdf As DAO.QueryDef Set db = CurrentDb Set qdf = db.QueryDefs("qryMitarbeiterLoeschen") qdf.Execute dbFailOnError Debug.Print qdf.RecordsAffected Set qdf = Nothing Set db = Nothing End Sub Listing 8.42: Anwenden einer Aktionsabfrage per QueryDef
Die Execute-Methode enthält einen Parameter, der hier mit der Konstanten dbFailOnError gefüllt ist. Dieser Konstante sorgt dafür, dass beim Auftreten eines Fehlers beim Bearbeiten eines der betroffenen Datensätze alle bereits getätigten Änderungen zurückgesetzt werden. Eine weitere interessante Konstante heißt dbSeeChanges: Diese löst einen Laufzeitfehler aus, wenn ein anderer Benutzer Daten ändert, die von Ihnen bearbeitet wurden.
8.11 Transaktionen Mit DAO lassen sich mehrere Datenoperationen in einer Transaktion zusammenfassen. Das bedeutet, dass die Änderungen an den Daten erst durchgeführt werden, wenn die Transaktion mit der entsprechenden DAO-Methode ausdrücklich abgeschlossen wird. In der Praxis ist das interessant, wenn etwa ein Geldtransfer von einem Konto zum anderen Konto abgebildet werden soll. Dazu wird der Kontostand des ersten Kontos vermindert und der des zweiten Kontos erhöht. Es müssen unbedingt beide Aktionen durchgeführt werden, da sonst eine Inkonsistenz entsteht, wenn einer der beiden Vorgänge ohne den anderen durchgeführt wird.
Transaktionen
423
Transaktionen beziehen sich immer auf den angegebenen Workspace. Damit ist sichergestellt, dass immer nur ein Benutzer Änderungen während einer Transaktion durchführt. Sie selbst müssen allerdings dafür sorgen, dass wirklich auch nur die geplanten Datenänderungen innerhalb einer Transaktion erfolgen – anderenfalls verwirft Access gegebenenfalls Änderungen, die vielleicht gar nicht Bestandteil der Transaktion sein sollen. Das Handhaben von Transaktionen ist eigentlich ganz einfach. Abbildung 8.10 zeigt den prinzipiellen Ablauf einer Transaktion. Der Start erfolgt mit der BeginTransAnweisung. Nach dem Ändern der Daten prüft man, ob alle Vorgänge erfolgreich durchgeführt werden konnten, und ruft dann eine der beiden Methoden CommitTrans oder Rollback auf.
BeginTrans
Datensätze bearbeiten
Bearbeitung übernehmen?
Nein Ja
CommitTrans
Rollback
Abbildung 8.10: Ablauf einer Transaktion
Im Code sieht das etwa folgendermaßen aus: Public Sub Transaktion(lngKonto1ID As Long, lngKonto2ID As Long, _ curBetrag As Currency) Dim Dim Dim Dim
wrk As DAO.Workspace db As DAO.Database curKonto1Alt As Currency curKonto2Alt As Currency
424
8 Dim curKonto1Neu As Currency Dim curKonto2Neu As Currency Set wrk = DBEngine.Workspaces(0) Set db = wrk.Databases(0) 'Transaktion starten wrk.BeginTrans 'Alte Kontostände ermitteln und zwischenspeichern curKonto1Alt = FLookup("Kontostand", "tblKonten", _ "KontoID = " & lngKonto1ID) curKonto2Alt = FLookup("Kontostand", "tblKonten", _ "KontoID = " & lngKonto2ID) 'Umbuchung vornehmen db.Execute "UPDATE tblKonten SET Kontostand = Kontostand - " _ & curBetrag & " WHERE KontoID = " & lngKonto1ID db.Execute "UPDATE tblKonten SET Kontostand = Kontostand + " _ & curBetrag & " WHERE KontoID = " & lngKonto2ID 'Neue Kontostände ermitteln und zwischenspeichern curKonto1Neu = FLookup("Kontostand", "tblKonten", "KontoID = " _ & lngKonto1ID) curKonto2Neu = FLookup("Kontostand", "tblKonten", "KontoID = " _ & lngKonto2ID) 'Prüfen, ob die gewünschten Änderungen durchgeführt wurden 'und Änderungen entweder durchführen oder verwerfen If curKonto1Neu = curKonto1Alt - curBetrag _ And curKonto2Neu = curKonto2Alt + curBetrag Then wrk.CommitTrans Debug.Print "Transaktion erfolgreich." Else wrk.Rollback Debug.Print "Transaktion nicht erfolgreich." End If Set db = Nothing Set wrk = Nothing End Sub
Listing 8.43: Beispiel einer Transaktion
DAO
Transaktionen
425
Die DLookup-Anweisung läuft nicht im gleichen Kontext der Transaktion und ist daher an dieser Stelle nutzlos. Sie kann zwar die Daten vor der Änderung ermitteln, aber hat keinen Einblick in die innerhalb der Transaktion temporär geänderten Daten. Daher verwendet die Prozedur eine alternative Funktion namens FLookup, die neben der Transaktionsfähigkeit außerdem noch schneller als die klassische DLookup-Funktion ist. Das folgende Listing zeigt den Aufbau der Funktion. Sie liefert genau die gleichen Ergebnisse wie die DLookup-Funktion, enthält allerdings keine Fehlerbehandlung. Public Function FLookup(strField As String, strTable As String, _ strCriteria As String) As Variant Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset(strTable, dbOpenDynaset) rst.FindFirst strCriteria If Not rst.NoMatch Then FLookup = rst(strField) Else FLookup = Null End If rst.Close Set rst = Nothing Set db = Nothing End Function Listing 8.44: Eine schnelle DLookup-Variante auf DAO-Basis
Der Vollständigkeit halber finden Sie noch eine schnellere Variante, die direkt mit einem SQL-Ausdruck arbeitet: Public Function FLookup(strField As String, strTable As String, _ strCriteria As String) As Variant Dim strSQL As String On Error Resume Next strSQL = "SELECT [" & strField & "] FROM [" & strTable & "]" If Len(strCriteria) > 0 Then strSQL = strSQL & " WHERE " & strCriteria FLookup = DBEngine(0)(0).OpenRecordset(strSQL, dbOpenForwardOnly)(0) End Function
Ein Praxisbeispiel zum Thema »Transaktionen« finden Sie in Kapitel 4 im Abschnitt 4.6.2, »Undo in Haupt- und Unterformularen«.
9 ADO Im vorherigen Kapitel haben Sie bereits erfahren, dass DAO die bevorzugte Datenzugriffs-Bibliothek ist, wenn es um die Entwicklung reiner Access-Anwendungen geht. ADO (ActiveX Data Objects) war von Microsoft als Nachfolger von DAO geplant. Es eröffnet wesentlich mehr Möglichkeiten, die sich aber vor allem dann bemerkbar machen, wenn Sie ein alternatives Backend wie die MSDE oder den Microsoft SQL Server (diese beiden werden nachfolgend synonym als SQL Server bezeichnet) verwenden. Der Fokus dieses Buchs richtet sich auf die Entwicklung reiner Access-Datenbankanwendungen. Daher sollen die Möglichkeiten von Access in Zusammenarbeit mit dem SQL Server hier nicht betrachtet werden. Entwickler, die jetzt noch reine AccessAnwendungen programmieren, aber diese im Hinblick auf einen späteren Wechsel auf den SQL Server direkt auf dieses Backend vorbereiten möchten, sollen natürlich nicht außen vor bleiben – zumal ADO auch für reine Access-Anwendungen einige Features bereithält, die DAO nicht bietet. Wegen des gegenüber DAO wesentlich größeren Funktionsumfangs könnte über ADO ein eigenes Buch geschrieben werden. Aus Platzgründen wird das Thema hier jedoch ein wenig eingeschränkt – und zwar so, dass Sie die Techniken, die Sie im vorherigen Kapitel über DAO kennen gelernt haben, auch mit ADO einsetzen können. Natürlich soll auch die eine oder andere Spezialität von ADO nicht unerwähnt bleiben. Viele bereits in Kapitel 8, »DAO«, enthaltene Informationen gelten auch für den Umgang mit ADO. Dies bezieht sich vor allem auf formale Techniken wie den Umgang mit Auflistungen, die Verwendung von Punkt oder Ausrufezeichen für den Bezug auf Elemente und Eigenschaften oder das Deklarieren und Instanzieren von Objekten. Wenn Sie Informationen zu diesen Themen benötigen, schlagen Sie am besten im oben genannten Kapitel nach. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_09\ ADO.mdb.
428
9
ADO
9.1 Zugriff auf eine Datenquelle herstellen Obgleich Sie im Folgenden feststellen werden, dass viele Vorgehensweisen unter DAO und ADO gleich ablaufen, unterscheidet sich das Objektmodell von ADO in einigen Punkten vom DAO-Objektmodell. Das macht sich beim Zugriff auf die gewünschte Datenbank sofort bemerkbar, wie der folgende Abschnitt zeigen wird.
9.1.1 Connection und ConnectionString Das beginnt damit, dass nicht das Database-Objekt, sondern das Connection-Objekt Ursprung aller lesenden, schreibenden und sonstigen Zugriffe auf die Tabellen der Datenbank ist. Das Connection-Objekt enthält immer einen ConnectionString, der Informationen über die Verbindung zur gewünschten Datenbank enthält. Diesen ConnectionString stellt man entweder selbst zusammen oder man lässt sich dabei unterstützen. Die einfachste Methode dazu ergibt sich beim Zugriff auf die aktuelle Datenbank. In diesem Fall brauchen Sie einfach nur auf das Connection-Objekt des bestehenden CurrentProject-Objekts zuzugreifen. Public Sub Verbindung() Dim cnn As ADODB.Connection Set cnn = CurrentProject.Connection With cnn Debug.Print cnn.ConnectionString End With End Sub Listing 9.1: Erzeugen einer Verbindung zur aktuellen Datenbank und Ausgabe der ConnectionString-Eigenschaft
Die Verbindungszeichenfolge für die aktuelle Datenbank sieht etwa so aus – gut, dass Sie diesen Ausdruck nicht selbst zusammenstellen müssen: Provider=Microsoft.Jet.OLEDB.4.0;User ID=Admin;Data Source=E:\ADO.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database=C:\Dokumente und Einstellungen\Administrator\Anwendungsdaten\Microsoft\Access\System.mdw;Jet OLEDB:Registry Path=Software\Microsoft\Office\11.0\Access\Jet\4.0;Jet OLEDB:Database Password="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:New Database Password="";Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False
Was die Parameter im Einzelnen bedeuten, soll hier gar nicht aufgeschlüsselt werden – für die Verwendung der Datenbank, zu der auch das VBA-Projekt gehört, reicht die Kenntnis, dass CurrentProject.Connection die richtige Verbindung liefert.
Zugriff auf eine Datenquelle herstellen
429
Für den Fall, dass Sie einmal eine Verbindung zu einer externen Datenquelle herstellen möchten, brauchen Sie den ConnectionString auch nicht unbedingt manuell zusammenzustellen. Zur Ermittlung der passenden Parameter können Sie auch einen Dialog verwenden, der bei der Festlegung der Parameter behilflich ist. Für die Verwendung dieses Dialogs benötigen Sie ein Objekt der Bibliothek Microsoft OLE DB Service Component 1.0 Type Library. Die folgende Routine erzeugt eine Instanz dieses DataLinks-Objekts und zeigt den Dialog zum Anlegen einer neuen Verbindungszeichenfolge an (siehe Abbildung 9.1). Public Function ConnectionStringErmitteln() ConnectionStringErmitteln = CreateObject("DataLinks").PromptNew End Function Listing 9.2: Aufruf des Dialogs zum Ermitteln einer Verbindungszeichenfolge
Abbildung 9.1: Anlegen einer neuen Verbindung
Mit dem gleichen Dialog können Sie nicht nur die Eigenschaften neuer Verbindungen auswählen und zurückgeben lassen, sondern auch die Eigenschaften von CurrentProject.Connection etwas übersichtlicher ausgeben (siehe Abbildung 9.2). Dazu müssen Sie die aufrufende Routine geringfügig abändern:
430
9
ADO
Public Function ConnectionStringBearbeiten() ConnectionStringBearbeiten = _ CreateObject("DataLinks").PromptEdit(CurrentProject.Connection) End Function Listing 9.3: Aufrufen des Dialogs Datenverknüpfungseigenschaften für eine bestehende Verbindungszeichenfolge
Abbildung 9.2: Bearbeiten von CurrentProject.Connection
9.2 Manipulation des Datenmodells Die Manipulation von Tabellen, Feldern und anderen Datenbankobjekten erfolgt nicht über ADO selbst, sondern über die Objekte, Methoden und Eigenschaften der Bibliothek ADOX. Die entsprechende Bibliothek im Verweise-Dialog heißt Microsoft ADO Ext. 2.7 for DDL and Security.
9.2.1 Anlegen einer Tabelle Zum Anlegen einer Tabelle benötigen Sie das Catalog-Objekt der ADOX-Bibliothek. Dieses wird mit der Eigenschaft ActiveConnection auf die aktuelle Datenbank eingestellt. Zum Löschen einer eventuell schon vorhandenen gleichnamigen Tabelle stellt das Catalog-Objekt die Delete-Methode der Tables-Auflistung zur Verfügung.
Manipulation des Datenmodells
431
Anschließend wird ein neues Table-Objekt erstellt und mit dem Namen tblUnternehmen versehen. Bevor die Tabelle an die Tables-Auflistung angehängt und damit verfügbar gemacht wird, fügen Sie zwei Felder an: UnternehmenID und Unternehmen. Dabei werden zwei unterschiedliche Vorgehensweisen verwendet: Die erste instanziert zunächst ein neues Column-Objekt, füllt dessen Eigenschaften mit den entsprechenden Daten und fügt es dann an die Columns-Auflistung des Table-Objekts an. Die zweite übergibt die benötigten Informationen direkt beim Anfügen eines neuen Feldes an das TableObjekt. Nach dem Anlegen werden die Tables-Auflistung und das Datenbankfenster aktualisiert. Public Sub TabelleAnlegen_Unternehmen() Dim cat As ADOX.Catalog Dim tbl As ADOX.Table Dim col As ADOX.Column Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Bestehende Tabelle löschen cat.Tables.Delete "tblUnternehmen" 'Verweis auf neues Table-Objekt Set tbl = New ADOX.Table 'Name der Tabelle zuweisen tbl.Name = "tblUnternehmen" 'Feld neu erstellen und per Objektvariable referenzieren Set col = New ADOX.Column col.Name = "UnternehmenID" col.Type = adInteger tbl.Columns.Append col 'Noch ein Feld erstellen, kurze Fassung tbl.Columns.Append "Unternehmen", adVarWChar, 30 'Tabelle anhängen und Katalog aktualisieren With cat.Tables .Append tbl .Refresh End With 'Datenbankfenster aktualisieren Application.RefreshDatabaseWindow
432
9
ADO
Set tbl = Nothing Set cat = Nothing End Sub Listing 9.4: Anlegen einer Tabelle mit ADOX
Konstanten für Datentypen unter ADO und ADOX Tabelle 9.1 zeigt die häufigsten ADOX-Konstanten für die unterschiedlichen Datentypen. Konstante
Datentyp
adBigInt
Big Integer
adBinary
Binary
adBoolean
Boolean
dbUnsignedTinyInt
Byte
adChar
Char
adCurrency
Currency
adDate
Date/Time
adNumeric
Decimal
adDouble
Double
adGUID
GUID
adSmallInt
Integer
adInteger
Long
adLongVarBinary
Long Binary (OLE Object)
adLongVarWChar
Memo
adNumeric
Numeric
adSingle
Single
adWChar, adVarWChar
Text (Unicode)
dbTime
Time
adDBTimeStamp
Time Stamp
Tabelle 9.1: Konstanten für den Datentyp
9.2.2 Autowert anlegen Wenn Sie das Feld UnternehmenID als Autowert festlegen möchten, müssen Sie in obiger Routine noch einige Zeilen hinter dem Anlegen des Feldes hinzufügen: 'Anlegen eines Autowertes With tbl.Columns("UnternehmenID")
Manipulation des Datenmodells
433
.ParentCatalog = cat .Properties("AutoIncrement") = True End With AutoIncrement ist eine Eigenschaft, die nur für Access-Datenbanken gilt.
9.2.3 Löschen einer Tabelle Das Löschen einer Tabelle erfolgt über die Delete-Methode der Tables-Auflistung. Die folgende Routine löscht die soeben erstellte Tabelle und aktualisiert die Tables-Auflistung sowie das Datenbankfenster. Public Sub TabelleLoeschen_Unternehmen() Dim cat As ADOX.Catalog Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Bestehende Tabelle löschen On Error Resume Next cat.Tables.Delete "tblUnternehmen" On Error GoTo 0 'Katalog aktualisieren cat.Tables.Refresh 'Datenbankfenster aktualisieren Application.RefreshDatabaseWindow Set cat = Nothing End Sub Listing 9.5: Löschen einer Tabelle mit ADOX
9.2.4 Erstellen eines Index Mit der folgenden Routine fügen Sie der Tabelle tblUnternehmen einen Primärindex auf dem Feld UnternehmenID hinzu. Public Sub IndexErstellen() Dim Dim Dim Dim Dim
cat As ADOX.Catalog idx As ADOX.Index tbl As ADOX.Table col As ADOX.Column idxs As ADOX.Indexes
434
9
ADO
'Catalog instanzieren und auf aktuelle Datenbank einstellen Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Tabelle festlegen Set tbl = cat.Tables("tblUnternehmen") 'Verweis auf Indexes-Auflistung erstellen Set idxs = tbl.Indexes 'Neues Index-Objekt erstellen Set idx = New ADOX.Index With idx 'Index-Objekt mit Eigenschaften ausstatten .Name = "PrimaryKey" .PrimaryKey = True .Unique = True 'Column-Objekt mit zu indizierendem Feld erzeugen 'und zur Auflistung der indizierten Columns hinzufügen Set col = New ADOX.Column col.Name = "UnternehmenID" .Columns.Append col End With 'Index an die Auslistung Indexes anfügen idxs.Append idx Set Set Set Set Set
col = Nothing idx = Nothing idxs = Nothing tbl = Nothing cat = Nothing
End Sub Listing 9.6: Anlegen eines Index mit ADOX
9.2.5 Löschen eines Index Zum Entfernen eines Index verwenden Sie die folgende Routine. Sie verwendet die Delete-Methode der Indexes-Auflistung zum Entfernen des Index. Public Sub IndexLoeschen() Dim cat As ADOX.Catalog Dim tbl As ADOX.Table Dim idxs As ADOX.Indexes
Manipulation des Datenmodells
435
Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection Set tbl = cat.Tables("tblUnternehmen") Set idxs = tbl.Indexes 'Index aus der Auflistung löschen idxs.Delete "PrimaryKey" Set tbl = Nothing Set idxs = Nothing Set cat = Nothing End Sub Listing 9.7: Löschen eines Index aus der Auflistung der Indizes einer Tabelle
9.2.6 Erstellen einer Beziehung Um eine Beziehung zwischen zwei Tabellen herzustellen, verwenden Sie die folgende Routine. Voraussetzung ist, dass das Fremdschlüsselfeld der Detailtabelle den gleichen Datentyp wie das Primärschlüsselfeld der Mastertabelle hat. Außerdem muss der Primärschlüssel der Mastertabelle eindeutig sein. Sind die Voraussetzungen erfüllt (und die angegebenen Tabellen beziehungsweise Felder vorhanden), legt die Routine die Beziehung aus Abbildung 9.3 an. Wenn Sie zusätzlich Löschweitergabe oder Aktualisierungsweitergaben definieren möchten, müssen Sie die Eigenschaften DeleteRule und UpdateRule des key-Objekts mit den entsprechenden Werten bestücken. Mit der Konstanten adRICascade sorgen Sie für die Weitergabe der jeweiligen Aktion. Public Sub BeziehungErstellen() Dim cat As ADOX.Catalog Dim tbl As ADOX.Table Dim key As ADOX.key Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Tabelle mit dem Fremdschlüsselfeld festlegen Set tbl = cat.Tables("tblMitarbeiter") 'Neuen Key instanzieren und Eigenschaften zuweisen Set key = New ADOX.key key.Name = "ForeignKey" key.Type = adKeyForeign 'Tabelle mit Primärschlüssel festlegen
436
9
ADO
key.RelatedTable = "tblUnternehmen" 'Verknüpfungsfeld der Detailtabelle angeben key.Columns.Append "UnternehmenID" 'Optional: Lösch- oder Aktualisierungsweitergabe key.DeleteRule = adRICascade key.UpdateRule = adRICascade 'Verknüpfungsfeld der Mastertabelle angeben key.Columns("UnternehmenID").RelatedColumn = "UnternehmenID" 'Key an die Keys-Auflistung anhängen tbl.keys.Append key Set key = Nothing Set tbl = Nothing Set cat = Nothing End Sub Listing 9.8: Anlegen einer Beziehung zwischen zwei Tabellen
Abbildung 9.3: Mit ADOX erstellte Beziehung
9.2.7 Löschen einer Beziehung Die Beziehung beziehungsweise den Fremdschlüssel löschen Sie mit der folgenden Routine: Public Sub BeziehungLoeschen() Dim cat As ADOX.Catalog Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Beziehung in Form des Fremdschlüssels löschen
Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten
437
cat.Tables("tblMitarbeiter").keys.Delete "ForeignKey" Set cat = Nothing End Sub Listing 9.9: Löschen einer Beziehung
9.3 Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten In den folgenden Abschnitten erfahren Sie, wie Sie auf die Daten der Datenbank zugreifen können.
9.3.1 Ausgeben aller Tabellen Im vorigen Kapitel zum Thema DAO haben Sie erfahren, wie Sie mit DAO alle Tabellen ausgeben oder prüfen, ob eine Tabelle vorhanden ist. Unter ADO (nicht ADOX!) gibt es eine derartige Funktion nicht – dafür bietet Access aber ab Version 2000 die AllTables-Auflistung. Die AllTables-Auflistung ist ein Element des CurrentData-Objekts, das wiederum zum Application-Objekt gehört. Statt einer ADO-Routine zum Ausgeben aller Tabellen finden Sie also nun zumindest eine Routine, die kein DAO verwendet: Public Sub TabellenAusgeben() Dim obj As AccessObject For Each obj In CurrentData.AllTables Debug.Print obj.Name Next obj End Sub Listing 9.10: Alle Tabellen der Datenbank ausgeben
9.3.2 Prüfen, ob eine Tabelle vorhanden ist Auch hier brauchen Sie nicht mit ADO zu arbeiten, sondern können die AllTablesAuflistung verwenden: Public Function IstTabelleVorhanden_ADO(strTabellenname As String) As Boolean Dim objTable As AccessObject
438
9
ADO
On Error Resume Next Set objTable = CurrentData.AllTables (strTabellenname) IstTabelleVorhanden_ADO = Not objTable Is Nothing End Function Listing 9.11: Prüfen des Vorhandenseins einer Tabelle
9.3.3 Datensatzgruppe auf Basis einer Tabelle öffnen Das Öffnen einer Datensatzgruppe erfolgt anders als unter DAO. Die Methode zum Öffnen ist keine Methode des übergeordneten Objekts (bei DAO das Database-Objekt), sondern eine Methode des ADO-Recordset-Objekts selbst. Die Open-Methode erwartet den Namen der zu öffnenden Tabelle oder Abfrage beziehungsweise einen SQL-Ausdruck, den Connection-String sowie zwei Parameter zur Angabe des Cursortyps und des Sperrmechanismus. Die Beschreibung der verschiedenen Möglichkeiten finden Sie im Anschluss an die Routine zum Öffnen einer Datensatzgruppe. Hier tritt offen zu Tage, weshalb es sich lohnt, bei der Variablendeklaration explizit die Bibliothek mit anzugeben, aus der die Objekte stammen: Wenn Sie das versäumt haben und die DAO-Bibliothek ist in der Liste der Verweise oberhalb der ADO-Bibliothek angeordnet, sucht die folgende Routine vergeblich die OpenMethode des Recordset-Objekts. Public Sub DatensatzgruppeOeffnen() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblUnternehmen", cnn, adOpenDynamic, adLockOptimistic With rst 'etwas mit der Datenatzgruppe machen End With rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 9.12: Öffnen einer Datensatzgruppe mit ADO
Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten
439
Das eigentliche Öffnen der Datensatzgruppe kann auch so erfolgen: rst.ActiveConnection = cnn rst.LockType = adLockOptimistic rst.CursorType = adOpenKeyset rst.Open "tblUnternehmen"
Dabei werden zunächst die Eigenschaften festgelegt und erst dann wird die Datensatzgruppe geöffnet.
9.3.4 Cursor-Typen Beim Öffnen einer Datensatzgruppe unter ADO können Sie folgende Cursor-Typen für die Eigenschaft CursorType verwenden: adOpenDynamic: Liefert eine Gruppe von Datensätzen und zeigt Datensatzänderungen anderer Benutzer an (entspricht dbOpenDynaset unter DAO). adOpenForwardOnly: Liefert einen Snapshot der gewünschten Datensätze zum Zeitpunkt des Öffnens des Recordset-Objekts, kann nur vorwärts durchlaufen werden (entspricht dbOpenForwardOnly unter DAO). adOpenKeyset: Liefert Verweise auf die Datensätze der zugrunde liegenden Tabellen (entspricht keiner DAO-Konstante genau, ist aber fast äquivalent zu dbOpenDynaset und etwas schneller als adOpenDynamic). adOpenStatic: Liefert einen Snapshot der gewünschten Datensätze zum Zeitpunkt des Öffnens des Recordset-Objekts (entspricht dbOpenSnapshot unter DAO).
9.3.5 Sperrung von Daten Mit dem Parameter LockType legen Sie fest, wie die eingelesenen Daten gesperrt werden sollen: adLockReadonly: Öffnet ein schreibgeschütztes Recordset. adLockPessimistic: Sperrt die komplette Speicherseite, in der sich der von einer Änderung betroffene Datensatz befindet, sobald die Bearbeitung beginnt. adLockOptimistic: Sperrt die komplette Speicherseite, in der sich der von einer Änderung betroffene Datensatz befindet, erst, wenn der Datensatz aktualisiert wird. adLockBatchOptimistic: Wie adLockOptimistic, aber für die UpdateBatch-Methode.
440
9
ADO
9.3.6 Datensätze eines Recordsets durchlaufen Zum Durchlaufen der Datensätze eines Recordsets verwenden Sie beispielsweise die Do While-Schleife, in der Sie nach dem Durchführen der gewünschten Aktion jeweils mit der MoveNext-Methode einen Datensatz weiter springen. Als Abbruchbedingung dient die EOF-Eigenschaft der Datensatzgruppe, die den Wert True erhält, wenn der Datensatzzeiger über den letzten Datensatz hinaus verschoben wurde. Public Sub DatensaetzeDurchlaufen() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic Do While Not rst.EOF With rst 'etwas mit dem aktuellen Datensatz tun End With rst.MoveNext Loop rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 9.13: Datensatzgruppe durchlaufen
Das Pendant zur EOF-Eigenschaft ist die BOF-Eigenschaft. Sie erhält den Wert True, wenn der Datensatzzeiger sich vor dem ersten Datensatz der Datensatzgruppe befindet. Neben der MoveNext-Methode gibt es noch die Methoden MoveFirst, MoveLast und MovePrevious zum Bewegen innerhalb der Datensatzgruppe.
9.3.7 Anzahl der Datensätze in einer Datensatzgruppe ermitteln Um die Anzahl der Datensätze zu ermitteln, dürfen Sie nicht die Konstante adForwardOnly für die Eigenschaft CursorType einsetzen. Außerdem müssen Sie für die Eigenschaft CursorPosition den Parameter adUseClient verwenden, da serverseitige Datensatz-Cursor unter JET das Zählen von Datensätzen nicht unterstützen.
Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten
441
Public Sub Datensatzanzahl() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.CursorLocation = adUseClient rst.CursorType = adOpenForwardOnly rst.LockType = adLockOptimistic rst.ActiveConnection = cnn rst.Open "tblUnternehmen" Debug.Print rst.RecordCount End Sub Listing 9.14: Ermitteln der Datensatzanzahl eines Recordsets
9.3.8 Prüfen, ob eine Datensatzgruppe leer ist Eine einfache Möglichkeit, um herauszufinden, ob eine Datensatzgruppe leer ist, besteht darin zu schauen ob die EOF- und die BOF-Eigenschaften der Datensatzgruppe gleichzeitig leer sind: Public Sub LeereDatensatzgruppe() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblMitarbeiter", cnn, adOpenForwardOnly, adLockOptimistic If rst.BOF And rst.EOF Then MsgBox "Die Datensatzgruppe ist leer." End If rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 9.15: Prüfen, ob eine Datensatzgruppe leer ist
442
9
ADO
9.3.9 Ausgabe des Inhalts eines Recordsets Manchmal möchte man den Inhalt eines Recordsets auf die Schnelle betrachten oder etwa in einer Textdatei speichern. Dabei leistet die GetString-Methode gute Dienste. Diese Methode ist sehr flexibel und kann, mit zusätzlichen Parametern ausgestattet, die Spalten auch als formatierten Text zurückgeben. Zu den möglichen (optionalen) Parametern markieren Sie GetString im Code und betätigen die (F1)-Taste. Public Sub DatensatzgruppeAusgeben() … rst.Open "tblUnternehmen", cnn, adOpenForwardOnly, adLockOptimistic Debug.Print rst.GetString … End Sub Listing 9.16: Ausgeben der Daten einer Datensatzgruppe
9.3.10 Speichern der Daten in einem Array Wenn Sie die Daten einer Datensatzgruppe in einem Array weiter verarbeiten möchten, können Sie mit der GetRows-Methode ein zweidimensionales Array mit den in der Datensatzgruppe enthaltenen Daten füllen. Die GetRows-Methode hat drei Parameter: Rows gibt an, wie viele Datensätze eingelesen werden sollen, Start enthält ein Bookmark auf den ersten einzulesenden Datensatz und Fields die Position oder den Namen des einzulesenden Feldes beziehungsweise ein Array mit den Namen der einzulesenden Felder. Ohne die Angabe von Parametern liest GetRows alle Datensätze mit allen Feldern ein. Public Sub DatensatzgruppeInArray() Dim Dim Dim Dim Dim
cnn As ADODB.Connection rst As ADODB.Recordset varRecordset() As Variant i As Integer j As Integer
Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblUnternehmen", cnn, adOpenForwardOnly, adLockOptimistic varRecordset = rst.GetRows() For i = 0 To UBound(varRecordset, 2) For j = 0 To UBound(varRecordset, 1) Debug.Print varRecordset(j, i) Next j Next i
Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten
443
rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 9.17: Einlesen der Daten einer Datensatzgruppe in ein Array
9.3.11 Abfragen mit Parametern verwenden Während Sie unter DAO das QueryDef-Objekt und die Parameters-Auflistung verwendet haben, um Abfragen mit Parametern zu handhaben, stellt ADO für diesen Zweck das Command-Objekt zur Verfügung. Das folgende Beispiel zeigt, wie Sie ein Recordset auf Basis einer mit Parametern versehenen Abfrage öffnen. Sie verwenden hier nicht wie bei DAO eine Parameters-Auflistung, sondern geben den Wert eines einzelnen Parameters als String und mehrere Werte in der richtigen Reihenfolge als String-Array an – bei anderen Datentypen für die Parameter verwenden Sie dann eher ein Variant-Array. Public Sub Parameterabfrage() Dim Dim Dim Dim
cnn As ADODB.Connection cmd As ADODB.Command rst As ADODB.Recordset lngRecordsAffected As Long
Set cnn = CurrentProject.Connection 'Command-Objekt instanzieren Set cmd = New ADODB.Command 'Aktuelle Verbindung zuweisen cmd.ActiveConnection = cnn 'auszuführende Abfrage angeben cmd.CommandText = "qryMitarbeiter" 'Typ des Commands festlegen cmd.CommandType = adCmdTable 'Command-Objekt ausführen. Der erste Parameter wird nur für 'Aktionsabfragen benötigt, der zweite enthält die Werte für die 'Abfrageparameter. Ein Parameter wird als String, mehrere als 'Array übergeben. Set rst = cmd.Execute(, "Minhorst") Do While Not rst.EOF Debug.Print rst!Vorname, rst!Nachname rst.MoveNext
444
9
ADO
Loop Set rst = Nothing Set cnn = Nothing End Sub Listing 9.18: Erzeugen eines Recordsets auf Basis einer Parameterabfrage
9.4 Datensätze suchen Zur Suche von Datensätzen gibt es mehrere Möglichkeiten. Wie unter DAO können Sie indizierte Felder mit der Seek-Methode durchsuchen, anderenfalls hilft die FindMethode weiter. Diese ist allerdings nicht so flexibel wie die Find…-Methoden von DAO, wie Sie nachfolgend lesen können. Am einfachsten ist es jedoch, die gesuchten Datensätze direkt in der Datenherkunft des Recordset-Objekts einzugrenzen.
9.4.1 Gesuchte Datensätze per Source-Eigenschaft des Recordsets ermitteln Die Source-Eigenschaft eines Recordset-Objekts enthält die dem Recordset-Objekt zugrunde liegende Tabelle oder Abfrage. Sie können hier auf drei Arten bereits mit dem Öffnen des Recordset-Objekts die gewünschten Daten ausfindig machen: Direkte Angabe einer SELECT-Anweisung rst.Open "SELECT * FROM Artikel WHERE Artikelname LIKE 'A%'", cnn, adOpenKeyset, adLockOptimistic
Angabe einer gespeicherten Abfrage rst.Open "qryArtikelMitPreisGroesser50", cnn, adOpenKeyset, adLockOptimistic
Kombination aus SELECT-Anweisung und gespeicherter Abfrage rst.Open "SELECT * FROM qryArtikelMitPreisGroesser50 WHERE Artikelname LIKE 'T%'", cnn, adOpenKeyset, adLockOptimistic
Joker in Zeichenketten unter ADO und SQL Wenn Sie wie in den obigen Beispielen Vergleichsausdrücke mit Platzhaltern verwenden möchten, müssen Sie die SQL Server-Syntax verwenden. Dabei entspricht der Platzhalter für beliebig viele Zeichen dem Prozentzeichen (%) und nicht wie in VBA oder Abfragen, die Sie über die Abfrageentwurfsansicht erstellen (siehe Abbildung 9.4), dem Sternchen (*). Der Platzhalter für ein einzelnes Zeichen entspricht dem Unterstrich (_) und nicht wie in VBA oder Abfragen dem Fragezeichen (?).
Datensätze suchen
445
Abbildung 9.4: Joker für beliebig viele Zeichen in Abfragen
9.4.2 Seek Wenn Sie mit der Seek-Methode nach Daten suchen möchten, müssen zwei Bedingungen erfüllt sein: Das zu durchsuchende Feld muss indiziert sein und Sie müssen die Konstante adCmdTableDirect als Option beim Öffnen der Datensatzgruppe festlegen. Ersteres prüfen Sie ganz einfach, indem Sie in der Entwurfsansicht einer Tabelle den Indizes-Dialog einblenden (Menüeintrag Ansicht/Indizes, siehe Abbildung 9.5). Letzteres impliziert, dass Sie nur auf einzelne Tabellen, aber nicht auf Abfragen oder verknüpfte Tabellen zugreifen können. Die Option adcmdTableDirect verwenden Sie mit der Open-Methode des RecordsetObjekts. Die folgende Routine zeigt, wie Sie mit der Seek-Methode einen bestimmten Datensatz einer Tabelle finden und den Wert seines Primärschlüsselfeldes ausgeben. Im Gegensatz zur weiter unten vorgestellten Find-Methode enthält das RecordsetObjekt nicht alle Datensätze, die dem Suchkriterium entsprechen, sondern es wird lediglich der Datensatzzeiger auf einem Datensatz platziert, der den mit der SeekMethode übergebenen Parametern entspricht. Dabei gibt es verschiedene Varianten, die Sie mit dem Parameter SeekOption übergeben. Die wichtigsten sind folgende: adSeekFirstEQ: Setzt den Datensatzzeiger auf den ersten Datensatz mit dem angegebenen Wert. adSeekLastEQ: Setzt den Datensatzzeiger auf den letzten Datensatz mit dem angegebenen Wert.
446
9
ADO
Abbildung 9.5: Anzeigen der Indizes einer Tabelle
Seek ist unter den gegebenen Bedingungen die schnellste Möglichkeit, um auf einen
bestimmten Datensatz zuzugreifen. Public Sub SuchenMitSeek() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset 'Recordset mit direktem Zugriff auf die Tabelle öffnen rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic, adCmdTableDirect 'Index festlegen: Achtung, Indexname und nicht den Feldnamen verwenden! rst.Index = "Artikelname" 'Suche starten rst.Seek "Chocolade", adSeekFirstEQ
Datensätze suchen
447
'Aktuellen Datensatz anzeigen If Not rst.EOF Then Debug.Print rst![Artikel-Nr] End If rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 9.19: Suche mit der Seek-Methode
9.4.3 Find Die Find-Methode ist weniger flexibel als die Find…-Methoden von DAO. Sie fasst zwar die Funktion der Find…-Methoden von DAO zusammen, beschränkt allerdings beispielsweise die möglichen Kriterien auf ein einziges. Die Find-Methode bietet vier Parameter, von denen nur die Angabe des Suchkriteriums Pflicht ist. Als Suchkriterium dient ein aus Feldname, Vergleichsoperator und Vergleichswert zusammengesetzter Ausdruck, wobei die gleichen Regeln wie für die WHERE-Klausel von SQL-Abfragen gelten (siehe auch Kapitel 7, »Access-SQL«). Die übrigen Parameter: SkipRecords legt fest, wie viele Datensätze von der aktuellen Position aus übersprungen werden sollen, und ist beispielsweise wichtig, wenn Sie bereits einen Datensatz gefunden haben und bei der Suche des nächsten Datensatzes nicht auf dem aktuellen Datensatz stehen bleiben möchten. SearchDirection erwartet eine der Konstanten adSearchForward oder adSearchBackward und sucht in der entsprechenden Richtung. Start erwartet die Position des Datensatzzeigers für einen vom ersten Datensatz abweichenden Startpunkt. Wenn die Find-Methode einen Datensatz findet, positioniert sie den Datensatzzeiger auf dem gefundenen Datensatz. Sollen weitere Datensätze gefunden werden wie etwa im folgenden Beispiel, verwenden Sie innerhalb einer Do While-Schleife erneut die Find-Methode – mit dem gleichen Kriterium, aber dem Wert 1 für den Parameter SkipRecords. Dies hat den Grund, dass die Find-Methode mit der Suche immer in dem Datensatz beginnt, auf dem sich aktuell der Datensatzzeiger befindet. Eine solche Suche bedingt immer die Prüfung auf die Eigenschaft EOF oder BOF der Datensatzgruppe (je nach Suchrichtung), da dies die Position des Datensatzzeigers ist, wenn kein Datensatz gefunden wurde. Das entspricht dann der Eigenschaft Recordset.NoMatch unter DAO.
448
9
ADO
Public Sub SuchenMitFind() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Dim strKriterium As String Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic strKriterium = "[Artikelname] LIKE 'A*'" rst.Find strKriterium Do While Not rst.EOF Debug.Print rst!Artikelname rst.Find strKriterium, 1 Loop … End Sub Listing 9.20: Suche nach allen Datensätzen, deren Artikelname mit A beginnt
9.4.4 Filtern Genau wie DAO enthält das Recordset-Objekt auch unter ADO eine Eigenschaft namens Filter. Unter DAO weist man einem Recordset-Objekt den Filter zu und erhält das gefilterte Ergebnis, wenn man ein weiteres Recordset-Objekt auf Basis des ersten öffnet. Dies ist unter ADO einfacher: Hier weisen Sie einfach das gewünschte Filterkriterium zu und können direkt im gleichen Recordset auf das gefilterte Ergebnis zugreifen. Public Sub FilternEinesRecordset() … rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic rst.Filter = "Artikelname LIKE 'A*'"
Do While Not rst.EOF Debug.Print rst!Artikelname rst.MoveNext Loop … End Sub Listing 9.21: Filtern aller Datensätze, deren Artikelname mit A beginnt
Datensätze suchen
449
Um den Filter zu entfernen, verwenden Sie die Konstante adFilterNone anstelle eines Parameters: rst.Filter = adFilterNone
9.4.5 Sortieren Die Datensätze eines Recordsets können Sie entweder bereits über die Datenherkunft oder durch nachträgliches Hinzufügen eines Sortierkriteriums sortieren. In der Datenherkunft gibt es zwei Möglichkeiten: Sie öffnen die Datensatzgruppe mit der Option adTableDirect. In diesem Fall kommt die im Dialog Indizes angegebene Sortierung zur Anwendung. Sie öffnen eine gespeicherte Abfrage mit voreingestellter Sortierreihenfolge oder geben einen SQL-Ausdruck mit einem entsprechenden Sortierkriterium an.
Sortieren nach dem Öffnen des Recordsets Wie in DAO bietet auch ADO eine Eigenschaft zum Angeben eines Sortierkriteriums. Voraussetzung für seinen Einsatz ist, dass das Recordset-Objekt einen clientseitigen Cursor verwendet. Wie auch die Filter-Eigenschaft wirkt sich die Sort-Eigenschaft unmittelbar auf die aktuelle Datensatzgruppe aus. Nach dem Öffnen des Recordsets können Sie eine Sortierung mit der Sort-Eigenschaft vornehmen: Public Sub SortierenEinesRecordset() … rst.CursorLocation = adUseClient
rst.Open "Artikel", cnn, adOpenDynamic, adLockOptimistic rst.Sort = "Artikelname ASC"
Do While Not rst.EOF Debug.Print rst!Artikelname rst.MoveNext Loop … End Sub Listing 9.22: Nachträgliches Sortieren einer Datensatzgruppe
450
9
ADO
9.4.6 Lesezeichen Auch ADO-Recordsets bieten eine Bookmark-Eigenschaft. Bookmarks dienen dazu, sich die Position eines Datensatzes zu merken und auf einen gemerkten Datensatz zurückzuspringen. Daher ist diese Eigenschaft les- und schreibbar. Unter Verwendung eines Bookmarks können Sie beispielsweise von einem bestimmten Datensatz an das Ende der Datensatzgruppe und anschließend wieder zurück zum ursprünglichen Datensatz springen. Public Sub SpringenMitBookmark() … Dim varLesezeichen As Variant … rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic rst.Find "Artikelname = 'Chai'" Debug.Print rst!Artikelname varLesezeichen = rst.Bookmark rst.MoveLast Debug.Print rst!Artikelname rst.Bookmark = varLesezeichen Debug.Print rst!Artikelname … End Sub Listing 9.23: Hin- und herspringen mit Bookmarks
9.5 Datensätze bearbeiten Daten lassen sich mit ADO auf verschiedene Art manipulieren. Nachfolgend erfahren Sie, wie Sie Daten mit Aktionsabfragen und mit den Methoden des Recordset-Objekts bearbeiten.
9.5.1 Datensatz anlegen Zum Anlegen eines neuen Datensatzes verwenden Sie AddNew- und die UpdateMethode. Dazwischen stellen Sie die Felder der Datensatzgruppe auf die gewünschten Werte ein. Public Sub DatensatzAnlegen() … rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic
Datensätze bearbeiten
451
rst.AddNew rst!Unternehmen = "Pearson Education Deutschland GmbH" rst.Update … End Sub Listing 9.24: Neuen Datensatz anlegen
Sie brauchen im Unterschied zu DAO die Update-Methode nicht auszuführen, wenn Sie den Datensatz wechseln, bevor Sie das Recordset-Objekt schließen. Wenn Sie nach dem Anlegen beispielsweise sofort noch einen weiteren Datensatz anlegen möchten, brauchen Sie die Update-Anweisung nur nach dem Anlegen des zweiten Datensatzes und vor dem Schließen der Datensatzgruppe aufzurufen: rst.AddNew rst!Unternehmen = "Pearson Education Deutschland GmbH" rst.AddNew rst!Unternehmen = "amisoft" rst.Update
Wenn Sie mit AddNew das Anlegen eines neuen Datensatzes starten und das RecordsetObjekt schließen, bevor Sie die Update-Methode ausgeführt haben, lösen Sie einen Laufzeitfehler aus: rst.AddNew rst!Unternehmen = "Pearson Education Deutschland GmbH" 'Schließen ohne Update löst Laufzeitfehler aus rst.Close
9.5.2 Datensatz bearbeiten Während Sie unter DAO vor dem Bearbeiten eines Datensatzes die Edit-Methode aufrufen mussten, können Sie unter ADO Änderungen am aktuellen Datensatz direkt vornehmen. Für das Abschließen der Änderungen gilt das Gleiche wie beim Anlegen eines neuen Datensatzes. Public Sub DatensatzAendern() … rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic rst.Find "Unternehmen = 'Pearson Education Deutschland GmbH'" rst!Unternehmen = "Addison Wesley"
452
9
ADO
rst.Update … End Sub Listing 9.25: Datensatz ändern
9.5.3 Datensatz löschen Zum Löschen eines Datensatzes verschieben Sie den Datensatzzeiger auf den zu löschenden Datensatz und entfernen diesen mit der Delete-Methode. Public Sub DatensatzLoeschen() … rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic rst.Find "Unternehmen = 'Pearson Education Deutschland GmbH'" rst.Delete … End Sub Listing 9.26: Löschen eines Datensatzes
9.5.4 Aktionsabfragen ausführen Aktionsabfragen führen Sie unter ADO mit der Execute-Methode aus. Diese Methode erwartet als ersten Parameter den auszuführenden SQL-Ausdruck. Als zweiten Parameter können Sie eine Variable angeben, in der die Anzahl der durch die Aktionsabfrage betroffenen Datensätze gespeichert wird. Diese können Sie nachher weiter verwenden. Public Sub AktionsabfrageAusfuehren() Dim cnn As ADODB.Connection Dim cmd As ADODB.Command Dim lngRecordsAffected Set cnn = CurrentProject.Connection cnn.Execute "INSERT INTO tblUnternehmen(Unternehmen) " _ & "VALUES('Addison-Wesley')", lngRecordsAffected Debug.Print lngRecordsAffected Set cnn = Nothing End Sub Listing 9.27: Ausführen einer Aktionsabfrage
Transaktionen
453
9.6 Transaktionen Transaktionen funktionieren unter ADO prinzipiell wie unter DAO. Der wichtigste Unterschied ist, dass die Bezeichnungen der drei Methoden zum Durchführen von Transaktionen vereinheitlicht wurden. Diese heißen jetzt: BeginTrans CommitTrans RollbackTrans Außerdem gehören die Methoden zum Connection-Objekt – unter DAO war es das Workspace-Objekt. Weitere Informationen zu Transaktionen finden Sie in Kapitel 8, Abschnitt 8.11, »Transaktionen«.
9.7 Besonderheiten von ADO gegenüber DAO ADO bietet einige Besonderheiten gegenüber DAO. So ist es möglich, eine Datensatzgruppe zu speichern oder Recordset-Objekte ohne Datenherkunft in Form einer Tabelle oder Abfrage zu verwenden.
9.7.1 Datensatzgruppe speichern Sie können eine Datensatzgruppe in einem Microsoft-eigenen Format oder im XMLFormat speichern. Die folgende Routine speichert den Inhalt der Tabelle Personal in der Datei Personal.xml im Verzeichnis der Datenbank. Public Sub DatensatzgruppeSpeichern() … rst.Open "Personal", cnn, adOpenStatic, adLockOptimistic rst.Save CurrentProject.Path & "\Personal.xml", adPersistXML … End Sub Listing 9.28: Speichern einer Datensatzgruppe
9.7.2 Datensatzgruppe laden Um die so gespeicherte Datensatzgruppe wieder verfügbar zu machen, verwenden Sie die Open-Methode des Recordset-Objekts. Allerdings geben Sie statt eines Tabellenoder Abfragenamens den Namen der Datei an.
454
9
ADO
Public Sub DatensatzgruppeEinlesen() Dim rst As New ADODB.Recordset rst.Open CurrentProject.Path & "\Personal.xml", , adOpenStatic, _ adLockOptimistic, adCmdFile Debug.Print rst.RecordCount Set rst = Nothing End Sub Listing 9.29: Einlesen einer Datensatzgruppe aus einer XML-Datei
9.7.3 Ungebundene Recordsets verwenden Unter ADO lassen sich Recordsets ohne Angabe einer Datenherkunft anlegen und zum Speichern von Daten verwenden – also ohne ein Connection-Objekt. Da ein ungebundenes Recordset keine Datenherkunft hat, besitzt es natürlich auch noch keine Felder. Diese können Sie ganz einfach mit der Append-Methode der Fields-Auflistung des Recordset-Objekts hinzufügen. Anschließend können Sie das Recordset ganz normal verwenden. Ein ungebundenes Recordset ist beispielsweise sehr nützlich, wenn Sie größere Datenmengen in Kombinationsfeldern, Listenfeldern oder sogar Datenblättern anzeigen möchten, diese aber nicht in einer Tabelle gespeichert werden sollen. Die Routine aus dem folgenden Beispiel legt eine Datensatzgruppe mit den beiden Feldern ModulID und Modulname an und fügt alle Module der aktuellen Datenbank hinzu. Anschließend gibt sie alle Einträge der Tabelle im Direktfenster aus. Public Sub UngebundeneDatensatzgruppe() Dim rst As ADODB.Recordset Dim objModul As AccessObject Dim i As Integer Set rst = New ADODB.Recordset rst.Fields.Append "ModulID", adInteger rst.Fields.Append "Modulname", adVarWChar, 255 rst.Open For Each objModul In CurrentProject.AllModules rst.AddNew rst!ModulID = i rst!Modulname = objModul.Name rst.Update Next objModul
Besonderheiten von ADO gegenüber DAO
455
rst.Update rst.MoveFirst Do While Not rst.EOF Debug.Print rst!ModulID, rst!Modulname rst.MoveNext Loop rst.Close Set rst = Nothing End Sub Listing 9.30: Anlegen einer ungebundenen Datensatzgruppe
9.7.4 Ereignisse von Datensatzgruppen Ein sehr interessantes Feature von ADO sind die Ereignisse der einzelnen Objekte wie Connection- oder Recordset-Objekt (siehe Abbildung 9.6). Diese sind gerade in Zusammenhang mit der Programmierung mehrschichtiger Anwendungen interessant.
Abbildung 9.6: Ereigniseigenschaften von Recordset-Objekten
456
9
ADO
Sie können die Ereignisse des Connection- und des Recordset-Objekts in einer eigenen Klasse kapseln und entsprechende Prozeduren hinzufügen, die durch die jeweiligen Ereignisse ausgelöst werden. Die komplette Kapselklasse soll hier nicht abgedruckt werden, Sie finden diese aber in der Beispieldatenbank Kap_09/ADO.mdb im Klassenmodul clsADORS. Die folgende Routine instanziert diese Klasse und löst durch einige Datensatzoperationen verschiedene Ereignisse des Connection- und des Recordset-Objekts aus. Public Sub TestADOEvents() Dim cADO As New clsADORS cADO.SetSQL DoEvents cADO.MoveRS DoEvents cADO.SetSQL DoEvents cADO.MoveRS DoEvents
"SELECT * FROM Personal" 2 "SELECT * FROM Lieferanten" 7
Set cADO = Nothing End Sub Listing 9.31: Instanzieren und Verwenden einer Klasse, die Connection- und Recordset-Ereignisse auslöst
Dies jedoch nur als Hinweis darauf, dass ADO durchaus Eigenschaften besitzt, die es von DAO positiv abheben. Wenn es Sie interessiert, welche Möglichkeiten die Ereigniseigenschaften der ADOObjekte bieten, schauen Sie sich die Beispieldatenbank Kap_09\ADOEvents auf der Buch-CD an. Dort finden Sie ein an ein ADO-Recordset gebundenes Formular, das die bei der Arbeit mit dem Recordset im Formular zusätzlich verfügbaren Ereignisse des Connection- und des Recordset-Objekts erkennen lässt.
10 Menüs Menüleisten, Symbolleisten und Kontextmenüs sind wichtige Elemente für die ergonomische Steuerung von Anwendungen. Damit Sie im Folgenden immer den Überblick behalten, sollen die verwendeten Begriffe hier kurz erläutert werden.
Menüleiste Eine Menüleiste ist in jeder Anwendung nur einmal vorhanden. Standardmäßig handelt es sich dabei um eine Symbolleiste mit dem Namen Menüleiste. Eine Auflistung der vorhandenen Symbolleisten finden Sie im Dialog Anpassen, den Sie wiederum über den Eintrag Anpassen des Kontextmenüs der Menüleiste oder den Menüeintrag Ansicht/ Symbolleisten/Anpassen öffnen (siehe Abbildung 10.1). Die Menüleiste lässt sich nicht mit herkömmlichen Mitteln ausblenden – weder über den Anpassen-Dialog noch über den Menüeintrag Ansicht/Symbolleisten, dort wird die Menüleiste gar nicht aufgeführt.
Abbildung 10.1: Der Anpassen-Dialog mit markierter Menüleiste
458
10
Menüs
Kontextmenüs Kontextmenüs sind Menüs, die durch Anklicken eines Objekts der Access-Oberfläche mit der rechten Maustaste angezeigt werden. Dabei wird – wie der Name schon sagt – ein Menü angezeigt, das speziell für dieses Objekt benötigte Einträge enthält. Im Anpassen-Dialog findet man Kontextmenüs nicht direkt. Statt dessen hakt man hier den Eintrag Kontextmenü an und findet alle vorhandenen Kontextmenüs in der nun erscheinenden Symbolleiste. Diese Symbolleiste enthält alle Kontextmenüs des gesamten Systems – wenn Sie etwa das Kontextmenü des Registers Tabellen des Datenbankfensters anpassen möchten, wählen Sie hier den Eintrag Datenbank (für das Datenbankfenster) und Tabelle/Abfrage aus (siehe Abbildung 10.2).
Abbildung 10.2: Die Kontextmenüs im Überblick
Symbolleisten Symbolleisten sind alle anderen in der Liste Symbolleisten des Anpassen-Dialogs aufgeführten Elemente. Kennzeichnungsmerkmale sind die folgenden: Sie können jederzeit ein- und ausgeblendet werden und lassen sich wie die Menüleiste frei am Bildschirm verschieben oder an den Rändern verankern.
Grundlagen zu Menüs
459
Oberbegriff: Menüs Wenn nachfolgend allgemein von Menüleisten, Symbolleisten und Kontextmenüs die Rede ist, werden diese einfach Menüs genannt. Die Beispieldatenbank zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_10\Menueleisten.mdb.
10.1 Grundlagen zu Menüs Die in Access eingebauten Menüs – das sind alle, die Sie beim ersten Start von Access vorfinden, und diejenigen, die im Zusammenhang mit der Anzeige bestimmter Objekte wie beispielsweise der Entwurfsansicht eingeblendet werden – lassen sich nicht entfernen, sondern lediglich ausblenden. Um den Menüs eine eigene Note zu verleihen, können Sie erstens die eingebauten Menüs bearbeiten und zweitens eigene Menüs hinzufügen und damit die eingebauten Menüs ersetzen. Die erste Möglichkeit sieht vor, dass Sie Einträge aus den eingebauten Menüs entfernen und eigene Einträge hinzufügen können. Das ist manchmal hilfreich, etwa wenn man einen nicht im eingebauten Menü enthaltenen Befehl nachrüsten oder verwaiste Einträge entsorgen möchte. Dabei gibt es nur ein Problem: Die Änderungen werden in der Registry von Windows benutzerbezogen gespeichert; auf einem anderen Rechner oder bei Anmeldung unter einem anderen Benutzernamen stehen die Änderungen also nicht mehr zur Verfügung. Für den Entwickler – also für Sie – ist das allerdings halb so schlimm, denn Sie werden beim Verwenden der Entwicklungsumgebung vermutlich nicht alle paar Stunden den Rechner wechseln. Bei einer Anwendung, die möglicherweise an viele Benutzer ausgeliefert wird, ist es allerdings nicht tragbar, wenn Menübefehle mal erscheinen und mal nicht. Hier kommen die benutzerdefinierten Menüs ins Spiel: Sie können nämlich auch eigene Menüs hinzufügen und die eingebauten Menüs durch diese ersetzen. Der große Vorteil benutzerdefinierter Menüs ist, dass diese in der aktuellen Access-Anwendung gespeichert werden. Das ist für den Entwickler nicht praktikabel, denn so muss er sich die Menüs in jeder neuen Datenbank erst einmal neu anlegen, aber für Anwendungen, die an den Anwender weitergegeben werden sollen, ist dies eine optimale Lösung. Einmal erstellte benutzerdefinierte Menüs lassen sich leicht in andere Datenbanken importieren. Dazu verwenden Sie den Objekte importieren-Dialog in der Zieldatenbank (Datei/Externe Dateien/Importieren), machen den Bereich Importieren durch einen Klick auf die Schaltfläche Optionen sichtbar und aktivieren die Option Menüs und Symbolleisten.
460
10
Menüs
Ausgangspunkt für jegliches manuelles Manipulieren von Menüs ist der Anpassen-Dialog – Änderungen an Menüs lassen sich nur durchführen, wenn dieser Dialog sichtbar ist. Gleichzeitig lassen sich die Menüs nicht mehr verwenden, sondern nur noch editieren beziehungsweise auf ihre Eigenschaften hin untersuchen und auch jegliche Aktivität in der VBA-Entwicklungsumgebung wird so unterdrückt. Mit VBA lässt sich jeder Vorgang nachbilden, den Sie mit der Benutzungsoberfläche von Access erledigen und mehr – Sie können damit etwa einzelne Untermenüs oder Einträge abhängig vom aktiven Objekt aktivieren oder deaktivieren oder auch benutzerabhängig ein- oder ausblenden.
Voraussetzungen für das Erstellen von Menüs per VBA Menüs – unter VBA Commandbars genannt – sind kein Access-spezifisches Feature, sondern werden von der Office-Bibliothek zur Verfügung gestellt. Wie gewohnt, müssen Sie zunächst einen Verweis auf die entsprechende Bibliothek erstellen, um auf das Objektmodell zugreifen zu können (siehe Abbildung 10.3).
Abbildung 10.3: Die Programmierung von Menüs setzt einen Verweis auf die Office-Bibliothek voraus.
Sie können natürlich auch auf den Verweis und Early Binding (weitere Informationen siehe Kapitel 6, »VBA«) verzichten und statt dessen alle benötigten Variablen als Object deklarieren – außerdem müssen Sie dann die verwendeten Konstanten durch die entsprechenden Zahlenwerte ersetzen. Wenn Sie so verfahren möchten, sollten Sie dennoch zunächst mit dem Verweis programmieren – die damit einhergehende Verwendung von
Beispielmenü
461
IntelliSense bringt gerade in unbekannten Objektmodellen große Geschwindigkeitsvorteile. Nach Fertigstellung der benötigten Prozeduren können Sie immer noch die OfficeObjektdeklarationen hinauswerfen und durch Object-Typen ersetzen.
10.2 Beispielmenü Als Beispiel werden Sie in den nächsten Abschnitten Menüs für eine fiktive Anwendung erstellen, die eine Menüleiste mit einigen Untermenüs sowie einige Kontextmenüs enthalten soll. Zusätzlich bekommt die Anwendung eine Symbolleiste, die allerdings nur ein Symbol enthält – und zwar zum Drucken des aktuellen Objekts. Die Menüleiste ist wie folgt aufgebaut (Untermenüs hinter dem Doppelpunkt, Untermenüpunkte in Klammern): Datei: Drucken, Beenden Stammdaten: Personen (Übersicht, Neu), Unternehmen (Übersicht, Neu) Berichte: Personen, Unternehmen Personen-Kombinationsfeld Das Personen-Kombinationsfeld soll die Möglichkeit zur Schnellauswahl der Detailseite zu einer Person bieten. Das Formular zur Verwaltung der Personen erhält außerdem ein Kontextmenü, mit dem sich der aktuelle Benutzer löschen, ein neuer Benutzer anlegen und ein Suchformular öffnen lässt. Das Menü legen Sie zuerst manuell an und anschließend erfahren Sie, wie Sie dies per VBA erledigen können und welche Vorteile die Verwendung von VBA bringt – etwa, die Drucken-Schaltfläche der Symbolleiste nur zu aktivieren, wenn gerade ein Bericht in der Vorschauansicht angezeigt wird, oder Menübefehle benutzerabhängig zur Verfügung zu stellen.
Warum Menüs per VBA hinzufügen? Die Benutzungsoberfläche von Access bietet eigentlich komfortable Möglichkeiten zum Anlegen von Menüs und ihren Elementen. Und die Menüstruktur einer Datenbankanwendung erstellt man ja eigentlich nur einmal. Warum wird dann nachfolgend beschrieben, wie sich Menüs, Untermenüs und Steuerelemente per VBA anlegen lassen? Der Grund ist einfach: Weil Sie nur so Menüs dynamisch erstellen und deren Eigenschaften auch dynamisch anpassen können. Die Office-Anwendungen bieten im Startmenü beispielsweise Zugriff auf die letzten geöffneten Dateien und die ändern sich gelegentlich. Vielleicht möchten Sie auch einmal ein Menü erstellen, dessen Einträge vom aktuellen Benutzer oder der Benutzergruppe abhängen, oder Menüeinträge
462
10
Menüs
dynamisch ein- und ausblenden oder aktivieren und deaktivieren. Letzteres ist beispielsweise für Funktionen interessant, die nur bei Anzeige bestimmter Objekte zur Verfügung stehen – etwa eine Drucken-Schaltfläche, die nur bei Anzeige eines Berichts aktiviert wird. Nachfolgend lernen Sie einen Satz Funktionen kennen, mit denen Sie die gängigsten Menüelemente mit einzeiligen VBA-Aufrufen erstellen können.
10.3 VBA: Objektmodell für den Zugriff auf Menüs Das Objektmodell aus Abbildung 10.4 enthält alle im aktuellen Kapitel verwendeten Objekte. Zu beachten ist, dass ein CommandBarControl vom Typ CommandBarPopup wiederum ein Menü ist. CommandBars CommandBar CommandBarControls CommandBarControl
Abbildung 10.4: Objektmodell für den Zugriff auf Menüs
Tabelle 10.1 enthält Konstanten und Zahlenwerte für die wichtigsten Typen von CommandBarControl-Elementen – genau genommen sind das zugleich auch alle verfügbaren Typen für benutzerdefinierte Menüs. Es gibt zwar noch einige weitere Konstanten, die aber bei der Verwendung meist Fehler auslösen und sich daher in der Praxis nicht einsetzen lassen. Sie werden gleich eine Routine kennen lernen, mit der sich alle Elemente der aktuell sichtbaren Menüs mit Beschriftung und Typ ausgeben lassen, und dort noch weitere Typen finden – die sind allerdings den eingebauten Menüs von Access vorbehalten. Konstante
Zahlenwert
Beschreibung
msoControlButton
1
Einfache Schaltfläche, kann mit Text und/oder einem Symbol versehen werden
msoControlDropdown
3
Kombinationsfeld
msoControlPopup
10
Untermenü
Tabelle 10.1: Mögliche Typen für CommandBarControl-Elemente
VBA: Objektmodell für den Zugriff auf Menüs
463
10.3.1 Zugriff auf die Menüstruktur Am einfachsten lässt sich die Verwendung des Objektmodells an einem Beispiel erläutern. Die folgenden beiden Prozeduren bilden einen rekursiven Algorithmus für die Ausgabe der aktuell in Access angezeigten Menüs, Untermenüs und Steuerelemente wie in Abbildung 10.5. Die erste Prozedur MenuestrukturAusgeben durchläuft alle Elemente der CommandBarsAuflistung, also alle insgesamt enthaltenen Menüs. Für jedes sichtbare Menü ruft sie die Prozedur MenuestrukturZweigAusgeben auf. Diese Routine durchläuft alle Steuerelemente des per Commandbar-Objekt übergebenen Menüs und prüft, ob es sich dabei um Elemente des Typs msoControlPopup handelt. Ist das der Fall, gibt es ein weiteres Untermenü und einen rekursiven Aufruf der Prozedur MenuestrukturZweigAusgeben – natürlich nicht, ohne zuvor die Beschriftung des Menüs ausgegeben zu haben. Handelt es sich bei dem aktuellen Steuerelement hingegen nicht um ein weiteres Untermenü, werden einfach dessen Beschriftung und dessen Typ ausgegeben. Im Kopf der Prozedur MenuestrukturZweigAusgeben ist der Parameter cbr ausdrücklich als Object deklariert, weil dort entweder ein Commandbar-Objekt (von der Routine MenuestrukturAusgeben) oder ein CommandBarControl-Objekt (bei Selbstaufruf) ankommt. Public Sub MenuestrukturAusgeben Dim cbr As CommandBar Dim cbc As CommandBarControl 'Alle CommandBar-Objekte durchlaufen... For Each cbr In CommandBars '... und untersuchen, falls sichtbar. If cbr.Visible = True Then Debug.Print cbr.Name 'Steuerelemente des Commandbarobjekts untersuchen MenuestrukturZweigAusgeben cbr, 0 End If Next cbr End Sub Listing 10.1: Startprozedur für die rekursive Untersuchung der Menüs und der enthaltenen Elemente
Public Sub MenuestrukturZweigAusgeben(cbr As Object, intEbene As Integer) Dim cbc As CommandBarControl 'Alle Steuerelemente des Commandbar-Objekts durchlaufen For Each cbc In cbr.Controls
464
10
'Falls msoControlPopup: Steuerelement ist neues Menü. If cbc.Type = msoControlPopup Then 'Ausgeben des Menünamens Debug.Print Space(intEbene * 2) & cbc.Caption, cbc.Type 'und Untersuchung der Steuerelemente des Untermenüs MenuestrukturZweigAusgeben cbc, intEbene + 1 Else 'Ausgeben des Steuerelementnamens Debug.Print Space(intEbene * 2) & cbc.Caption, cbc.Type End If Next cbc End Sub Listing 10.2: Rekursiver Teil der Ausgabe aller angezeigten Menüs, Untermenüs und Steuerelemente
Abbildung 10.5: Ausgabe der Menüstruktur der Menüleiste
Menüs
Hinzufügen eines Menüs
465
10.3.2 Besonderheiten bei der Verwendung des Objektmodells für Menüs Das Objektmodell für Menüs birgt einige Besonderheiten: Der Index von Elementen der Commandbars- beziehungsweise Controls-Auflistungen beginnt bei 1 und nicht, wie von anderen VBA-Auflistungen gewohnt, bei 0. Genau wie Formulare und Unterformulare können CommandBar-Objekte des Typs msoControlPopup in mehreren Ebenen verschachtelt sein.
10.4 Hinzufügen eines Menüs Obwohl beim Hinzufügen eines Menüs über die Benutzungsoberfläche von Access zunächst noch nicht feststeht, welchen Menütyp Sie verwenden möchten, ist unter Access immer von Symbolleisten die Rede. Lassen Sie sich davon nicht irritieren: Jedes Menü oder – im Access-Jargon – jede Symbolleiste kann Symbolleiste, Menüleiste und Kontextmenü sein. Was die VBA-Programmierung angeht, ist es ähnlich: Die Handhabung beim Erstellen dieser Menüs und bei ihrer Steuerung ist bei allen Menüarten identisch. Die Unterschiede liegen lediglich in der Darstellungsart und in dem Ort, an dem sie angezeigt werden.
10.4.1 Hinzufügen eines Menüs per Benutzungsoberfläche Zum Hinzufügen eines Menüs über die Benutzungsoberfläche sind lediglich zwei Schritte erforderlich: 1. Anklicken der Schaltfläche Neu des Anpassen-Dialogs 2. Eingeben des Namens des neuen Menüs Abbildung 10.6 zeigt die neue Symbolleiste (unten rechts). Wenn Sie eine Symbolleiste umbenennen möchten, klicken Sie im Anpassen-Dialog auf die Schaltfläche Umbenennen… und geben den neuen Namen im Dialog Symbolleiste umbenennen ein. Entweder können Sie das frei schwebende Menü nun direkt am oberen Fensterrand verankern oder erst weiterbearbeiten. Auch das Löschen einer Symbolleiste erfolgt über den Anpassen-Dialog. Dazu markieren Sie die gewünschte Symbolleiste und klicken auf die Schaltfläche Löschen. Den Namen des Menüs können Sie auch im Dialog Symbolleisteneigenschaften eingeben; dort finden Sie außerdem weitere Eigenschaften.
466
10
Menüs
Abbildung 10.6: Anlegen und umbenennen einer neuen Symbolleiste
Die wichtigste Einstellung hier ist der Typ des Menüs: Sie können zwischen den Typen Menüleiste, Symbolleiste und Popup wählen. Die anderen Optionen legen die Möglichkeiten der Benutzer zum Anpassen dieses Menüs fest. Wenn der Dialog einmal geöffnet ist, können Sie mit dem oberen Listenfeld auch andere Menüs zur Bearbeitung auswählen (siehe Abbildung 10.7).
Abbildung 10.7: Eigenschaften einer Symbolleiste
Hinzufügen eines Menüs
467
10.4.2 Hinzufügen eines Menüs per VBA Für das Hinzufügen eines Menüs ist die Add-Methode der CommandBars-Auflistung zuständig. Sie fragt einige der zukünftigen Eigenschaften des Menüs über die optionalen Parameter ab. Einige Parameter lassen sich später per Eigenschaft zuweisen, was in folgender Prozedur aus Gründen der Übersicht auch auf diese Weise erfolgt. Der Parameter Temporary und der Parameter MenuBar lassen sich jedoch nicht anders abbilden: Mit Temporary geben Sie an, ob das Menü nach dem Schließen der Anwendung automatisch wieder entfernt werden soll, und mit MenuBar legen Sie fest, ob das neue Menü nicht als Symbolleiste, sondern als Menüleiste erscheinen und die aktuelle Menüleiste ersetzen soll. Die Prozedur MenueHinzufuegen kapselt die zum Anlegen einer Symbolleiste oder Menüleiste notwendigen Schritte. Sie erwartet als Parameter lediglich den Menünamen sowie die Angabe, ob das Menü temporär und/oder als Menüleiste angelegt werden soll. Die Prozedur hat einen Nachteil: Auch wenn man durch Setzen des Parameters MenuBar dafür sorgt, dass das neue Menü als Menüleiste der Anwendung festgelegt wird (was auch zunächst geschieht), erscheint beim nächsten Öffnen der Anwendung wieder die gewohnte Menüleiste – zusätzlich zu der neuen (siehe Abbildung 10.8). Eine Lösung für dieses Problem finden Sie weiter unten in Abschnitt 10.8.1.
Abbildung 10.8: Die neue Menüleiste (noch ohne Steuerelemente) erscheint nach dem Anlegen beim Neustart neben der Standard-Menüleiste.
Public Function MenueHinzufuegen(strMenuename As String, _ bolTemporary As Boolean, bolMenuBar As Boolean) As CommandBar Dim cbr As CommandBar 'eventuell vorhandene Version des Menüs löschen und 'falls nicht vorhanden - Fehler unterdrücken. On Error Resume Next CommandBars.Item("Hauptmenü").Delete 'Fehlerbehandlung wieder aktivieren On Error GoTo 0
468
10
Menüs
'Neues Menü erstellen und der Variablen cbr zuweisen Set cbr = CommandBars.Add(MenuBar:=bolMenuBar, Temporary:=bolTemporary) 'Name und Position festlegen With cbr .Name = strMenuename .Position = msoBarTop .Visible = True End With Set MenueHinzufuegen = cbr Set cbr = Nothing End Function Listing 10.3: Anlegen einer neuen Menüleiste
Aufruf zum Anlegen der Beispiel-Menüleiste Die Menüleiste aus dem Beispiel soll »Hauptmenü« heißen und die Standard-Menüleiste ersetzen. Außerdem soll sie beim Schließen der Anwendung nicht wieder entfernt werden: MenueHinzufuegen "Hauptmenü", False, True
Rückgabewert Beachten Sie, dass die Routine einen Objektverweis auf das erzeugte CommandBar-Objekt zurückgibt. Das macht an dieser Stelle vielleicht noch keinen Sinn; später werden Sie die Routine jedoch aus einer Prozedur heraus aufrufen, die das CommandBar-Objekt noch verwendet, um diesem beispielsweise Untermenüs hinzuzufügen.
10.4.3 Hinzufügen von Untermenüs Um das Beispielmenü zu realisieren, benötigen Sie im Hauptmenü drei Untermenüs. In den folgenden beiden Abschnitten erzeugen Sie diese zunächst per Benutzungsoberfläche und dann per VBA.
Hinzufügen von Untermenüs per Benutzungsoberfläche Besonders intuitiv ist die Funktion zum Hinzufügen von Untermenüs beziehungsweise von Steuerelementen des Typs msoControlPopup nicht: Sie finden den entsprechenden Eintrag im Anpassen-Dialog, wenn Sie auf der Registerseite Befehle im linken Kombinationsfeld den Eintrag Neues Menü auswählen. Den im rechten Fenster erscheinenden Eintrag gleichen Namens (siehe Abbildung 10.9) ziehen Sie in die soeben angelegte Menüleiste namens »Hauptmenü«.
Hinzufügen eines Menüs
469
Abbildung 10.9: Hinzufügen eines neuen Untermenüs
Anschließend müssen Sie nur noch den Namen des Untermenüs anpassen. Dazu klicken Sie einfach mit der rechten Maustaste auf den gewünschten Untermenüeintrag und geben für das Feld Name des Kontextmenüs den gewünschten Namen ein (siehe Abbildung 10.10).
Abbildung 10.10: Name des neuen Untermenüpunktes zuweisen
470
10
Menüs
Hinzufügen von Untermenüs per VBA Den Untermenüs widmet sich die folgende Funktion namens UntermenueHinzufuegen. Mit ihr lassen sich sowohl CommandBar-Elementen als auch CommandBarPopup-Steuerelementen Untermenüs hinzufügen. Die Funktion erwartet als Parameter den Namen des zu erstellenden Untermenüs und einen Objektverweis auf das Menü, in dem das Untermenü angelegt werden soll. Public Function UntermenueHinzufuegen(cbr As Object, _ strMenuename As String, _ Optional bolNeueGruppe As Boolean) As CommandBarPopup Dim cbc As CommandBarPopup Set cbc = cbr.Controls.Add(msoControlPopup) With cbc .Caption = strMenuename .BeginGroup = varNeueGruppe End With Set UntermenueHinzufuegen = cbc Set cbc = Nothing End Function Listing 10.4: Funktion zum Hinzufügen eines Untermenüs
Die Prozedur Hauptmenue zeigt, wie Sie die beiden Funktionen MenueHinzufuegen und UntermenueHinzufuegen zusammen verwenden können. Dabei speichert sie den Rückgabewert der Funktion MenueHinzufuegen in einer CommandBar-Variablen, um dem Menü die benötigten Untermenüs hinzuzufügen. Public Sub Hauptmenue() Dim cbr As CommandBar Dim cbp As CommandBarPopup Set Set Set Set
cbr cbp cbp cbp
= = = =
MenueHinzufuegen("Hauptmenü", False, True) UntermenueHinzufuegen(cbr, "&Datei") UntermenueHinzufuegen(cbr, "&Stammdaten") UntermenueHinzufuegen(cbr, "&Berichte")
End Sub Listing 10.5: Aufruf der Funktionen MenueHinzufuegen und UntermenueHinzufuegen
Hinzufügen eines Menüs
471
Nun fehlen noch die Untermenüpunkte im Untermenü Berichte – zu den Personen und Unternehmen soll es jeweils eine Schaltfläche zum Anzeigen der Übersicht und der Einzelansicht geben. Dazu brauchen Sie einfach nur die Prozedur Hauptmenue ein wenig zu erweitern. Dort verwenden Sie zwei verschiedene Objektvariablen des Typs CommandBarPopup – für jede Ebene eine. Für jede weitere Ebene richten Sie einfach eine weitere Objektvariable ein: Public Sub Hauptmenue() Dim cbr As CommandBar Dim cbpEbene1 As CommandBarPopup Dim cbpEbene2 As CommandBarPopup 'Menüleiste erstellen Set cbr = MenueHinzufuegen("Hauptmenü", False, True) 'Untermenüs erstellen Set cbpEbene1 = UntermenueHinzufuegen(cbr, "&Datei") Set cbpEbene1 = UntermenueHinzufuegen(cbr, "&Stammdaten") Set cbpEbene1 = UntermenueHinzufuegen(cbr, "&Berichte") 'Untermenüs der Berichte-Untermenüs erstellen Set cbpEbene2 = UntermenueHinzufuegen(cbpEbene1, "&Übersicht") Set cbpEbene2 = UntermenueHinzufuegen(cbpEbene1, "&Detailansicht") Set cbpEbene2 = Nothing Set cbpEbene1 = Nothing Set cbr = Nothing End Sub Listing 10.6: Hinzufügen verschachtelter Untermenüs
10.4.4 Hinzufügen von Schaltflächen Neben der Menüleiste und den Untermenüs fehlen jetzt nur noch die Schaltflächen.
Hinzufügen von Schaltflächen per Benutzungsoberfläche Sie fügen einem Menü Schaltflächen hinzu, indem Sie im Anpassen-Dialog zur Registerseite Befehle wechseln, dort im linken Listenfeld den Eintrag Datei auswählen und den Eintrag Benutzerdefiniert aus dem rechten Listenfeld an die gewünschte Stelle im neuen Menü ziehen. Wie dies mit einem Untermenü funktioniert, zeigt Abbildung 10.11: Fahren Sie einfach beim Ziehen über das entsprechende Hauptmenü, warten Sie, bis sich das Untermenü aufklappt, und fügen Sie dann die neue Schaltfläche hinzu.
472
10
Menüs
Abbildung 10.11: Hinzufügen einer Schaltfläche zu einem Untermenü
Anschließend können Sie per Kontextmenü direkt die Eigenschaften der neuen Schaltfläche einstellen – etwa den Namen und das anzuzeigende Schaltflächenbild. Um ein Schaltflächenbild hinzuzufügen, können Sie eines von einer anderen Symbolleiste kopieren und auf die neue Schaltfläche setzen. Dazu machen Sie die Quellsymbolleiste per Anpassen-Dialog sichtbar und betätigen für diese Schaltfläche den Kontextmenübefehl Schaltflächensymbol kopieren (siehe Abbildung 10.12). Schließlich wählen Sie die Zielschaltfläche aus und fügen das Symbol mit dem Befehl Schaltflächensymbol einfügen aus dem Kontextmenü ein. Das Kontextmenü bietet außerdem die Möglichkeit, den Eigenschaften-Dialog aus Abbildung 10.13 anzuzeigen. Hier lässt sich vor allem eine wichtige Einstellung vornehmen: Mit der Bei Aktion-Eigenschaft legen Sie nämlich fest, welche Funktion beim Klick auf die Schaltfläche ausgelöst werden soll. Die entsprechende Routine muss als Function-Prozedur ausgeführt werden und öffentlich verfügbar sein. Sie müssen die Routine auf jeden Fall mit führendem Gleichheitszeichen und abschließendem Klammernpaar angeben.
Hinzufügen eines Menüs
473
Abbildung 10.12: Einstellen von Eigenschaften per Kontextmenü
Abbildung 10.13: Eigenschaften einer Menüschaltfläche
Hinzufügen von Schaltflächen per VBA Wie bereits für Menüs und Untermenüs verwenden Sie zum Hinzufügen von Schaltflächen zu einem Menü eine Wrapperfunktion. Diese erwartet einen Verweis auf das
474
10
Menüs
Objekt, in dem es die neue Schaltfläche anzeigen soll, die Beschriftung der Schaltfläche, die auszuführende Funktion sowie optional einen TooltipText, ein Symbol und die Information, ob das Symbol eine neue Gruppe einleiten soll: Public Function SchaltflaecheHinzufuegen(cbr As Object, _ strBeschriftung As String, strAktion As String, _ Optional strTooltipText As String, Optional lngSymbol As Long, _ Optional bolNeueGruppe As Boolean) As CommandBarButton Dim cbb As CommandBarButton Set cbb = cbr.Controls.Add(msoControlButton) With cbb .Caption = strBeschriftung .TooltipText = strTooltipText .OnAction = strAktion .FaceId = lngSymbol .BeginGroup = bolNeueGruppe End With
'Beschriftung 'TooltipText 'ausgelöste Routine 'Symbol der Schaltfläche 'Trennstrich über diese Schaltfläche?
Set SchaltflaecheHinzufuegen = cbb Set cbb = Nothing End Function Listing 10.7: Funktion zum Anlegen einer neuen Schaltfläche
Die TooltipText-Funktion funktioniert scheinbar nur auf der obersten Menüebene und nicht in Untermenüs, obwohl dort die Eigenschaft angeboten wird. Möglicherweise ist dies ein Bug.
Schaltflächen mit Funktionen versehen Die Namen der Funktionen, die durch die Schaltflächen ausgelöst werden, haben Sie bereits in der Prozedur Hauptmenue angegeben. Die passenden öffentlichen Funktionen legen Sie in einem Standardmodul an. Die Funktion zum Drucken würde etwa folgendermaßen aussehen: Public Function mnuDrucken() On Error Resume Next DoCmd.RunCommand acCmdPrint End Function Listing 10.8: Funktion einer Menüschaltfläche
Hinzufügen eines Menüs
475
Die On Error Resume Next-Anweisung verhindert (mindestens) zwei potenzielle Fehlermeldungen bei folgenden Vorgängen: Der Drucken-Dialog soll aufgerufen werden, obwohl gar kein druckbares Objekt angezeigt wird – Fehler 2046. Der Druckvorgang wird im Drucken-Dialog abgebrochen – Fehler 2501.
Trennlinien einfügen Trennlinien machen es in Menüs manchmal einfacher, zusammenhängende Elemente zu identifizieren. Während Sie beim Anlegen von Menüs über die Benutzungsoberfläche einfach den Eintrag Gruppierung beginnen aktivieren, erledigen Sie dies unter VBA durch Setzen des Parameters BeginGroup auf den Wert True.
Das richtige Symbol für die FaceID-Eigenschaft finden Im Gegensatz zum Anlegen der Schaltfläche per Benutzungsoberfläche müssen die verwendeten Symbole hier per FaceID angegeben werden – das ist eine Nummer, die eines der eingebauten Symbole kennzeichnet. Nun hat man aber erstens keinen Überblick über alle vorhandenen Symbole und zweitens weiß man auch nicht, welche FaceID diese besitzen. Abhilfe schaffen Sie mit der folgenden Prozedur. Sie erstellt acht Symbolleisten mit je 500 Symbolen und fügt direkt einen TooltipText mit der FaceID des jeweiligen Symbols hinzu. Es gibt zwar deutlich mehr Symbole, aber nach dem viertausendsten sind die meisten leer. Durch Ändern des Endpunktes der Schleife auf einen Wert größer als acht können Sie auch noch weitere Symbole anzeigen lassen. Um die Übersicht zu wahren, verwendet die Routine die Height-Eigenschaft der Symbolleiste, um die Symbole in mehreren Zeilen darzustellen: Public Sub MenueMitAllenSymbolen() Dim cbr As CommandBar Dim cbb As CommandBarControl Dim i, j As Long DoCmd.Hourglass True 'Vorhandene Symbolleisten löschen On Error Resume Next For j = 1 To 8 CommandBars(1 + (j - 1) * 500 & "-" & 500 * j - 1).Delete Next j On Error GoTo 0 'Acht Symbolleisten ... For j = 1 To 8
476
10
Menüs
Set cbr = CommandBars.Add( _ Name:=1 + (j - 1) * 500 & "-" & 500 * j - 1, _ Position:=msoBarFloating) '...zu je 500 Symbolen anlegen For i = 1 + (j - 1) * 500 To 500 * j - 1 Set cbb = cbr.Controls.Add() With cbb .FaceId = i .TooltipText = Str(i) End With Next i cbr.Visible = True 'Höhe der Symbolleiste einstellen, damit diese nicht größer als die ‘Bildschirmbreite wird cbr.Height = cbr.Height * 8 Next j DoCmd.Hourglass False End Sub Listing 10.9: Erstellen von Symbolleisten mit allen vorhandenen Symbolen plus FaceID
Abbildung 10.14 zeigt die ersten 500 Symbole nach FaceID geordnet. Dank TooltipText können Sie die zu jedem Symbol gehörende FaceID durch Positionieren des Mauszeigers über dem jeweiligen Symbol anzeigen lassen.
Abbildung 10.14: Die ersten 500 Symbole nach FaceID sortiert
Hinzufügen eines Menüs
477
Speichern Sie die Routine zum Anzeigen der Symbole in einer eigenen Datenbank und erzeugen Sie dort die acht Symbolleisten. Wann immer Sie einem Symbolleistenelement ein Symbol zuweisen möchten, können Sie in dieser Datenbank schnell die FaceID nachschlagen oder einfach nur in den enthaltenen Symbolen stöbern. Außerdem sind diese Symbolleisten auch zum manuellen Kopieren in andere benutzerdefinierte Symbolleisten geeignet. Um den Beispielmenüs noch die geplanten Schaltflächen hinzuzufügen, passen Sie die Prozedur HauptmenueErstellen erneut an – diesmal nur ausschnittweise abgedruckt: Public Sub HauptmenueErstellen() Set cbpEbene1 = UntermenueHinzufuegen(cbr, "&Datei") 'Schaltflächen im Datei-Menü erstellen Set cbb = SchaltflaecheHinzufuegen(cbpEbene1, "&Drucken", _ "=mnuDrucken()", "Druckt das aktuelle Dokument", 4) Set cbb = SchaltflaecheHinzufuegen(cbpEbene1, "&Beenden", _ "=mnuBeenden()", "Beendet die Anwendung", , True) Set cbpEbene1 = UntermenueHinzufuegen(cbr, "&Stammdaten") 'Untermenüs des Stammdaten-Untermenüs erstellen Set cbpEbene2 = UntermenueHinzufuegen(cbpEbene1, "&Personen") 'Schaltflächen im Menü Stammdaten/Personen anlegen Set cbb = SchaltflaecheHinzufuegen(cbpEbene2, "&Übersicht", _ "=mnuPersonenUebersicht()", _ "Zeigt eine Übersicht der Personen an.", 3733) Set cbb = SchaltflaecheHinzufuegen(cbpEbene2, "&Neu", _ "=mnuPersonNeu()", "Legt einen neuen Personen-Datensatz an.", 3732) … End Sub Listing 10.10: Anlegen von Schaltflächen in einem Menü
Wo befinden sich die übrigen Schaltflächen-Eigenschaften? Die Prozedur SchaltflaecheHinzufuegen (Listing 10.7) unterstützt nur das Anlegen der wichtigsten Eigenschaften. Der Grund ist folgender: Man könnte leicht alle Eigenschaften in einer solchen Wrapperfunktion unterbringen. Sie bekämen dann aber eine meterlange Parameterliste, aus der man den Großteil der Eigenschaften nur alle Jubeljahre braucht. Wenn Sie die hier vorgestellten Wrapperfunktionen verwenden, um Ihre eigenen Anwendungen mit Schaltflächen auszustatten, können Sie dennoch leicht Abhilfe
478
10
Menüs
schaffen: Weisen Sie in der Hauptprozedur zum Anlegen der Menüs (hier die Prozedur Hauptmenue, Listing 10.10) einfach das von der Funktion SchaltflaecheAnlegen erzeugte Objekt (CommandBarButton) einer Objektvariablen zu und legen die zusätzlichen Eigenschaften über diese Funktion an. Beispiel: Sie möchten eine Schaltfläche direkt beim Erzeugen der Menüs deaktivieren. Das macht zum Beispiel im Falle der DruckenSchaltfläche Sinn – diese soll standardmäßig nicht aktiviert sein und nur aktiviert werden, wenn tatsächlich ein zu druckendes Objekt angezeigt wird (mehr dazu in Abschnitt 10.4.5). Den Aufruf zum Anlegen der Drucken-Schaltfläche aus der Prozedur Hauptmenue (Listing 10.10) etwa erweitern Sie folgendermaßen, um eine Tastenkombination anzugeben und die Schaltfläche zu deaktivieren (siehe Abbildung 10.15): Set cbb = SchaltflaecheHinzufuegen(cbpEbene1, "&Drucken", "=mnuDrucken()", _ "Druckt das aktuelle Dokument", 4) cbb.ShortcutText = "Strg+P" cbb.Enabled = False
Abbildung 10.15: Menüschaltfläche mit Extras
10.4.5 Schaltflächen dynamisch aktivieren und deaktivieren Die soeben vorgestellte Funktion verhindert beispielsweise die Anzeige einer Fehlermeldung, wenn der Drucken-Dialog geöffnet werden soll, obwohl gar kein Objekt geöffnet ist. Statt dessen passiert gar nichts, was den Benutzer sozusagen im Regen stehen lässt. Besser wäre es, die Drucken-Schaltfläche würde überhaupt erst aktiviert, wenn ein druckbares Objekt geöffnet ist. In professionellen Anwendungen sollte dies nur bei Berichten der Fall sein, was schon die Stellen einschränkt, an die Hand anzulegen wäre.
Drucken-Schaltfläche nur bei geöffnetem Bericht aktivieren Die Vorgabe, die Drucken-Schaltfläche nur zu aktivieren, wenn gerade ein Bericht angezeigt wird, lässt sich leicht umsetzen. Berichte bieten zwei Ereignisse, die beim Erhalt des Fokus und beim Verlust des Fokus ausgelöst werden. Nur spricht man bei Berichten von Aktivieren und Deaktivieren und die entsprechenden Ereigniseigenschaften heißen Bei Aktivierung und Bei Deaktivierung.
Hinzufügen eines Menüs
479
Die beiden Prozeduren zum Aktivieren und Deaktivieren der Drucken-Schaltfläche sehen wie folgt aus: Private Sub Report_Activate() CommandBars("Hauptmenü").Controls("&Datei"). _ Controls("&Drucken").Enabled = True End Sub Private Sub Report_Deactivate() CommandBars("Hauptmenü").Controls("&Datei"). _ Controls("&Drucken").Enabled = False End Sub Listing 10.11: Aktivieren und Deaktivieren einer Schaltfläche
Diese beiden Routinen fügen Sie einfach allen in der Anwendung enthaltenen Berichten hinzu. Der Bezug zur Menüschaltfläche – hier die Drucken-Schaltfläche – wird über die Namen der Einträge der CommandBars- und Controls-Auflistungen hergestellt (siehe auch Abschnitt 10.7, »Menüsteuerelemente referenzieren«).
10.4.6 Eigene Symbole verwenden Seit Access 2002 ist es relativ einfach möglich, eigene Symbole in Menüleisten zu verwenden – sogar mit transparenten Bereichen. Dazu ist allerdings ein wenig ExtraArbeit zu erledigen. Sie benötigen zwei Bitmap-Dateien – eine mit dem eigentlichen Symbol und eine mit einer Maske, die transparente und nicht transparente Bereiche markiert. Dabei kennzeichnet die Farbe Weiß die transparenten Bereiche und Schwarz die nicht transparenten Bereiche. Die folgende Prozedur erwartet einen Verweis auf die Schaltfläche mit dem zu ersetzenden Symbol sowie die Namen der Dateien mit dem Bild und der Maske als Parameter: Public Sub EigenesSymbol(cbc As CommandBarControl, strSymbol As String, _ strMaske As String) Dim picBild As IPictureDisp Dim picMaske As IPictureDisp 'Pfad zum neuen Symbol Set picBild = stdole.StdFunctions.LoadPicture(strSymbol) 'Pfad zur Transparenz-Maske (transparente Bereiche weiß, 'sichtbare Bereiche schwarz) Set picMaske = stdole.StdFunctions.LoadPicture(strMaske)
480
10
Menüs
'Zuweisen von Symbol und Maske an eine Schaltfläche With cbc .Picture = picBild .Mask = picMaske End With End Sub Listing 10.12: Zuweisen eines neuen Symbols
Auch diese Routine lässt sich in die Prozedur HauptmenueErstellen zum Anlegen der Beispielmenüs integrieren. Dazu fügen Sie direkt im Anschluss an die Zeile, in der eine neue Schaltfläche erstellt wird, etwa folgenden Aufruf hinzu: EigenesSymbol cbb, CurrentProject.Path & "\Drucker.bmp", _ CurrentProject.Path & "\Drucker_Maske.bmp"
Der Vorteil ist: Sie müssen die Grafikdateien nicht mit der Software ausliefern, wenn Sie diese einmal importiert haben. Die Ausnahme ist, wenn Sie die Menüs bei jedem Programmstart neu erstellen – dann sollten Sie die eigenen Symbole vielleicht in einer eigenen Symbolleiste horten.
Symbole exportieren Ebenso nützlich ist die Funktion zum Exportieren von Symbolen: Damit können Sie bestehende Symbole in je eine Bild- und eine Maske-Datei speichern. Für die DruckenSchaltfläche sieht das Ergebnis beispielsweise wie in Abbildung 10.16 aus.
Abbildung 10.16: Exportierte Variante von Schaltflächensymbolen
Den Export können Sie mit der folgenden Prozedur durchführen. Die Prozedur erwartet einen Objektverweis auf die Schaltfläche, deren Symbol exportiert werden soll, sowie die beiden Dateinamen zum Speichern der Bild- und der Maske-Datei:
Hinzufügen eines Menüs
481
Sub BildUndMaskeSpeichern(cbc As CommandBarControl, strBild As String, _ strMaske As String) Dim picPicture As IPictureDisp Dim picMask As IPictureDisp With cbc Set picPicture = .Picture Set picMask = .Mask End With stdole.SavePicture picPicture, strBild stdole.SavePicture picMask, strMaske End Sub Listing 10.13: Exportieren eines Schaltflächensymbols
10.4.7 Hinzufügen von Kombinationsfeldern Neben den Schaltflächen können Sie auch Kombinationsfelder in Menüs verwenden. Das ist sinnvoll, wenn Sie etwa eine Schnellauswahl bestimmter Elemente ermöglichen wollen. Leider lassen sich die Kombinationsfelder aus Menüs nicht wie herkömmliche Kombinationsfelder an eine Datensatzherkunft binden, sodass die einzelnen Einträge mit der AddItem-Methode hinzugefügt werden müssen. Ansonsten liefert dieses Steuerelement ähnliche Eigenschaften wie übliche Kombinationsfelder. Die folgende Funktion reiht sich in diejenigen zum Anlegen von Menüs, Untermenüs und Schaltflächen ein und legt ein Kombinationsfeld an: Public Function KombinationsfeldHinzufuegen(cbr As Object, _ strBeschriftung As String) As CommandBarComboBox Dim cbcb As CommandBarComboBox Set cbcb = cbr.Controls.Add(msoControlComboBox) With cbcb .Caption = strBeschriftung End With Set KombinationsfeldHinzufuegen = cbcb End Function Listing 10.14: Hinzufügen eines Kombinationsfeldes zu einem Menü
482
10
Menüs
Die Funktion hat nur zwei Parameter: das Objekt, dem das neue Kombinationsfeld hinzugefügt werden soll, und die Beschriftung des Kombinationsfeldes. Das sind nicht viele Eigenschaften für ein so mächtiges Steuerelement, aber die meisten Einstellungen sind so individuell, dass eine Parametrisierung verhältnismäßig aufwändig wäre. Wenn Sie Menüs mit einer Prozedur wie der aus Listing 10.10 anlegen möchten, können Sie also wie gehabt auf das von der Prozedur KombinationsfeldHinzufuegen zurückgegebene Objekt zugreifen und es Ihren Bedürfnissen anpassen.
Kombinationsfeldeigenschaften Kombinationsfelder lassen sich dank einiger Eigenschaften optisch ein wenig anpassen. Hier sind die wichtigsten: Style: Legt fest, ob ein Kombinationsfeld mit (msoComboLable) oder ohne Beschriftung (msoComboNormal) angezeigt wird. DropDownLines: Gibt an, wie viele Zeilen beim Aufklappen des Kombinationsfeldes maximal angezeigt werden (0 zeigt alle enthaltenen Werte an). DropDownWidth: Legt die Breite der ausgeklappten Liste fest (-1: angepasst an den breitesten Eintrag, 0: angepasst an die Steuerelementbreite, andere Breiten in Pixel). Weitere Eigenschaften finden Sie in den nächsten beiden Abschnitten.
Einträge hinzufügen Das Hinzufügen von Einträgen erfolgt über die AddItem-Methode. Folgendes Beispiel zeigt, wie Sie das Kombinationsfeld mit den Daten aus einer Tabelle füllen können: Set cbcb = KombinationsfeldHinzufuegen(cbr, "Personen:", True) With cbcb Dim db As DAO.Database Dim rst As Recordset Set db = CurrentDb Set rst = db.OpenRecordset("tblPersonen", dbOpenTable) 'Trennlinie vor dem Steuerelement einfügen .BeginGroup = True 'Fünf Zeilen beim Ausklappen anzeigen .DropDownLines = 5 'Breite des ausgeklappten Teils an den breitesten Eintrag anpassen .DropDownWidth = -1 'Aktion für das Betätigen des Kombinationsfeldes festlegen
Hinzufügen eines Menüs
483
.OnAction = "=mnuCboPersonen()" Do While Not rst.EOF 'Eintrag hinzufügen .AddItem rst!Nachname & ", " & rst!Vorname rst.MoveNext Loop Set rst = Nothing Set db = Nothing End With Listing 10.15: Füllen eines Kombinationsfeldes mit den Daten einer Tabelle
Abbildung 10.17 zeigt das fertige Menü mit dem ausgeklappten Kombinationsfeld.
Abbildung 10.17: Menüleiste mit Kombinationsfeld
Aktuellen Eintrag abfragen Der aktuelle Eintrag des Kombinationsfeldes lässt sich jederzeit mit den in der folgenden Routine enthaltenen Anweisungen lesen: Public Sub KombinationsfeldLesen() Dim cbcb As CommandBarComboBox Set cbcb = CommandBars("Hauptmenü").Controls("Personen:") With cbcb 'Position des aktuellen Eintrags ausgeben Debug.Print cbcb.Index 'Text des aktuellen Eintrags ausgeben Debug.Print cbcb.Text End With Set cbcb = Nothing End Sub Listing 10.16: Aktuellen Kombinationsfeldeintrag auswerten
484
10
Menüs
Einträge eindeutig identifizieren Probleme gibt es mit dem Kombinationsfeld, wenn es mehrere gleich lautende Einträge enthält. Da man keine zusätzlichen unsichtbaren Spalten anfügen kann, bleibt nur der Index eines Eintrags zur eindeutigen Identifizierung eines Datensatzes. Voraussetzung ist allerdings, dass Sie eine Abfrage mit durchnummerierten Datensätzen zugrunde legen, die etwa das Ergebnis aus Abbildung 10.18 liefert. Wichtig ist dabei ein Feld, das die Datensätze in der gewünschten Reihenfolge durchnummeriert.
Abbildung 10.18: Personen mit Reihenfolge-Feld als Index
Wie Sie ein solches Abfrageergebnis erhalten, erfahren Sie in Kapitel 3 in Abschnitt 3.8, »Nummerierung von Datensätzen«. Die Datensätze müssen Sie nun in der richtigen Sortierung einlesen und dazu entsprechend die Datenherkunft ändern. Die Zuweisung des Recordsets in Listing 10.15 ersetzen Sie durch folgende Anweisung: Set rst = db.OpenRecordset("qryPersonenMitIndex", dbOpenDynaset)
Schließlich passen Sie die Prozedur an, die beim Auswählen eines Eintrags des Kombinationsfeldes ausgelöst wird. Diese Prozedur liest nun den Index des ausgewählten Eintrags aus, ermittelt per DLookup die entsprechende PersonID und zeigt den passenden Datensatz in einem Formular an: Public Function mnuCboPersonen() Dim cbcb As CommandBarComboBox Dim lngReihenfolge As Long Dim lngPersonID As Long 'Objektverweis auf Kombinationsfeld erstellen
Hinzufügen von Symbolleisten
485
Set cbcb = CommandBars("Hauptmenü").Controls("Personen:") 'Index des aktuellen Eintrags ermitteln With cbcb lngReihenfolge = cbcb.ListIndex End With 'Wert des Feldes PersonID für diesen Reihenfolge-Eintrag ermitteln lngPersonID = DBEngine(0)(0).OpenRecordset( _ "SELECT PersonID FROM qryPersonenMitIndex WHERE Reihenfolge = " _ & lngReihenfolge).Fields(0) 'Formular öffnen und die Daten der ausgewählten Person anzeigen DoCmd.OpenForm "frmPersonen", WhereCondition:="PersonID = " & lngPersonID Set cbcb = Nothing End Function Listing 10.17: Anzeigen des im Kombinationsfeld ausgewählten Datensatzes im Formular
10.5 Hinzufügen von Symbolleisten Glücklicherweise lassen sich die verschiedenen Menüarten bis auf ganz wenige Ausnahmen exakt gleich behandeln. Nachdem Sie eine Menge über Menüleisten erfahren haben, können Sie Ihr Wissen auf Symbolleisten übertragen. Der einzige Unterschied ist, dass Symbolleisten in der Regel überwiegend Symbole enthalten – zumindest ist das bei den eingebauten Menüs der Fall. Hier und da taucht auch einmal ein Kombinationsfeld auf, aber Beschriftungen sind weit und breit nicht zu finden. Um die Benutzer Ihrer Anwendungen nicht zu irritieren, sollten Sie dies berücksichtigen und die Menüs so aufbauen, wie es der Benutzer gewohnt ist – und das dürften in vielen Fällen die gängigen Office-Anwendungen sein. Um eine Symbolleiste hinzuzufügen, rufen Sie einfach die gleiche Prozedur wie zum Erstellen der Menüleiste auf – mit dem Unterschied, dass Sie beim letzten Parameter den Wert False eintragen. Die folgende Routine legt eine Symbolleiste mit einer Drucken-Schaltfläche an: Public Sub Symbolleiste() Dim cbr As CommandBar Dim cbb As CommandBarButton 'Menüleiste erstellen Set cbr = MenueHinzufuegen("Symbolleiste", False, False)
486
10
Menüs
'Schaltfläche erstellen Set cbb = SchaltflaecheHinzufuegen(cbr, "&Drucken", "=mnuDrucken()", _ "Druckt das aktuelle Dokument", 4) cbb.Enabled = False Set cbr = Nothing End Sub Listing 10.18: Anlegen einer Symbolleiste mit einer Drucken-Schaltfläche
10.6 Hinzufügen von Kontextmenüs Kontextmenüs erstellen Sie genauso wie die anderen Menütypen. Der Unterschied ist, dass Sie diese als Elemente einer bestimmten vordefinierten Symbolleiste anlegen und dass Kontextmenüs nur bei Bedarf angezeigt werden.
Kontextmenüs für alle Fälle Kontextmenüs können Sie prinzipiell für alle möglichen Objekte erstellen, die über die Eigenschaft Kontextmenüleiste verfügen. Das ist bei Formularen und den enthaltenen Steuerelementen und bei Berichten der Fall; außerdem können Sie dem ApplicationObjekt ein Kontextmenü hinzufügen. Letzteres kann immer dann mit der rechten Maustaste angezeigt werden, wenn der Mauszeiger sich nicht in einem Bereich befindet, dem ein anderes Kontextmenü zugeordnet ist.
10.6.1 Hinzufügen eines Kontextmenüs per Benutzungsoberfläche Kontextmenüs sind – technisch gesehen – ganz normale Untermenüs, die sich unterhalb der eingebauten Symbolleiste Kontextmenü befinden. Benutzerdefinierte Kontextmenüs legen Sie im Untermenü Benutzerdefiniert an – wie Sie dieses finden, können Sie Abbildung 10.19 (weiter oben) entnehmen. Bevor Sie einem Objekt ein Kontextmenü zuordnen können, müssen Sie dieses zunächst erstellen. Dazu gehen Sie folgendermaßen vor: Legen Sie ein neues Menü an, wie unter 10.4.1 beschrieben. Stellen Sie die Eigenschaft Typ im Dialog Symbolleisteneigenschaften auf den Wert Popup ein. Das Menü verschwindet vom Bildschirm und ist nun im Menü Benutzerdefiniert der Symbolleiste Kontextmenü zu finden. Dieses machen Sie sichtbar, indem auf der Registerseite Symbolleisten des Anpassen-Dialogs einen Haken an der Symbolleiste Kontextmenü anbringen.
Hinzufügen von Kontextmenüs
487
Abbildung 10.19: Erstellen eines Kontextmenüs
Nachdem Sie das Kontextmenü erstellt haben, können Sie wie bei anderen Menüs benutzerdefinierte Schaltflächen hinzufügen. Das Beispielkontextmenü soll dem Anzeigen und Löschen von Datensätzen in einem Listenfeld dienen, daher nennen Sie es Kontextmenü_Listenfeld. Fügen Sie der Kontextmenüleiste zwei Einträge namens Löschen und Bearbeiten hinzu und stellen Sie deren Eigenschaft Bei Aktion auf =mnuPersonLoeschen() und =mnuPerson Anzeigen() ein. Damit sind die Arbeiten an den Menüs selbst bereits beendet – wenn Sie nicht noch ein passendes Symbol oder einen TooltipText hinzufügen möchten. Das Ergebnis soll wie in Abbildung 10.20 aussehen.
Abbildung 10.20: Benutzerdefiniertes Kontextmenü eines Listenfeldes
488
10
Menüs
Kontextmenü an ein Steuerelement binden Damit das Kontextmenü im richtigen Zusammenhang angezeigt wird, stellen Sie die Eigenschaft Kontextmenüleiste auf den entsprechenden Wert ein. Das ist einfach, denn die Eigenschaft offeriert ein Feld zur Auswahl aller benutzerdefinierten Kontextmenüs (siehe Abbildung 10.21).
Abbildung 10.21: Auswahl eines Kontextmenüs für ein Steuerelement
Eigenschaften anpassen Wenn Sie einmal nachträglich die Eigenschaften eines Kontextmenüs verändern möchten, werden Sie möglicherweise vergeblich versuchen, den passenden Dialog aufzurufen. Dies ist auch nicht direkt möglich, da die Untereinträge des Menüs Kontextmenü kein Kontextmenü besitzen, um den entsprechenden Dialog anzuzeigen. Statt dessen aktivieren Sie den Dialog Symbolleisteneigenschaften für eine beliebige andere Symbolleiste und wählen dann unter Ausgewählte Symbolleiste den gewünschten Eintrag aus. Die Eigenschaften der im Kontextmenü enthaltenen Steuerelemente sind hingegen wie üblich konfigurierbar.
Hinzufügen von Kontextmenüs
489
Kontextmenü löschen Auch das Löschen eines Kontextmenüs ist nicht gerade intuitiv durchzuführen. Sie müssen es erst im Dialog Symbolleisteneigenschaften wieder in eine Symbolleiste zurückverwandeln und können es erst dann über die entsprechende Schaltfläche des Anpassen-Dialogs löschen.
10.6.2 Hinzufügen eines Kontextmenüs per VBA Per VBA läuft das Anlegen eines Kontextmenüs erheblich geradliniger. Wie gehabt verwenden Sie eine Wrapper-Funktion zum Anlegen der Menüleiste. Der wichtigste Unterschied zur Prozedur, mit der Sie Symbolleisten und Menüleisten anlegen können, ist der, dass Sie als Position bei der Add-Methode die Konstante msoBarPopup übergeben. Public Function KontextmenueHinzufuegen(strMenuename As String, _ bolTemporary As Boolean) As CommandBar Dim cbr As CommandBar On Error Resume Next CommandBars.Item(strMenuename).Delete On Error GoTo 0 'Neues Menü erstellen und der Variablen cbr zuweisen Set cbr = CommandBars.Add(Position:=msoBarPopup, Temporary:=bolTemporary) 'Name und Position festlegen With cbr .Name = strMenuename End With Set KontextmenueHinzufuegen = cbr Set cbr = Nothing End Function Listing 10.19: Wrapper-Funktion zum Anlegen eines Kontextmenüs
Um das Kontextmenü für das Listenfeld aus Abbildung 10.20 nachzubilden, verwenden Sie die folgende Prozedur, die unter anderem die Routine KontextmenueHinzufuegen aufruft und dem so erstellten Kontextmenü anschließend die zwei benötigten Schaltflächen hinzufügt: Public Sub KontextmenueErstellen() Dim cbr As CommandBar Dim cbb As CommandBarButton 'Menüleiste erstellen
490
10
Menüs
Set cbr = KontextmenueHinzufuegen("Kontextmenü_Listenfeld", True) 'Schaltflächen erstellen Set cbb = SchaltflaecheHinzufuegen(cbr, "Anzeigen", _ "=mnuPersonAnzeigen()", _ "Zeigt Details zur markierten Person an", 1088) Set cbb = SchaltflaecheHinzufuegen(cbr, "Löschen", _ "=mnuPersonLoeschen()", "Löscht die markierte Person", 3124) Set cbb = Nothing Set cbr = Nothing End Sub Listing 10.20: Erstellen eines Kontextmenüs
10.7 Menüsteuerelemente referenzieren Wenn Sie ein Menüsteuerelement referenzieren möchten, um seinen Inhalt oder Status abzufragen oder um eine Eigenschaft zu ermitteln oder zu ändern, können Sie auf folgende Art darauf zugreifen: Hangeln Sie sich vom obersten Element des Menüs bis zum gewünschten Steuerelement hindurch. Bedenken Sie dabei, dass das oberste Element immer ein Element der CommandBars-Auflistung ist und alle anderen Elemente Bestandteil einer Controls-Auflistung. Der Zugriff auf das Kombinationsfeld aus dem Beispiel ist relativ einfach: Debug.Print CommandBars("Hauptmenü").Controls("Personen:").ListIndex
Beachten Sie, dass Sie beim CommandBar-Objekt den Wert der Eigenschaft Name und bei allen untergeordneten Elementen den Wert der Eigenschaft Caption für den Zugriff verwenden. Die kaufmännischen Und-Zeichen (&) können Sie angeben, brauchen es aber nicht. Für weiter verschachtelte Menüs fügt man so viele weitere Controls-Auflistungen mit dem angegebenen Element an, wie nötig: Für einen Menüeintrag Datei/Importieren/ Externe Dateien in der Menüleiste mit dem Namen Menu Bar würde der Aufruf zur Ausgabe einer Eigenschaft wie folgt aussehen: Debug.Print CommandBars("Menu Bar").Controls("Datei"). _ Controls("Importieren").Controls("Externe Dateien").<Eigenschaft>
Alternativ können Sie statt des Namens beziehungsweise der Beschriftung eines Menüelements über dessen Ordinalzahl auf dieses zugreifen. Menüelemente werden – wie bereits erwähnt – bei eins beginnend durchnummeriert. Für obiges Beispiel sähe der Aufruf so aus (zur Probe wird die Beschriftung des gewünschten Menüeintrags ausgegeben): Debug.Print Commandbars(4).Controls(1).Controls(3).Controls(1).Caption
Feinschliff
491
Hinweis: Für eine Referenz auf die eingebauten Menü- und Symbolleisten müssen Sie die englische Bezeichnung verwenden – also beispielsweise »Menu Bar« oder »Database«. Untergeordnete Elemente lassen sich über die Beschriftung ansprechen.
10.8 Feinschliff Nun fehlt noch ein wenig Feinarbeit. Beim Neustart der Anwendung erscheint immer wieder die Standardmenüleiste von Access und beim Anzeigen bestimmter Objekte wie Formularen oder Berichten sind immer noch entsprechende Symbolleisten sichtbar.
10.8.1 Menüleiste beim Anwendungsstart ersetzen Sie können der Anwendung so viele als Menüleiste gekennzeichnete Menüs hinzufügen, wie Sie möchten, die Standardmenüleiste von Access wird ohne weitere Maßnahmen immer wieder angezeigt. Sie können allerdings auf verschiedene Weise dafür sorgen, dass die Standardmenüleiste beim Start durch eine benutzerdefinierte Menüleiste ersetzt wird. Die erste Möglichkeit greift auf den Startoptionen-Dialog zurück. Dort finden Sie die Eigenschaft Menüleiste, die alle benutzerdefinierten Menüleisten anzeigt (siehe Abbildung 10.22).
Abbildung 10.22: Einstellen der beim Start anzuzeigenden Menüleiste
Das Gleiche funktioniert natürlich auch per VBA, wobei es zwei Möglichkeiten gibt, um die Menüleiste beim Start der Anwendung anzuzeigen.
492
10
Menüs
In beiden Fällen weisen Sie der Eigenschaft MenuBar des Application-Objekts den Namen der neuen Menüleiste zu. Das können Sie an verschiedenen Stellen erledigen. In vielen Fällen begrüßt eine Anwendung den Benutzer mit einem Startbildschirm, der Programmname, Versionsnummer und weitere Informationen anzeigt und sich dann von alleine ausblendet oder geschlossen werden muss. Wenn Sie ein solches Formular mit der entsprechenden Startoption beim Anwendungsstart anzeigen, können Sie wunderbar folgende Anweisung im Beim Öffnen-Ereignis unterbringen: Private Sub Form_Open(Cancel As Integer) Application.MenuBar = "Hauptmenü" End Sub Listing 10.21: Wechsel des Hauptmenüs beim Anzeigen des Startformulars
Wer kein Startformular verwendet, kann auch das Autoexec-Makro von Access einsetzen. In diesem Fall erstellen Sie beispielsweise folgende öffentliche Funktion in einem Standardmodul und rufen diese mit der AusführenCode-Aktion des besagten Makros auf. Public Function MenueleisteAendern Application.MenuBar = "Hauptmenü" End Function Listing 10.22: Ändern der Menüleiste per Makro und öffentlicher Funktion
10.8.2 Eingebaute Symbolleisten deaktivieren Access zeigt je nach aktivem Objekt eine oder mehrere spezielle Symbolleisten an. Auch einige eingebaute Kontextmenüs sind vorhanden. Wenn Sie Ihre Anwendung davon befreien möchten, ist der Start-Dialog das beste Mittel: Hier legen Sie zum Beispiel fest, dass keine Standardkontextmenüs angezeigt werden, das Datenbankfenster unsichtbar ist und eingebaute Symbolleisten sich beim Aktivieren verschiedener Objekte still verhalten (siehe Abbildung 10.23).
10.8.3 Menüs positionieren Mit der Maus lassen sich Menüs nach Wunsch positionieren – üblicherweise am oberen Rand, aber auch an den anderen Rändern oder auch frei schwebend. Das funktioniert natürlich auch per VBA. Die grundlegende Einstellung erfolgt über die Eigenschaft Position. Sie kann die Werte aus Tabelle 10.2 annehmen.
Feinschliff
493
Abbildung 10.23: Diese Einstellungen garantieren zusammen mit benutzerdefinierten Menüleisten eine individuelle Anwendung.
Weitere Eigenschaften für das Festlegen der Position heißen Left und Top. Diese Eigenschaften legen den Abstand vom linken und vom oberen Fensterrand fest beziehungsweise geben diesen zurück. Konstante
Wert
Beschreibung
msoBarBottom
3
Unterer Fensterrand
msoBarFloating
4
Frei schwebend
msoBarLeft
0
Linker Fensterrand
msoBarMenuBar
6
Menüleiste, kann nicht per Position-Eigenschaft zugewiesen werden
msoBarPopup
5
Kontextmenü, kann nicht per Position-Eigenschaft zugewiesen werden
msoBarRight
2
Rechter Fensterrand
msoBarTop
1
Oberer Fensterrand
Tabelle 10.2: Konstanten für die Position von Menüs
Das Formular frmMenuesVerschieben enthält einige Beispielprozeduren zum Positionieren von Menüs. Wenn Sie das Formular öffnen, sind noch alle Steuerelemente leer – sobald Sie aber den Namen eines Menüs eingeben, zeigt es die Position und die Werte der Eigenschaften Left und Top an: Private Sub MenuepositionAktualisieren(strMenue As String) Me!cboPosition = CommandBars(strMenue).Position Me.txtLeft = CommandBars(strMenue).Left Me.txtTop = CommandBars(strMenue).Top End Sub Listing 10.23: Ermitteln der Position von Menüs
494
10
Menüs
Die übrigen Steuerelemente ermöglichen die Auswahl beziehungsweise Eingabe von Werten für die Eigenschaften Position, Left und Top des angegebenen Menüs (siehe Abbildung 10.24). Dafür sorgen die Ereigniseigenschaften, die jeweils nach dem Aktualisieren der Steuerelemente ausgelöst werden: Private Sub cboPosition_AfterUpdate() Dim strMenue As String strMenue = Me!txtMenue CommandBars(strMenue).Position = Me.cboPosition MenuepositionAktualisieren strMenue End Sub Private Sub txtLeft_AfterUpdate() Dim strMenue As String strMenue = Me!txtMenue CommandBars(strMenue).Left = Me.txtLeft MenuepositionAktualisieren strMenue End Sub Private Sub txtMenue_AfterUpdate() Dim strMenue As String strMenue = Me!txtMenue MenuepositionAktualisieren strMenue End Sub Private Sub txtTop_AfterUpdate() Dim strMenue As String strMenue = Me!txtMenue CommandBars(strMenue).Top = Me.txtTop MenuepositionAktualisieren strMenue End Sub Listing 10.24: Routinen zum Positionieren von Menüs
Abbildung 10.24: Beispiel für das Verschieben und Positionieren von Menüs
Feinschliff
495
10.8.4 Eigenschaften von Menüs in der Registry Viele Eigenschaften von Menüs, wie Position, Sichtbarkeit und andere, werden in der Registry gespeichert, damit sie beim nächsten Aufruf der Anwendung noch das gleiche Aussehen wie vor dem Schließen aufweisen. Dabei wird für jedes Menü ein Eintrag in dem in Abbildung 10.25 gezeigten Schlüssel gespeichert. Der Name setzt sich aus den Zeichen ACB und dem Menünamen zusammen. Wichtig ist dies deshalb, weil die Einstellungen für ein Menü zugleich für alle Menüs mit dem gleichen Namen gelten – Sie sollten also jedem Menü ein anwendungsspezifisches Kürzel anhängen, damit Sie nicht die Menüleisten mehrerer Anwendungen durcheinander werfen (hier etwa ACBHauptmenü_Beispieldatenbank).
Abbildung 10.25: Informationen über Menüleisten in der Registry
11 Debugging, Fehlerbehandlung und Fehlerdokumentation Die drei in der Kapitelüberschrift aufgeführten Themen sind eng miteinander verknüpft und können über Erfolg oder Scheitern einer Anwendung entscheiden. Grundsätzlich greifen alle drei Mechanismen erst, wenn etwas im Argen liegt – allerdings an unterschiedlichen Stellen. Die klassischen Debugging-Mechanismen von Access helfen vorrangig bei der Entwicklung einer Anwendung und liefern beispielsweise an Stellen Anhaltspunkte, an denen der Code unerwartete Ergebnisse liefert. Fehlerbehandlung und Fehlerdokumentation arbeiten sowohl bei der Entwicklung als auch beim Einsatz der Anwendung Hand in Hand. Die Fehlerbehandlung sorgt dafür, dass eine Anwendung auch beim Auftreten eines Fehlers stabil weiterläuft, während die Fehlerdokumentation für das Notieren von Fehlern sorgt, damit der Entwickler diesen schnell und unkompliziert auf den Grund gehen kann. Letzteres macht sich im Übrigen sowohl während der Entwicklung gut als auch während des Einsatzes beim Benutzer.
11.1 Fehlerarten Die bei der Programmierung und beim Einsatz einer Anwendung auftretenden Fehler lassen sich in mehrere Kategorien einteilen. Der Schwierigkeitsgrad beim Auffinden der Fehler und die dazu benötigten Werkzeuge sind je nach Kategorie unterschiedlich.
11.1.1 Syntaxfehler Syntaxfehler sind vermutlich am leichtesten zu finden, weil die Entwicklungsumgebung sich selbst darum kümmert. Dabei handelt es sich um unvollständige Code-Konstrukte, fehlende beziehungsweise überzählige Klammern oder Anführungszeichen oder die Verwendung von Schlüsselwörtern an Stellen, an denen sie nicht erlaubt sind. Wenn Sie beispielsweise eine If Then-Verzweigung einfügen und vor dem Sprung in die nächste Zeile zwar das Schlüsselwort If und die Bedingung, aber nicht das Schlüsselwort Then eingegeben haben, teilt Access Ihnen dies unmissverständlich mit (siehe Abbildung 11.1).
498
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 11.1: Syntaxfehler werden an Ort und Stelle geahndet.
Andere Syntaxfehler deckt die Entwicklungsumgebung nicht auf dem Fuße auf. Meist handelt es sich dabei um fehlende, falsche oder überflüssige Zeilen. Bleiben Sie bei obigem Beispiel: Wenn Sie die erste Zeile der If Then-Anweisung vervollständigen, ist Access zunächst zufrieden. Auch, wenn Sie dann an einer anderen Stelle weiterprogrammieren, ohne die abschließende End If-Anweisung hinzuzufügen und diese dann vergessen. Darum kümmert sich Access aber später, und zwar spätestens beim Aufrufen der Prozedur. Die Meldung aus Abbildung 11.2 zeigt exakt an, vor welcher Zeile sie spätestens die fehlende End If-Anweisung vermisst.
Abbildung 11.2: Unvollständige Code-Konstrukte findet Access erst beim Kompilieren oder Ausführen.
Fehlerarten
499
Das gleiche Ergebnis erhalten Sie, wenn Sie das VBA-Projekt mit dem Menübefehl Debuggen/Kompilieren von kompilieren. Das Ausführen einer Routine erreichen Sie übrigens am schnellsten durch die Betätigung der Taste (F5), während sich die Einfügemarke innerhalb der zu startenden Routine befindet. Das klappt allerdings auch nur in Routinen ohne Übergabeparameter, und das auch nur in Standardmodulen.
Syntaxprüfung und weitere Optionen Die Syntaxprüfung während der Eingabe ist eine nützliche Funktion, die Sie auf Fehler aufmerksam macht, während Sie gedanklich noch in der betroffenen Routine stecken. Würden solche Fehler erst nach dem Schreiben einiger Zeilen Code oder gar ganzer Routinen beim nächsten Debuggen oder Test bemerkt, müssten Sie sich zunächst in die jeweiligen Codezeilen hineindenken. Manch einer wird durch diese Funktion aber in seinem Tatendrang gestört. Die Hinweise auf Syntaxfehler können natürlich auch einmal nervig sein – etwa wenn Sie in einer If Then-Anweisung eine Variable verwenden möchten, deren Namen Sie gerade nicht im Kopf haben. Wenn Sie dann schnell nach oben blättern, um den Variablennamen zu ermitteln, macht Ihnen die Syntaxprüfung einen Strich durch die Rechnung. Deshalb wird es manchen freuen, dass man diese »Live«-Syntaxprüfung abschalten kann, und zwar im Optionen-Dialog der VBA-Entwicklungsumgebung (Menüeintrag Extras/Optionen, Option Automatische Syntaxüberprüfung). Dadurch bleiben allerdings nur die Fehlermeldungen aus, fehlerhafte Zeilen sind aber weiterhin in roter Schrift markiert – Sie verpassen also nichts.
Variablendeklaration erzwingen Ein weiterer Garant für nicht bemerkte Fehler ist die fehlende Deklaration von Variablen. Im einfachsten Fall weist man einer nicht explizit deklarierten Variablen, die eigentlich Zahlen enthalten sollte, einen Text zu und eckt damit irgendwo an. Damit das nicht passiert, sollten Sie tunlichst alle verwendeten Variablen deklarieren. Da selbst erfahrene Programmierer dies einmal vergessen, können Sie mit der folgenden Zeile im Modulkopf nachhelfen: Option Explicit
Access meldet sich anschließend bei jeder Verwendung einer nicht deklarierten Variablen. Damit Sie auch das Setzen dieser Zeile nicht mal vergessen, können Sie im Dialog aus Abbildung 11.3 die Option Variablendeklaration erforderlich aktivieren. Access fügt die Option Explicit-Anweisung dann automatisch in jedes neue Modul ein.
500
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 11.3: Optionen für die VBA-Entwicklungsumgebung
11.1.2 Laufzeitfehler Laufzeitfehler werden nicht durch den Compiler von Access entdeckt. Sie fallen erst bei der Durchführung der entsprechenden Routine auf. Typische Beispiele für Laufzeitfehler sind Divisionen durch 0, Zuweisung von Werten falschen Datentyps (besonders oft in Zusammenhang mit dem Wert Null) oder durch Verweise auf Objekte, die nicht instanziert wurden oder aus anderen Gründen nicht zur Verfügung stehen. Im Falle eines Laufzeitfehlers zeigt die VBA-Entwicklungsumgebung eine Fehlermeldung an, die eine Fehlernummer und eine Fehlerbeschreibung enthält. Laufzeitfehler sind natürlich meist unerwünscht. Manchmal lässt sich ein Laufzeitfehler allerdings auch für die Ablaufsteuerung innerhalb einer Routine zweckentfremden oder liefert gar eine performantere oder einfacher zu implementierende Variante. Ein Beispiel ist das Instanzieren von OLE-Servern: Hier versucht man zunächst, ein bestehenes Objekt zu referenzieren. Ist keine Instanz vorhanden, löst dies einen Fehler aus. Dieser Fehler wird allerdings abgefangen und in der Fehlerbehandlungsroutine eine neue Instanz der benötigten Objekts erstellt. Mehr dazu erfahren Sie in diesem Kapitel unter 11.3.3, »Funktionale Fehlerbehandlung« oder in Kapitel 6, Abschnitt 6.8.4, »Zugriff per Late Binding«.
11.1.3 Logische Fehler Logische Fehler werden gar nicht von Access gemeldet. Sie schlagen sich vielmehr in falschen Ergebnissen bei der Ausführung von Routinen nieder – entweder geben diese unerwartete Werte zurück, speichern falsche Daten in den Tabellen der Datenbank oder machen auf ähnliche Weise auf sich aufmerksam – früher oder später.
Debugging in der VBA-Entwicklungsumgebung
501
Logische Fehler aufdecken Beim Aufdecken logischer Fehler sind gute Debugging-Möglichkeiten das A und O. Wenn Sie die Möglichkeit haben, Ein- und Ausgangswerte von Funktionen prüfen oder gar die Inhalte von Variablen innerhalb der Routinen zu kontrollieren, lassen sich die meisten Fehler mit mehr oder weniger Schritten eingrenzen und schlussendlich auch finden. Die VBA-Entwicklungsumgebung liefert einige sinnvolle Werkzeuge für das Debugging, wie der folgende Abschnitt zeigt.
11.2 Debugging in der VBA-Entwicklungsumgebung Die VBA-Entwicklungsumgebung bietet einige Werkzeuge zum Debuggen von Code. »Debugging« fasst eigentlich alle Tätigkeiten zur Fehlerbehebung zusammen, wozu das Auffinden, Diagnostizieren und Beheben von Fehlern – oder eben »Bugs« – gehört. Interessant ist in diesem Zusammenhang die Herkunft der Bezeichnung »Bug»: Laut der Internetseite http://www.waterholes.com/~dennette/1996/hopper/bug.htm stammt dieser Begriff aus der Zeit des zweiten Weltkriegs und bezieht sich auf eine Motte, die ein Relais des Computers Mark I bei der Arbeit behinderte. Diese Motte wurde damals als (toter) Beweis in ein Logbuch eingeklebt, das sich zurzeit in der Smithsonian Institution in Washington befindet. Die Debugging-Werkzeuge der VBA-Entwicklungsumgebung konzentrieren sich auf das Melden und Anzeigen von Laufzeitfehlern und das Nachverfolgen von VariablenWerten, wobei dazu unterschiedliche Möglichkeiten zur Verfügung stehen. Nachfolgend lernen Sie die einzelnen Werkzeuge kennen.
11.2.1 Die Debuggen-Symbolleiste Die Debuggen-Symbolleiste liefert die Möglichkeit, alle nachfolgend beschriebenen Debugging-Tools per Mausklick aufzurufen und den Ablauf von Routinen durch das Starten und Beenden, das Setzen von Haltepunkten oder das schrittweise Durchlaufen zu steuern (siehe Abbildung 11.4).
Abbildung 11.4: Symbolleiste zum Aufrufen der Debugging-Funktionen
502
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
11.2.2 Das Direktfenster Das wichtigste, weil flexibelste Element der VBA-Entwickungsumgebung ist das Direktfenster. Sie können damit folgende Aktionen durchführen: Aufrufen von Function- und Sub-Prozeduren (mit oder ohne Parameter) Aufrufen von Standardfunktionen und Ausgabe der Ergebnisse mit der Debug.Print-Anweisung Untersuchen von Formularen und Berichten und deren Eigenschaften während der Anzeige in der Formular- beziehungsweise Berichtsansicht durch Ausgeben von Werten mit der Debug.Print-Anweisung Ausgabe von Werten der Variablen einer gerade angehaltenen Routine sowie Anpassen von Variablen Ausgabe von Debug.Print-Anweisungen, die Sie an beliebigen Stellen im Quellcode platzieren können Sollte das Direktfenster einmal nicht sichtbar sein, können Sie es mit der Tastenkombination (Strg) + (G) sichtbar machen. Das gilt übrigens auch für die Access-Entwicklungsumgebung: Dort führt diese Tastenkombination zum Öffnen der VBAEntwicklungsumgebung und zum Aktivieren des Direktfensters. Das Direktfenster sieht wie in Abbildung 11.5 aus und kann wie eine flexiblere Variante der Eingabeaufforderung verwendet werden. Geben Sie dort einfach die gewünschten Ausdrücke ein und lassen Sie das Ergebnis ausgeben.
Abbildung 11.5: Testen von Funktionen im Direktfenster
Debug.Print und die Kurzformen Die vollständige Anweisung mit Angabe des Debug-Objekts müssen Sie nur außerhalb des Direktfensters – also innerhalb von Modulen – verwenden. Das Debug-Objekt ist das Direktfenster, daher reicht dort die Anweisung Print oder die abgekürzte Form ?.
Debugging in der VBA-Entwicklungsumgebung
503
11.2.3 Haltepunkte Zum punktuellen Untersuchen des VBA-Codes und aktueller Zustände von Variablen können Sie im VBA-Code Haltepunkte setzen. Das ist zum Beispiel sinnvoll, wenn Sie ahnen, dass an einer bestimmten Stelle im Quellcode etwas nicht so läuft, wie es laufen sollte, und Sie sich die in der Umgebung vorhandenen Variablen einmal näher ansehen möchten. In solch einem Fall setzen Sie einen Haltepunkt, indem Sie einfach in die graue Leiste links neben dem Quellcode klicken. Auf die gleiche Weise entfernen Sie den Haltepunkt auch wieder. Die zweite Methode zum Setzen eines Haltepunktes ist das Positionieren der Einfügemarke auf der gewünschten Zeile und die Betätigung der Taste (F9) – mit dieser Taste entfernen Sie vorhandene Haltepunkte auch wieder. Haben Sie wie in Abbildung 11.6 einen Haltepunkt gesetzt, müssen Sie die Routine nur noch starten. Sobald die Abarbeitung an diesem Punkt angelangt ist, wird die Ausführung unterbrochen und die aktuelle Zeile gelb hinterlegt.
Abbildung 11.6: Ein Haltepunkt in einer VBA-Prozedur
Sie können dann durch Überfahren mit dem Mauszeiger die Werte der im Code enthaltenen Ausdrücke anzeigen lassen (siehe Abbildung 11.7).
Abbildung 11.7: Anzeige der Werte von Ausdrücken im Haltemodus
504
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Alternativ können Sie sich den Inhalt von Ausdrücken auch im Direktfenster ausgeben lassen – etwa, wenn Sie den Wert für einen anderen Zweck weiterverwenden möchten. Um den Wert aus Abbildung 11.7 im Direktfenster auszugeben, benutzen Sie den folgenden Ausdruck: Debug.Print tdf.Fields.Count
Von Anweisung zu Anweisung Wenn Sie nach dem Erreichen des Haltepunktes Schritt für Schritt durch den Code laufen möchten, verwenden Sie dazu die Schaltfläche »Einzelschritt« (F8).
Von Haltepunkt zu Haltepunkt Wenn Sie mehrere Haltepunkte in der Anwendung gesetzt haben, können Sie die Abarbeitung des Codes nach Erreichen eines Haltepunktes mit der Taste (F5) fortsetzen. Der Code läuft dann bis zum nächsten Haltepunkt weiter, sofern zwischendurch nichts Unvorhergesehenes passiert.
Den Ablauf einer Routine im Haltemodus beeinflussen Der Haltemodus ist relativ mächtig: Sie können etwa den Zeiger, der die aktuell auszuführende Zeile markiert, verschieben und damit in den Programmablauf eingreifen. Damit können Sie beispielsweise einen Laufzeitfehler überbrücken, wenn Sie diesen erst später beheben und zunächst die Routine weiter untersuchen möchten oder auch im Ablauf zurückspringen. Dies ist allerdings mit Vorsicht zu genießen, da somit beispielsweise auch Zählervariablen wiederholt erhöht werden können.
Haltepunkte fixieren Die am linken Rand des Codemoduls markierten Haltepunkte halten nicht lange – wenn Sie die Access-Anwendung einmal geschlossen haben und wieder öffnen, sind die Haltepunkte verschwunden. Wenn Sie einen Haltepunkt benötigen, der etwas haltbarer ist, fügen Sie vor der Zeile, die Sie sonst mit einem Haltepunkt markiert hätten, die Anweisung Stop ein.
11.2.4 Die Aufrufliste Manchmal kann es wichtig sein, wenn Sie wissen, über welche Prozeduren Sie zu der aktuellen Routine gelangt sind. Das ist vor allem interessant, wenn Sie einen Haltepunkt in einer Routine platziert haben, die von mehreren anderen Prozeduren aus aufgerufen wird. Die Aufrufliste zeigt nur Informationen an, wenn der Ablauf durch einen Haltepunkt unterbrochen wurde (siehe Abbildung 11.8).
Debugging in der VBA-Entwicklungsumgebung
505
Abbildung 11.8: Die Aufrufliste zeigt die aufgerufenen Routinen in der Hierarchie an.
11.2.5 Ausdrücke überwachen Das Fenster Überwachungsausdrücke ermöglicht die Überwachung bestimmter Ausdrücke über den gesamten Ablauf einer Anwendung. Sie zeigen dieses Fenster an, indem Sie den Menüeintrag Ansicht/Überwachungsfenster auswählen. Um einen Ausdruck zur Überwachung anzulegen, können Sie diesen beispielsweise im aktuellen Codefenster markieren und dann im Kontextmenü den Eintrag Überwachung hinzufügen auswählen. Der Dialog Überwachung hinzufügen (siehe Abbildung 11.9) enthält dann direkt den gewünschten Ausdruck. Hier gibt es nun drei Möglichkeiten: Der Ausdruck soll einfach nur überwacht werden. Der Ablauf soll unterbrochen werden, wenn der Ausdruck einen bestimmten Wert annimmt. In diesem Fall müssen Sie den Ausdruck noch um das entsprechende Kriterium erweitern, etwa fld.Name ="MitarbeiterID". Der Ablauf soll unterbrochen werden, sobald der Wert geändert wurde.
Ausdrücke per VBA-Code überwachen Dass Sie mit der Debug.Print-Anweisung beliebige Ausdrücke während des Ablaufs von Routinen ausgeben können, haben Sie bereits weiter oben erfahren. Damit lässt sich die Überwachung von Ausdrücken gut nachbilden, was der ersten Option der Überwachungs-Eigenschaften entsprechen würde.
506
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 11.9: Anlegen eines neuen Überwachungsausdrucks
Auch die zweite Option lässt sich simulieren: Um den Ablauf einer Prozedur zu unterbrechen, wenn ein Ausdruck einen bestimmten Wert enthält, verwenden Sie die Debug.Assert-Anweisung. Als Parameter legen Sie einen boolschen Ausdruck mit dem gewünschten Ergebnis fest.
11.2.6 Das Lokal-Fenster Das Lokal-Fenster zeigt direkt alle Variablen und deren Werte einschließlich der Eigenschaften von Objekten der laufenden Prozedur an (siehe Abbildung 11.10). Um die Eigenschaften von Objekten anzuzeigen, klicken Sie auf das Plus-Symbol (+) vor dem jeweiligen Objekt. Auch dieses Debugging-Tool funktioniert nur in Zusammenhang mit angehaltenem Code.
Fehlerbehandlung in VBA
507
Abbildung 11.10: Das Lokal-Fenster zeigt alle Variablen der laufenden Routine an.
11.3 Fehlerbehandlung in VBA Wenn man von Fehlerbehandlung spricht, bezieht sich dies auf Laufzeitfehler. Andere Fehler wie Syntax- oder Kompilierzeitfehler werden bereits vor dem Start einer Routine gemeldet – hier gibt es auch keine Möglichkeit einer externen Behandlung. Laufzeitfehler lassen sich unter VBA wie in anderen Programmiersprachen »behandeln«; das heißt, dass Sie mit zusätzlichem VBA-Code auf Fehler reagieren können.
Fehlerbehandlung für benutzerdefinierte Fehlermeldungen Meist verwendet man eine Fehlerbehandlung dazu, eine benutzerdefinierte Fehlermeldung anzuzeigen, anstatt dem Benutzer der Anwendung die tatsächliche Fehlermeldung mit Blick auf die fehlerhafte Stelle im Code zu präsentieren. Auf diese Weise lassen sich in vielen Fällen aussagekräftigere Meldungen ausgeben. Wenn Sie die Access-Anwendung als Runtime-Version weitergeben (mehr dazu in Kapitel 17, Abschnitt 17.2, »Weitergeben von Access-Datenbanken«), gehört eine solche Fehlerbehandlung quasi zum Pflichtprogramm – erweitert um eine Möglichkeit, Informationen zu
508
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
den Fehlern in irgendeiner Weise zur Weiterleitung an den Entwickler verfügbar zu machen. Wie das funktioniert, erfahren Sie weiter unten in Abschnitt 11.4, »Fehlerdokumentation und -übermittlung«.
Fehlerbehandlung als Vereinfachung Oft ist es gar nicht geplant, den Benutzer vom Auftreten eines Fehlers in Kenntnis zu setzen – wenn Sie beispielsweise eine Routine verwenden, um eine temporäre Tabelle anzulegen, löschen Sie zuvor eine eventuell vorhandene Tabelle gleichen Namens. Wenn Sie nicht sicher wissen, ob die Tabelle überhaupt vorhanden ist, und keinen Code schreiben wollen, der dies überprüft, sorgen Sie einfach dafür, dass die Tabelle gelöscht wird, wenn diese vorhanden ist, und dass der zu erwartende Fehler, der beim Versuch, eine nicht vorhandene Tabelle zu löschen, auftritt, nicht angezeigt wird – ein Beispiel folgt weiter unten.
Fehlerbehandlung als Lieferant für Bedingungs-Kriterien Manchmal verwendet man Fehler sogar als eine Art »Verzweigung« – wenn Sie etwa eine Referenz auf ein Word-Objekt benötigen, versuchen Sie zunächst, eine bestehende Instanz anzusprechen. Ist diese nicht vorhanden, löst dieser Versuch einen Fehler aus. Direkt danach werten Sie aus, ob ein Fehler auftrat und erzeugen in Abhängigkeit davon eine neue Instanz von Word oder verwenden die existierende Instanz.
11.3.1 Elemente der Fehlerbehandlung Damit VBA überhaupt irgendetwas anderes macht, als eine Standardfehlermeldung auszugeben, teilen Sie der Routine mit einer On Error-Anweisung mit, dass eine Alternative zur Standardfehlermeldung ansteht.
Fehlerbehandlung einleiten Dabei gibt es folgende Varianten: On Error Resume Next: Ignoriert sämtliche Fehler und bearbeitet einfach die folgende Zeile. On Error Goto <Markierung>: Lässt die Routine im Fehlerfall mit dem hinter <Markierung>: befindlichen Code fortfahren. On Error Goto 0: Sorgt dafür, dass nachfolgend auftretende Fehler wieder von Access selbst behandelt werden.
Fehlerbehandlung in VBA
509
Klassischer Aufbau einer Fehlerbehandlung Typischerweise ist eine Fehlerbehandlung wie in folgendem Listing aufgebaut. Die Elemente der Fehlerbehandlung sind fett gedruckt. Den Start macht die On Error-Anweisung, die angibt, dass die Routine im Falle eines Fehlers an der Markierung Beispielfehler_Err: fortgeführt werden soll. Diese Markierungen sind an der Stelle der Markierung mit abschließendem Doppelpunkt (:) anzugeben, beim Verweis aber ohne Doppelpunkt. Zwischen der Markierung Beispielfehler_Err: und dem Ende der Routine ist Platz für die eigentliche Fehlerbehandlung. Hier können Sie Informationen zum Fehler auswerten und entsprechende Schritte einleiten – mehr dazu weiter unten. Nach dem Behandeln des Fehlers erfolgt ein erneuter Sprung zur Marke Beispielfehler_Exit:. Dort bringen Sie diejenigen Anweisungen unter, die trotz Fehler unbedingt noch ausgeführt werden müssen. Dabei kann es sich um das Schließen von Objekten, die Freigabe von Objektvariablen oder auch das Löschen temporärer Daten handeln. Wenn die Routine ohne Fehler durchläuft, bleiben die Markierungen bedeutungslos. Das heißt, dass die hinter der Markierung Beispielfehler_Exit befindlichen Anweisungen ausgeführt werden, als ob diese Markierung dort gar nicht vorhanden sei. Die Exit-Anweisung schließlich sorgt dafür, dass die Routine beim Verlauf ohne Fehler nicht dennoch mit der Fehlerbehandlung endet. Public Function Beispielfehler() On Error GoTo Beispielfehler_Err
… 'Fehlerbehandlung Beispielfehler_Exit:
'Restarbeiten … Exit Function Beispielfehler_Err:
'Fehler auswerten und reagieren GoTo Beispielfehler_Exit
End Function Listing 11.1: Grundgerüst einer Fehlerbehandlung
Fehler auswerten Eine oft gesehene Art der Fehlerauswertung ist die Ausgabe der Fehlerinformationen, etwa durch eine Meldung wie in folgendem Beispiel. Der Fehler durch die Division durch Null führt zur Meldung aus Abbildung 11.11:
510
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Public Function DivisionDurchNull() On Error GoTo DivisionDurchNull_Err Dim i As Integer i = 1 / 0 'Fehlerbehandlung DivisionDurchNull_Exit: 'Restarbeiten Exit Function DivisionDurchNull_Err: MsgBox "Fehlernummer: " & Err.Number _ & vbCrLf & "Beschreibung: " & Err.Description
GoTo DivisionDurchNull_Exit End Function Listing 11.2: Fehlermeldung per Meldungsfenster
Abbildung 11.11: Diese Fehlermeldung resultiert aus der Fehlerbehandlung aus Listing 11.2.
Obwohl diese Art der Fehlermeldung in vielen Beispieldatenbanken zu finden ist, bringt sie leider nicht viel. Als Entwickler wird man hoffentlich nicht mit einer solchen Fehlermeldung arbeiten, da sie nicht den geringsten Aufschluss über den Ort gibt, an dem der Fehler auftritt. Auch in Datenbankanwendungen, die zur Weitergabe bestimmt sind, ist diese Fehlerbehandlung aus oben genanntem Grund nicht besonders hilfreich. Die Fehlermeldung soll ja nicht nur den Benutzer über das Auftreten eines Fehlers informieren, sondern auch Informationen liefern, die der Benutzer an den Entwickler weitergeben kann.
Das Err-Objekt Bevor es untergeht, sollen Sie noch das Err-Objekt kennen lernen, das für das Bereitstellen etwa der Fehlernummer oder der Fehlerbeschreibung zuständig ist. Mit diesem Objekt können Sie gut prüfen, ob zu einem bestimmten Zeitpunkt ein Fehler aufgetreten ist. Wenn die Eigenschaft Err.Number den Wert 0 zurückliefert, liegt derzeit kein Fehler vor.
Fehlerbehandlung in VBA
511
Das Err-Objekt enthält unter anderem die folgenden Eigenschaften und Methoden: Clear: Löscht die Eigenschaften des Err-Objekts. Das bewirkt unter anderem auch ein On Error Goto 0. Number: Gibt die Fehlernummer zurück. Description: Gibt die Beschreibung des Fehlers zurück. Raise: Löst einen benutzerdefinierten Fehler aus (weitere Informationen in Abschnitt 11.3.4, »Funktionale Fehlerbehandlung«).
Nach der Fehlerbehandlung Wenn der Fehler behandelt wurde, gibt es verschiedene Möglichkeiten zur Fortsetzung: Mit der fehlerhaften Zeile fortfahren: Resume Mit der ersten Zeile hinter der Fehlerzeile fortfahren: Resume Next Mit dem Code hinter irgendeiner Markierung fortfahren: Resume <Markierung> oder Goto <Markierung>
11.3.2 Benutzerdefinierte Fehlerbehandlung temporär ausschalten Manchmal kann es sinnvoll sein, die Fehlerbehandlung zu deaktivieren. Dazu kommentieren Sie dann einfach die On Error…-Anweisung aus. Ist davon nur eine Routine betroffen, hält sich der Aufwand in Grenzen. Anders ist es, wenn Sie die Fehlerbehandlung in mehreren Routinen ausschalten möchten. Glücklicherweise stellt die VBA-Entwicklungsumgebung eine Möglichkeit zum anwendungsweiten Abschalten der benutzerdefinierten Fehlerbehandlung zur Verfügung. Damit alle Fehler von Access angezeigt werden, als ob keine benutzerdefinierte Fehlerbehandlung vorhanden sei, stellen Sie die Option Unterbrechen bei Fehlern des Optionen-Dialogs auf den Wert Bei jedem Fehler ein (siehe Abbildung 11.12). Die Routine aus Listing 11.2 erzeugt dann die Standardfehlermeldung von Access (siehe Abbildung 11.13). Normalerweise sollten Sie die Option aber auf den Wert Bei nicht verarbeiteten Fehlern einstellen.
512
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 11.12: Optionen für die Handhabung von Fehlern einstellen
Abbildung 11.13: Access-eigene Fehlermeldung
11.3.3 Funktionale Fehlerbehandlung Fehlermeldungen können auch zur Vereinfachung von Funktionalität verwendet werden. Wie das funktioniert, zeigen die beiden folgenden Beispiele.
Prüfen auf Duplikate und Einfügen von Daten auf einen Streich Im ersten Beispiel erzeugt das Anfügen eines Wertes an eine Tabelle einen Fehler, wenn der Wert schon vorhanden und das Feld mit einem eindeutigen Index versehen ist.
Fehlerbehandlung in VBA
513
Man könnte diesen Fehler umgehen, indem man zuvor prüft, ob der anzulegende Wert schon in der Zieltabelle enthalten ist. Das würde einen Zugriff auf die Zieltabelle erfordern, das anschließende Hinzufügen des Datensatzes einen weiteren. Den ersten Zugriff können Sie sich sparen, indem Sie den Datensatz einfach direkt hinzufügen und den im Falle eines doppelten Datensatzes auftretenden Fehler entsprechend behandeln. Das folgende Listing zeigt, wie das aussehen kann. Dort ist eine Fehlerbehandlung wie oben beschrieben eingebaut, die im Auswertungsteil prüft, ob der ausgelöste Fehler die Nummer 3022 hat. Dies ist die Nummer des Fehlers, der beim Hinzufügen von bereits vorhandenen Werten in ein eindeutiges Feld auftritt. Statt der von Access ausgegebenen Standardmeldung (siehe Abbildung 11.14) erhält der Benutzer der Anwendung eine deutlich aussagekräftigere Meldung (siehe Abbildung 11.15). Public Function FirmaAnfuegen(strFirma As String) On Error GoTo FirmaAnfuegen_Err Dim db As DAO.Database Set db = CurrentDb db.Execute "INSERT INTO tblFirmen(Firma) VALUES('" & strFirma & "')", _ dbFailOnError 'Fehlerbehandlung FirmaAnfuegen_Exit: 'Restarbeiten Set db = Nothing Exit Function FirmaAnfuegen_Err: If Err.Number = 3022 Then MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." End If GoTo FirmaAnfuegen_Exit End Function Listing 11.3: Routine mit einer zielgerichteten Fehlerbehandlung
Vorhandensein von Objektinstanzen per Fehlerbehandlung prüfen Eine andere Möglichkeit, die Fehlerbehandlung als Ersatz komplexerer Funktionalität zu nutzen ist folgende. Hier soll eine Instanz eines Objekts erzeugt werden, sofern noch keine Instanz dieses Objekts vorhanden ist. Dazu greift man einfach auf die benötigte Instanz zu und prüft, ob daraus ein Fehler resultiert. Das ist der Fall, wenn das Objekt noch nicht existiert; anderenfalls erzeugt der Zugriff keinen Fehler.
514
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 11.14: Fehler beim Anfügen eines doppelten Wertes in ein Feld mit eindeutigem Index
Abbildung 11.15: Benutzerdefinierte Fehlermeldung als Ersatz für die Meldung aus Abbilung 11.14
Die folgende Routine versucht, mit der GetObject-Anweisung auf eine bestehende Instanz von Word zuzugreifen. Durch die vorherige On Error Resume Next-Anweisung läuft die Prozedur auf jeden Fall weiter, egal ob ein Fehler auftritt oder auch nicht. Ob es einen Fehler gab, wird in der nächsten Zeile geprüft: Die Fehlernummer 429 deutet darauf hin, dass die Zuweisung des Objekts fehlgeschlagen ist, worauf eine neue Instanz erzeugt wird. Wenn Word auf dem Rechner gar nicht installiert ist, erzeugt auch dies einen Fehler mit der Nummer 429, der mit einer entsprechenden Fehlermeldung quittiert wird. Zwischendurch müssen Sie das Err-Objekt übrigens immer wieder initialisieren, da sonst auch die Fehlernummer beibehalten wird. Dazu können Sie die Clear-Methode des Err-Objekts verwenden, aber auch eine folgende On Error-Anweisung initialisiert das Err-Objekt. Public Sub ZugriffAufWord() Dim objWord As Object On Error Resume Next Set objWord = GetObject(, "Word.Application") If Err.Number = 429 Then 'Err-Objekt initialisieren On Error Resume Next Set objWord = CreateObject("Word.Application") If Err.Number = 429 Then
Fehlerbehandlung in VBA
515
Msgbox "Word ist auf diesem Rechner nicht installiert", vbCritical End If End If On Error GoTo 0 objWord.Visible = True End Sub Listing 11.4: Word-Instanz holen und bei Fehler neu erstellen
11.3.4 Benutzerdefinierte Fehler Mit der bereits weiter oben kurz vorgestellten Raise-Methode des Err-Objekts können Sie benutzerdefinierte Fehler erzeugen. Benutzerdefinierte Fehler erzeugen? Reichen die von Access erzeugten Fehler nicht? Vermutlich schon. Die Raise-Methode bietet vielmehr die Möglichkeit, Eingabefehler oder dergleichen in einem Zuge mit den sonstigen Fehlern zu behandeln und diesbezüglich eine einheitliche Form zu finden. Die Funktion aus Listing 11.3 bietet ein gutes Beispiel. Mit den Erweiterungen aus folgendem Listing prüft die Funktion, ob eine leere Zeichenkette als Firmenname übergeben wurde. Ist das der Fall, löst sie einen Fehler mit der Nummer vbObjectError + 1 aus, wobei vbObjectError eine Konstante ist, die eine Kollision mit den eingebauten Fehlernummern verhindern soll. Beim Auftreten dieses Fehlers springt die Routine wie bei einem herkömmlichen Fehler in die Fehlerbehandlungsroutine und gibt eine entsprechende Meldung aus. Public Function FirmaAnfuegen(strFirma As String) On Error GoTo FirmaAnfuegen_Err Dim db As DAO.Database If Len(strFirma) = 0 Then Err.Raise vbObjectError + 1 End If
Set db = CurrentDb db.Execute "INSERT INTO tblFirmen(Firma) VALUES('" & strFirma _ & "')", dbFailOnError Set db = Nothing 'Fehlerbehandlung FirmaAnfuegen_Exit: 'Restarbeiten Exit Function FirmaAnfuegen_Err: Select Case Err.Number Case 3022 MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." Case vbObjectError + 1 MsgBox "Bitte geben Sie einen Firmennamen an."
516
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
End Select GoTo FirmaAnfuegen_Exit End Function Listing 11.5: Auslösen eines benutzerdefinierten Fehlers
Sie können der Raise-Methode noch weitere Parameter wie Source (entspricht dem aktuellen VBA-Projekt) oder Description übergeben.
11.4 Fehlerdokumentation und -übermittlung Ziel der Fehlerbehandlung in Anwendungen, die man an andere Benutzer weitergibt, muss sein, diesen eine möglichst robuste Anwendung zu bieten, die sich nach Auftreten eines Fehlers nicht wortlos verabschiedet, sondern dem Benutzer die Möglichkeit bietet, dem Entwickler Informationen über aufgetretene Fehler zukommen zu lassen. Für die Robustheit ist freilich nicht nur die Fehlerbehandlung, sondern die komplette Anwendung verantwortlich. Indirekt führt aber auch eine sinnvolle Fehlerbehandlung wieder zu mehr Robustheit, wenn diese eine Übermittlung der Informationen zu aufgetretenen Fehlern und damit deren Behebung ermöglicht. Die Übermittlung von Fehlermeldungen per Telefon und E-Mail enthält normalerweise folgenden Satz aus dem Munde des Entwicklers: »Also nun versuchen Sie noch einmal, den Fehler zu reproduzieren. Und wenn dann die Fehlermeldung erscheint, machen Sie davon einen Screenshot. Und dann klicken Sie auf Debuggen und machen noch einen Screenshot und schicken mir den bitte zu.« Worauf der Benutzer nicht selten eine der folgenden Antworten liefert: »Also, ich weiß wirklich nicht mehr, wodurch ich diesen Fehler ausgelöst habe …« oder »Was ist denn ein Screenshot?« Irgendwie erhält man dann zwar die gewünschten Informationen, aber dennoch muss man hier zu der Erkenntnis kommen: So funktioniert das nicht. Die Übermittlung von Fehlern sollte so weit automatisiert sein, dass der Benutzer höchstens noch eine Textdatei mit den notwendigen Informationen per E-Mail an den Entwickler schickt. Und dies zu realisieren, ist gar nicht so schwer.
11.4.1 Wichtige Fehlerinformationen Damit Sie, der Entwickler, etwas mit einer Fehlermeldung des Kunden anfangen können, benötigen Sie folgende Informationen: Datum/Zeit Datenbankpfad und -name Modul, in dem der Fehler aufgetreten ist
Fehlerdokumentation und -übermittlung
517
Routine, in der der Fehler aufgetreten ist Nummer der fehlerhaften Zeile Aktueller Benutzer Arbeitsrechner Fehlernummer Fehlerbeschreibung Bemerkungen Mit diesen Informationen können Sie den Fehler vermutlich nachvollziehen, und wenn das nicht genügend Anhaltspunkte liefert, können Sie immer noch den Anwender kontaktieren und nach näheren Umständen fragen – das sollte aber eigentlich nicht mehr notwendig sein. Natürlich können noch weitere Informationen wie etwa der Rechnername bei der Fehlersuche weiterhelfen – diese können Sie bei Bedarf leicht hinzufügen.
Zeilen nummerieren Wenn Sie sich die Auflistung durchsehen, werden Sie vermutlich an einer Stelle hängen bleiben: bei der Nummer der fehlerhaften Zeile. Wie bekommt man die den nun heraus? Die Frage ist verständlich: Immerhin sind die Zeiten, in denen man Basic-Programme mit Zeilennummern versah, lange vorbei – und als Sprungmarke kommen die bereits oben erwähnten Zeichenketten zum Zuge. Und nun soll man sich für eine Fehlerdokumentation in die Frühzeit der Programmierung zurückversetzen? In der Tat liefert VBA keine versteckte Zeilennummer oder eine andere interne Möglichkeit, die Nummer einer fehlerhaften Zeile zu ermitteln. Sie müssen tatsächlich alle Zeichen einer Routine durchnummerieren, wenn Sie wissen wollen, in welcher Zeile der Fehler auftritt – einige Zeilen lassen sich zwar nicht nummerieren, doch dazu später mehr. Die nächste Frage lautet dann: Wie teile ich der Fehlerbehandlung denn nun mit, in welcher Zeile der Fehler auftrat? Dazu gibt es wiederum eine nicht dokumentierte Funktion. Sie heißt Erl und liefert – sofern vorhanden – die Nummer der Zeile, in der der letzte Fehler aufgetreten ist. Zum Nachweis ein kleines Experiment: Fügen Sie der Routine aus Listing 11.2 Zeilennummern hinzu und ergänzen Sie die Fehlermeldung wie folgt: Public Function DivisionDurchNull() 10 On Error GoTo DivisionDurchNull_Err 20 Dim i As Integer 30 i = 1 / 0 40 'Fehlerbehandlung
518
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
50 DivisionDurchNull_Exit: 60 'Restarbeiten 70 Exit Function 80 DivisionDurchNull_Err: 90 MsgBox "Fehlernummer: " & Err.Number _
& vbCrLf & "Beschreibung: " & Err.Description _ & vbCrLf & "Zeile: " & Erl
GoTo DivisionDurchNull_Exit End Function
100
Listing 11.6: Routine mit Zeilennummern und Ausgabe der Zeilennummer im Falle eines Fehlers
Das Ausführen der Routine führt zu der Meldung aus Abbildung 11.16 – die fehlerhafte Zeile wird also wie geplant ausgegeben.
Abbildung 11.16: Fehlermeldung mit Zeilenangabe
Komplettes VBA-Projekt mit Zeilennummern und Fehlerbehandlungen ausstatten Da prinzipiell überall einmal der Fehlerteufel zuschlagen kann, sollten Sie den kompletten Code mit Zeilennummern und entsprechend erweiterten Fehlerbehandlungen ausstatten. Was? Den ganzen Code? Jede kleine Routine in jedem einzelnen Modul? Und soll ich etwa die Nummerierung immer wieder anpassen, wenn ich mal ein paar Zeilen einfüge? Keine Angst: Dieses Buch verlangt nichts von Ihnen, was nicht den Aufwand lohnte. Und in diesem Fall liefert es auch noch ein Tool mit, das stundenlange Tipparbeit in Sekundenbruchteilen erledigt. Das besagte Tool heißt accessVBATools und liefert eine neue Symbolleiste mit vier Befehlen (siehe Abbildung 11.17): Modul nummerieren: Nummeriert alle Zeilen des aktuellen Moduls durch. Modul entnummerieren: Entfernt die Nummerierung des aktuellen Moduls. Fehlerbehandlung hinzufügen: Fügt der Routine, in der sich aktuell die Einfügemarke befindet, eine Fehlerbehandlung hinzu. Fehlerdokumentations-Funktion hinzufügen: Fügt dem VBA-Projekt ein Standardmodul mit einer Funktion zur Dokumentation von Fehlern hinzu.
Fehlerdokumentation und -übermittlung
519
Abbildung 11.17: accessVBATools im Einsatz
Die Funktionen lassen sich nicht nur über die Symbolleiste, sondern auch über das Kontextmenü des aktuellen Moduls aufrufen (siehe Abbildung 11.18).
Abbildung 11.18: Aufruf der accessVBATools per Kontextmenü
520
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Natürlich gibt es noch weitere Tools, die Funktionen wie das Nummerieren von Zeilen oder andere mitbringen – mehr dazu im Anhang zu diesem Buch. Das hier beschriebene Tool liefert allerdings die nachfolgend dargestellte Fehlerbehandlung und die darauf abgestimmte Fehlerdokumentationsfunktion mit.
11.4.2 Einsatz der accessVBATools Die vier Funktionen der accessVBATools helfen Ihnen dabei, Ihre Anwendungen mit einer professionellen Fehlerbehandlung und -dokumentation zu versehen. Das Tool liegt auf der Buch-CD in Form einer .dll-Datei unter dem Dateinamen Kap_11\accessVBATools.dll vor. Diese .dll-Datei kopieren Sie in ein Verzeichnis Ihrer Wahl (vorzugsweise c:\Windows\System32) und registrieren diese über den Ausführen …-Dialog von Windows mit der Anweisung regsvr32.exe c:\Windows\ System32\accessVBATools.dll. Anschließend öffnen Sie die VBA-Entwicklungsumgebung neu und finden die neue Symbolleiste sowie die Einträge im Kontextmenü vor.
Nummerierungen hinzufügen und entfernen Mit den beiden Befehlen Modul nummerieren und Modul entnummerieren können Sie die Nummerierung nach Belieben hinzufügen und wieder entfernen – zumindest in der Entwicklungsphase. Das macht das Nummerieren besonders einfach: Wenn Sie die Anwendung testen, fügen Sie die Nummerierung hinzu, wenn Sie Änderungen am Code vornehmen, entfernen Sie die Nummerierung, ändern den Code und fügen die Nummerierung wieder hinzu. Sobald Sie eine Anwendung einmal weitergegeben haben, müssen Sie eine unveränderte Kopie dieser Version behalten, um auf Fehlermeldungen reagieren zu können, die der Benutzer Ihnen mitteilt – sonst würden die Zeilennummern unter Umständen doch wieder nicht weiterhelfen.
Fehlerbehandlung hinzufügen Die dritte Funktion der accessVBATools fügt einer Prozedur eine Fehlerbehandlung wie in folgendem Listing hinzu – in diesem Fall ausgehend von einem leeren Prozedurrumpf: Public Sub Beispielfehlerbehandlung() On Error GoTo Beispielfehlerbehandlung_Err 'Fehlerbehandlung Beispielfehlerbehandlung_Exit: 'Restarbeiten Exit Sub
Fehlerdokumentation und -übermittlung
521
Beispielfehlerbehandlung_Err: Call Fehlerbehandlung("Fehlerbehandlung - Modul1 ", _ "Beispielfehlerbehandlung", Erl, "Bemerkungen: ./.") GoTo Beispielfehlerbehandlung_Exit End Sub Listing 11.7: Eine ansonsten nackte Routine nach der Bestückung mit einer Fehlerbehandlung
Die Fehlerbehandlung entspricht weitgehend dem weiter oben vorgeschlagenen Muster. Einzige Ausnahme ist, dass keine Fehlermeldung per MsgBox-Anweisung ausgegeben wird, sondern eine Funktion namens Fehlerbehandlung aufgerufen wird – was eine passende Überleitung zur vierten Funktion ist.
Funktion zur Fehlerdokumentation hinzufügen Die noch fehlende Funktion Fehlerbehandlung wird durch den letzten Eintrag der accessVBATools hinzugefügt – und zwar samt eigenem Standardmodul. Dieses hat den folgenden Inhalt: Option Compare Database Option Explicit Private Declare Function GetComputerName Lib "kernel32.dll" _ Alias "GetComputerNameA" (ByVal lpBuffer As String, _ ByRef nSize As Long) As Long Public Function Fehlerbehandlung(strModul As String, strRoutine As String, _ lngZeile As Long, Optional strBemerkungen As String) On Error Resume Next Open CurrentProject.Path & "\Fehler.log" For Append As #1 Print #1, "Datum: " & Format(Now, "yyyy-mm-dd, hh:nn:ss") Print #1, "Datenbankpfad: " & CurrentDb.Name Print #1, "Modul: " & strModul Print #1, "Routine: " & strRoutine Print #1, "Benutzer: " & CurrentUser() Print #1, "Rechner: " & ComputerName() Print #1, "Fehlernummer: " & Err.Number Print #1, "Fehlerbeschreibung: " & Err.Description Print #1, "Zeile: " & lngZeile Print #1, "Bemerkungen: " & strBemerkungen Print #1, "" Close #1 Reset MsgBox "Es ist ein Fehler aufgetreten. " & vbCrLf _ & "Weitere Informationen finden Sie in der Datei Fehler.log im " _ & " Verzeichnis dieser Datenbank."
522
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
End Function Function ComputerName() As String Dim strComputer As String Dim n As Long n = 255 strComputer = String$(n, 0) Call GetComputerName(strComputer, n) ComputerName = Left(strComputer, n) End Function Listing 11.8: Diese Routinen legen einen Eintrag für eine Fehlermeldung in einer Textdatei an.
Wenn Sie nach Anlegen dieses Moduls eine fehlerhafte und nummerierte Routine starten, die die Prozedur Fehlerbehandlung aufruft, zeigt Access die folgende Meldung an (siehe Abbildung 11.19).
Abbildung 11.19: Die Fehlerbehandlung sorgt beim Auftreten eines Fehlers für die Anzeige dieser Meldung.
Die dort angegebene Datei sieht wie in Abbildung 11.20 aus. Die Datei enthält einige Informationen, die zum Auffinden der meisten Fehler führen sollten. Sie können die Fehlerbehandlung natürlich nach Belieben erweitern, um etwa Informationen über das verwendete Betriebssystem, die Office-Version und mehr zu extrahieren. Sie müssen dem Benutzer nur noch mitteilen, dass er Ihnen diese kleine Textdatei zusendet. Dazu können Sie leicht die in der Prozedur aus Listing 11.8 ausgegebene Meldung anpassen – etwa mit einer Erweiterung wie »Zum Beheben des Fehlers schicken Sie diese Textdatei bitte an <E-Mail-Adresse>«.
Abbildung 11.20: Dokumentation eines Fehlers per Textdatei
Fehlerbehandlung in Formularen
523
Bemerkungen in der Fehlerbehandlung Der Aufruf der oben beschriebenen Fehlerdokumentationsfunktion bietet einen Parameter namens Beschreibung an. Hier können Sie zusätzliche Informationen unterbringen – etwa um die Werte bestimmter Variablen innerhalb der fehlerhaften Routine auszugeben.
Mögliche Erweiterungen der Fehlerbehandlung Wer regelmäßig mit Microsoft-Produkten arbeitet, ist vermutlich bereits einmal bei einem Programmabsturz mit der damit verbundenen Anfrage der Anwendung konfrontiert worden, nähere Informationen an Microsoft zu senden. Solch eine Funktion ist natürlich auch für Access-Anwendungen denkbar. Die notwendigen Informationen könnten dabei via E-Mail (entweder über den vorhandenen E-Mail-Client oder direkt über die WinSock-Schnittstelle) oder auch durch automatischen Aufruf einer Internetseite mit entsprechenden Parametern übertragen werden. Leider würde die Ausarbeitung einer solchen Lösung den Rahmen dieses Kapitels sprengen.
11.5 Fehlerbehandlung in Formularen Die bisher vorgestellten Möglichkeiten der Fehlerbehandlung beziehen sich komplett auf Fehler, die beim Ablauf von VBA-Routinen auftreten – wenn auch manchmal andere Ursachen dahinter stehen, wie etwa Fehler in eingebetteten SQL-Anweisungen. Formulare können Fehler liefern, ohne auch nur eine einzige Zeile VBA-Code zu enthalten. Die dabei ausgegebenen Fehlermeldungen sind für normale Benutzer manchmal ebenso wenig aussagekräftig wie solche, die durch Fehler im VBA-Code ausgelöst werden. Deshalb gilt auch hier: Behandeln Sie alle denkbaren Fehler und sorgen Sie dafür, dass der Benutzer Fehlermeldungen erhält, mit denen er etwas anfangen kann. Das folgende Beispiel greift das Problem mit den doppelten Datensätzen in eindeutigen Feldern aus Listing 11.3 erneut auf – diesmal allerdings ohne VBA-Code, aber mit einem Formular zur Eingabe von Firmen. Ein doppelter Eintrag führt zu der gleichen Meldung wie unter VBA, nur die Fehlernummer fehlt (siehe Abbildung 11.21). Auch hier besteht zum Glück die Möglichkeit einzugreifen. Ansatzpunkt ist die Ereigniseigenschaft Bei Fehler des Formulars. Die dadurch ausgelöste Prozedur sieht als nacktes Gerüst wie in folgendem Listing aus: Private Sub Form_Error(DataErr As Integer, Response As Integer) End Sub Listing 11.9: Die Ereignisprozedur Form_Error im Rohbau
524
11
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 11.21: Doppelte Werte in eindeutigen Feldern werden auch in Formularen mit einer unverständlichen Meldung kommentiert.
Die Prozedur hat zwei Parameter, von denen der erste von Access mit der Fehlernummer gefüllt wird und der zweite einen Wert erwartet, der Access über die weitere Vorgehensweise bezüglich des Fehlers informiert. Für den Parameter Response gibt es folgende Werte: acDataErrContinue: Die Standardfehlermeldung von Access wird nicht angezeigt. acDataErrDisplay: Die Standardfehlermeldung von Access wird angezeigt.
11.5.1 Behandlung von Formularfehlern Wenn Sie einen Formularfehler wie den obigen behandeln möchten, müssen Sie zunächst einmal dessen Fehlernummer ermitteln. Dazu legen Sie die folgende Prozedur für die Ereigniseigenschaft Bei Fehler an und lassen sich darüber die Fehlernummer ausgeben, die mit dem Parameter DataErr übergeben wird (siehe Abbildung 11.22): Private Sub Form_Error(DataErr As Integer, Response As Integer) MsgBox "Fehler-Nummer: " & DataErr End Sub Listing 11.10: Routine zum Ermitteln der Fehlernummer
Abbildung 11.22: Fehlernummer des Fehlers im Formular
Wenn Sie die Fehlernummer kennen, können Sie diesen Fehler gezielt behandeln. Dazu erstellen Sie in der Ereignisprozedur On_Error eine geeignete Fehlerbehandlung:
Fehlerbehandlung in Formularen
525
Private Sub Form_Error(DataErr As Integer, Response As Integer) Select Case DataErr Case 3022 Response = acDataErrContinue MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." Case Else Response = acDataErrDisplay End Select End Sub Listing 11.11: Behandlung von Formularfehlern
Die Routine sorgt dafür, dass beim Auftreten eines Fehlers mit der Nummer 3022 eine aussagekräftige Meldung ausgegeben und die Standardfehlermeldung durch Setzen des Parameters Response auf den Wert acDataErrContinue unterdrückt wird. Bei allen anderen Fehlernummern wird hingegen die dafür vorgesehene Standardmeldung ausgegeben.
11.5.2 Formularfehler dokumentieren Auch die in Formularen auftretenden Fehler sollten Sie dokumentieren, sofern diese nicht anderweitig verarbeitet werden. Um den Fehler aus Listing 11.11 brauchen Sie sich keine Gedanken mehr zu machen. Die Benutzer Ihrer Datenbankanwendung finden aber sicher noch den einen oder anderen Fehler, mit dem Sie nicht gerechnet haben. Dafür bauen Sie eine Fehlerdokumentation wie weiter oben beschrieben ein. In der Ereignisprozedur On_Error sieht das wie folgt aus: Private Sub Form_Error(DataErr As Integer, Response As Integer) Select Case DataErr Case 3022 Response = acDataErrContinue MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." Case Else Call Fehlerbehandlung("Form_frmFirmen ", "", 0, "Bemerkungen: Fehlernummer " & DataErr) Response = acDataErrContinue End Select End Sub Listing 11.12: Behandlung unvorhergesehener Fehler in Formularen
Da Formularfehler keine Fehlernummer über das Err-Objekt bereitstellen, wird hier alternativ die Fehlernummer in die Bemerkungen zum Fehler eingetragen.
12 Performance Einer der wichtigsten Aspekte für die Akzeptanz einer Anwendung beim Benutzer ist ihre Performance. Eine Anwendung, die Aufgaben nicht zügig in der gewünschten Zeit erledigt, ist von vornherein zum Scheitern verurteilt – die Benutzer verzichten in diesem Fall oft lieber darauf. Deshalb widmet dieses Buch dem Thema Performance ein eigenes Kapitel. Dass dies gerechtfertigt ist, werden Sie im Folgenden sehen: Access birgt in allen Bereichen eine Menge Möglichkeiten für »Performance-Schlucker« und ebenso viel Potenzial für Optimierungen: Dies zieht sich durch alle Bereiche wie Tabellen, Abfragen, Formulare, Berichte und Module. Die Beispieldatenbank zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_12\Performance.mdb.
12.1 Tabellen Tabellen bilden die Basis einer Datenbankanwendung. Der Aufbau der Tabellen und der Verknüpfungen zwischen den Tabellen entscheidet wesentlich über die Performance der gesamten Anwendung, da alle weiteren Objekte wie Abfragen, gebundene Formulare und Berichte wie auch viele VBA-Prozeduren auf Tabellen zugreifen.
12.1.1 Normalisieren des Datenmodells Das Normalisieren des Datenmodells ist der erste Schritt, um einen schnellen Zugriff auf die in den Tabellen enthaltenen Daten zu ermöglichen. Welche Schritte zur Normalisierung eines Datenmodells notwendig sind, erfahren Sie in Kapitel 2, »Tabellen und Datenmodellierung«. Für die Performance ist die Normalisierung des Datenmodells wichtig, weil Sie damit beispielsweise die Menge der gespeicherten Daten reduzieren – etwa durch das Extrahieren redundanter Daten in verknüpften Tabellen.
528
12
Performance
Wenn Sie dabei neue Tabellen erzeugen, versäumen Sie nicht, eine Beziehung zwischen den neu entstandenen Tabellen zu erstellen. Das bringt einen PerformanceGewinn bei der Verknüpfung dieser Tabellen mittels JOIN in Abfragen. Falls Sie Beziehungen verwenden, bei denen beim Löschen eines Datensatzes in der Detailtabelle auch alle verknüpften Datensätze der Mastertabelle gelöscht werden sollen, können Sie keine schnellere Methode wählen, als die Löschweitergabe für diese Beziehung zu aktivieren. Diese Einstellung nehmen Sie in den Beziehungseigenschaften im Beziehungen-Fenster vor (siehe Abbildung 12.1). Gleiches gilt für die Aktualisierungsweitergabe, die Sie im gleichen Dialog aktivieren können.
Abbildung 12.1: Aktivieren der Löschweitergabe für eine Beziehung
Eine Suche über mehrere verknüpfte Tabellen, deren Daten zuvor in einer einzigen Tabelle gespeichert waren, kann unter Umständen auch langsamer sein als mit der vorherigen allein stehenden Tabelle. Immerhin kostet das Auflösen von Verknüpfungen innerhalb einer Abfrage ebenso Zeit wie die Suche nach den gewünschten Datensätzen. Wenn Sie tatsächlich einmal eine solche Konstellation vermuten, können Sie je nach Anwendungsfall mit einer INSERT INTO-Aktionsabfrage eine temporäre Tabelle aus den verknüpften Tabellen erzeugen, die keine Verknüpfungen mehr aufweist. In der Regel sind durch das Normalisieren des Datenmodells keine Geschwindigkeitseinbußen zu erwarten, die ein nicht normalisiertes Design rechtfertigen.
Tabellen
529
12.1.2 Indizes Die richtigen Indizes ermöglichen einen schnellen Zugriff auf die Daten einer Tabelle. Dieser Zugriff erfolgt meist durch Abfragen und bezieht sich oft auf mehr als eine Tabelle. Damit die Jet-Engine die Verknüpfungen schnell verarbeiten kann, sollten alle an einer Verknüpfung beteiligten Felder indiziert sein. Beim Primärschlüsselfeld ist das ohnehin der Fall: Es wird nicht nur indiziert, sondern es ist auch noch eindeutig und lässt keine Null-Werte zu. Auch für Fremdschlüsselfelder ist ein Index zu empfehlen. Wenn Sie eine Beziehung zwischen zwei Tabellen etwa im Beziehungen-Fenster herstellen (siehe Abbildung 12.2), legt Access automatisch einen Index für das Fremdschlüsselfeld der Detailtabelle an (siehe Abbildung 12.3).
Abbildung 12.2: Das Anlegen einer Beziehung im Beziehungen-Fenster …
Abbildung 12.3: … führt zum automatischen Anlegen eines Index für das Fremdschlüsselfeld.
530
12
Performance
Auch für Felder, die nicht fester Bestandteil einer Beziehung sind, aber gegebenenfalls in einer Abfrage als Schlüsselfeld dienen, legt man einen Index an. Indizieren Sie auch Felder, die als Sortier- oder Filterkriterium in Abfragen dienen. Sparen Sie dabei nur jene Felder aus, die nur wenige unterschiedliche Werte enthalten wie beispielsweise Ja/Nein-Felder.
Kriterium für das Indizieren von Feldern Ob ein Index abhängig von den Datensätzen sinnvoll ist oder nicht, lässt sich mit der Eigenschaft DistinctCount eines Index über folgende Funktion berechnen. Die Funktion ermittelt das Verhältnis der enthaltenen verschiedenen Werte zu der Gesamtanzahl der Werte. Dazu übergeben Sie der Routine den Namen der zu prüfenden Tabelle und des Index. Ein Aufruf könnte beispielsweise wie folgt aussehen und das angegebene Ergebnis liefern (Beispiel aus der Nordwind-Datenbank): ? GetIndexDistinct ("Bestellungen", "Versanddatum") 0,467469879518072
Wenn das Ergebnis zwischen 0,1 und 0,5 liegt, macht der angelegte Index Sinn. Die Funktion sieht folgendermaßen aus: Function GetIndexDistinct(strTabelle As String, strIndex As String) As Double Dim db As Database Dim tdf As TableDef Dim lngDistinct As Long Dim lngRecordCount As Long Set db = CurrentDb Set tdf = dbs.TableDefs(strTabelle) lngDistinct = tdf.Indexes(strIndex).DistinctCount lngRecordCount = tdf.RecordCount GetIndexDistinct = lngDistinct / lngRCnt Set tdf = Nothing Set dbs = Nothing End Function Listing 12.1: Diese Routine prüft, ob ein Feld indexiert werden sollte.
Die Routine funktioniert nicht mit verknüpften Tabellen, da die Recordcount-Eigenschaft des TableDef-Objekts hier den Wert –1 ausgibt. Und hinterfragen Sie nicht den Sinn dieser Funktion, wenn Sie diese mit einem Primär- oder eindeutigen Schlüssel testen – hier liefert die Funktion logischerweise den Wert 1.
Tabellen
531
Manchmal ist weniger mehr Das Indizieren von Feldern ist allerdings kein Wundermittel, wenn es um Performance-Steigerungen geht. Wenn Sie mehrspaltige Indizes verwenden, binden Sie so wenig Felder wie möglich in den Index ein. Verwenden Sie außerdem eindeutige Indizes, wenn dies möglich ist. Und zu guter Letzt: Indizieren Sie nicht zu viele Felder! Bedenken Sie, dass eine Datenbankanwendung nicht nur zur Auswahl von Daten gedacht ist, sondern dass damit auch Daten angelegt, bearbeitet und gelöscht werden sollen. Bei all diesen Operationen müssen die Indizes aktualisiert werden, was wiederum zu Lasten der Performance geht.
12.1.3 Datentypen Die Auswahl passender Datentypen beeinflusst nicht nur die Performance, sondern auch den benötigten Speicherplatz der Datenbankanwendung.
Kleinstmögliche Datentypen wählen Wenn Sie Felder in Tabellen anlegen, wählen Sie den kleinstmöglichen Datentyp aus. Verwenden Sie kein Memo-Feld, wenn auch ein Textfeld reicht. Auch Zahlenformate bieten eine Menge Einsparpotenzial. Suchen Sie einfach aus folgender Tabelle den passenden Wertebereich heraus und verwenden Sie den entsprechenden Datentyp. Die Spalte Speicherbedarf gibt Ihnen Auskunft darüber, wie sehr die einzelnen Datentypen Speicherplatz und Performance beeinflussen (siehe Tabelle 12.1). Datentyp
Speicherbedarf
Wertebereich
Byte
1 Byte
0 bis 255
Boolean
2 Bytes
True oder False
Integer
2 Bytes
–32.768 bis 32.767
Long (lange Ganzzahl)
4 Bytes
–2.147.483.648 bis 2.147.483.647
Single (Gleitkommazahl mit einfacher Genauigkeit)
4 Bytes
–3,402823E38 bis –1,401298E-45 für negative Werte; 1,401298E-45 bis 3,402823E38 für positive Werte
Double (Gleitkommazahl mit doppelter Genauigkeit)
8 Bytes
–1,79769313486231E308 bis –4,94065645841247E-324 für negative Werte; 4,94065645841247E-324 bis 1,79769313486232E308 für positive Werte
Currency (skalierte Ganzzahl)
8 Bytes
–922.337.203.685.477,5808 bis 922.337.203.685.477,5807
Tabelle 12.1: Datentypen und ihr Speicherbedarf
532
12
Performance
Datentyp
Speicherbedarf
Wertebereich
Decimal
14 Bytes
+/–79.228.162.514.264.337.593.543.950.335 ohne Dezimalzeichen; +/–7,9228162514264337593543950335 mit 28 Nachkommastellen; die kleinste Zahl ungleich Null ist +/–0,0000000000000000000000000001
Tabelle 12.1: Datentypen und ihr Speicherbedarf (Fortsetzung)
Die landläufige Meinung, dass man Textfelder möglichst klein halten sollte, um nicht unnötig Speicherplatz zu verschwenden, ist nicht richtig. In der Tat können Sie jedes Textfeld mit 255 Zeichen als Feldgröße einrichten; wenn Sie dort aber nur Postleitzahlen mit je sieben Stellen ablegen, werden auch nur sieben Bytes belegt.
Gleiche oder ähnliche Datentypen in Beziehungen Primärschlüsselfeld und Fremdschlüsselfeld einer Beziehung sollten den gleichen Datentyp besitzen. In einem Fall ist das (zumindest was den Namen betrifft) nicht möglich: Wenn das Primärschlüsselfeld den Datentyp Autowert hat, können Sie das Fremdschlüsselfeld natürlich nicht auf den gleichen Datentyp einstellen. Der Autowert entspricht dem Datentyp Long und seltener (zum Beispiel in replizierbaren Datenbanken) dem Typ Replikations-ID (GUID).
12.2 Abfragen Wenn eine Tabelle nicht gerade genau die Felder enthält, die für die Anzeige in einem Formular oder Bericht oder für die Verwendung in einer VBA-Prozedur benötigt werden, verwenden Sie eine Abfrage, um nur die notwendigen Informationen abzufragen.
12.2.1 Abfragen und die Jet-Engine Für die Optimierung von Abfragen kann die Kenntnis der bei der Verarbeitung von Abfragen durch die Jet-Engine verwendeten Technik hilfreich sein. Die folgenden Abschnitte fassen den Inhalt diverser Beiträge der Knowledge-Base und weitere Quellen zusammen. [http://msdn.microsoft.com/archive/default.asp?url=/archive/enus/dnaraccessdev/html/odc_jetdatabaseengine20ausersoverview.asp] Abfragen entstehen auf unterschiedliche Weise: durch Verwendung der Abfrage-Entwurfsansicht durch Eingabe eines SQL-Ausdrucks als Datenherkunft von Formularen und Berichten oder als Datensatzherkunft von Kombinations- und Listenfeldern
Abfragen
533
durch Zusammenstellung eines SQL-Ausdrucks in Form eines Strings und anschließender Verwendung in einer VBA-Prozedur Wie auch immer die Abfrage entstanden ist, verwendet die Jet-Engine letzten Endes den dahinter stehenden SQL-Ausdruck.
Kompilieren einer Abfrage Die Abfrage wird vor ihrer Ausführung zunächst kompiliert. Das heißt, dass die JetEngine die Basisinformationen aus der Abfrage ermittelt: Basistabellen: Tabellen, die an der Abfrage beteiligt sind Tabellenfelder aus den Ausgabefeldern der Abfrage Vorhandene Indizes in den beteiligten Tabellen Kriterien Verknüpfungsfelder zwischen zwei Tabellen Sortierfelder
Optimierung der Abfrage Anschließend optimiert die Jet-Engine die Abfrage auf Basis dieser Informationen. Dabei erstellt sie unterschiedliche Ausführungspläne und ermittelt daraus die schnellste Variante. Den größten Einfluss auf die Wahl des Ausführungsplans hat dabei die Analyse der Basistabellen und der Verknüpfungen zwischen diesen Tabellen. Um den Zugriff auf die Basistabellen zu optimieren, verwendet die Jet-Engine drei Tabellen-Zugriffsstrategien: Table Scan, Index Range oder Rushmore Restriction. Welche dieser Techniken die Jet-Engine benutzt, hängt von der Größe der jeweiligen Tabelle, der Anzahl der enthaltenen Indizes und der Art und Menge der Kriteriumsfelder ab. Beim Table Scan durchsucht die Jet-Engine jeden einzelnen Datensatz der Tabelle. Diese Methode wird angewendet, wenn Kriterienfelder nicht indiziert sind oder wenn die Kriterien die Ergebnismenge nur geringfügig einschränken. Die Index Range-Methode wird verwendet, wenn nur ein indiziertes Feld mit einem Kriterium versehen ist. Die Suche nach den gewünschten Datensätzen erfolgt über den Index dieses Kriterienfeldes. Die Rushmore Restriction kommt zum Zuge, wenn mehr als ein Kriterienfeld indiziert ist. Weitere Informationen finden Sie weiter unten in diesem Kapitel, Abschnitt »Abfragen mit Rushmore optimieren«.
534
12
Performance
Auch die Beziehungen zwischen den Tabellen spielen eine Rolle bei der Optimierung von Abfragen. Für die Auswertung der Beziehungen werden fünf verschiedene Strategien verwendet, die sich Nested Iteration Join, Index Join, Lookup Join, Merge Join und Index-Merge Join nennen. Welche Strategie zum Zuge kommt, hängt von der Beschaffenheit der beteiligten Tabellen, Felder und Indizes, dem Vorkommen von Null-Werten und anderen Faktoren ab. Weitere Informationen hierzu finden Sie in [Access 2002 Desktop Developer’s Handbook, Litwin, Getz, Gunderloy, Sybex].
Abfragen mit Rushmore optimieren [http://msdn.microsoft.com/archive/default.asp?url=/archive/enus/dnaraccessdev/html/Rushmore.asp] Rushmore ist eine Technik zur Optimierung von Abfragen, die auf das Vorhandensein von mehreren Restriktionen auf indizierten Feldern abzielt. Die Rushmore-Optimierung
funktioniert mit den folgenden Operationen: Index Intersection: Diese Operation wird auf Abfragekriterien folgenden Aussehens angewendet, wobei alle Kriterienfelder indiziert sein müssen: Spalte1 = "Ausdruck1" AND Spalte2 = "Ausdruck2"
Der Clou ist, dass die Jet-Engine bei der Rushmore-Optimierung nicht mit den eigentlichen Vergleichsoperatoren, sondern mit ihrem Index arbeitet. Dabei sucht die JetEngine zunächst nach allen Datensätzen, die die erste Bedingung erfüllen, und anschließend nach allen Datensätzen, die die zweite Bedingung erfüllen, und ermittelt schließlich die Schnittmenge daraus. Index Union: Die zweite Operation zielt auf durch OR verknüpfte Kriterien nach folgen-
dem Schema ab: Spalte1 = "Ausdruck1" OR Spalte2 = "Ausdruck2"
Die Abfrage ermittelt hier ebenfalls alle Datensätze, die dem ersten, und alle Datensätze, die dem zweiten Kriterium entsprechen. Allerdings bildet sie hier nicht die Schnittmenge, sondern die Vereinigungsmenge der beiden Ergebnisse. Rushmore optimiert obige Abfragen deshalb so gut, weil es »Bitmaps« aus den IndexWerten erstellt und diese indiziert.
Abfragen
535
Kombiniere, kombiniere … Um die Abfrage wirklich optimal ausführen zu können, ermittelt die Jet-Engine den ungefähren Aufwand für die in Frage kommenden Tabellen-Zugriffsstrategien in Kombination mit den möglichen Verknüpfungsstrategien und wendet schließlich die am günstigsten erscheinende Variante an.
Ausführung der Abfrage Wird die Abfrage ausgeführt, tritt noch ein sehr wichtiger Faktor auf: der RecordsetTyp. Diese Eigenschaft, die Sie beispielsweise in gespeicherten Abfragen im Eigenschaftsfenster einstellen können (siehe Abbildung 12.4), entscheidet maßgeblich über die Geschwindigkeit der Abfrage. Eine Abfrage mit dem Recordset-Typ Dynaset gibt beispielsweise ein Ergebnis zurück, das lediglich die eindeutigen Schlüsselfelder – soweit vorhanden – der in der Abfrage enthaltenen Tabellen enthält. Es werden nur zusätzliche Daten für die Datensätze in den Speicher geladen, die beispielsweise für die Anzeige im Formular benötigt werden. Abfragen des Recordset-Typs Snapshot laden die kompletten Daten direkt in den Speicher – also alle Datensätze mit allen angegebenen Feldern. Das dauert natürlich erheblich länger – erst recht, wenn nicht alle Daten in den Speicher passen und ein Teil des Abfragergebnisses auf der Festplatte zwischengespeichert werden muss.
Abbildung 12.4: Einstellen des Recordset-Typs einer Abfrage
536
12
Performance
Abfragestrategien unter der Lupe Access bietet die Möglichkeit, die bei einer Abfrage verwendeten Informationen (zumindest teilweise) auszuwerten. Mit der Anpassung eines Registry-Eintrags können Sie dafür sorgen, dass Access Informationen über die Durchführung von Abfragen in einer Textdatei ausgibt. Den entsprechenden Schlüssel müssen Sie zunächst in der Registry unter HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\4.0\Engines\Debug hinzufügen. Wählen Sie dazu aus dem Kontextmenü des in Abbildung 12.5 markierten Schlüssels den Eintrag Neu/Zeichenfolge aus und nennen Sie den neuen Eintrag Jetshowplan. Stellen Sie dort den Wert auf ON ein. Anschießend schreibt die Jet-Engine Informationen über die Ausführungspläne der Abfragen in eine Datei namens showplan.out. Diese Funktion ist nicht offiziell dokumentiert und leider weist sie kein genau vorhersagbares Verhalten bezüglich des Speicherorts dieser Datei auf. Ein guter Tipp ist es jedenfalls, zunächst im Ordner Dokumente und Einstellungen des aktuellen Benutzers nach dieser Datei zu suchen und anschließend im Ordner der zuletzt ausgeführten MDB. Die Funktion lässt sich leider nicht auf eine einzelne Datenbank beschränken (es sei denn, Sie aktivieren und deaktivieren diesen Schlüssel per VBA von der entsprechenden Datenbank aus). Da diese Option demnach für alle Datenbanken gilt, wird die Datei Showplan.out schnell sehr groß; behalten Sie diese daher im Auge oder schalten Sie die Funktion nur bei Bedarf ein. Um die Funktion zu deaktivieren, entfernen Sie den Schlüssel entweder komplett oder stellen den Wert auf Off ein.
Abbildung 12.5: Aktivieren der Ausgabe des Jet-Engine-Protokolls
Abfragen
537
Das folgende Listing zeigt den Inhalt der Datei showplan.out für die Abfrage Bestellungen der Nordwind-Datenbank. Der erste Teil enthält die für die Auswahl der Strategie für den Zugriff auf die Tabellen ermittelten Informationen, der zweite Teil die Reihenfolge bei der Abarbeitung der Verknüpfungen. --- Rechnungen --- Inputs to Query Table 'Versandfirmen' Table 'Personal' Table 'Kunden' Using index 'Primärschlüssel' Having Indexes: Primärschlüssel 91 entries, 1 page, 91 values which has 1 column, fixed, unique, primary-key, no-nulls PLZ 91 entries, 1 page, 87 values which has 1 column, fixed Ort 91 entries, 1 page, 69 values which has 1 column, fixed Firma 91 entries, 1 page, 91 values which has 1 column, fixed Table 'Bestellungen' Table 'Artikel' Table 'Bestelldetails' Using index 'BestellungenBestelldetails' Having Indexes: BestellungenBestelldetails 2155 entries, 5 pages, 831 values which has 1 column, fixed Bestell-Nr 2155 entries, 5 pages, 831 values which has 1 column, fixed Artikel-Nr 2155 entries, 4 pages, 77 values which has 1 column, fixed ArtikelBestelldetails 2155 entries, 4 pages, 77 values which has 1 column, fixed - End inputs to Query 01) Sort table 'Bestellungen' 02) Inner Join table 'Versandfirmen' to result of '01)' using temporary index join expression "Versandfirmen.[Firmen-Nr]=Bestellungen.VersandÜber" 03) Sort table 'Personal' 04) Inner Join result of '02)' to result of '03)' using temporary index join expression "Bestellungen.[Personal-Nr]=Personal.[Personal-Nr]" 05) Inner Join result of '04)' to table 'Kunden' using index 'Kunden!Primärschlüssel' join expression "Bestellungen.[Kunden-Code]=Kunden.[Kunden-Code]" 06) Inner Join result of '05)' to table 'Bestelldetails'
538
12
Performance
using index 'Bestelldetails!BestellungenBestelldetails' join expression "Bestellungen.[Bestell-Nr]=Bestelldetails.[Bestell-Nr]" 07) Sort table 'Artikel' 08) Inner Join result of '06)' to result of '07)' using temporary index join expression "Bestelldetails.[Artikel-Nr]=Artikel.[Artikel-Nr]" Listing 12.2: Inhalt der Datei showplan.out nach dem Durchführen einer komplexen Abfrage
Wenn Sie beim Tuning Ihrer Abfragen ins Detail gehen möchten, kann die Ausgabe der Jet-Engine durchaus weiterhelfen. Wenn Sie der Bestellungen-Abfrage beispielsweise ein Kriterium für das Feld PLZ hinzufügen wie in Abbildung 12.6 und dieses Feld nicht indiziert ist, erhalten Sie im Ausführungsplan folgende Zeile als ersten Schritt: 01) Restrict rows of table Bestellungen by scanning testing expression "Bestellungen.PLZ="42100""
Das Schlüsselwort scanning bedeutet in diesem Zusammenhang, dass ein Table Scan durchgeführt wird – ein Index für dieses Feld ist nicht vorhanden.
Abbildung 12.6: Abfrage mit Kriterium
Abfragen
539
Besser wäre es, das Feld PLZ der zugrunde liegenden Tabelle Bestellungen mit einem Index zu versehen – auf diese Weise könnte die Rushmore-Technik verwendet werden. Die entsprechende Zeile in der Datei showplan.out sieht dann folgendermaßen aus: 01) Restrict rows of table Bestellungen using rushmore for expression "Bestellungen.PLZ="42100""
12.2.2 Datenbank mit kompilierten Abfragen ausliefern Die Jet-Engine kompiliert eine Abfrage beim ersten Ausführen dieser Abfrage nach dem Speichern. Das Kompilieren beinhaltet auch das Optimieren, weshalb es sinnvoll sein kann, die Abfrage zu bestimmten Anlässen neu zu kompilieren.
Bei Änderungen neu kompilieren Wie Sie weiter oben erfahren haben, hängt der Ausführungsplan und damit die Reihenfolge und Geschwindigkeit der einzelnen Schritte bei der Durchführung der Abfrage wesentlich von Eigenschaften wie den Tabellen-Eigenschaften wie Indizes und Beziehungen und von Daten-Eigenschaften wie der Anzahl der Datensätze und der dadurch belegten Speicherseiten ab. Wenn Sie Änderungen am Entwurf der Tabellen vornehmen, auf denen die zu optimierende Abfrage basiert, sollten Sie die Abfrage neu kompilieren, damit diese den neuen Gegebenheiten entsprechend optimiert wird. Auch die Anzahl der in den Tabellen enthaltenen Datensätze spielt eine Rolle. Versuchen Sie, die Tabellen möglichst mit realen Daten zu füllen, um die Abfragen optimiert ausliefern zu können.
Auslieferung im kompilierten Zustand Die Jet-Engine kompiliert eine Abfrage erst neu, wenn Sie eine Änderung am Entwurf vorgenommen haben, diese dann speichern und ausführen. Das Kompilieren ist in vielen Fällen aufwändiger als die eigentliche Durchführung der Abfrage. Liefern Sie daher die Datenbank immer mit kompilierten Abfragen aus. So erhält der Benutzer direkt bei der ersten Verwendung der Abfrage optimale Performance. Das Komprimieren (nicht zu verwechseln mit dem »Kompilieren«) versetzt unter anderem alle Abfragen wieder in den unkompilierten Zustand. Der Hintergrund ist, dass die Tabellenstatistiken, auf deren Basis die Abfragen beim Kompilieren optimiert werden, aktualisiert werden. Um eine auf dem aktuellen Stand befindliche und optimierte Datenbank auszuliefern, komprimieren Sie zunächst die Datenbank, führen dann alle Abfragen einmal aus, ohne diese zwischendurch in der Entwurfsansicht zu öffnen, und zeigen zusätzlich alle Formulare und Berichte einmal an, um auch die dort enthaltenen Datenherkünfte und Datensatzherkünfte zu optimieren.
540
12
Performance
Ist eine Abfrage kompiliert? Wenn Sie auf Nummer Sicher gehen wollen, können Sie in der Systemtabelle MSysObjects prüfen, ob eine Abfrage kompiliert ist oder nicht. Das gilt nicht nur für gespeicherte Abfragen, sondern auch für Abfragen, die als SQL-Ausdruck in der Eigenschaft Datenherkunft von Formularen und Berichten oder in der Eigenschaft Datensatzherkunft von Kombinations- und Listenfeldern enthalten sind. Die Tabelle MSysObjects und die anderen Systemtabellen machen Sie sichtbar, indem Sie im Register Ansicht des Optionen-Dialogs die Option Systemobjekte aktivieren. Anschließend finden Sie im Datenbankfenster die Tabelle MSysObjects, die nach dem Ausblenden einiger Felder wie in Abbildung 12.7 aussieht. Die unteren Namen sollten Sie im Abfragen-Register des Datenbankfensters der Nordwind-Datenbank finden können; aber jene mit den kryptisch anmutenden Bezeichnungen nicht. Bei diesen Abfragen (es handelt sich tatsächlich um Abfragen – darauf weist der Wert 5 in Feld Type hin) handelt es sich um nicht gespeicherte SQL-Ausdrücke in Datenherkünften und Datensatzherkünften. Das davor angezeigte Feld namens Lv gibt den Kompilierzustand der Abfrage an: Leere Felder bedeuten hier, dass die Abfrage nicht kompiliert ist. Um dies zu testen, öffnen Sie einfach ein Formular und schauen Sie sich nachher erneut diese Tabelle an. Die Namen der nicht gespeicherten Abfragen setzen sich wie folgt zusammen: »~sq_« ist das gemeinsame Präfix all dieser Abfragen, »f« steht für Formular, »r« für Bericht (Report), »c« für ein Formular-Steuerelement und »d« für ein Berichts-Steuerelement.
12.2.3 Gespeicherte Abfragen versus Ad-hoc-Abfragen Gespeicherte Abfragen haben den Vorteil, dass man sie durch Speichern und Kompilieren optimiert und damit in folgenden Einsätzen auf die dabei gewonnenen Erkenntnisse bezüglich der Performance zugreifen kann. So genannte Ad-hoc-Abfragen, die in VBA-Routinen zusammengesetzt und erst bei Bedarf das erste Mal kompiliert und optimiert werden, ziehen in der Regel den Kürzeren, was die Performance angeht – in den meisten Fällen sind daher gespeicherte Abfragen vorzuziehen. Auch hier gibt es allerdings eine Ausnahme: Weiter oben wurde bereits beschrieben, dass man Abfragen vor der Auslieferung möglichst mit der zu erwartenden Anzahl Datensätze in den betroffenen Tabellen kompiliert und speichert. Das ist aber nicht immer möglich. Wenn die Anzahl der Datensätze völlig anders ausfällt als geplant, ist eine gespeicherte Abfrage möglicherweise weniger performant als eine Abfrage, die vor jeder Ausführung neu erstellt wird.
Abfragen
541
Abbildung 12.7: Abfragen und ihr aktueller Zustand bezüglich der Kompilierung
12.2.4 Abfragen auf Performance trimmen Das Design von Abfragen bietet jede Menge Fallstricke, wenn es um gute Performance geht. Die folgenden Abschnitte enthalten deshalb den nötigen »Feinschliff« für Ihre Abfragen.
Nur notwendige Felder anzeigen Je mehr Felder eine Abfrage anzeigt, desto mehr leidet die Performance – das gilt vor allem für Abfragen mit dem Recordset-Typ Snapshot. Zeigen Sie daher nur die unbedingt notwendigen Felder an. Prüfen Sie vor allem genau, ob die Abfrage gegebenenfalls Kriterienfelder enthält, die nicht unbedingt angezeigt werden müssen. Diese lassen sich über das entsprechende Kontrollkästchen ausblenden. Auch die Anzahl der Tabellen, aus denen Felder angezeigt werden müssen, spielt eine Rolle. Je mehr Tabellen eines oder mehrere Felder zum Abfrageergebnis beitragen, desto langsamer die Abfrage.
542
12
Performance
Kurze Feldbezeichnungen und Alias-Namen verwenden Weitere Vorteile bei der Performance liefern kurze Namen von Tabellenfeldern sowie Alias-Namen für Tabellen, wie folgendes Beispiel zeigt: SELECT t2.[Bestell-Nr], t2.[Artikel-Nr], t1.Artikelname, t2.Einzelpreis, t2.Anzahl, t2.Rabatt, CCur(t2.Einzelpreis*[Anzahl]*(1-[Rabatt])/100)*100 AS Endpreis FROM Artikel AS t1 INNER JOIN Bestelldetails AS t2 ON t1.[Artikel-Nr] = t2.[Artikel-Nr] ORDER BY t2.[Bestell-Nr];
statt SELECT Bestelldetails.[Bestell-Nr], Bestelldetails.[Artikel-Nr], Artikel.Artikelname, Bestelldetails.Einzelpreis, Bestelldetails.Anzahl, Bestelldetails.Rabatt, CCur(Bestelldetails.Einzelpreis*[Anzahl]*(1-[Rabatt])/ 100)*100 AS Endpreis FROM Artikel INNER JOIN Bestelldetails ON Artikel.[Artikel-Nr] = Bestelldetails.[Artikel-Nr] ORDER BY Bestelldetails.[Bestell-Nr];
Sternchen zählen Soll eine Abfrage die Anzahl der gefundenen Datensätze zurückgeben, wenden Sie die Count-Funktion auf den Ausdruck * (Sternchen) an und nicht auf eines der Felder wie beispielsweise das Primärschlüsselfeld.
Keine Funktionen und berechneten Ausdrücke Verwenden Sie in Abfragen nach Möglichkeit keine eingebauten oder benutzerdefinierten Funktionen oder berechneten Ausdrücke. Dazu gehören Funktionen wie IIf oder DLookup und weitere Domänen-Funktionen.
Kriterien für Schlüsselfelder variieren Manchmal verwendet man Kriterien für ein Feld, über das eine Beziehung zu einer anderen Tabelle hergestellt wird. Das Primärschlüsselfeld der beteiligten Mastertabelle hat zwar den gleichen Wert wie das entsprechende Fremdschlüsselfeld der Detailtabelle. Es kann aber einen Unterschied in der Verarbeitungsgeschwindigkeit machen, auf welches der beiden Felder Sie ein Kriterium anwenden. Bei dieser Konstellation müssen Sie ausprobieren, wo das Kriterium für eine schnellere Abarbeitung sorgt.
Formulare
543
12.3 Formulare Die Möglichkeiten zur Verbesserung der Performance von Formularen lassen sich in zwei Kategorien einteilen: Die einen beeinflussen die Zeit, bis das Formular geöffnet und einsatzbereit ist, und die anderen die Arbeit mit dem Formular selbst.
12.3.1 Formulare offen halten oder schließen? Je nachdem, wie oft Sie ein Formular verwenden, sollten Sie in Erwägung ziehen, es nicht jedes Mal zu schließen und neu zu öffnen, sondern das Formular einfach unsichtbar zu machen. Dazu verwenden Sie die Eigenschaft Sichtbar, die Sie folgendermaßen in VBA einstellen können: 'Ausblenden Me.Visible = False 'Einblenden Me.Visible = False
Andererseits fressen offene Formulare Speicherplatz, sodass Sie diese Vorgehensweise tunlichst nicht mit allen Formularen durchexerzieren sollten. Außerdem besteht die Gefahr, dass Sie einmal mit Screen.ActiveForm das aktuelle Formular ermitteln möchten, es sich bei diesem jedoch um ein unsichtbares Formular handelt.
12.3.2 Daten des Formulars Die im Formular angezeigten Daten stammen aus einer oder mehreren Tabellen, die entweder direkt oder in Form einer Abfrage angesprochen werden. Verwenden Sie eine Abfrage, können Sie entweder auf eine gespeicherte Abfrage zurückgreifen oder geben den entsprechenden SQL-Ausdruck direkt für die Eigenschaft Datenherkunft ein. Gleiches gilt im Übrigen auch für die Datensatzherkunft von Kombinations- und Listenfeldern. Hier gibt es einige Regeln: Die Datenherkunft von Formularen beziehungsweise Datensatzherkunft von Kombinations- und Listenfeldern sollte möglichst eine Abfrage sein, die nur die benötigten Felder und – noch wichtiger – nur die benötigten Datensätze enthält. Daher ist in den meisten Fällen eine Abfrage die beste Wahl. Wenn Sie eine Abfrage verwenden, können Sie diese entweder speichern und den Namen der Abfrage als Datenherkunft oder Datensatzherkunft angeben oder direkt den SQL-Ausdruck der Abfrage in die Eigenschaft Datenherkunft oder Datensatzherkunft eintragen.
544
12
Performance
Es stimmt nicht, dass als Abfrage gespeicherte Datenherkünfte Vorteile bringen, weil diese vorkompiliert und damit optimiert werden können. SQL-Ausdrücke, die Sie für die Eigenschaften Datenherkunft oder Datensatzherkunft eintragen, werden beim Ausführen genauso kompiliert wie gespeicherte Abfragen. Interessant ist auch die Variante, die ins Spiel kommt, wenn Sie ein Formular nur zur Eingabe von Daten verwenden möchten. Öffnen Sie ein solches Formular mit folgender Anweisung, wenn Sie dazu VBA verwenden: DoCmd.OpenForm "", DataMode:=acFormAdd
Anderenfalls können Sie die Eigenschaft Daten eingeben im Eigenschaftsfenster des Formulars auf den Wert Ja einstellen.
12.3.3 Steuerelemente Je mehr Steuerelemente Sie in einem Formular anlegen, desto schlechter wird die Performance. Aber auch die Anordnung der Steuerelemente spielt eine Rolle: Übereinander angeordnete Steuerelemente sorgen beispielsweise für eine weitere Verschlechterung. Das könnte beispielsweise der Fall sein, wenn Sie ein Textfeld und ein Kombinationsfeld direkt übereinander legen, um je nach Bedarf das eine oder andere anzuzeigen. Und auch die Gestaltung eines assistenten-ähnlichen Formulars, das mehrere Schritte eines Assistenten in einem Formular enthält und die verschiedenen Stufen nacheinander ein- beziehungsweise ausblendet, ist performancetechnisch betrachtet keine optimale Lösung – dann doch lieber verschiedene Formulare, die nacheinander geöffnet werden.
Einfache Steuerelemente verwenden Es gibt unterschiedlich gewichtete Steuerelemente, was die Performance angeht. Das gilt gerade für die verschiedenen Möglichkeiten zur Auswahl von Informationen. Wenn Sie eine feste, nicht allzu große Anzahl Optionen zur Auswahl bereitstellen möchten, verwenden Sie dazu eine Optionsgruppe. Diese ist bereits performanter als ein Kombinationsfeld oder ein Listenfeld. Sollte einmal die Entscheidung zwischen einem Kombinations- oder Listenfeld und einem Unterformular anstehen und keine wesentlichen Punkte für eine der beiden Möglichkeiten sprechen, verwenden Sie ein Kombinations- oder Listenfeld.
Formulare
545
Bild-Steuerelement vor! Wenn Sie Bilder in einem Formular anzeigen möchten, verwenden Sie dazu das BildSteuerelement statt des ungebundenen Objekt-Steuerelements. Generell sollten Sie jedoch nur so verfahren, wenn es unbedingt notwendig ist. In vielen Fällen sollte eine Schaltfläche ausreichen, die ein Popup-Formular mit dem gewünschten Bild öffnet.
Schaltflächen durch Hyperlinks ersetzen Wenn Sie eine Schaltfläche lediglich verwenden, um damit ein weiteres Formular zu öffnen, können Sie dazu auch ein Bezeichnungsfeld mit einen Hyperlink verwenden. Damit sparen Sie die Schaltfläche ein. Die Zuordnung des entsprechenden Links zu einem Bezeichnungsfeld erfolgt ganz einfach über den Dialog aus Abbildung 12.8.
Abbildung 12.8: Setzen eines Hyperlinks auf ein zu öffnendes Formular
Den Hyperlink können Sie natürlich auch manuell eintragen – in diesem Fall würde er folgendermaßen lauten: Form frmKontakte
Wie Abbildung 12.8 zeigt, lassen sich auch andere Objekte der Datenbank über einen Hyperlink öffnen.
546
12
Performance
Kombinationsfelder und Listenfelder Für Kombinations- und Listenfelder gelten die gleichen Regeln wie für das Formular selbst: Die zugrunde liegende Datenquelle sollte in Form einer gespeicherten Abfrage angegeben werden, die nur die notwendigen Felder und Datensätze enthält. Dabei sollte das gebundene Feld indiziert sein. Wenn Sie die automatische Ergänzung bei der Eingabe von Einträgen in ein Kombinationsfeld nicht benötigen, lassen Sie diese Funktion weg, indem Sie die Eigenschaft Automatisch ergänzen auf den Wert Nein einstellen. Das angezeigte Feld im Kombinationsfeld sollte den Datentyp String haben, da andere Datentypen sonst erst konvertiert werden müssten. Sollte der Inhalt eines Kombinations- oder Listenfeldes aus einer Tabelle kommen, auf die über das Netzwerk zugegriffen wird, prüfen Sie, ob sich die in dieser Tabelle gespeicherten Daten oft oder überhaupt ändern. Falls nicht, kopieren Sie die Daten in eine lokale Tabelle und greifen Sie auf diese Daten zu.
Unterformulare Unterformular-Steuerelemente enthalten externe Formulare und zählen damit zu den ressourcen-hungrigsten Steuerelementen. Sie zeigen Daten aus einer eigenen Datenherkunft an, enthalten gegebenenfalls weitere Kombinations- oder Listenfelder und müssen in den meisten Fällen auch noch mit dem Hauptformular synchronisiert werden. Diese Synchronisation erfolgt über die Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformular-Steuerelements. Zur Optimierung der Performance sollten Sie die in diesen Eigenschaften angegebenen Felder in den zugrunde liegenden Tabellen indizieren. Schlechter für die Performance eines Formulars als ein Unterformular sind zwei oder mehr Unterformulare. Wenn diese alle gleichzeitig sichtbar sein sollen, müssen Sie in den sauren Apfel beißen; in manchen Fällen werden die Daten aber auch auf verschiedenen Seiten eines Register-Steuerelements angezeigt. In diesem Fall gibt es Optimierungspotenzial: Unterformular-Steuerelemente, die kein Unterformular enthalten, brauchen Sie auch nicht mit Daten zu füllen. Auf diese Weise müssen beim Blättern durch die Datensätze des Hauptformulars nicht angezeigte Unterformulare auch nicht mit Daten gefüllt werden. Um nur im aktuellen Register enthaltene Unterformular-Steuerelemente mit den entsprechenden Formularen zu füllen, gehen Sie folgendermaßen vor:
Formulare
547
1. Legen Sie die Unterformulare wie gewohnt an, um Größe und Position einzustellen. 2. Stellen Sie die Eigenschaft Herkunftsobjekt der Unterformular-Steuerelemente auf eine leere Zeichenkette ein. 3. Weisen Sie dem Unterformular-Steuerelement, das beim Öffnen des Formulars sichtbar ist, das anzuzeigende Formular zu: Private Sub Form_Open(Cancel As Integer) Me!.SourceObject = _ "" End Sub
4. Sorgen Sie beim Wechsel der Registerseite dafür, dass nicht angezeigte Unterformulare ausgeblendet und sichtbare eingeblendet werden: Private Sub RegisterStr0_Change() Select Case Me!RegisterStr0 Case 0 Me!frmProjekte1.SourceObject Me!frmProjekte2.SourceObject Case 1 Me!frmProjekte1.SourceObject Me!frmProjekte2.SourceObject End Select End Sub
= "" = "" = "" = ""
12.3.4 VBA in Formularen Die Programmierung von Formularen erlaubt weitere Möglichkeiten zur Code-Optimierung – oder auch nicht, wie der folgende Abschnitt zeigt.
Formular von Code befreien Formulare ohne Code werden deutlich schneller geladen. Stellt sich die Frage, was man mit einem Formular ohne VBA-Code alles anstellen kann. Die Antwort lautet: Alles, was auch mit einem Formular mit VBA-Code geht. Sie können ein Formular sogar komplett von seinem Formularmodul befreien und dennoch auf den gewohnten Komfort von VBA zugreifen. Um das Formular von seinem Modul zu befreien, stellen Sie einfach die Eigenschaft Enthält Modul auf den Wert Nein ein. Wenn das Formularmodul zu diesem Zeitpunkt bereits Code enthält, meckert Access natürlich – prüfen Sie also, ob Sie den enthaltenen Code noch benötigen (siehe Abbildung 12.9).
548
12
Performance
Abbildung 12.9: Diese Meldung erscheint vor dem Entfernen des Formularmoduls.
Wohin aber nun mit dem VBA-Code? Legen Sie die Funktionalität, die durch ein bestimmtes Ereignis ausgelöst werden soll, in einer öffentlichen Funktion an. Sie müssen dann lediglich den Namen der aufzurufenden Funktion für die entsprechende Ereigniseigenschaft angeben (siehe Abbildung 12.10). Wichtig ist die Angabe der Klammern hinter dem Funktionsnamen.
Abbildung 12.10: Verwenden einer Funktion anstelle einer Ereignisprozedur
Die in einem Standardmodul angelegte Funktion könnte etwa so aussehen: Public Function frmKontakte_BeimAnzeigen() MsgBox "Dies ist eine externe Ereignisprozedur" End Function Listing 12.3: Diese Funktion wird durch ein Ereignis aufgerufen.
12.4 Berichte Viele für Formulare vorgestellte Maßnahmen zur Verbesserung der Performance lassen sich auf Berichte übertragen; es gibt aber auch einige Berichts-Spezialitäten.
Berichte
549
12.4.1 Datenherkunft unsortiert übergeben Sortierungen und Gruppierungen übernimmt ein Bericht selbst, wie in dem dafür vorgesehenen Fenster zu sehen (siehe Abbildung 12.11). Daher sollten Sie keine sortierte oder gruppierte Datenherkunft verwenden.
Abbildung 12.11: Sortierungen und Gruppierungen nimmt man im Bericht selbst vor.
12.4.2 Keine Funktionen und Ausdrücke in Sortierungen und Gruppierungen Abbildung 12.11 liefert ein Negativbeispiel: Ausdrücke und Funktionen in Sortierungen und Gruppierungen sind nicht sinnvoll, da sie zunächst ausgewertet werden müssen. Besser wäre es, die beiden Felder Nachname und Vorname zu indizieren und einzeln nach den beiden Feldern zu sortieren.
12.4.3 Bericht nur öffnen, wenn er Daten enthält Berichte ohne Daten lassen sich zwar in der Berichts- oder Vorschauansicht öffnen, aber Sinn macht das natürlich nicht. Um dem Benutzer diesen unnötigen Zeitaufwand zu ersparen, treffen Sie geeignete Maßnahmen. Dazu brauchen Sie noch nicht einmal selbst eine Anweisung zur Prüfung der Anzahl der Datensätze in der Datensatzherkunft zu schreiben, sondern können die Ereigniseigenschaft Bei Ohne Daten nutzen, die genau dann ausgelöst wird, wenn der Bericht keine Daten enthält. Hier können Sie Code anlegen, um eine entsprechende Meldung auszugeben und das Öffnen des Berichts abzubrechen: Private Sub Report_NoData(Cancel As Integer) MsgBox "Der Bericht enthält keine Daten." Cancel = True End Sub Listing 12.4: Ereignisprozedur, die das Öffnen eines leeren Berichts unterbindet
550
12
Performance
Wenn Sie es ganz gut mit dem Benutzer meinen, lassen Sie es allerdings gar nicht erst so weit kommen: Deaktivieren Sie einfach die Schaltfläche zum Öffnen des Berichts, wenn abzusehen ist, dass dieser keine Daten enthält.
12.5 VBA Im Kapitel 6, »VBA« erhalten Sie einige Informationen, wie Sie VBA-Routinen so optimieren, dass diese eine hohe Wiederverwendbarkeit und eine gute Lesbarkeit erhalten. Performancetechnisch gesehen gehen Sie damit den richtigen Weg: Einerseits strukturieren Sie den Code besser und machen ihn damit wesentlich leichter lesbar, was eine gute Vorarbeit für folgende Performance-Optimierungen ist. Andererseits ergibt sich nach umfangreichen Code-Optimierungsmaßnahmen vermutlich eine Aufteilung der Funktionalität auf wesentlich mehr Module als zuvor: Und das liefert eine direkt messbare Performancesteigerung, denn Access lädt immer das komplette Modul in den Speicher, auch wenn es nur eine öffentliche Variable oder eine einzige Routine daraus verwendet. Standardfunktionen, die Sie oft verwenden, können Sie auch in einem großen Modul sammeln – wenn diese nach dem ersten Aufruf einmal im Speicher liegen, braucht für die folgenden Aufrufe kein Modul mehr nachgeladen zu werden. Nachfolgend finden Sie einige grundlegende Tipps zur Optimierung von VBA-Code und einige Verbesserungsmöglichkeiten für die Programmierung von Routinen für den Datenzugriff mit DAO und ADO.
12.5.1 Performance von VBA-Code optimieren Das Kapitel 6 »VBA«, enthält unter anderem Informationen über das Schreiben optimalen VBA-Codes. Die dort dargestellten Möglichkeiten beziehen sich nicht primär auf das Erreichen einer besseren Performance, sondern einer besseren Struktur, Lesbarkeit und Wiederverwendbarkeit. Tipps für eine bessere Performance finden Sie in den folgenden Abschnitten.
Variablen und Datentypen Was für die Festlegung der Datentypen von Tabellenfeldern gilt, trifft auch für die Deklaration von Variablen in VBA-Routinen zu. Sie sollten immer den kleinstmöglichen Datentyp wählen. Dazu gehört, dass Sie überhaupt einen Datentyp festlegen, denn sonst verwendet Access automatisch den Datentyp Variant. Dieser kann beliebige Daten aufnehmen und belegt entsprechenden Speicherplatz. Um sicherzugehen, dass Sie jeder Variablen einen Datentyp zuweisen, schreiben Sie die folgende Zeile in den Kopf ihrer Module: Option Explicit
VBA
551
Ist eine Variable nicht deklariert, kompiliert Access das Modul nicht (siehe Abbildung 12.12).
Abbildung 12.12: Kein Kompilieren ohne Variablendeklaration
Bei der Wahl des richtigen Datentyps für eine Variable hilft Tabelle 12.1 weiter – verwenden Sie einfach den Datentyp, mit dessen Wertebereich Sie vermutlich auskommen. Dabei gibt es wiederum eine Ausnahme: Wenn Sie mit ganzzahligen Variablen rechnen möchten, verwenden Sie möglichst den Datentyp Long. Windows als 32bit-System kann mit diesem Zahlentyp schneller rechnen.
Early Binding statt Late Binding Deklarieren Sie Objektvariablen direkt mit dem konkreten Objekttyp und nicht mit dem Datentyp Object. Dadurch kann Access Early Binding verwenden – die Bibliothek mit den benötigten Objekten, Methoden und Eigenschaften ist Access bekannt und ermöglicht so einen schnelleren Zugriff auf die enthaltenen Objekte. Dazu müssen Sie zuvor einen Verweis auf die entsprechende Objektbibliothek erstellen (siehe Abbildung 12.13).
552
12
Performance
Abbildung 12.13: Ein Verweis auf die passende Objektbibliothek ist Voraussetzung für die Verwendung von Early Binding.
Die folgende Routine zeigt ein Beispiel für Early Binding: Public Function EarlyBinding() Dim cbr As CommandBar For Each cbr In Application.CommandBars Debug.Print cbr.Name Next cbr Set cbr = Nothing End Function Listing 12.5: Beispiel für das Early Binding von Objekten …
Wenn Sie die Routine ohne Early Binding realisieren wollten, würde das wie in folgendem Quellcode aussehen: Public Function LateBinding() Dim cbr As Object For Each cbr In Application.CommandBars Debug.Print cbr.Name Next cbr Set cbr = Nothing End Function Listing 12.6: … und die gleiche Funktionalität mit Late Binding
VBA
553
Early Binding liefert einen weiteren Vorteil: Da die Objektbibliothek und die enthalte-
nen Elemente bekannt sind, können Sie bei der Eingabe IntelliSense nutzen. Das ist nicht nur für die Auswahl der Methoden und Eigenschaften interessant, sondern gerade für die Verwendung von Parametern. Sie brauchen dort nicht die nackten Zahlenwerte zu verwenden, sondern können auf die entsprechenden Bezeichnungen zurückgreifen.
Objektvariablen verwenden, wenn ein Objekt mehr als einmal referenziert wird In vielen Fällen ist es gute Gewohnheit, ein Objekt zu deklarieren und zu instanzieren, bevor Sie auf dessen Elemente zugreifen. Das beste Beispiel dafür ist sicher die Verwendung von db und rst für Database- und Recordset-Objekte: Public Sub ObjekteMehrfachNutzen() Dim db As DAO.Database Dim rst As DAO.Database Set db = CurrentDb Set rst = db.OpenRecordset("tblKontakte", dbOpenDynaset) Debug.Print rst!Vorname Debug.Print rst!Nachname rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 12.7: Verwendung einer Objektvariablen
Wann immer Sie mehr als einmal auf ein Objekt zugreifen, sollten Sie es mit einer geeigneten Objektvariablen referenzieren. Wenn Sie beispielsweise mehrere Steuerelemente eines Formulars auslesen, legen Sie einfach eine Referenz auf das Formular an. Die erste Variante einer Routine zum Auslesen der Textfelder eines Formulars stellt für jeden einzelnen Zugriff die Verbindung zum Formular her: Public Function FormularAuslesenOhneReferenz() Dim strVorname As String Dim strNachname As String strVorname = Forms!frmKontakte!Vorname strNachname = Forms!frmKontakte!Nachname End Function Listing 12.8: Direkter Zugriff auf die Eigenschaften eines Formulars
554
12
Performance
Die zweite Variante erfordert zwar ein wenig mehr Code, ist jedoch schneller: Public Function FormularAuslesenMitReferenz() Dim frm As Form Dim strVorname As String Dim strNachname As String Set frm = Forms!frmKontakte strVorname = frm!Vorname strNachname = frm!Nachname Set frm = Nothing End Function Listing 12.9: Zugriff auf Steuerelemente eines Formulars via Objektvariable
Me statt Verweis auf Forms- oder Reports-Auflistung Verwenden Sie den Ausdruck Me statt der Variante über die Formular- oder BerichtsAuflistung. Die folgende Routine ist die langsamere Möglichkeit: Public Function FormularbezugMitFormsFormularname() Dim strVorname As String strVorname = Forms!frmKontakte!Vorname End Function Listing 12.10: Die langsame Variante für den Zugriff auf objektinterne Elemente …
Die nächste Variante verwendet das Schlüsselwort Me und ist um rund 40% schneller: Public Function FormularbezugMitMe() Dim strVorname As String strVorname = Me!Vorname End Function Listing 12.11: … und die schnellere Variante
Variablen verwenden, wenn konstante Werte mehr als einmal ermittelt werden Wenn Sie mehrmals im Code die gleiche Funktion wie etwa eine Domänen-Funktion (Dlookup, Dmax, Dcount) verwenden, deren Ergebnis vermutlich in der Zwischenzeit nicht verändert wird, speichern Sie das Ergebnis in einer Variablen.
Variablen auf jeden Fall verwenden, wenn der Inhalt Start- oder End-Wert einer Schleife markiert Obiger Tipp ist in einem Fall auch dann anzuwenden, wenn der Inhalt der Variablen scheinbar nur einmal verwendet wird – und zwar innerhalb einer Schleife. Anderenfalls wird der Wert bei jedem Schleifendurchlauf neu berechnet.
VBA
555
Das folgende Beispiel sieht sehr harmlos aus, aber der Inhalt des Feldes txtAnzahl wird mit jedem Durchlauf neu ausgelesen: For i = 1 to Me!txtAnzahl ...
Verwenden Sie also statt dessen folgende Codezeilen, um den benötigten Wert zuvor in einer Variablen zu speichern: Dim lngAnzahl As Long lngAnzahl = Me!txtAnzahl For i = 1 to lngAnzahl ...
Zeichenketten-Funktionen sparsam verwenden Zeichenketten-Funktionen sind sehr aufwändig. Versuchen Sie, diese möglichst sparsam einzusetzen.
String-Variante von Zeichenketten-Funktionen verwenden Es gibt jede Zeichenketten-Funktion in zwei Ausführungen: mit und ohne angehängtes Dollar-Zeichen ($). Der Unterschied der Funktionen liegt im Datentyp des Rückgabewertes: Die Version der Textfunktionen mit dem Dollar-Zeichen liefert einen String zurück, die ohne Dollar-Zeichen einen Wert vom Datentyp Variant. Da man das Ergebnis von Zeichenketten-Funktionen meistens in einer String-Variablen speichert, ist bei der Verwendung der Variant-Version der Zeichenketten-Funktion noch eine zusätzliche interne Umwandlung des Datentyps erforderlich.
Zeichenketten vergleichen mit StrComp Das gängige Mittel zum Vergleichen zweier Zeichenketten ist die Verwendung des Gleichheitszeichens als Operator. VBA bietet eine dazu besser geeignete Funktion namens StrComp an. Die Funktion erwartet die zu vergleichenden Zeichenketten und gegebenenfalls eine Einstellung für die Vergleichsmethode.
Leere Zeichenkette testen über die Länge der Zeichenkette Ob eine Zeichenkette leer ist, prüft man üblicherweise durch einen Vergleich mit der Zeichenkette "". Das ist nicht der schnellste Weg. Performanter ist es, die Länge der Zeichenkette zu ermitteln und diesen Wert mit 0 zu vergleichen.
556
12
Performance
Leere Zeichenkette per vbNullString zuweisen Eine Zeichenkette leert man normalerweise durch Zuweisen einer leeren Zeichenkette: str = ""
Etwas schneller geht es mit der Konstanten vbNullString: str = vbNullString
Zeichenverkettung mit dem kaufmännischen Und (&) vermeiden Wenn zwei Zeichenketten miteinander verknüpft werden müssen, dann ist das &-Zeichen unvermeidlich. Es ist aber sehr beliebt, längere Zeichenketten der Übersichtlichkeit halber auf mehrere Codezeilen zu verteilen – vor allem bei dynamisch zusammengesetzten SQL-Ausdrücken: Public Function UndOperator1() Dim strSQL As String strSQL = "SELECT tblKontakte.Vorname, tblKontakte.Nachname " strSQL = strSQL & "FROM tblKontakte " strSQL = strSQL & "WHERE tblKontakte.Vorname = 'André' " strSQL = strSQL & "AND tblKontakte.Nachname = 'Minhorst'" End Function Listing 12.12: Zusammengesetzter SQL-Ausdruck
Die folgende Variante ist schneller, weil die Zeichenkette nicht mehr verknüpft werden muss. Wenn Sie also alles aus Ihrer Datenbankanwendung herausholen möchten, wandeln Sie alle Ausdrücke, die ohne Not mit dem Kaufmanns-Und verbunden sind, wieder in einen einzigen Ausdruck um: Public Function UndOperator2() Dim strSQL As String strSQL = "SELECT tblKontakte.Vorname,..." End Function
_
Listing 12.13: SQL-Ausdruck in einer Zeile
Statische oder dynamische Arrays? Die Größe eines Arrays können Sie bereits bei der Deklaration festlegen (statisches Array) oder erst später (dynamisches Array).
VBA
557
Bei der statischen Variante geben Sie die Anzahl der vorgesehenen Einträge bereits bei der Deklaration an: Public Function ArrayStatisch() Dim i As Long Dim lng(10000) As Long For i = 1 To 10000 lng(i) = i Next i End Function Listing 12.14: Verwendung eines statischen Arrays
Die dynamische Variante sieht etwas anders aus. Hier geben Sie bei der Deklaration noch keinen Wert an und redimensionieren die Größe des Arrays bei Bedarf. Public Function ArrayDynamisch() Dim i As Long Dim lng() As Long For i = 1 To 10000 ReDim Preserve lng(i) lng(i) = i Next i End Function Listing 12.15: Verwendung eines dynamischen Arrays
Die beiden Varianten haben Vor- und Nachteile. Die statische Variante reserviert von vornherein ein Array von einer Größe, die Sie vielleicht gar nicht benötigen. Und falls doch, wird im Durchschnitt die Hälfte des reservierten Speicherplatzes nicht gebraucht. Andererseits kostet die ständige Redimensionierung wesentlich mehr Zeit – hier ist also zwischen benötigtem Speicherplatz und Geschwindigkeit zu entscheiden.
Logische Ausdrücke vereinfachen Logische Ausdrücke landen oft in den übersichtlichen If…Then-Konstrukten. Das sieht dann beispielsweise so aus: Public Function IfThen1() Dim i As Integer Dim bol As Boolean If i = 1 Then bol = True Else
558
12
Performance
bol = False End If End Function Listing 12.16: Ermitteln eines Boolean-Wertes per If...Then-Konstrukt …
Etwas schneller geht es mit der folgenden Variante, die außerdem noch einige Zeilen Code einspart: Public Function IfThen2() Dim i As Integer Dim bol As Boolean bol = (i = 1) End Function Listing 12.17: … und über die direkte Auswertung des Ausdrucks
Sollte Ihnen die Übersicht bei der zweiten Variante etwas zu kurz kommen, können Sie allerdings auch auf den hier relativ geringen Geschwindigkeitsvorteil verzichten.
Boolean-Werte switchen Wenn Sie einer Boolean-Variablen, die den Wert True enthält, den Wert False zuweisen möchten oder umgekehrt, verwenden Sie vermutlich die folgende Variante: Public Function Invertieren1() Dim x As Boolean If x = True Then x = False Else x = True End If End Function Listing 12.18: Lange Variante der Invertierung einer Boolean-Variablen …
Etwas schneller und Platz sparender ist die folgende Variante: Public Function Invertieren2() Dim x As Boolean x = Not x End Function Listing 12.19: … und die kürzere, etwas schnellere Lösung
VBA
559
Performance von Schleifen mit fester Durchlaufzahl Wenn Sie die Anzahl Durchläufe einer Schleife kennen, ist eine For…Next-Schleife schneller als eine Do…While- oder Do…Loop-Schleife. Die beiden folgenden Funktionen durchlaufen eine Schleife jeweils tausend Mal. Die zweite Variante enthält bereits im nackten Zustand eine Anweisung mehr, die den Zähler erhöht. Dadurch ist sie wesentlich langsamer als die erste Variante. Der Unterschied wird allerdings immer kleiner, je mehr Anweisungen sich innerhalb der Schleifen befinden. Public Function Schleife1() Dim i As Integer For i = 1 To 1000 'Etwas tun... Next i End Function Listing 12.20: Diese For…Next-Schleife erledigt die gleiche Arbeit …
Public Function Schleife2() Dim i As Integer Do While Not i = 1000 'Etwas tun... i = i + 1 Loop End Function Listing 12.21: … wie diese Do…While-Schleife, nur schneller.
If Then oder IIf? Die IIf-Funktion wird gerne als einzeiliger Ersatz für einfache If…Then-Konstrukte verwendet. Sie wertet den im ersten Parameter angegebenen Ausdruck aus und gibt für das Ergebnis True den Wert des zweiten Parameters und für das Ergebnis False den dritten Parameter zurück. Diese Funktion hat aber gegenüber If…Then entscheidende Nachteile: Die IIf-Funktion wertet immer beide möglichen Antworten aus. Dadurch wird erstens Zeit vergeudet und zweitens können nicht eingeplante Nebeneffekte auftreten. Das folgende Beispiel veranschaulicht dies. Die IIf-Anweisung soll das Ergebnis der Division der Variablen a und b ausgeben, aber nur, wenn b nicht 0 ist. Ist das doch der Fall, würde die Division einen Division-durch-Null-Fehler erzeugen, was durch die IIf-Funktion abgefangen werden soll. Das funktioniert aber nicht: Obwohl die Bedingung erfüllt ist und eigentlich der Text Division durch 0 ausgegeben werden müsste, erscheint die Fehlermeldung, die eigentlich verhindert werden sollte.
560
12
Performance
Public Function Division() Dim a As Integer Dim b As Integer a = 1 b = 0 Debug.Print IIf(b = 0, "Division durch 0", a / b) End Function Listing 12.22: Unerwünschte Nebeneffekte mit der IIf-Funktion
Die gleiche Variante mit einem If…Then-Konstrukt sieht folgendermaßen aus. Performancetests ergaben keine wesentlichen Vorteile für eine der beiden Varianten, sodass Sie die If…Then-Variante wegen der besseren Kalkulierbarkeit einsetzen sollten. Public Function Division2() Dim a As Integer Dim b As Integer a = 1 b = 0 If b = 0 Then Debug.Print "Division durch Null" Else Debug.Print a / b End If End Function Listing 12.23: Diese Variante bringt keine bösen Überraschungen.
12.5.2 Punkt oder Ausrufezeichen In vielen Fällen lässt Access die synonyme Verwendung von Ausrufezeichen und Punkt zu – etwa beim Bezug auf die Steuerelemente eines Formulars: Debug.Print Forms!frmKontakte!txtVorname
oder Debug.Print Forms.frmKontakte.txtVorname
Setzen Sie das Ausrufezeichen, wann immer es möglich ist. Diese Variante ist immer schneller als die mit Punkt.
12.5.3 Datenzugriff optimieren Access bietet verschiedene Techniken für den Zugriff auf die in der Datenbank gespeicherten Daten – DAO und ADO. Im Folgenden finden Sie einige Tipps rund um den Einsatz dieser beiden Bibliotheken.
VBA
561
Datenzugriff möglichst per gespeicherter Abfrage statt mit ADO oder DAO Wenn Sie per Code auf Daten zugreifen, sollten Sie nach Möglichkeit mit gespeicherten Abfragen arbeiten. Sie können damit nicht nur Daten abfragen, sondern diese durch die Verwendung von Aktionsabfragen auch ändern. Letzteres lässt sich leicht durch ein Beispiel belegen. Die nachfolgenden Prozeduren ändern beide den Inhalt des Feldes Vorname für alle Datensätze der Tabelle tblKontakte. Die erste Variante verwendet dafür DAO und durchläuft per Code alle Datensätze einer zuvor geöffneten Datensatzgruppe auf Basis der Tabelle tblKontakte. Public Function Aktionsabfrage1() Dim db As DAO.Database Dim rst As Recordset Set db = CurrentDb Set rst = db.OpenRecordset("SELECT Vorname FROM tblKontakte", _ dbOpenDynaset) Do While Not rst.EOF rst.Edit rst!Vorname = "André" rst.Update rst.MoveNext Loop Set db = Nothing End Function Listing 12.24: Aktualisieren von Daten mittels DAO …
Die zweite Variante setzt auf eine gespeicherte Abfrage namens qryUpdateKontakte, deren SQL-Code wie folgt aussieht: UPDATE tblKontakte SET tblKontakte.Vorname = "André";
Die gespeicherte Aktionsabfrage führt die Aufgabe wesentlich schneller aus als die DAO-Variante. Das ist auch nicht verwunderlich, da hier die Jet-Engine direkt mit der Änderung der Daten beauftragt wird – und auf solche Fälle ist sie ja nun spezialisiert. Public Function Aktionsabfrage2() Dim db As DAO.Database Dim qdf As DAO.QueryDef Set db = CurrentDb Set qdf = db.QueryDefs("qryUpdateKontakte") qdf.Execute Set qdf = Nothing Set db = Nothing End Function Listing 12.25: … und per gespeicherter Abfrage
562
12
Performance
Wenn Sie nichts anderes mehr mit den Objektvariablen anfangen und nur die Abfrage ausführen möchten, können Sie natürlich auch auf die Variablen verzichten und mit einer einzigen Abfrage auskommen: CurrentDb.QueryDefs("qryUpdateKontakte").Execute
Tipp: Noch schneller ist die Variante mit DBEngine(0)(0) statt CurrentDB. Diese hat allerdings auch Nachteile, wie in Kapitel 8 in Abschnitt 8.5, »Aktuelle Datenbank referenzieren«, beschrieben ist.
12.6 Sonstige Performance-Tipps Die folgenden Tipps lassen sich nicht in eine der vorherigen Kategorien einordnen, tragen aber durchaus ihren Teil zur Performance-Steigerung bei.
12.6.1 Verwendung als .mde-Datei Wenn der Benutzer die Datenbankanwendung nicht selbst bearbeiten können soll, liefern Sie die Datenbank als .mde-Datei aus. Dadurch überführen Sie den kompletten Quellcode in den kompilierten Zustand, den er auch nicht mehr verlässt. Außerdem ist der durch eine .mde-Datei benötigte Speicherplatz geringer, da diese keinen Quellcode mehr enthält (zur Sicherheit: Löschen Sie niemals das Original der .mde-Datei – bisher ist es noch niemandem gelungen, eine .mde-Datei wieder in die Variante mit Quellcode zurückzuverwandeln).
12.6.2 Verwendung als .mdb-Datei Wenn Sie die Datenbank als .mdb-Datei weitergeben und der Benutzer zwar den Quellcode einsehen können, aber nicht unbedingt Unterstützung durch Kommentare oder ein übersichtliches Code-Layout im Quellcode erhalten soll, dann entfernen Sie Kommentare, Leerzeilen und Leerzeichen. Was zur Optimierung von Ladezeiten von HTML-Seiten beiträgt, kommt auch dem Compiler recht: Er muss sich nur noch mit dem reinen Quellcode beschäftigen und spart so die Zeit, um nicht benötigte Bereiche herauszufiltern. Das ist allerdings auch nur interessant, wenn der Code hin und wieder dekompiliert wird – also während der Entwicklung. Ansonsten liefert man ja ohnehin eine kompilierte Datenbank aus. Prüfen Sie ebenfalls regelmäßig, ob alle in der Datenbank enthaltenen Routinen benötigt werden. Gerade wenn Sie Module mit Standard-Funktionen verwenden, die Sie standardmäßig in jede neue Datenbank importieren, sollten Sie vor der Weitergabe die Funktionen herauswerfen, die von keiner anderen Routine aufgerufen werden.
Sonstige Performance-Tipps
563
12.6.3 Arbeitsgruppen-Informationsdatei auf aktueller Access-Version halten Wenn Sie eine Access-Datenbank in eine höhere Access-Version konvertieren, die eine .mdw-Datei verwendet, sollten Sie auch diese auf den aktuellen Stand bringen.
12.6.4 Exklusiver Zugriff bei Einzelplatzanwendungen Wenn Sie allein auf eine Datenbank zugreifen, öffnen Sie diese im Exklusiv-Modus. Diese Einstellung nehmen Sie auf der Registerseite Weitere des Optionen-Dialogs vor. Auf keinen Fall sollten Sie ein Datenbank-Backend, auf das Sie allein zugreifen, auf einem anderen Rechner als dem lokalen ablegen (weitere Informationen zum Aufteilen von Datenbanken finden Sie in Kapitel 17, »Installation, Betrieb und Wartung«). Unter Umständen würde man so verfahren, um das Backend mit dem Inhalt eines Servers mit zu sichern – zu Gunsten der Performance sollten Sie sich allerdings die Arbeit machen, auf anderem Wege für eine geeignete Sicherung zu sorgen.
12.6.5 Komprimieren der Datenbank Durch das Komprimieren einer Datenbank werden nicht nur gelöschte Datensätze endgültig aus der Datenbank entfernt, was die Größe der Datenbank-Datei verringert, sondern auch die Tabellenstatistiken aktualisiert. Das ist für die Erstellung der Ausführungspläne von Abfragen wichtig, da diese sonst über längere Zeit mit falschen Angaben arbeiten und gegebenenfalls die Abfragen schlechter optimieren. Wenn Sie sicherstellen möchten, dass die Datenbank regelmäßig komprimiert wird, aktivieren Sie die Option Beim Schließen komprimieren auf der Registerseite Allgemein des Optionen-Dialogs. Achtung: Mit der Option Beim SCHLIEßEN KOMPRIMIEREN sorgen Sie ausschließlich dafür, dass bei aufgeteilten Datenbanken nur das Frontend einer Datenbank komprimiert wird, nicht aber das Backend mit den Daten!
12.6.6 Objektnamen-Autokorrektur abschalten Die Objektnamen-Autokorrektur sorgt dafür, dass Änderungen – etwa an Feldnamen einer Tabelle – automatisch auf Steuerelemente übertragen werden, die auf ein solches Feld zugreifen. Offensichtlich wirkt sich die Aktivierung dieser Option auch im laufenden Betrieb auf die Performance aus, weshalb Sie diese Option nach Fertigstellung einer Anwendung abschalten sollten.
564
12
Performance
12.6.7 Unterdatenblätter abschalten Unterdatenblätter gibt es seit Access 2000. Sie ermöglichen die Anzeige von Daten aus verknüpften Tabellen innerhalb der Datenblattansicht einer anderen Tabelle (siehe Abbildung 12.14). Dieses Feature ist standardmäßig aktiviert und kostet unter Umständen Rechenzeit beim direkten Öffnen von Tabellen in Datenblattansicht – vornehmlich mit wachsender Anzahl verknüpfter Tabellen.
Abbildung 12.14: Einfaches Beispiel für ein Unterdatenblatt
Um diese Funktion zu deaktivieren, stellen Sie den Wert der Eigenschaft Unterdatenblattname auf den Wert [Keines] ein.
Abbildung 12.15: Deaktivieren der Anzeige von Unterdatenblättern
Performance-Unterschiede messen
565
12.6.8 Rechtschreibprüfung ausschalten Wenn Sie die Rechtschreibprüfung nicht benötigen, können Sie diese ebenfalls ausschalten. Die passende Option finden Sie auf der Registerseite Rechtschreibung des Optionen-Dialogs. Die Auswirkungen dürften allerdings eher sparsam ausfallen.
12.7 Performance-Unterschiede messen Sie haben in diesem Kapitel einige Tipps und Hinweise darauf gefunden, wie Sie die Performance einer Anwendung verbessern können. Dazu müssen Sie an vielen Stellen herumschrauben; manche Tipps bringen einen garantierten Performance-Gewinn, andere sind mit Vorsicht zu genießen. Wie findet man nun heraus, ob eine Maßnahme den gewünschten Erfolg gebracht hat? Locker formuliert lautet die Antwort: Nehmen Sie sich eine Stoppuhr zur Hand und lassen Sie die jeweiligen Varianten gegeneinander antreten. Im Entwicklerjargon heißt das: Programmieren Sie die Funktionalität, die zur Zeitmessung notwendig ist, und beginnen Sie mit der Optimierung.
12.7.1 Werkzeug für Performance-Tests selbst gebaut Wenn Sie erst einmal wissen, wie Sie die Systemzeit mit der gewünschten Genauigkeit ermitteln, ist der Rest ein Kinderspiel. Wie so oft hilft eine API-Funktion aus: Sie trägt den sinnvollen Namen TimeGetTime und liefert eine Zahl vom Datentyp Long, die die Anzahl Millisekunden seit dem Start von Windows enthält. Deklarieren Sie die Funktion wie folgt: Private Declare Function timeGetTime Lib "winmm.dll" () As Long
Anschließend können Sie einfach über den Funktionsnamen auf die Funktion zugreifen. Der folgende Ausdruck gibt die Systemzeit im Direktfenster aus: Debug.Print timeGetTime
Das Messen der Performance ist gleichbedeutend mit dem Messen der Zeit, die Access für eine bestimmte Aufgabe benötigt. Das Ganze macht natürlich wenig Sinn, wenn es keine Vergleichsmöglichkeiten gibt – Sie werden also zum Zweck der PerformanceSteigerung immer mit mindestens zwei Varianten zur Erledigung der Aufgabe arbeiten und vergleichen, welche der beiden die Aufgabe schneller erledigt. Voraussetzung für die Zeitmessung bestimmter Vorgänge ist, dass sich diese Vorgänge per VBA starten lassen und das Ende auch per VBA erkannt wird. Da sich in Access fast jeder Vorgang per VBA steuern lässt, bedeutet diese Voraussetzung keine Einschränkung, sondern in manchen Fällen lediglich eine Erschwerung der Aufgabe.
566
12
Performance
Ist diese Voraussetzung erfüllt, messen Sie die Zeit eines Vorgangs einfach, indem Sie vor dem Start die Systemzeit erfassen, den Vorgang durchführen und anschließend die Differenz aus der aktuellen und der beim Start erfassten Systemzeit bilden. Im Code kann das wie folgt aussehen: ... 'Startzeit merken: lngStartzeit = timeGetTime 'zu testende Funktion: Call Testfunction 'Differenz aus aktueller Zeit und Startzeit ausgeben: Debug.Print "Testdauer:" & timeGetTime – lngStartzeit ...
Nun dauern die zu testenden Vorgänge vermutlich meist nur wenige Millisekunden und benötigen auch nicht immer exakt die gleiche Zeit, sodass Sie die zu testende Funktionalität mit dem Ziel einer hohen Genauigkeit doch lieber mehrmals durchführen und die benötigte Zeit messen sollten. Je mehr Durchläufe, desto genauer das Ergebnis.
Testframework Und um dabei den Code nicht mit Zeitmessungen zu überladen, erstellen Sie ein kleines Framework zum einfachen Testen der Performance der unterschiedlichen Varianten einer Funktionalität. Sie können mit dem nachfolgend vorgestellten Framework sowohl Prozeduren aus Standardmodulen als auch aus Formularen und Klassenmodulen testen. Der Test der Prozeduren aus Standardmodulen erfordert allerdings einen etwas anderen Ansatz als der von Prozeduren in Formular- und Klassenmodulen. Damit das Framework flexibel ist und Sie die zu testenden Prozeduren und die übrigen Informationen als Parameter eines einfachen Aufrufs übergeben können, benötigen Sie eine Funktion, der Sie nur den Namen der auszuführenden Routine übergeben müssen.
Aufrufen von Funktionen in Standardmodulen Funktionen lassen sich ganz einfach mit der Eval-Anweisung aufrufen. Die EvalAnweisung erwartet lediglich die Angabe des Namens der auszuführenden Funktion inklusive öffnender und schließender Klammer als Parameter: Eval "Funktionsname()"
Performance-Unterschiede messen
567
Diese Form des Aufrufs reicht für den Performancetest. Üblicherweise verwendet man die Eval-Anweisung allerdings, um das Resultat einer Funktion zurückzugeben – dann weist man das Ergebnis einer Variablen zu oder gibt es im Direktfenster aus: Debug.Print Eval("LateBinding()")
Die Eval-Anweisung arbeitet nur mit Funktionen und nicht mit Sub-Prozeduren. Daher müssen Sie die zu testenden Anweisungen entweder direkt in eine Funktion schreiben oder die entsprechende Sub-Prozedur durch eine Funktion aufrufen lassen.
Aufrufen von Funktionen und Prozeduren in Formular- und Klassenmodulen Für den Aufruf von Funktionen in Formular- und Klassenmodulen können Sie die Eval-Anweisung nicht verwenden. Selbst das Aufrufen öffentlich deklarierter Funktionen erfordert hier die Angabe des Objekts, in dem die Funktionen enthalten sind. Die Eval-Anweisung kann Funktionen nur über den reinen Funktionsnamen aufrufen. Hier kommt die CallByName-Methode ins Spiel: Sie ermöglicht die Ausführung von Funktionen und Sub-Prozeduren (genau genommen sogar auch von Property-Prozeduren, aber das ist in diesem Zusammenhang irrelevant). Die CallByName-Methode hat folgende Syntax: CallByName(object, procname, calltype,[args()])
Die Parameter erwarten die folgenden Informationen: object: Objektvariable mit Verweis auf das Objekt, das die auszuführende Prozedur enthält procname: Name der Prozedur (ohne Klammern!) calltype: Erwartet eine der Konstanten vbLet, vbGet, vbSet oder vbMethod. Im vorliegenden Fall ist vbMethod der richtige Wert. args(): Array für die Werte, die an eventuell vorhandene Parameter der in proctype benannten Prozedur übergeben werden müssen
Eigenschaften und Methoden des Frameworks Das Framework besteht aus einer einzigen Klasse mit zwei Methoden und einigen Eigenschaften. Die Klasse stellt die folgenden Eigenschaften zur Eingabe der für den Test benötigten Parameter bereit: ErsteRoutine: Name der ersten Routine ZweiteRoutine: Name der zweiten Routine
568
12
Performance
AnzahlDurchgaenge: Anzahl der Durchgänge jeder Funktion Objekt: Objektvariable mit Verweis auf ein Objekt, in dem sich die Prozeduren befinden Die ersten drei Eigenschaften müssen vor dem Aufruf des Performancetests übergeben werden. Die Letzte brauchen Sie nur mit einer Objektvariablen zu belegen, wenn die zu testende Routine sich in einem Formular- oder Klassenmodul befindet.
Test von Prozeduren aus Standardmodulen Für den Test einer öffentlichen Funktion eines Standardmoduls rufen Sie die Methode Performancetest_Standard auf. Diese startet die Zeitmessung, indem sie die Startzeit in der Variablen lngStartzeit speichert. Anschließend wird die erste Funktion in einer Schleife mit der in der Eigenschaft AnzahlDurchgaenge angegebenen Anzahl aufgerufen. Die Differenz der jetzigen Systemzeit und der Startzeit wird in der Variablen mErsteZeit gespeichert. Auf die gleiche Weise ermittelt die Methode die Zeit für die Durchläufe der zweiten Funktion und speichert sie in der Variablen mZweiteZeit. Nach der Zeitmessung berechnet die Methode noch zwei Werte: die absolute Differenz zwischen den gemessenen Zeiten und die relative Differenz. Die ermittelten Werte können anschließend über die Eigenschaften ErsteZeit, ZweiteZeit, AbsoluteDifferenz und RelativeDifferenz ausgelesen werden.
Test von Prozeduren aus Formular- und Klassenmodulen Der Test öffentlicher Prozeduren aus Formular- und Klassenmodulen läuft ähnlich wie der von Prozeduren in Standardmodulen ab. Sie müssen lediglich noch eine Objektvariable mit einem Verweis auf eine Instanz des Formulars oder der Klasse übergeben, in der sich die Prozedur befindet. Beachten Sie, dass die zu testenden Prozeduren als öffentlich deklariert sein müssen. Anschließend rufen Sie die Methode Performancetest_Objekt auf. Die Ergebnisse werden in den gleichen Eigenschaften zur Verfügung gestellt wie bei der zuvor beschriebenen Methode.
Die Klasse clsZeitmessung Nachfolgend finden Sie das Listing mit der kompletten Klasse clsZeitmessung. Es enthält die Eigenschaften zur Eingabe der Parameter sowie zur Ausgabe der Ergebnisse und die beiden Prozeduren Performancetest_Standard und Performancetest_Objekt.
Performance-Unterschiede messen Option Compare Database Option Explicit Private Declare Function timeGetTime Lib "winmm.dll" () As Long Dim Dim Dim Dim Dim Dim Dim Dim Dim Dim
lngStartzeit As Long mZeit As Long mErsteFunktion As String mZweiteFunktion As String mErsteZeit As Long mZweiteZeit As Long mAnzahlDurchgaenge As Long mAbsoluteDifferenz As Double mRelativeDifferenz As Double mObjekt As Object
Public Property Set Objekt(obj As Object) Set mObjekt = obj End Property Public Property Let ErsteFunktion(strErsteFunktion As String) mErsteFunktion = strErsteFunktion End Property Public Property Let ZweiteFunktion(strZweiteFunktion As String) mZweiteFunktion = strZweiteFunktion End Property Public Property Let AnzahlDurchgaenge(lngAnzahlDurchgaenge As Long) mAnzahlDurchgaenge = lngAnzahlDurchgaenge End Property Public Property Get AbsoluteDifferenz() As Double AbsoluteDifferenz = mAbsoluteDifferenz End Property Public Property Get RelativeDifferenz() As Double RelativeDifferenz = mRelativeDifferenz End Property Public Property Get Zeit() Zeit = mZeit End Property Public Property Get ErsteZeit() As Long ErsteZeit = mErsteZeit End Property Public Property Get ZweiteZeit() As Long
569
570
12
Performance
ZweiteZeit = mZweiteZeit End Property Private Sub Starten() lngStartzeit = timeGetTime End Sub Private Sub Stoppen() mZeit = timeGetTime - lngStartzeit End Sub Public Sub Performancetest_Standard() Dim l As Long Starten For l = 1 To mAnzahlDurchgaenge Eval mErsteProzedur Next l Stoppen mErsteZeit = mZeit Starten For l = 1 To mAnzahlDurchgaenge Eval mZweiteProzedur Next l Stoppen mZweiteZeit = mZeit If mErsteZeit = 0 Or mZweiteZeit = 0 Then Debug.Print "###Zeit nicht messbar, AnzahlDurchgaenge erhöhen." Else mAbsoluteDifferenz = mZweiteZeit - mErsteZeit mRelativeDifferenz = AbsoluteDifferenz / mZweiteZeit * 100 End If End Sub Public Sub Performancetest_Objekt() Dim l As Long Starten For l = 1 To mAnzahlDurchgaenge CallByName mObjekt, mErsteFunktion, VbMethod Next l Stoppen mErsteZeit = mZeit Starten For l = 1 To mAnzahlDurchgaenge CallByName mObjekt, mZweiteFunktion, VbMethod Next l Stoppen mZweiteZeit = mZeit If mErsteZeit = 0 Or mZweiteZeit = 0 Then Debug.Print "###Zeit nicht messbar, AnzahlDurchgaenge erhöhen."
Performance-Unterschiede messen
571
Else mAbsoluteDifferenz = mZweiteZeit - mErsteZeit mRelativeDifferenz = AbsoluteDifferenz / mZweiteZeit * 100 End If End Sub Listing 12.26: Die Klasse clsZeitmessung
Einsatz der Klasse clsZeitmessung Die Methoden und Eigenschaften lassen sich am einfachsten in einer Routine wie in folgendem Listing verwenden. Die Prozedur ZeitMessenStandard deklariert und instanziert ein Objekt des Typs clsZeitmessung. Es weist die Anzahl der Durchgänge zu und übergibt die Namen der zu vergleichenden Funktionen. Nach dem Ausführen des Tests mit der Methode Performancetest_ Standard fragt die Routine die ermittelten Werte ab und gibt sie im Testfenster aus. Public Sub ZeitMessenStandard(strErsteProzedur As String, _ strZweiteProzedur As String, lngAnzahlDurchgaenge As Long) Dim objZeitmessung As clsZeitmessung Set objZeitmessung = New clsZeitmessung With objZeitmessung .AnzahlDurchgaenge = lngAnzahlDurchgaenge .ErsteProzedur = strErsteProzedur & "()" .ZweiteProzedur = strZweiteProzedur & "()" .Performancetest_Standard Debug.Print "Zeit '" & strErsteProzedur & "': " & .ErsteZeit & "ms" Debug.Print "Zeit '" & strZweiteProzedur & "': " & .ZweiteZeit & "ms" Debug.Print "Absolute Differenz: " & .AbsoluteDifferenz & "ms" Debug.Print "Relative Differenz: " _ & Format(.RelativeDifferenz, "0.00") & "%" End With Set objZeitmessung = Nothing End Sub Listing 12.27: Diese Routine verwendet die Methoden und Eigenschaften der Klasse clsZeitmessung.
Bei beiden Funktionen, die Sie weiter oben in Abschnitt »Objektvariablen verwenden, wenn ein Objekt mehr als einmal referenziert wird« kennen gelernt haben, sehen der Aufruf der Prozedur ZeitMessen im Direktfenster und das Ergebnis wie folgt aus: ZeitmessenStandard "FormularAuslesenOhneReferenz", "FormularAuslesenMitReferenz", 10000 Zeit 'FormularAuslesenOhneReferenz': 1360ms Zeit 'FormularAuslesenMitReferenz': 1228ms
572
12
Performance
Absolute Differenz: -132ms Relative Differenz: -9,30%
Wenn Sie zwei Prozeduren miteinander vergleichen möchten, die sich in einem Formular oder einer Klasse befinden, verwenden Sie die Prozedur ZeitMessenObjekt. Diese Funktion erwartet zusätzlich eine Objektvariable mit der Instanz des Formulars oder der Klasse, in dem sich bzw. in der sich die zu testende Routine befindet. Außerdem müssen die zu testenden Routinen mit dem Schlüsselwort Public deklariert sein. Die Prozedur ZeitMessenObjekt liefert genau die gleichen Informationen zurück wie die Prozedur ZeitMessenStandard. Public Sub ZeitMessenObjekt(strErsteProzedur As String, _ strZweiteProzedur As String, lngAnzahlDurchgaenge As Long, obj As Object) Dim objZeitmessung As clsZeitmessung Set objZeitmessung = New clsZeitmessung With objZeitmessung Set .Objekt = obj .AnzahlDurchgaenge = lngAnzahlDurchgaenge .ErsteProzedur = strErsteProzedur .ZweiteProzedur = strZweiteProzedur .Performancetest_Objekt Debug.Print "Zeit '" & strErsteProzedur & "': " & .ErsteZeit & "ms" Debug.Print "Zeit '" & strZweiteProzedur & "': " & .ZweiteZeit & "ms" Debug.Print "Absolute Differenz: " & .AbsoluteDifferenz & "ms" Debug.Print "Relative Differenz: " _ & Format(.RelativeDifferenz, "0.00") & "%" End With Set objZeitmessung = Nothing End Sub Listing 12.28: Prozedur zur Performancemessung von Prozeduren in Formular- und Klassenmodulen
Ein Beispiel für den Einsatz dieser Prozedur ist ein Performancetest, der typische Formular-Eigenheiten unter die Lupe nimmt. Weiter oben in 12.5.1 unter »Me statt Verweis auf Forms- oder Reports-Auflisting« finden Sie zwei Prozeduren, die ein Textfeld eines Formulars einmal über das Schlüsselwort Me! und einmal über Forms!! referenzieren. Der Performancevergleich liefert folgendes Ergebnis: Zeitmessenobjekt "FormularbezugMitFormsFormularname", "FormularbezugMitMe", 10000, Forms("frmKontakte") Zeit 'FormularbezugMitFormsFormularname': 571ms Zeit 'FormularbezugMitMe': 407ms Absolute Differenz: -164ms Relative Differenz: -40,29%
Die Variante mit dem Schlüsselwort Me ist also um rund 40% schneller.
13 Objektorientierte Programmierung Wenn Sie im Internet nach Informationen über den Einsatz objektorientierter Programmierung in Zusammenhang mit Microsoft Access suchen, müssen Sie eine Menge Geduld mitbringen. Das macht sich erst recht bemerkbar, wenn Sie erstmal diejenigen Seiten oder Newsgroup-Beiträge herausfiltern müssen, in denen Entwickler über die Vor- und Nachteile der objektorientierten Entwicklung unter Access beziehungsweise VB/VBA diskutieren oder beleuchten, ob sich VB/VBA überhaupt »objektorientiert« nennen darf. Warum geht dieses Buch also so ausführlich auf objektorientierte Techniken ein, wenn dieses Thema selbst in Entwicklerkreisen anscheinend umstritten ist und sich nur wenige Entwickler diesem Thema widmen? Die Antwort ist einfach: Erstens arbeiten Sie, wenn Sie mit VBA entwickeln, ohnehin schon mehr oder weniger bewusst mit Objekten. Beispiele dafür sind Formulare, Berichte, Steuerelemente, Recordsets oder andere Anwendungen wie Word oder Excel. Dabei greifen Sie beim Programmieren wie selbstverständlich auf die per IntelliSense komfortabel einsetzbaren Methoden und Eigenschaften der Objekte zu. Den Umgang mit Objekten sind Sie also gewohnt – warum sollten Sie nicht von benutzerdefinierten Klassen profitieren? Und zweitens werden Sie, selbst wenn Sie nur kleine Häppchen Objektorientierung in Form des einen oder anderen Klassenmoduls zum Kapseln einer bestimmten Funktionalität in Ihre Anwendungen einbauen, davon profitieren und möglicherweise schnell mehr davon verwenden wollen. Ein weit verbreitetes Beispiel ist das Klassenmodul FileDialog von Karsten Pries (auf der Buch-CD unter Kap_13/FileDialog.cls zu finden). Das Klassenmodul stellt Eigenschaften und Methoden zur Verfügung, um auf einfache Weise benutzerdefinierte Dialoge zur Auswahl von Verzeichnissen oder Dateien anzuzeigen. Dazu ist nicht viel mehr notwendig, als das Klassenmodul in die Zieldatenbank zu importieren, ein entsprechendes Objekt zu instanzieren, die Eigenschaften festzulegen und den gewünschten Dialog zu öffnen. Anschließend lässt sich die ausgewählte Datei beziehungsweise das Verzeichnis über eine Eigenschaft des Objekts auslesen. Wenn Sie etwa einen Dialog zur Auswahl einer zu öffnenden Datei per Mausklick auf eine Schaltfläche eines Formulars anzeigen möchten (siehe Abbildung 13.1), reichen die folgenden Zeilen in der Beim Klicken-Ereignisprozedur der Schaltfläche:
574
13
Objektorientierte Programmierung
Private Sub cmdDateiAuswaehlen_Click() Dim objFiledialog As FileDialog Set objFiledialog = New FileDialog objFiledialog.ShowOpen Me.txtDateiname = objFiledialog.FileName End Sub Listing 13.1: Beispiel für die Verwendung einer Klasse
Die hier verwendeten Anweisungen rufen den Standard-Dialog auf; das Klassenmodul FileDialog bietet jedoch noch viele weitere Eigenschaften zum Anpassen des Dialogs: So lassen sich etwa der Fenstertitel, die zu filternden Dateien und das beim Öffnen ausgewählte Verzeichnis festlegen. Auch die Auswahl mehrerer Dateien gleichzeitig kann man durch Setzen einer einzigen Eigenschaft aktivieren. Warum macht man so etwas? Um in Access 2000 und älteren Versionen einen Dateidialog anzuzeigen, konnte man ein OCX-Steuerelement namens CommonDialog für die Anzeige von Dateidialogen verwenden, das aber bei der Weitergabe von Anwendungen Probleme bereitete, oder die entsprechenden API-Befehle einsetzen. Da letztere die zuverlässigere Variante schien, aber der direkte Zugriff auf die API nicht besonders komfortabel ist, lag es nahe, eine entsprechende Kapselklasse zu erstellen. Seit Access 2002 stellt die Office-Bibliothek zwar ein eigenes FileDialog-Objekt zur Verfügung, das aber in Access keinen Dialog zum Festlegen einer zu speichernden Datei enthält. Dieses Beispiel zeigt bereits einige Vorteile der Verwendung von Klassen: Komplizierte Funktionen lassen sich hinter einfachen Methodenaufrufen verstecken. Getestete und funktionstüchtige Klassen lassen sich beliebig wieder verwenden. Parameter müssen nicht mehr mit dem Funktionsaufruf übergeben werden, sondern können übersichtlich als Eigenschaften des Objekts festgelegt werden. Objekte bieten eine komfortable Programmierschnittstelle: IntelliSense verrät Ihnen genau, welche Eigenschaften und Methoden die Instanz eines Klassenmoduls bereitstellt. Dadurch sind Objekte quasi selbst dokumentierend. Darüber hinaus gibt es noch einige weitere Vorteile: Zusammenhängende Daten, die sonst als Variablen mehr oder weniger öffentlich irgendwo im Quellcode zu finden sind, lassen sich als Eigenschaften eines Klassenmoduls zusammenfassen. Methoden, die sich genau auf diese zu einem Klassenmodul zusammengefassten Daten beziehen, lassen sich nur in diesem Zusammenhang aufrufen.
575
Abbildung 13.1: Mit vier Anweisungen zum Datei öffnen-Dialog dank FileDialog-Klassenmodul
Die in einem Klassenmodul zusammengefassten Eigenschaften und Methoden können Objekte aus der Realität abbilden. Änderungen an einem Klassenmodul, das möglicherweise an vielen Stellen instanziert wird, müssen nur an einer Stelle durchgeführt werden. Sie können gleichzeitig mit mehreren Instanzen eines Klassenmoduls arbeiten. Mit Objekten lassen sich mehrschichtige Anwendungen realisieren. Objekte können den Datenzugriff kapseln. Damit können Sie Anwendungen erstellen, ohne sich auf eine bestimmte Datenquelle wie Tabellen einer Datenbank, Daten im XML-Format oder eine einfache Textdatei festzulegen. In den folgenden Abschnitten erfahren Sie, wie Sie eigene Klassen erstellen und diese einsetzen. Das anschließende Kapitel greift diese Kenntnisse auf und vermittelt Ihnen die Grundlagen für den professionellen Einsatz von Objekten in Access. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_13\Objektorientierung.mdb.
576
13
Objektorientierte Programmierung
13.1 Abstrakte Datentypen, Klassen und Objekte »One way of thinking of a class is as an abstract data type plus inheritance and polymorphism.« (Steve McConnell in »Code Complete 2«, Microsoft Press) Steve McConnell beschreibt abstrakte Datentypen im oben genannten Buch als eine Sammlung von Daten und Operationen, die mit diesen Daten arbeiten, wobei die Operationen der Anwendung den Zugriff auf die enthaltenen Daten und deren Änderung erlauben. Nimmt man das einleitende Zitat hinzu und zieht die fehlenden Möglichkeiten der Vererbung und Polymorphie unter VBA ab, ergibt sich Folgendes: Unter VBA entspricht ein abstrakter Datentyp einer Klasse. Außen vor bleibt dabei die Möglichkeit der Schnittstellenvererbung unter VBA (mehr dazu in Abschnitt 13.9, »Schnittstellen und Vererbung«). Ein abstrakter Datentyp ist im Gegensatz zu Basisdatentypen wie String, Integer oder Long oder zusammengesetzten Datentypen (etwa aus Basisdatentypen zusammengesetzten Strukturen) eine Beschreibung einer Schnittstelle zu Daten oder Datenstrukturen und deren Operationen. Die Betonung liegt dabei auf Beschreibung, denn ein abstrakter Datentyp ist unabhängig von der Implementierung in einer konkreten Programmiersprache. Ein abstrakter Datentyp zeichnet sich außerdem durch folgende Eigenschaften aus: Die Kapselung sorgt für das Verbergen der Realisierung der enthaltenen Operationen. Die Kapselung verhindert unkontrollierte Zugriffe auf die enthaltenen Daten und sorgt damit für deren Integrität; die Daten können nur über die definierte Schnittstelle geändert werden. Der abstrakte Datentyp ist universell einsetzbar und unabhängig von der Implementation. »Abstrakt« sind abstrakte Datentypen, weil sie reale Objekte modellieren und dabei nur ihre wichtigsten Eigenschaften und Funktionen berücksichtigen. Durch Abstrahieren werden komplexe Strukturen und Zusammenhänge vereinfacht; erst dadurch lassen sich komplizierte Objekte datentechnisch abbilden. Eine Klasse ist eine Implementierung eines abstrakten Datentyps in einer bestimmten Programmiersprache wie beispielsweise VBA. Die Implementierungsdetails werden dabei in einem Klassenmodul festgelegt. Die Gemeinsamkeiten zwischen dem abstrakten Datentyp und der Klasse beschränken sich dabei auf die fest definierte Schnittstelle, die aus den Methoden und Eigenschaften besteht. Die Implementierung kann und wird vermutlich in jeder Programmiersprache anders aussehen. Die Klasse kann neben den für die Realisierung der Schnittstelle notwendigen Methoden und Eigenschaften natürlich auch private Variablen, Funktionen und Prozeduren enthalten. Mehr dazu erfahren Sie in Abschnitt 13.3, »Klassenmodule«.
Objekte
577
13.2 Objekte Wenn Sie einige Erfahrung mit VBA haben (was wahrscheinlich ist, wenn Sie sich bis hierhin durchgeschlagen haben), sind Sie vermutlich mit der Verwendung von Objekten innerhalb von VBA-Routinen vertraut. Dennoch finden Sie noch einmal eine Zusammenfassung der dabei verwendeten Techniken und einige Hinweise, wie Sie häufige Fehlerquellen umgehen.
13.2.1 Eingebaute Objekte Im Urzustand enthält eine Access-Datenbank Verweise auf mindestens drei Bibliotheken, die einige für die Programmierung benötigte Objekte zur Verfügung stellen: Visual Basic for Applications Microsoft Access x.y Object Library Microsoft DAO x.y Object Library oder Microsoft ActiveX Data Objects x.y Library Die Methoden, Eigenschaften und Ereignisse dieser Objekte stehen jederzeit zur Verfügung, ihre Verwendung erfolgt ohne vorheriges Instanzieren. Zum größten Teil greift man vermutlich auf diese Objekte zu, ohne dass man sich im Klaren darüber ist, dass es sich auch bei den herkömmlichen Methoden und Eigenschaften um Teile eines Objekts handelt. Ein Beispiel verdeutlicht dies. Mit der folgenden Anweisung können Sie beispielsweise den aktuellen Datenbankbenutzer herausfinden: Debug.Print CurrentUser
Der folgende Aufruf belegt, dass diese Eigenschaft zur Microsoft Access x.y Object Library gehört: Debug.Print Access.CurrentUser
beziehungsweise Debug.Print Access.Application.CurrentUser
MultiUse-Klassen und GlobalMultiUse-Klassen Vielleicht haben Sie schon einmal festgestellt, dass man manche Objekte unter VBA ohne Weiteres verwenden kann, während man andere erst instanzieren muss. Der Grund ist, dass es tatsächlich unterschiedliche Arten von zugrunde liegenden Klassen gibt: Die so genannten GlobalMultiUse-Klassen brauchen Sie nicht zu instanzieren. Die Eigenschaften und Methoden dieser Klassen stehen ohne Ihr Zutun global zur Verfügung. Beispiele sind DBEngine, DoCmd oder Application. Es gibt auch eine Menge Global-
578
13
Objektorientierte Programmierung
MultiUse-Klassen, die Sie niemals bemerken – so sind beispielsweise alle Datums- und Zeitfunktionen Methoden der Klasse Datetime. Wenn Sie neugierig geworden sind, zu welcher Klasse die eine oder andere Eigenschaft oder Methode gehört, zeigen Sie einfach den Objektkatalog an und geben als Suchbegriff den gewünschten Ausdruck ein. Beispiele für Klassen, die Sie zunächst instanzieren müssen, sind Ihnen vermutlich ausreichend geläufig – Database und Recordset etwa dürften Ihnen schon das eine oder andere Mal über den Weg gelaufen sein.
Beispiele für Objekte Alle Elemente, die in VBA Eigenschaften, Methoden oder Ereignisse bereitstellen, sind Objekte. Manche davon stehen nicht nur für den Zugriff per VBA, sondern direkt in der Benutzungsoberfläche zur Verfügung. Dazu gehören beispielsweise: Fenster Menüleisten Formulare Berichte Textfelder Schaltflächen Alle genannten Elemente (und natürlich noch mehr) lassen sich auch per VBA ansprechen.
Funktionen zum Instanzieren neuer Objekte Neben den Methoden und Eigenschaften stellt die Access-Bibliothek wiederum Objekte beziehungsweise Funktionen zur Ermittlung von Verweisen auf die entsprechenden Objekte zur Verfügung. Wenn Sie beispielsweise CurrentDb verwenden, könnte der Eindruck entstehen, dass es sich dabei bereits um ein Objekt handelt. Der Eindruck verstärkt sich, wenn Sie folgendes Beispiel betrachten: Debug.Print Access.CurrentDb.Name
Die Anweisung bewirkt die Ausgabe des Verzeichnisses und des Dateinamens der aktuellen Datenbankdatei. Der Zugriff auf die Eigenschaft Name erfolgt dabei aber nicht über das »Objekt« CurrentDb, sondern über das Objekt, das durch die CurrentDbMethode zurückgeliefert wird. Und dabei handelt es sich wiederum um eine neue Instanz der aktuellen Datenbank.
Objekte
579
Man könnte diese »implizite« Instanzierung über Funktionen noch weiter treiben. Die folgende Anweisung gibt die Anzahl Datensätze der angegebenen Tabelle zurück: Debug.Print CurrentDb.OpenRecordset("tblKontakte").RecordCount
Diese Anweisung erstellt nicht nur eine neue Instanz der aktuellen Datenbank, sondern innerhalb dieser Instanz auch noch eine neue Datensatzgruppe, um deren Datensatzanzahl auszugeben. Für die Ausgabe zu Testzwecken ist die Verwendung einer solchen Anweisung sicher legitim, innerhalb von Prozeduren sollten Sie diese Anweisungen aber nur für den einfachen Gebrauch einsetzen – hier sind sie dann allerdings auch schneller. Wenn Sie aber öfter auf ein solches Objekt zugreifen werden, sollten Sie Objektvariablen verwenden und darüber auf die benötigten Eigenschaften und Methoden zugreifen. Die Verwendung der Objektvariablen sieht wie in folgender Routine aus: Public Function DatensaetzeZaehlen() 'Objektvariablen deklarieren Dim db As DAO.Database Dim rst As DAO.Recordset 'Objektvariable auf eine bestehende Instanz setzen Set db = Access.CurrentDb 'Neue Instanz eines Objekts erzeugen und 'per Objektvariable darauf verweisen Set rst = db.OpenRecordset("tblKontakte", dbOpenDynaset) Debug.Print rst.RecordCount '… weitere Aktionen mit dem Recordsetobjekt … rst.Close 'Objektvariablen freigeben Set rst = Nothing Set db = Nothing End Function Listing 13.2: Beispiel für das Setzen von Objektvariablen
Wenn Sie die mit CurrentDB erzeugte Instanz der aktuellen Datenbank mit einer Objektvariablen des Typs Database referenzieren, haben Sie einen entscheidenden Vorteil: Sie können die Objektvariable anschließend wieder auf den Wert Nothing setzen und gehen nicht das Risiko ein, dass die Instanz über den gewünschten Zeitraum hinaus existiert.
580
13
Objektorientierte Programmierung
Gleiches gilt für das im Beispiel erzeugte Recordset-Objekt: Dieses können Sie durch die Verwendung einer Objektvariable erstens schließen (gibt Speicherplatz frei) und zweitens die Objektvariable leeren.
Auflistungen Viele Objekte lassen sich über Auflistungen ansprechen. Das Database-Objekt enthält beispielsweise eine TableDefs-Auflistung, über die man auf alle TableDef-Objekte zugreifen kann. Auflistungen stellen je nach Typ unterschiedliche Elemente zur Verfügung – in diesem Fall eine Count-Eigenschaft zur Ausgabe der Anzahl der enthaltenen Objekte und die drei Methoden Append, Delete und Refresh. Auf die Elemente von Auflistungen können Sie auf unterschiedliche Art zugreifen. Wenn Sie den Namen des Elements kennen, können Sie ihn in Klammern angeben, um auf das Objekt zuzugreifen: Debug.Print .TableDefs("tblKontakte").RecordCount
Eine andere Möglichkeit bietet der Index der jeweiligen Auflistung: Debug.Print CurrentDb.TableDefs(0).RecordCount
Die folgenden beiden Prozeduren enthalten Beispiele für die Verwendung von Auflistungen. Public Sub TableDefsAuflisten_I() Dim db As DAO.Database Dim tdf As TableDef Set db = CurrentDb For Each tdf In db.TableDefs Debug.Print tdf.Name Next tdf Set db = Nothing End Sub Public Sub TableDefsAuflisten_II() Dim db As DAO.Database Dim intAnzahl As Integer Set db = CurrentDb intAnzahl = db.TableDefs.count For i = 0 To intAnzahl Debug.Print db.TableDefs(i).Name - 1
Objekte
581 Next i
Set db = Nothing End Sub Listing 13.3: Beispiele für die Verwendung von Auflistungen
13.2.2 Erzeugen eines Objekts Wie Sie bereits oben erfahren haben, lassen sich manche Objekte über implizite Methoden wie die CurrentDB-Methode oder die OpenRecordset-Methode erzeugen. In manchen Fällen ist allerdings eine explizite Instanzierung erforderlich. Die ADODBBibliothek erfordert im Vergleich zu ihrer Vorgängerin, der DAO-Bibliothek, häufiger die explizite Instanzierung: Public Sub ExpliziteInstanzierung() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset 'implizite Instanzierung Set cnn = CurrentProject.Connection 'explizite Instanzierung Set rst = New ADODB.Recordset rst.Open "tblKontakte", cnn, adOpenDynamic, adLockOptimistic Debug.Print rst.RecordCount rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 13.4: Beispiele für implizite und explizite Instanzierung von Objektvariablen
13.2.3 Zugriff auf die Methoden, Eigenschaften und Ereignisse eines Objekts Die Methoden und Eigenschaften von Objekten können über den Punkt-Operator angesprochen werden.
Eigenschaften Der Zugriff auf die Eigenschaften eines Objekts kann prinzipiell lesend und schreibend erfolgen, aber auch auf eine der beiden Möglichkeiten beschränkt sein.
582
13
Objektorientierte Programmierung
Das Schreiben eines Wertes in eine Eigenschaft erfolgt mit folgender Syntax: .<Eigenschaft> =
Beispiel für das Füllen eines Textfeldes im aktuellen Formular (Schlüsselwort Me) mit einer neuen Zeichenkette: Me!txtBeispiel.Value = "Text"
Da es sich bei der Eigenschaft Value um die Default-Eigenschaft von Textfeldern handelt, können Sie diese Anweisung auch abkürzen: Me!txtBeispiel = "Text"
Das Lesen einer Eigenschaft eines Objekts sieht ähnlich aus: .<Eigenschaft>
Folgende Beispielanweisung liest den Inhalt eines Textfeldes im aktuellen Formular und gibt ihn im Direktfenster aus: Debug.Print Me.txtBeispiel
Etwas anders sieht es aus, wenn die Eigenschaft einen Verweis auf ein Objekt enthält. Das Setzen einer solchen Eigenschaft auf ein Objekt folgt dieser Syntax: Set .<Eigenschaft> =
Methoden Auch Methoden werden über die Punkt-Syntax referenziert: .<Methode>
Wie bei herkömmlichen VBA-Routinen verbergen sich auch hinter den Methoden eines Objekts Function- und Sub-Prozeduren mit oder ohne Parameter. Grundsätzlich ruft man Sub-Methoden mit Parameter ohne Klammern auf: .<Methode> <Parameterliste>
Function-Methoden sollen Werte zurückliefern, also muss man die Syntax mit Klammern verwenden: = .<Methode>(<Parameterliste>)
Klassenmodule
583
13.2.4 Lebensdauer eines Objekts Objekte beginnen ihr Dasein mit der Instanzierung. Wie lange ein Objekt »lebt«, hängt von zwei Faktoren ab – dem Gültigkeitsbereich und einer eventuellen manuellen Zerstörung. Es gibt folgende Gültigkeitsbereiche: Global: Die globale Gültigkeit wird in einem Standardmodul über die Deklaration einer öffentlichen Objektvariablen erreicht. Der Gültigkeitsbereich endet entweder mit der manuellen Zerstörung der Variablen oder mit dem Beenden der Anwendung. Modulweit: Die Objektvariable wird in einem Klassenmodul oder Formular-/ Berichtsmodul deklariert. Der Gültigkeitsbereich beginnt mit dem Instanzieren eines Objekts auf Basis des Klassenmoduls beziehungsweise mit dem Öffnen eines Formulars oder Berichts oder dem Instanzieren des jeweiligen Moduls.
13.3 Klassenmodule Wenn Sie in Ihrer Anwendung mit benutzerdefinierten Objekten arbeiten möchten, müssen Sie zunächst entsprechende Klassenmodule anlegen und darin die Eigenschaften und Methoden festlegen, die das Objekt zur Verfügung stellen soll.
13.3.1 Anlegen eines Klassenmoduls Das Anlegen eines Klassenmoduls mit seinen Eigenschaften und Methoden lernen Sie am Beispiel eines Bankkontos kennen. Zum Anlegen eines Klassenmoduls gibt es mehrere Möglichkeiten: Im Access-Hauptfenster wählen Sie den Menüeintrag Einfügen/Klassenmodul. Im VBA-Editor verwenden Sie entweder denselben Menüeintrag wie im AccessHauptfenster oder den Eintrag Einfügen/Klassenmodul des Projektexplorers (siehe Abbildung 13.2). Den Projektexplorer aktivieren Sie am schnellsten mit (Strg) + (r).
13.3.2 Benennen des Klassenmoduls Das neue Klassenmodul erhält automatisch den Namen Klasse1 (außer, dieser Name ist bereits vergeben – dann wird statt der 1 die nächst höhere noch nicht verwendete Zahl angehängt). Diesen Namen ersetzen Sie natürlich durch eine sinnvollere Bezeichnung, die aus dem Präfix cls (im Gegensatz zum Präfix mdl für Standardmodule) und dem Singular des Namens des beschriebenen Objekts besteht. Im vorliegenden Fall soll ein Objekt mit den Eigenschaften und Methoden eines Kontos erzeugt werden, also vergeben Sie für das entsprechende Klassenmodul den Namen clsKonto.
584
13
Objektorientierte Programmierung
Abbildung 13.2: Einfügen eines neuen Klassenmoduls
Um den Modulnamen zu ändern, müssen Sie das Modul zunächst speichern ((Strg) + (S)). Im nun erscheinenden Speichern unter-Dialog tragen Sie dann den gewünschten Namen ein. Zum späteren Ändern des Namens eines Klassenmoduls gibt es zwei Möglichkeiten: Klicken Sie im Datenbankfenster einfach auf den zu ändernden Eintrag oder ändern Sie den Namen direkt im VBA-Editor, indem Sie das Eigenschaftsfenster aktivieren ((F4)) und dort die Änderung vornehmen. Beide Varianten setzen das vorherige Speichern des Klassenmoduls voraus.
13.4 Eigenschaften einer Klasse Bevor Sie Eigenschaften und Methoden im Klassenmodul anlegen, empfiehlt sich ihre Skizzierung in Form eines Klassendiagramms wie in Abbildung 13.3. Das scheint im vorliegenden Fall vielleicht ein wenig übertrieben, aber es ist sicher kein Fehler, sich vor dem Programmieren noch einmal mit dem Plan auseinander zu setzen. Die Kontoklasse hat die vier Eigenschaften Besitzer, Kontonummer, Kontostand und Dispositionsrahmen sowie die beiden Methoden Einzahlen() und Auszahlen(). Den Besitzer könnte man auch in einer eigenen Klasse modellieren, aber für dieses einfache Beispiel soll sein Name in der Kontoklasse gespeichert werden.
Eigenschaften einer Klasse
585
clsKonto Besitzer Kontonummer Kontostand Dispositionsrahmen Einzahlen() Auszahlen()
Abbildung 13.3: Die Kontoklasse im Überblick
13.4.1 Öffentliche und nicht öffentliche Eigenschaften Das Klassenmodul enthält für jede Eigenschaft der Klasse eine Variable. Eine Variable, die von außen änderbar sein soll, deklariert man in Standardmodulen als öffentliche Variable. Der Inhalt des Klassenmoduls clsKonto würde dann folgendermaßen aussehen: Option Compare Database Option Explicit Public Public Public Public
Besitzer As String Kontonummer As Long Kontostand As Currency Dispositionsrahmen As Currency
Listing 13.5: Kontoklasse mit öffentlichen Eigenschaften
Die Eigenschaften des Kontos wären dann von allen Stellen aus les- und schreibbar, von denen man auch auf das Objekt zugreifen könnte. Sie könnten beispielsweise die folgenden Anweisungen im Testfenster ((Strg) + (G)) absetzen und damit den Kontostand lesen und auf einen beliebigen Wert ändern (Details über das Instanzieren eines Objekts und den Zugriff auf dessen Eigenschaften finden Sie in Abschnitt 13.2, »Objekte«): Set objKonto = New clsKonto objKonto.Kontostand = 100 Debug.Print objKonto.Kontostand
Die Debug.Print-Anweisung gibt im Testfenster den Wert 100 als aktuellen Kontostand aus, der zuvor manuell auf diesen Wert eingestellt wurde. Die Kapselung wird mit der öffentlichen Deklaration der Variablen wie mit einem Vorschlaghammer zerstört. Das wird gerade im Beispiel des Bankkontos deutlich: Auch sorgfältig festgelegte Geschäftsregeln können nicht greifen, wenn man sie von außen umgehen kann. In diesem Beispiel könnte man etwa den Kontostand so verändern, dass er nicht mehr mit dem Dispositionsrahmen vereinbar ist.
586
13
Objektorientierte Programmierung
13.4.2 Zugriff auf die Eigenschaften einer Klasse kontrollieren Sie müssen die Eigenschaften also auf irgendeine Weise vor dem direkten Zugriff von außen schützen und damit die Kapselung realisieren. Dazu sind zwei Schritte erforderlich. 1. Deklarieren Sie die Variablen der Klasse mit dem Schlüsselwort Private und verhindern Sie so den direkten Zugriff von außen. 2. Erstellen Sie öffentliche Methoden für den kontrollierten Zugriff auf die privaten Eigenschaften. Schritt 1 lässt sich leicht in die Tat umsetzen. Das folgende Listing enthält die als Private deklarierten Eigenschaften. Außerdem hat jede Eigenschaft ein m als Präfix erhalten. Damit kennzeichnet man die Member-Variablen eines Objekts – das sind Variablen, die zwar privat sind, aber über entsprechende Prozeduren gelesen oder geschrieben werden können. Option Compare Database Option Explicit Private Private Private Private
mBesitzer As clsKunde mKontonummer As Long mKontostand As Currency mDispositionsrahmen As Currency
Listing 13.6: Deklaration privater Variablen in einem Klassenmodul
Nachdem die Variablen nun vor dem unkontrollierten Zugriff von außen geschützt sind, können Sie die Möglichkeit zum Lesen und Schreiben der Variable nach Bedarf freigeben. Dazu verwenden Sie so genannte Eigenschaftsprozeduren. Es gibt drei unterschiedliche Arten von Eigenschaftsprozeduren: 1. Eine Property Get-Prozedur gibt je nach dem Variablentyp einen Verweis auf ein Objekt oder den Wert der Variablen zurück. 2. Eine Property Let-Prozedur schreibt den als Parameter übergebenen Wert in die angegebene Variable. Diese Prozedurart können Sie nur verwenden, wenn die Variable einen konkreten Wert erhält. 3. Eine Property Set-Prozedur weist der angegebenen Variablen einen Verweis auf das als Parameter übergebene Objekt zu. Diese Prozedurart ist das Pendant für die Property Let-Prozedur für Objektvariablen.
Eigenschaften einer Klasse
587
Skalare Variable versus Objektvariable Bei der Verwendung von Eigenschafts-Prozeduren für den Zugriff auf skalare Variablen und Objektvariablen gibt es einige Unterschiede. Der Grund sind Unterschiede zwischen den Datentypen selbst. Damit Sie jeweils den richtigen Fall auswählen, finden Sie nachfolgend eine kurze Erläuterung und Einordnung dieser beiden Variablenarten. Skalare Variablen sind Variablen mit eingebauten Datentypen wie String, Integer oder Long, Aufzählungstypen und benutzerdefinierte Datentypen. Diese Datentypen haben gemein, dass sie konkrete Werte enthalten und je nach Datentyp den entsprechenden Speicherplatz reservieren. Die Zuweisung von Werten an solche Datentypen erfolgt durch Verwendung des Gleichheitszeichens und des entsprechenden Wertes: intZahl = 1
Im Gegensatz dazu enthalten Objektvariablen kein Objekt, sondern lediglich einen Verweis darauf. Das bringt einige Besonderheiten mit sich. Speicherplatz wird nach der Deklaration nur für den Verweis selbst reserviert. Der Verweis auf das Objekt erfordert die Verwendung des Schlüsselwortes Set: Set objKunde = New clsKunde
Es können auch mehrere Objektvariablen auf dasselbe Objekt verweisen: Set objKunde = New clsKunde Set objPartner = objKunde
Vergleiche zweier Objektvariablen erfolgen über den Operator Is: If objKunde Is objPartner Then MsgBox "Kunde und Partner sind gleich." End If
Die Prüfung, ob eine Objektvariable auf ein Objekt verweist, erfolgt über den Vergleich mit dem Operator Is und dem Vergleichswert Nothing: If objKunde Is Nothing Then MsgBox "Kunde ist nicht gesetzt." End If
Das Aufheben des Verweises erfolgt beim Verlassen des Gültigkeitsbereichs (also etwa beim Beenden der Prozedur, in der der Verweis gesetzt wurde) oder durch explizites Setzen des Verweises auf den Wert Nothing.
588
13
Objektorientierte Programmierung
Tipp: Setzen Sie jede erzeugte Objektvariable per Code wieder auf den Wert Nothing. Manchmal benötigt man einen Verweis auf eine Objektvariable längst nicht mehr, obwohl ihr Gültigkeitsbereich noch nicht verlassen wurde. Zugunsten einer ressourcenschonenden Programmierweise sollten Sie eine Objektvariable daher auf den Wert Nothing setzen, sobald diese nicht mehr benötigt wird. Am besten fügen Sie dem Code mit dem Objektverweis auch direkt eine Anweisung zum Leeren des Objektverweises hinzu und schreiben erst dann den Code, der auf die Objektvariable zugreift.
13.4.3 Property Let: Setzen von skalaren Variablen Wenn Sie den schreibenden Zugriff auf eine Variable eines Objekts erlauben möchten, müssen Sie dazu eine Property Let-Prozedur verwenden. Eine solche Prozedur erwartet als Parameter den Wert, auf den die Variable gesetzt werden soll. Das folgende Listing enthält ein Beispiel für die Variable Kontonummer: Public Property Let Kontonummer(lngKontonummer As String) mKontonummer = lngKontonummer End Property Listing 13.7: Property-Prozedur für den schreibenden Zugriff auf eine private Variable
Die Property Let-Prozedur erwartet den Eingabeparameter lngKontonummer nicht in der für Prozeduren üblichen Art, auch wenn es die Notation des Prozedurkopfs erwarten ließe. Statt dessen weisen Sie den Wert einfach der Eigenschaft mit dem Namen der Prozedur zu: Public Sub KontonummerZuweisen() Dim objKonto As clsKonto Set objKonto = New clsKonto 'Kontonummer zuweisen objKonto.Kontonummer = 123456789 '... Set objKonto = Nothing End Sub Listing 13.8: Zuweisen einer Objekt-Eigenschaft
Eigenschaften einer Klasse
589
13.4.4 Property Set: Setzen von Objektvariablen Das Zuweisen von Objektvariablen funktioniert prinzipiell genauso wie das Zuweisen einer skalaren Variablen. Der Unterschied ist, dass die Eigenschaftsprozedur das Schlüsselwort Set statt Let enthält und dass die eigentliche Zuweisung das Schlüsselwort Set erfordert: Public Property Set Besitzer(objBesitzer As clsKunde) Set mBesitzer = objBesitzer End Property Listing 13.9: Diese Property-Prozedur erlaubt den schreibenden Zugriff auf eine Objektvariable.
Der Zugriff auf private Objektvariablen über eine Property Set-Prozedur erfolgt wie im folgenden Beispiel: Public Sub KontoErzeugen() Dim objKonto As clsKonto Set objKonto = New clsKonto With objKonto Set .Besitzer = New clsKunde End With End Sub Listing 13.10: Die Objektvariable Besitzer erhält einen Verweis auf ein neues Kundenobjekt.
13.4.5 Property Get: Lesen von skalaren Variablen und Objektvariablen Property Get-Prozeduren dienen dem lesenden Zugriff sowohl auf skalare als auch auf
Objektvariablen. Diese Prozeduren arbeiten prinzipiell wie Funktionen: Sie geben den Inhalt der gewünschten Membervariablen des Objekts als Funktionswert zurück.
Skalare Variablen lesen Die Eigenschafts-Prozedur im folgenden Listing erlaubt den lesenden Zugriff auf die private Variable mKontonummer. Die Routine weist dem Rückgabewert Kontonummer den Inhalt der Variablen mKontonummer zu. Public Property Get Kontonummer() As String Kontonummer = mKontonummer End Property Listing 13.11: Property-Prozedur für den lesenden Zugriff auf eine private Variable
590
13
Objektorientierte Programmierung
Mit der folgenden Prozedur weisen Sie der Eigenschaft Kontonummer zunächst einen Wert zu und verwenden dann die Property Get-Prozedur, um den Wert in einem Meldungsfenster auszugeben: Public Sub KontonummerAusgeben() Dim objKonto As clsKonto Set objKonto = New clsKonto 'Wert zuweisen per Property Let objKonto.Kontonummer = 123456789 'Wert ausgeben per Property Get MsgBox "Die Kontonummer lautet: " & objKonto.Kontonummer Set objKonto = Nothing End Sub Listing 13.12: Aufeinander folgendes Aufrufen der Property Let- und der Property Get-Prozedur des Konto-Objekts
Objektvariablen lesen Das Lesen von Objektvariablen erfolgt ebenfalls mit einer Property Get-Prozedur. Allerdings verwendet man zum Zuweisen des Verweises innerhalb der Prozedur das Set-Schlüsselwort: Public Property Get Besitzer() As clsKunde Set Besitzer = mBesitzer End Property Listing 13.13: Property Get-Prozedur für eine Objektvariable
Das Zuweisen der Objektvariablen eines Objekts an eine andere Objektvariable erfolgt mit der Set-Anweisung und der entsprechenden Eigenschaft des Objekts: Set objKunde = objKonto.Besitzer
13.4.6 Vertrauen ist gut, Kontrolle ist besser Weiter oben war vom »kontrollierten Zugriff« auf Eigenschaften die Rede. Nachdem Sie nun wissen, wie Sie den schreibenden und lesenden Zugriff auf die Eigenschaften einer Klasse realisieren, müssen Sie nur noch entsprechende Kontrollfunktionen einbauen. Die Property Let-/Set-/Get-Prozeduren sind nämlich nicht auf die Anweisung zum Weiterleiten des Übergabewertes beschränkt, sondern können noch weitere Anweisungen aufnehmen. So könnten Sie beispielsweise das Setzen des Kontostandes in Abhängigkeit vom Dispositionsrahmen kontrollieren: Public Property Let Kontostand(curKontostand As Currency) If curKontostand < mDispositionsrahmen Then MsgBox "Der gewünschte Kontostand konnte nicht eingestellt werden."
Methoden einer Klasse
591
Else mKontostand = curKontostand End If End Property Listing 13.14: Prüfen einer Eingabe in einer Property Let-Prozedur
Wenn Sie wie mit der folgenden Prozedur den Kontostand auf einen Wert einstellen möchten, der unter dem Dispositionsrahmen liegt, erscheint eine entsprechende Meldung. Public Sub KontostandEinstellen() Dim objKonto As clsKonto Set objKonto = New clsKonto objKonto.Dispositionsrahmen = 0 objKonto.Kontostand = -100 Set objKonto = Nothing End Sub Listing 13.15: Einstellen eines ungültigen Kontostands
Auf die gleiche Weise lässt sich beispielsweise protokollieren, wann bestimmte Daten abgerufen oder geändert wurden.
13.5 Methoden einer Klasse Es gibt zwei unterschiedliche Arten von Methoden, die Sie schon von der prozeduralen Programmierung her kennen: Function-Prozeduren und Sub-Prozeduren. Eine Klasse kann private und öffentliche Methoden enthalten, aber nur die öffentlichen Methoden sind über die Schnittstelle ansprechbar. Für Function- und Sub-Prozeduren gelten dabei genau die gleichen Regeln wie bei der prozeduralen Programmierung: Sie können beiden Parameter übergeben, aber nur eine Funktion liefert auch einen Wert zurück. Die folgende Sub-Prozedur Einzahlen erwartet als Parameter den einzuzahlenden Betrag und ändert den Wert der Membervariablen mKontostand: Public Sub Einzahlen(curBetrag As Currency) mKontostand = mKontostand + curBetrag End Sub Listing 13.16: Beispiel einer Objekt-Methode
592
13
Objektorientierte Programmierung
Die Prozedur lässt sich mit der Routine aus folgendem Listing prüfen: Public Sub Einzahlung() Dim objKonto As clsKonto Set objKonto = New clsKonto objKonto.Kontostand = 0 objKonto.Einzahlen 100 MsgBox "Der neue Kontostand beträgt EUR " & objKonto.Kontostand Set objKonto = Nothing End Sub Listing 13.17: Anwendung der Einzahlen-Methode
Als Beispiel für eine Function-Prozedur dient der Auszahlungsvorgang: Da eine Auszahlung wegen fehlender Deckung fehlschlagen könnte, gibt die folgende Funktion den Wert False zurück, falls die Auszahlung nicht möglich ist: Public Function Auszahlen(curBetrag As Currency) As Boolean If mKontostand - curBetrag < mDispositionsrahmen Then Auszahlen = False Else mKontostand = mKontostand - curBetrag Auszahlen = True End If End Function Listing 13.18: Beispiel einer Objekt-Methode mit Rückgabewert
Der folgende Beispielaufruf der Methode Auszahlen versucht, einen größeren Betrag als erlaubt abzuheben: Public Sub Auszahlung() Dim objKonto As clsKonto Set objKonto = New clsKonto objKonto.Kontostand = 0 objKonto.Dispositionsrahmen = -100 If objKonto.Auszahlen(200) = False Then MsgBox "Der Betrag konnte nicht ausgezahlt werden." End If Set objKonto = Nothing End Sub Listing 13.19: Test der Auszahlen-Methode
Standardereignisse in Klassen
593
13.6 Standardereignisse in Klassen VBA-Klassenmodule liefern zwei Standardereignisse mit: Initialize und Exit. Sie können die entsprechenden Ereignisprozeduren einfügen, indem Sie im linken Kombinationsfeld des Codefensters den Eintrag Class und im rechten das gewünschte Ereignis auswählen (siehe Abbildung 13.4). Diese Ereignisse werden beim Initialisieren und beim Zerstören jeder Instanz dieser Klasse ausgelöst. Sie können dort beispielsweise Anweisungen zum Instanzieren von in der Klasse benötigten Objekten unterbringen. Ein Beispiel für den Einsatz dieser Ereignisse finden Sie etwa in Abschnitt 13.8.2, »Benutzerdefinierte Auflistungsklassen«.
Abbildung 13.4: Einfügen der Standardereignisse einer Klasse
13.7 Benutzerdefinierte Ereignisse Benutzerdefinierte Ereignisse sind eine sehr mächtige Funktion von VBA. Bevor Sie selbst solche Ereignisse anlegen und lernen, wie Sie diese aufrufen und abfangen können, sehen Sie sich ein Objekt an, das bereits Ereignisse besitzt, und schauen Sie, wie Sie dessen Ereignisse abfangen und für Ihre Zwecke einsetzen können.
13.7.1 Ereignisse abfangen Ein Beispiel für ein Objekt, auf dessen Ereignisse Sie in jedem Fall angewiesen sind, ist das Webbrowser-Steuerelement. Angenommen Sie möchten eine Internetseite aufrufen und eine bestimmte Information daraus abfragen – und das auch noch automatisiert.
594
13
Objektorientierte Programmierung
Dazu benötigen Sie nicht den Internet Explorer, sondern das oben erwähnte Steuerelement, das sich bequem in ein Formular einbauen lässt: 1. Legen Sie ein neues Formular an und öffnen Sie es in der Entwurfsansicht. 2. Wählen Sie aus der Menüleiste den Eintrag Einfügen/ActiveX-Steuerelement… aus. 3. Markieren Sie im nun erscheinenden Dialog den Eintrag Microsoft Webbrowser und klicken Sie auf OK (siehe Abbildung 13.5).
Abbildung 13.5: Auswahl eines ActiveX-Steuerelements
Access fügt das Webbrowser-Steuerelement in das Formular ein. Ändern Sie den Namen des Steuerelements auf ctlWebbrowser ab (siehe Abbildung 13.6). Nun legen Sie eine Schaltfläche namens cmdLoad an, mit der Sie das Laden einer bestimmten Internetseite (beispielsweise http://www.access-im-unternehmen.de) starten. Und dann folgt endlich das Beispiel für ein Ereignis: Wenn die Seite fertig geladen ist, soll das Formular ein Meldungsfenster anzeigen. Damit ein Klick auf die Schaltfläche das Laden der Seite auslöst, hinterlegen Sie eine entsprechende Prozedur für die Ereigniseigenschaft Beim Klicken der Schaltfläche. Im Codefenster müssen Sie nun zunächst eine Objektvariable mit einem Verweis auf das Webbrowser-Steuerelement deklarieren und diesem das Steuerelement zuweisen. Normalerweise würden Sie dabei wie in Abbildung 13.7 vorgehen. Da Sie allerdings auf die Ereignisse des Webbrowser-Steuerelements zugreifen möchten, müssen Sie das entsprechende Objekt mit dem Schlüsselwort WithEvents deklarieren. Außerdem werden Sie früher oder später von mehreren Prozeduren auf das Webbrowser-Objekt zugreifen wollen und dieses auch nicht erst beim Klicken auf die Schaltfläche zum Laden der Internetseite instanzieren. Der folgende Code erfüllt alle diese Voraussetzungen:
Benutzerdefinierte Ereignisse
Abbildung 13.6: Das Webbrowser-Steuerelement
Abbildung 13.7: Eigenschaften und Methoden des Webbrowser-Steuerelements
Option Compare Database Dim WithEvents objWebbrowser As WebBrowser Private Sub cmdLoad_Click() 'Eine Internetseite laden objWebbrowser.Navigate "http://www.access-im-unternehmen.de" End Sub
595
596
13
Objektorientierte Programmierung
Private Sub objWebbrowser_NavigateComplete2(ByVal pDisp As Object, _ URL As Variant) 'Wenn fertiggeladen, Meldung ausgeben MsgBox objWebbrowser.LocationURL End Sub Private Sub Form_Close() 'Zerstören des Objektverweises beim Schließen Set objWebbrowser = Nothing End Sub Private Sub Form_Open(Cancel As Integer) 'Zuweisen des Objekts beim Öffnen des Formulars Set objWebbrowser = Me!ctlWebbrowser.Object End Sub Listing 13.20: Klassenmodul des Webbrowser-Formulars
Die Deklaration mit WithEvents kann nur in Klassenmodulen erfolgen – in allein stehenden und auch in denen von Formularen und Berichten. Sie können nicht das Schlüsselwort New in Zusammenhang mit WithEvents verwenden. Durch die Verwendung des Schlüsselworts WithEvents bei der Deklaration des Webbrowser-Objekts stehen alle Ereignisse dieses Objekts im Formularmodul zur Verfügung. Im Beispielcode kommt davon das Ereignis NavigateComplete2 zur Geltung: Es wird ausgelöst, wenn das Webbrowser-Steuerelement die angeforderte Seite fertig geladen hat. Die Prozedur objWebbrowser_NavigateComplete2 fängt also ein Ereignis eines eingebundenen Objekts ab und gibt eine Meldung mit dem URL der geladenen Seite aus.
13.7.2 Eigene Ereignisse anlegen Genau wie das Webbrowser-Objekt im vorherigen Beispiel können auch benutzerdefinierte Klassen Ereignisse bereitstellen. Benutzerdefinierte Ereignisse bieten die Möglichkeit, von außen auf bestimmte Ereignisse in einer betroffenen Klasse zu »lauschen«. Und das funktioniert folgendermaßen: 1. Sie legen in einer Klasse ein Ereignis fest. Dazu verwenden Sie das Event-Schlüsselwort. 2. Bei Bedarf löst irgendeine Prozedur innerhalb der Klasse dieses Ereignis aus. Dazu ist die RaiseEvent-Anweisung erforderlich. 3. Die Klasse mit dem Ereignis deklarieren Sie in der Klasse, die auf das Eintreten des Ereignisses reagieren soll, mit dem Schlüsselwort WithEvents. 4. Schließlich legen Sie eine entsprechende Ereignisprozedur an.
Benutzerdefinierte Ereignisse
597
Das folgende Beispiel füllt die Vorgehensweise mit Leben. Dabei spielen zwei Formulare die Hauptrolle: Das erste Formular namens frmKontakte enthält ein Listenfeld zur Auswahl von Kontakten. Per Doppelklick auf das Listenfeld oder mit einem einfachen Klick auf die Schaltfläche soll das Formular frmKontaktDetails geöffnet werden und den ausgewählten Datensatz anzeigen (siehe Abbildung 13.8).
Abbildung 13.8: Formulare des Beispiels für benutzerdefinierte Ereignisse
Ziel der Übung ist, dem Detailformular ein Ereignis zu verpassen, das beim Ändern des angezeigten Datensatzes ausgelöst wird. Das aufrufende Formular soll mit einer Ereignisprozedur auf dieses Ereignis reagieren und den Inhalt des Listenfeldes aktualisieren. Nun öffnet man Detailformulare in der Regel als modalen Dialog, indem man die OpenForm-Methode des DoCmd-Objekts verwendet und dabei den Parameter WindowMode auf True setzt. Die Ausführung der aufrufenden Prozedur wird dann so lange unterbrochen, bis das aufgerufene Formular entweder unsichtbar gemacht oder geschlossen wird – dann sorgt eine entsprechende Requery-Methode für die Aktualisierung des Inhalts des Listenfeldes. Diese Methode wird auch aufgerufen, wenn der Benutzer den Kontakt-Datensatz nur ansehen möchte und ihn gar nicht ändert – in diesem Fall also völlig unnötig.
598
13
Objektorientierte Programmierung
Hinzufügen des Ereignisses Der erste Schritt auf dem Weg zur benutzerdefinierten Ereignisbehandlung ist das Anlegen des Ereignisses. Das Ereignis soll Change heißen und es sind keine Parameter notwendig. Dementsprechend sieht die Deklaration des Ereignisses wie folgt aus: Public Event Change()
Auslösen des Ereignisses Nachdem Sie das Ereignis angelegt haben, müssen Sie einen Zeitpunkt auswählen, an dem das Ereignis ausgelöst werden soll. Die Ereignisprozedur Nach Aktualisierung des Formulars scheint die richtige Wahl zu sein: Sie wird nur ausgelöst, wenn der Datensatz geändert wurde und deshalb gespeichert werden soll. Legen Sie die Ereignisprozedur an, indem Sie die beiden Kombinationsfelder im Codefenster auf die Einträge Form und AfterUpdate einstellen (siehe Abbildung 13.9) und ergänzen Sie den automatisch angelegten Prozedurrumpf wie folgt: Private Sub Form_AfterUpdate() 'Ereignis auslösen RaiseEvent Change End Sub Listing 13.21: Auslösen eines Ereignisses
Abbildung 13.9: Anlegen der Ereignisprozedur, die wiederum ein Ereignis auslösen soll
Benutzerdefinierte Ereignisse
599
Auf ein Ereignis reagieren Um mit einer Ereignisprozedur auf ein Ereignis zu reagieren, müssen einige Voraussetzungen erfüllt sein: Nur Klassenmodule (einschließlich Formular- und Berichtsmodule) können Ereignisprozeduren implementieren. Die Klasse, die das Ereignis enthält, muss in der Klasse, in der mit einer Ereignisprozedur auf das Ereignis reagiert werden soll, mit dem Schlüsselwort WithEvents in Form einer Objektvariablen deklariert werden: Private WithEvents objKontaktDetail As Form_frmKontaktDetail
Die Objektvariable muss auf eine Instanz der Klasse mit dem Ereignis verweisen. Die erste Bedingung ist erfüllt, da die Ereignisprozedur im aufrufenden Formular ausgewertet werden soll. Die für die zweite Bedingung notwendige Codezeile können Sie einfach von dort in das Klassenmodul des Übersichtsformulars übernehmen. Ein kleines Problem ist die dritte Bedingung, denn, wie bereits weiter oben erwähnt, öffnet man ein Formular in der Regel mit der DoCmd.OpenForm-Anweisung – gerade, weil dies offensichtlich der einzige Weg ist, ein Formular modal zu öffnen. Außerdem lässt sich so bequem eine Where-Bedingung mitgeben: DoCmd.OpenForm "frmSchnellsuchePerKombifeld", _ WhereCondition:="KontaktID = " & Me!lstKontakte, _ WindowMode:=acDialog
Das ist aber auch nur die halbe Wahrheit: Formulare stellen eine Eigenschaft namens Modal zur Verfügung, die allerdings im Eigenschaftsfenster nicht auftaucht. Die Eigenschaft lässt sich also nur per Code ändern – und das auch nur in der Entwurfsansicht des Formulars. Danach lässt sich das Formular ausschließlich im modalen Modul öffnen – aber für dieses Beispiel ist das durchaus in Ordnung. Öffnen Sie also das Formular frmKontaktDetail in der Entwurfsansicht und geben Sie im Testfenster die folgende Anweisung ein: Forms!frmKontaktDetail.Modal = True
Anschließend speichern und schließen Sie das Formular. Nun können Sie auf die DoCmd.OpenForm-Anweisung verzichten, eine neue Instanz des Formulars mit der New-Anweisung erstellen und direkt mit der Objektvariable objKontaktDetail darauf verweisen. Die notwendige Where-Bedingung ersetzen Sie durch die Verwendung der beiden Eigenschaften Filter und FilterOn. Erst durch Setzen der Visible-Eigenschaft auf den Wert True wird das Formular sichtbar gemacht – als modaler Dialog.
600
13
Objektorientierte Programmierung
Die Voraussetzungen zum Abfangen von Ereignissen des so geöffneten Formulars per Ereignisprozedur wären damit erfüllt. Private Sub Anzeigen() 'Wenn Kontakt ausgewählt If Not IsNull(Me!lstKontakte) Then 'Neues Formular instanzieren Set objKontaktDetail = New Form_frmKontaktDetail With objKontaktDetail 'Filter setzen .Filter = "KontaktID = " & Me!lstKontakte .FilterOn = True 'Sichtbar machen .Visible = True End With End If End Sub Listing 13.22: Setzen der Objektvariable auf eine neue Instanz des Formulars frmKontaktDetail
Nun müssen Sie im aufrufenden Formular nur noch die gewünschte Ereignisprozedur anlegen. Dazu wählen Sie im linken Kombinationsfeld des Codefensters den Eintrag objKontaktDetail aus – der Prozedurrumpf für das einzige zur Verfügung stehende Ereignis wird automatisch angelegt (siehe Abbildung 13.10).
Abbildung 13.10: Anlegen einer Ereignisprozedur für die benutzerdefinierte Eigenschaft
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
601
Den Prozedurrumpf müssen Sie nun nur noch mit der für die Aktualisierung notwendigen Anweisung füllen: Private Sub objKontaktDetail_Change() 'Listenfeld nach Änderungen im Detailformular aktualisieren Me.lstKontakte.Requery End Sub Listing 13.23: Ereignisprozedur zum Aktualisieren des Listenfeldes
13.8 Benutzerdefinierte Auflistungen mit dem Collection-Objekt Viele VBA-Objekte sind Bestandteil von Auflistungen und enthalten selbst wiederum Auflistungen mit anderen Objekten. Ein Beispiel aus der DAO-Bibliothek ist das Database-Objekt, das eine TableDefs-Auflistung mit TableDef-Objekten enthält, die wiederum eine Fields-Auflistung mit Field-Objekten bereitstellt. Diese Möglichkeit ist sehr interessant, da man damit leicht auf die komplette Sammlung enthaltener Objekte zugreifen kann. Das sieht dann beispielsweise so wie im folgenden Listing aus. Die Routine TabellenUndFelder gibt alle Tabellen der aktuellen Datenbank inklusive Feldnamen aus. Dabei durchläuft sie zwei For Each-Schleifen: die erste bearbeitet die Auflistung TableDefs des Database-Objekts, die zweite die Auflistung Fields des aktuellen TableDef-Objekts. Die Ausgabe sieht wie in Abbildung 13.11 aus. Public Function TabellenUndFelder() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field Set db = CurrentDb For Each tdf In db.TableDefs Debug.Print tdf.Name For Each fld In tdf.Fields Debug.Print " " & fld.Name Next fld Debug.Print "=================" Next tdf
602
13
Objektorientierte Programmierung
Set db = Nothing End Function Listing 13.24: Ausgabe von Tabellen und Feldern per Auflistung
Abbildung 13.11: Ausgabe verschachtelter For Each-Schleifen über zwei Collections
Auflistungen mit dem Collection-Objekt Das Collection-Objekt ermöglicht die benutzerdefinierte Verwendung von Auflistungen, die nicht nur Zahlen oder Zeichenketten, sondern komplexe Objekte enthalten können. Diese können auch wieder Auflistungen enthalten, sodass sich die oben dargestellte Technik zum Durchlaufen von einfachen oder verschachtelten Auflistungen realisieren lässt (siehe Abschnitt 13.8.3, »Benutzerdefinierte Auflistungsklassen«). Bevor Sie sich an ein solches – zugegebenermaßen relativ komplexes – Beispiel begeben, lernen Sie an einem einfacheren Beispiel den grundlegenden Umgang mit dem Collection-Objekt kennen.
13.8.1 Auflistungen selbst gemacht Ein Beispiel für die Verwendung des Collection-Objekts ist eine Auflistung zum Speichern globaler Variablen. Globale Variablen sind generell anfällig, da sie leicht aus Versehen geändert werden können. Ein wenig sicherer ist die Verwendung einer öffentlichen Auflistung. Das folgende Listing zeigt den Inhalt des Moduls mdlGlobal,
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
603
das eine öffentliche Auflistung namens colVariablen deklariert und zwei Routinen zum Setzen und zum Lesen von Variablen bietet: Option Compare Database Option Explicit Public colVariablen As VBA.Collection Public Sub SetVariable(strVariable As String, strWert As Variant) If colVariablen Is Nothing Then Set colVariablen = New VBA.Collection End If On Error Resume Next 'Variable hinzufügen colVariablen.Add strWert, strVariable 'Variable ist bereits vorhanden If Err.Number = 457 Then 'Element löschen ... colVariablen.Remove (strVariable) '... und neu hinzufügen colVariablen.Add strWert, strVariable End If End Sub Public Function GetVariable(strVariable As String) As Variant On Error Resume Next 'Variable ermitteln GetVariable = colVariablen(strVariable) 'Falls nicht vorhanden, Fehlermeldung ausgeben If Err.Number <> 0 Then MsgBox "Variable mit diesem Namen nicht vorhanden!", vbCritical End If End Function Public Sub DeleteVariable(strVariable As String) On Error Resume Next
604
13
Objektorientierte Programmierung
colVariablen.Remove strVariable End Sub Public Function CountVariables() On Error Resume Next CountVariables = colVariablen.count If Err = 91 Then CountVariables = 0 End If End Function Listing 13.25: Beispiel für die Anwendung des Collection-Objekts
Hinzufügen von öffentlichen Variablen Die Routine SetVariable erwartet zwei Parameter: den Namen der Variablen und den zu speichernden Wert. Folgender Aufruf würde etwa eine Variable namens Datenbankpfad und als Wert den Pfad der aktuellen Datenbank als Listeneintrag speichern: SetVariable "Datenbankpfad", CurrentProject.Parent
Die Prozedur verwendet die Add-Methode des Collection-Objekts, um das Wertepaar hinzuzufügen.
Abfragen von öffentlichen Variablen Mit der Funktion GetVariable fragen Sie einen bestehenden Wert ab. Dabei verwendet die Funktion den Variablennamen als Schlüssel des Collection-Objekts und ermittelt so den gewünschten Wert: Debug.Print GetVariable("Datenbankpfad")
Löschen von öffentlichen Variablen Für die Entfernung von Einträgen aus einem Collection-Objekt ist die Remove-Methode zuständig. Sie erwartet den Index des zu entfernenden Eintrags. Ein Beispielaufruf lautet wie folgt: DeleteVariable "Datenbankpfad"
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
605
Anzahl gespeicherter Variablen Auch die Anzahl der Elemente einer Auflistung lässt sich ermitteln. Dazu dient die Count-Eigenschaft des Collection-Objekts.
13.8.2 Benutzerdefinierte Auflistungsklassen Eine benutzerdefinierte Auflistungsklasse kapselt das Collection-Objekt und erweitert dieses um ein beziehungsweise zwei Features (eines davon funktioniert unter Access 2003, aber nicht mit älteren Versionen von Access). Nachfolgend finden Sie das Listing der kompletten Auflistungsklasse, die – relativ sinnfrei – Namen in der Auflistung speichert, ausgibt, löscht und die Anzahl der enthaltenen Einträge ermittelt. Eingebaute Auslistungen lassen sich per For Each-Schleife durchlaufen; außerdem kann man auf eine Standard-Eigenschaft zugreifen und damit Bezüge abkürzen – etwa DBEngine(0) statt DBEngine.Workspaces(0). Ersteres klappt mit dem Collection-Objekt unter Access ab Version 2003, Letzteres selbst unter Access 2003 nicht. Für beide liefert die folgende Variante mit ein paar Tricks nach: Option Compare Database Option Explicit Private mNamen As Collection Private Sub Class_Initialize() Set mNamen = New Collection End Sub Private Sub Class_Terminate() Set mNamen = Nothing End Sub 'Default-Property: Property Get Item(Index As Long) As String 'Attribute Item.VB_UserMemId = 0
Item = mNamen(Index) End Property Property Get Count() As Long Count = mNamen.Count End Property Function AddItem(AName As String) As Long mNamen.Add AName AddItem = Count
606
13
Objektorientierte Programmierung
End Function Sub Remove(Index As Long) mNamen.Remove Index End Sub 'Enumeration der Collection nach außen ermöglichen: Property Get NewEnum() As IUnknown 'Attribute Enumerate.VB_UserMemId = -4
Set NewEnum = mNamen.[_NewEnum] End Property Listing 13.26: Klassenmodul einer Auflistungsklasse
Damit die genannten Features unter Access ab Version 2000 funktionieren, sind folgende Schritte notwendig: Exportieren Sie die Klasse in eine .cls-Datei. Löschen Sie die Klasse aus dem VBA-Projekt. Entfernen Sie in der exportierten Datei die Kommentarzeichen vor den beiden fett gedruckten Zeilen. Importieren Sie die Klasse wieder. Die beiden ehemals auskommentierten Zeilen sind nun nicht mehr sichtbar. Es handelt sich dabei um verborgene Eigenschaften, die aber durchaus ihren Zweck erfüllen. Das können Sie mit der folgenden Prozedur nachweisen: Public Sub BeispielCollection() Dim objNamen As New clsNamenTest Dim varName As Variant With objNamen .AddItem .AddItem .AddItem .AddItem
"Trowitzsch" "André" "Minhorst" "Addison-Wesley"
Debug.Print "Gespeichert sind " & .Count & " Einträge:" For Each varName In objNamen Debug.Print varName Next varName End With Debug.Print "Der erste Eintrag war: " & objNamen(1)
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
607
Set objNamen = Nothing End Sub Listing 13.27: Test der benutzerdefinierten Auflistungsklasse mit Enumeration und Standardeigenschaft
13.8.3 Nachbildung relationaler Beziehungen per Auflistungsklasse Solange Anwendungen einen überschaubaren Rahmen haben und nicht allzu viel VBA-Code notwendig ist, um alle Aufgaben zu erledigen, benötigen Sie Techniken wie die nachfolgend vorgestellte sicher nicht. Unter Umständen müssen Sie aber innerhalb einer Datenbankanwendung beispielsweise Berechnungen durchführen und Daten ermitteln, die über viele Tabellen verteilt sind. In diesem Fall wäre es sicher angenehm, wenn Sie etwa auf verknüpfte Tabellen und ihre Felder über hierarchisch angeordnete Objekte zugreifen könnten – also genau wie im oben genannten Beispiel mit dem Database-Objekt und den enthaltenen Tabellen, die wiederum aus mehreren Feldern bestehen. Als einfaches Beispiel dienen zwei Tabellen, die Mitarbeiter und ihre Abwesenheitszeiten durch Urlaub, Krankheit oder Fortbildung enthalten. Die beiden Tabellen stehen in einer 1:n-Beziehung zueinander. Zusätzlich sollen zum Mitarbeiterobjekt die Abteilung und zum Abwesenheitsobjekt die Abwesenheitsart aus den jeweiligen LookupTabellen hinzugefügt werden (siehe Abbildung 13.12).
Abbildung 13.12: Diese Tabellen sollen durch Objekte abgebildet werden.
Ziel ist es nun, etwa wie in der folgenden Routine auf die in den Tabellen enthaltenen Daten zugreifen zu können. Dort wird ein Objekt namens Personal mit dem Typ clsPersonal instanziert, das eine Auflistung namens AlleMitarbeiter enthält. Die Auf-
608
13
Objektorientierte Programmierung
listung erlaubt genau wie die oben vorgestellten eingebauten Auflistungen das Durchlaufen der einzelnen Elemente und sogar das Ausgeben der Eigenschaften dieser Elemente wie MitarbeiterID, Vorname oder Nachname. Die Ausgabe dieser Routine soll wie in Abbildung 13.12 aussehen: Public Function AlleMitarbeiterAusgeben() Dim Personal As clsPersonal Dim Mitarbeiter As clsMitarbeiter Dim i As Integer Set Personal = New clsPersonal For Each Mitarbeiter In Personal.AlleMitarbeiter With Mitarbeiter Debug.Print "MitarbeiterID: " & .MitarbeiterID Debug.Print "Name: " & .Nachname & ", " & .Vorname Debug.Print "================" End With Next Mitarbeiter Set Personal = Nothing End Function Listing 13.28: Zugriff auf eine Mitarbeiterauflistung
Abbildung 13.13: Ausgabe der Routine aus Listing 13.28
Für die Realisierung eines Zugriffs per Objekt auf in der Datenbank gespeicherte Daten benötigen Sie zwei Klassen namens clsPersonal und clsMitarbeiter. Die erste Klasse enthält lediglich die Deklaration des Collection-Objekts zur Aufnahme der Mitarbeiter-Objekte und eine Property Get-Prozedur zum Zusammenstellen und Zurückgeben des Collection-Objekts.
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
609
Dim mAlleMitarbeiter As Collection Public Property Get AlleMitarbeiter() As Collection 'Wenn die Auflistung noch nicht existiert ... If mAlleMitarbeiter Is Nothing Then 'Auslistung instanzieren Set mAlleMitarbeiter = New Collection Dim db As DAO.Database Dim rst As DAO.Recordset 'Mitarbeiterobjekt instanzieren Dim objMitarbeiter As clsMitarbeiter 'Datenbankobjekte instanzieren, 'um Mitarbeiter in die Objekte zu laden Set db = CurrentDb Set rst = db.OpenRecordset("SELECT tblMitarbeiter.MitarbeiterID " _ & "FROM tblMitarbeiter", dbOpenDynaset) 'Für jeden Mitarbeiter ein eigenes Objekt anlegen 'und an die Auflistung anfügen Do While Not rst.EOF 'Neues Mitarbeiterobjekt erstellen Set objMitarbeiter = New clsMitarbeiter 'Füllen des Objekts mit den Mitarbeitereigenschaften objMitarbeiter.Laden rst!MitarbeiterID rst.MoveNext 'Mitarbeiterobjekt zur Auflistung hinzufügen mAlleMitarbeiter.Add objMitarbeiter Set objMitarbeiter = Nothing Loop Set rst = Nothing Set db = Nothing End If
610
13
Objektorientierte Programmierung
Set AlleMitarbeiter = mAlleMitarbeiter End Property Listing 13.29: Die Klasse clsPersonal liefert eine Collection mit Mitarbeiter-Objekten zurück.
Die zweite Klasse, clsMitarbeiter, dient dem Erstellen der einzelnen MitarbeiterObjekte. Sie enthält die Property Let- und Propety Set-Methoden für die Eigenschaften MitarbeiterID, Vorname, Nachname und Abteilung sowie die Function-Methode Laden, die den Mitarbeiter mit der angegebenen MitarbeiterID in das Objekt lädt. Dim Dim Dim Dim
mMitarbeiterID As Long mVorname As String mNachname As String mAbteilung As String
Public Property Get MitarbeiterID() As Long MitarbeiterID = mMitarbeiterID End Property Public Property Let MitarbeiterID(lngMitarbeiterID As Long) mMitarbeiterID = lngMitarbeiterID End Property Public Property Get Vorname() As String Vorname = mVorname End Property Public Property Let Vorname(strVorname As String) mVorname = strVorname End Property Public Property Get Nachname() As String Nachname = mNachname End Property Public Property Let Nachname(strNachname As String) mNachname = strNachname End Property Public Property Get Abteilung() As String Abteilung = mAbteilung End Property Public Property Let Abteilung(strAbteilung As String) mAbteilung = strAbteilung End Property
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
611
Public Function Laden(lngMitarbeiterID As Long) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb 'ID des Mitarbeiter zuweisen mMitarbeiterID = lngMitarbeiterID 'Datensatz mit der angegebenen MitarbeiterID öffnen '(enthält Mitarbeiterdaten und Abteilung) Set rst = db.OpenRecordset("SELECT tblMitarbeiter.*, " _ & "tblAbteilungen.Abteilung FROM tblMitarbeiter " _ & "INNER JOIN tblAbteilungen ON tblMitarbeiter.AbteilungID " _ & "= tblAbteilungen.AbteilungID WHERE MitarbeiterID = " _ & mMitarbeiterID, dbOpenDynaset) 'Falls Datensatz vorhanden, Eigenschaften zuweisen If Not rst.EOF Then mVorname = rst!Vorname mNachname = rst!Nachname mAbteilung = rst!Abteilung Laden = True End If Set rst = Nothing Set db = Nothing End Function Listing 13.30: Code der Klasse clsMitarbeiter
Mit diesen beiden Klassen können Sie wie in Listing 13.28 auf die Auflistung aller Mitarbeiter und die Eigenschaften der einzelnen Listeneinträge zugreifen. Der Aufwand hierfür ist gar nicht so hoch, wenn man vom relativ umfangreichen Code absieht. Dieser enthält aber ohnehin fast nur Property-Prozeduren, deren Herstellung keine große Denkleistung erfordert – Sie können ähnliche Klassen mit ein wenig Fleißarbeit leicht selbst nachbauen.
13.8.4 »Echtes« Objekt mit Auflistung Die Auflistung des vorherigen Beispiels wurde durch ein abstraktes Objekt realisiert – es gibt in der Datenbank keine Objekte namens Personal. Es diente lediglich als Basis für die Auflistung AlleMitarbeiter. Im folgenden Beispiel erhält ein echtes Objekt eine Auflistung mit untergeordneten Objekten. Dazu greifen Sie das Mitarbeiter-Objekt aus dem vorherigen Beispiel auf und erweitern es um eine Auflistung der Abwesenheiten, die in der Tabelle tblAbwesenheiten gespeichert sind.
612
13
Objektorientierte Programmierung
Die Klasse clsMitarbeiter ist nur geringfügig zu erweitern: Hier fügen Sie im Modulkopf lediglich die Deklaration des Auflistungsobjekts für die Abwesenheiten und zusätzlich die Property Get-Prozedur Abwesenheiten hinzu. Diese Property lädt beim ersten Aufruf der Collection Abwesenheiten aus einer aufrufenden Routine die Abwesenheitsdaten des aktuellen Mitarbeiters in die einzelnen Elemente der Auflistung. Diese stehen dann bei diesem und den folgenden Aufrufen weiter zur Verfügung, ohne dass Sie sie erneut laden müssen. Dim mAbwesenheiten As Collection Public Property Get Abwesenheiten() As Collection 'Falls die Abwesenheiten-Auflistung noch nicht existiert ... If mAbwesenheiten Is Nothing Then 'Neue Abwesenheiten-Auflistung erstellen Set mAbwesenheiten = New Collection Dim db As DAO.Database Dim rst As DAO.Recordset Dim objAbwesenheit As clsAbwesenheit Set db = CurrentDb 'Datensatzgruppe aller Abwesenheiten zum aktuellen 'Mitarbeiter öffnen Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten " _ & "WHERE MitarbeiterID = " & mMitarbeiterID, dbOpenDynaset) 'Alle Abwesenheiten durchlaufen Do While Not rst.EOF Set objAbwesenheit = New clsAbwesenheit 'Abwesenheit in das Objekt laden objAbwesenheit.Laden rst!AbwesenheitID rst.MoveNext 'Abwesenheit zur Auflistung hinzufügen mAbwesenheiten.Add objAbwesenheit Set objAbwesenheit = Nothing Loop Set rst = Nothing Set db = Nothing
Benutzerdefinierte Auflistungen mit dem Collection-Objekt
613
End If Set Abwesenheiten = mAbwesenheiten End Property Listing 13.31: Erweiterung der Klasse clsMitarbeiter aus Listing 13.30
Die Klasse clsAbwesenheiten Fehlt nur noch die Klasse, die als Basis für die Abwesenheits-Objekte dient. Diese ist ähnlich wie die Klasse clsMitarbeiter aufgebaut – sie hat ein paar Eigenschaften mit den entsprechenden Property Let- und Property Get-Prozeduren und eine Methode zum Laden der Daten einer Abwesenheit in das Objekt. Private Private Private Private
mAbwesenheitID As Long mAbwesenheitsart As String mStartdatum As Date mEnddatum As Date
Public Property Get AbwesenheitID() As Long AbwesenheitID = mAbwesenheitID End Property Public Property Let AbwesenheitID(lngAbwesenheitID As Long) mAbwesenheitID = lngAbwesenheitID End Property Public Property Get Abwesenheitsart() As String Abwesenheitsart = mAbwesenheitsart End Property Public Property Let Abwesenheitsart(strAbwesenheitsart As String) mAbwesenheitsart = strAbwesenheitsart End Property Public Property Get Startdatum() As Date Startdatum = mStartdatum End Property Public Property Let Startdatum(datStartdatum As Date) mStartdatum = datStartdatum End Property Public Property Get Enddatum() As Date Enddatum = mEnddatum End Property Public Property Let Enddatum(datEnddatum As Date) mEnddatum = datEnddatum End Property
614
13
Objektorientierte Programmierung
Public Function Laden(lngAbwesenheitID As Long) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb 'ID der Abwesenheit zuweisen mAbwesenheitID = lngAbwesenheitID 'Datensatz mit der angegebenen AbwesenheitID öffnen '(enthält Mitarbeiterdaten und Abteilung) Set rst = db.OpenRecordset("SELECT tblAbwesenheiten.*, " _ & "tblAbwesenheitsarten.Abwesenheitsart FROM tblAbwesenheiten " _ & "INNER JOIN tblAbwesenheitsarten ON " _ & "tblAbwesenheiten.AbwesenheitsartID = " _ & "tblAbwesenheitsarten.AbwesenheitsartID " _ & "WHERE AbwesenheitID = " & mAbwesenheitID, dbOpenDynaset) 'Falls Datensatz vorhanden, Eigenschaften zuweisen If Not rst.EOF Then mAbwesenheitID = rst!AbwesenheitID mAbwesenheitsart = rst!Abwesenheitsart mStartdatum = rst!Startdatum mEnddatum = rst!Enddatum Laden = True End If Set rst = Nothing Set db = Nothing End Function Listing 13.32: Inhalt des Klassenmoduls clsAbwesenheiten
Was tun mit Mitarbeiter- und Abwesenheits-Objekten? Einige Abschnitte weiter oben haben Sie zunächst eine Routine kennen gelernt, die komfortabel über Objekte und ihre Auflistungen zugreift und dann die notwendigen Klassen clsPersonal und clsMitarbeiter erstellt. Mit der hinzugekommenen Klasse clsAbwesenheiten können Sie nun die Klasse clsMitarbeiter als Objekt mit einer Abwesenheiten-Collection verwenden. Das folgende Listing zeigt eine Beispielanwendung, in der zu einem Mitarbeiter alle vorhandenen Abwesenheiten ausgegeben werden. Durch die sorgfältigen Vorbereitungen – dem Erstellen der zugrunde liegenden Klassen – sind hier nur noch wenige Zeilen Code notwendig, um die Informationen von mehreren Tabellen auszugeben. Public Function MitarbeiterUndAbwesenheitenAusgeben(lngMitarbeiterID As Long) Dim Mitarbeiter As clsMitarbeiter Dim Abwesenheit As clsAbwesenheit
Schnittstellen und Vererbung
615
Dim i As Integer Set Mitarbeiter = New clsMitarbeiter If Mitarbeiter.Laden(lngMitarbeiterID) = True Then Debug.Print "Vorname: " & Mitarbeiter.Vorname Debug.Print "Nachname: " & Mitarbeiter.Nachname Debug.Print "Abteilung: " & Mitarbeiter.Abteilung For Each Abwesenheit In Mitarbeiter.Abwesenheiten With Abwesenheit Debug.Print "================" Debug.Print .Abwesenheitsart Debug.Print .Startdatum & " - " & .Enddatum End With Next Abwesenheit End If Set Mitarbeiter = Nothing End Function Listing 13.33: Ausgabe der Mitarbeiter und ihrer Abwesenheiten
13.9 Schnittstellen und Vererbung Weiter oben haben Sie erfahren, dass die öffentlichen Eigenschaften und Methoden die Schnittstelle einer Klasse ausmachen. Die Schnittstelle und ihre Implementierung sind in einer einzigen Klasse enthalten. Das ist in den meisten Fällen durchaus ausreichend. Es gibt aber Fälle, in denen das relativ unschöne Effekte haben kann.
13.9.1 Beispiel für den Einsatz der Schnittstellenvererbung Stellen Sie sich einmal vor, Sie müssten regelmäßig Daten aus verschiedenen Quellen in Ihre Datenbank importieren. Zum Steuern des Importvorgangs – also etwa zur Auswahl der Quelldatei und des für den Import zu verwendenden Algorithmus – benutzen Sie ein Formular wie in Abbildung 13.14.
Abbildung 13.14: Steuerung des Imports aus unterschiedlichen Quellen
616
13
Objektorientierte Programmierung
Hinter der Schaltfläche Importieren verbirgt sich etwa folgender Code: Private Sub cmdImportieren_Click() Select Case Me.ogrImportart Case 1 'Text (.txt) 'Import der Daten aus der Textdatei '[... viele weitere Zeilen Code] MsgBox "Import aus der Textdatei ist erfolgt." Case 2 'XML (.xml) 'Import der Daten aus der XML-Datei '[... viele weitere Zeilen Code] MsgBox "Import aus der XML-Datei ist erfolgt." Case 3 'Access-Tabelle (.mdb) 'Import aus der Access-Tabelle '[... viele weitere Zeilen Code] MsgBox "Import aus der Access-Tabelle ist erfolgt." Case 4 'Excel-Tabelle (.xls) 'Import aus der Excel-Tabelle '[... viele weitere Zeilen Code] MsgBox "Import aus der Excel-Tabelle ist erfolgt." End Select End Sub Listing 13.34: Aufbau des Importvorgangs auf prozedurale Art
Diese Vorgehensweise dürfte durchaus gängig sein. Der erste Schritt zu einer objektorientierten Variante ist die Erstellung je einer eigenen Klasse für die einzelnen ImportFunktionen. Damit nimmt man schon einmal einige Funktionalität aus der GUISchicht der Anwendung – also dem Formular und dessen Klassenmodul – heraus. Die durch die Schaltfläche Importieren ausgelöste Prozedur wertet jetzt nur noch die Benutzereingaben aus und lässt das entsprechende Objekt den Rest erledigen: Private Sub cmdImportieren_Click() Dim strDateiname As String strDateiname = Me!txtDateiname Select Case Me.ogrImportart Case 1 'Text (.txt) Dim objImport_txt As clsImport_txt Set objImport_txt = New clsImport_txt If objImport_txt.Import(strDateiname) = True Then MsgBox "Import aus der Textdatei ist erfolgt." End If Case 2 'XML (.xml) Dim objImport_xml As clsImport_xml Set objImport_xml = New clsImport_xml If objImport_xml.Import(strDateiname) = True Then MsgBox "Import aus der XML-Datei ist erfolgt."
Schnittstellen und Vererbung
617
End If Case 3 'Access-Tabelle (.mdb) Dim objImport_mdb As clsImport_mdb Set objImport_mdb = New clsImport_mdb If objImport_mdb.Import(strDateiname) = True Then MsgBox "Import aus der Access-Tabelle ist erfolgt." End If Case 4 'Excel-Tabelle (.xls) Dim objImport_xls As clsImport_xls Set objImport_xls = New clsImport_xls If objImport_xls.Import(strDateiname) = True Then MsgBox "Import aus der Excel-Tabelle ist erfolgt." End If End Select End Sub Listing 13.35: Diese Variante des Imports basiert auf der Verwendung unterschiedlicher Klassen.
Die Klassenmodule der verwendeten Objekte haben alle eine Methode namens Import. Da je nach Import-Art jeweils ein Objekt verwendet wird, könnte die Import-Funktionalität auch in Methoden mit anders lautenden Namen untergebracht werden. Sie werden allerdings gleich sehen, dass die Auswahl gleicher Methodennamen durchaus sinnvoll ist. Nachfolgendes Listing zeigt den Inhalt eines Klassenmoduls am Beispiel clsImport_mdb: Option Compare Database Option Explicit Public Function Import(strDateiname As String) 'Import aus der Access-Tabelle '[... viele weitere Zeilen Code] Import = True End Function Listing 13.36: Aussehen eines der Import-Klassenmodule
13.9.2 Vereinheitlichen per Schnittstellenvererbung Wenn nun schon alle Klassen die gleiche Methode zur Verfügung stellen, warum sollte man diese nicht auch wie eine Klasse ansteuern, anstatt jede Klasse bei Bedarf einzeln zu deklarieren und zu instanzieren? Genau hier setzt die Schnittstellenvererbung an: Sie deklarieren in einem Klassenmodul alle Elemente der Schnittstelle, die alle betroffenen Klassen gemeinsam haben. In den Klassenmodulen für die unterschiedlichen Importe implementieren Sie die Elemente der Schnittstelle und legen mit einer entsprechenden Anweisung fest, welche Schnittstelle implementiert wird. Im Klassendiagramm sieht das wie in Abbildung 13.15 aus. Die Schnittstelle IImport stellt die öffentliche Methode Import() zur Verfügung, die von den Implementierungsklassen auf
618
13
Objektorientierte Programmierung
verschiedene Art umgesetzt wird. Um die Objekte des Beispiels zu komplettieren, wird auch das Formular zur Auswahl des Import-Algorithmus dargestellt.
«interface» IIm port Import()
clsIm port_txt Import()
«benutzt»
clsIm port_m db
clsIm port_xm l
Import()
Import()
clsIm port_xls Import()
Abbildung 13.15: Klassendiagramm zur Veranschaulichung der Schnittstellenvererbung
13.9.3 Realisierung der Schnittstellenvererbung Im Code sind zur Anwendung der Schnittstellenvererbung drei Schritte erforderlich: 1. Erstellen der Schnittstelle in Form eines eigenen Klassenmoduls mit den öffentlichen Eigenschaften und Prozeduren 2. Anpassen oder Erstellen der Klassen, die die Schnittstelle implementieren 3. Anpassen der Klassen, die auf die Schnittstelle zugreifen sollen
Erstellen der Schnittstelle Die Schnittstelle enthält die Deklaration aller Eigenschaften und Methoden, die in allen Implementierungen realisiert werden müssen. Deklaration bedeutet in diesem Fall, dass nur die Rümpfe der jeweiligen Methoden, aber keinesfalls Implementierungsdetails angelegt werden. Die Schnittstelle für obiges Beispiel enthält lediglich die Methode Import(): Option Compare Database Option Explicit Public Function Import(strDateiname As String) End Function Listing 13.37: Klassenmodul mit einer Schnittstelle
Schnittstellen und Vererbung
619
Das Klassenmodul enthält keine Merkmale, die es von anderen Klassenmodulen unterscheiden – mit Ausnahme der Tatsache, dass die Methode keine Implementierung enthält, und mit einem zweiten Unterschied, der hier nicht offensichtlich wird: Schnittstellen-Klassenmodule erhalten ein I (für »Interface«) statt des für Klassenmodule üblichen cls als Präfix. Das obige Klassenmodul heißt folglich IImport.
Implementieren der Schnittstelle Beim Implementieren bestehender Klassenmodule sind erhebliche Umbauarbeiten angesagt. Eine Anweisung legt die zu implementierende Schnittstelle fest (beispielsweise Implements IImport). Die Eigenschaften und Methoden, die in der Schnittstelle enthalten sind und implementiert werden sollen, müssen als private Elemente deklariert werden. Die Namen der betroffenen Eigenschaften und Methoden erhalten ein Präfix, das aus der Bezeichnung der Schnittstelle und einem Unterstrich besteht (zum Beispiel IImport_Import). Dementsprechend sind auch die Variablen zum Speichern der Rückgabewerte von Funktionen anzupassen. Die Implementierung der Schnittstelle IImport für den Import aus Textdateien sieht beispielsweise wie in folgendem Listing aus: Option Compare Database Option Explicit Implements IImport Private Function IImport_Import(strDateiname As String) 'Import der Daten aus der Textdatei '[... viele weitere Zeilen Code] MsgBox "Import aus Textdatei läuft..." IImport_Import = True End Function Listing 13.38: Inhalt des Klassenmoduls clsImport_txt
Anpassen der Klassen, die auf die Schnittstelle zugreifen Auch die auf die unterschiedlichen Implementierungen einer Schnittstelle zugreifenden Routinen muss man ordentlich umbauen. Das nachfolgende Listing zeigt das Aussehen der neuen Version der Routine aus Listing 13.35. Besonderes Augenmerk sollten Sie der Vorgehensweise bei der Deklaration und Instanzierung der Schnittstelle und der Import-Objekte widmen. Die Objektvariable objImport wird zunächst als Interface IImport deklariert. Erst im Select Case-Teil der
620
13
Objektorientierte Programmierung
Routine erfolgt die Instanzierung des Objekts – dort wird je nach gewähltem ImportAlgorithmus eine Instanz der entsprechenden Implementierung erzeugt. Ebenfalls zu beachten ist, dass der Zugriff auf das einzige Element der Schnittstelle – das heißt auf die Import-Funktion – nur noch von einer Stelle aus erfolgt. Private Sub cmdImportieren_Click() Dim objImport As IImport Dim strDateiname As String strDateiname = Me!txtDateiname Select Case Me.ogrImportart Case 1 'Text (.txt) Set objImport = New clsImport_txt Case 2 'XML (.xml) Set objImport = New clsImport_xml Case 3 'Access-Tabelle (.mdb) Set objImport = New clsImport_mdb Case 4 'Excel-Tabelle (.xls) Set objImport = New clsImport_xls End Select If objImport.Import(strDateiname) = True Then MsgBox "Import ist erfolgt." End If End Sub Listing 13.39: Zugriff auf unterschiedliche Implementierungen einer Schnittstelle
13.9.4 Was vom Beispiel übrig bleibt … Leider lassen sich mit einem einzigen Beispiel nicht immer alle theoretischen Grundlagen abdecken. Deshalb finden Sie nachfolgend die wichtigsten Regeln bei der Verwendung der Schnittstellenvererbung im Überblick. Die Schnittstelle enthält nur die öffentlich verfügbaren Elemente der zu implementierenden Klassen. Da öffentliche Variablen dem Prinzip der Kapselung widersprechen, handelt es sich dabei also nur um folgende Elemente: Property Let-/Get-/Set-Prozeduren Function-Prozeduren Sub-Prozeduren Die Variablen, auf die die Property Let-/Get-/Set-Prozeduren zugreifen, legt man ausschließlich in den Implementierungen der Schnittstellen an.
Schnittstellen und Vererbung
621
Vereinfachung im VBA-Editor Nach dem Anlegen der Implements-Anweisung in der Implementierung stellt der VBAEditor die Schnittstelle und ihre Elemente in den beiden Kombinationsfeldern oben im Code-Fenster zur Verfügung (siehe Abbildung 13.16). Bei Auswahl von Import im rechten Kombinationsfeld wird automatisch die entsprechende Rumpfprozedur IImport_Import im Code-Fenster angelegt – sie braucht also nicht manuell in den Code geschrieben zu werden.
Abbildung 13.16: Auswahl der Elemente der Schnittstelle
Schnittstelle als Pflichtprogramm Besonders wichtig ist die Umsetzung aller Elemente der Schnittstelle in sämtlichen Implementierungen. Die Anwendung lässt sich sonst gar nicht erst kompilieren – Abbildung 13.17 zeigt die nicht ganz unberechtigte Antwort des Compilers.
Weitere Anwendungsmöglichkeiten der Schnittstellenvererbung Genau wie der Import von Daten ist natürlich auch der Zugriff auf unterschiedliche Backends einer Datenbankanwendung ein sinnvoller Einsatzort für die Schnittstellenvererbung. In diesem Fall würde man allerdings nicht wie im obigen Beispiel ad hoc, sondern beispielsweise einmalig beim Anwendungsstart die Art des Backends und damit die Implementierung der Datenzugriffsschicht festlegen. Eine Technik, die praktisch gar nicht ohne Schnittstellenimplementierung auskommt, ist die testgetriebene Entwicklung. Da dort Einheiten getestet werden, müssen die von der aktuell zu testenden Klasse verwendeten Objekte durch Dummy-Objekte ersetzt werden, die entweder die Funktion des eigentlich zu verwendenden Objekts simulieren oder aber prüfen, ob die von der zu testenden Klasse ausgehende Interaktion den Vorgaben entspricht.
622
13
Objektorientierte Programmierung
Abbildung 13.17: Kein Kompilieren ohne vollständige Schnittstellenimplementierung
Eigentlich sollte dieses Buch noch ein Kapitel zum Thema »Testgetriebene Entwicklung mit Access« enthalten. Dieses Thema hat es im Wesentlichen aus zwei Gründen nicht in das Buch geschafft: Erstens unterscheidet sich die testgetriebene Entwicklung sehr von der herkömmlichen Ad-hoc-Programmierung, die bei Access-Entwicklern relativ weit verbreitet ist. Daher dürften sich nur sehr wenige Leser überhaupt für das Thema interessieren geschweige denn diese Technik auch anwenden. Zweitens handelt es sich bei der testgetriebenen Entwicklung mit Access noch um Pionierarbeit. Die Werkzeuge sind noch nicht ausgereift und es fehlt auch an Erfahrung und Feedback, um das Thema ausreichend fundiert in diesem Buch wiederzugeben. Wer dennoch an dieser Technik interessiert ist, findet das ursprünglich für die Veröffentlichung in diesem Buch vorgesehene Kapitel im Internet unter http://www.access-entwicklerbuch.de. Dort erhalten Sie auch die jeweils aktuelle Fassung des Tools accessUnit.
14 Objektorientierung im Praxiseinsatz Im vorherigen Kapitel haben Sie bereits einige Ansätze für den Einsatz der Objektorientierung mit Access und VBA kennen gelernt. In diesem Kapitel folgen nun weitere Möglichkeiten. Sie erfahren, wie Sie mehrschichtige Anwendungen mit Access entwickeln, Standardfunktionen von Formularen wie Öffnen, Schließen, Suche per Kombinationsfeld und andere in eine eigene Klasse auslagern und wie Sie komfortabel mehrere Instanzen eines Formulars öffnen können, um etwa mehrere Datenblätter zu Artikeln anzuzeigen.
14.1 Standardfunktionen von Formularen auslagern In manchen Fällen macht der Mehraufwand für die Erstellung mehrschichtiger Anwendungen beziehungsweise des Einsatzes von Objektklassen für Tabellen oder Datenzugriffsklassen definitiv keinen Sinn, weil die geplante Anwendung einfach nur ein kleines Tool zur Verwaltung weniger Daten ist – ohne dass größere Erweiterungswünsche abzusehen sind. Diese Bedingung sollten Sie allerdings genau überprüfen – nach den Erfahrungen des Autors trifft sie nur zu, wenn Sie selbst alleiniger Benutzer der Anwendung sind (alle anderen erkennen in der Regel erst nach Fertigstellung einer Anwendung, welches »Potenzial« noch dahintersteckt …). Die Beispiele zu diesem Abschnitt finden Sie auf der Buch-CD unter Kap_14\ObjektorientierteTechniken.mdb. Es handelt sich dabei um die Tabelle tblKontakte, das Formular frmKontakte sowie das Modul clsFormCode.
Formulare zur Datenbearbeitung Die meisten Access-Formulare dienen der Bearbeitung von Daten, die direkt an eine Tabelle gebunden sind. Diese Formulare haben immer den gleichen Aufbau und die gleichen Funktionen – nur die angezeigten Daten unterscheiden sich. Ein Formular zum Bearbeiten von Daten hat in der Regel einige der folgenden Eigenschaften: Direkte Bindung zur Tabelle mit den angezeigten Daten Je ein Steuerelement für jedes Feld der Tabelle
624
14
Objektorientierung im Praxiseinsatz
Möglichkeit zur schnellen Auswahl von Datensätzen, etwa ein Kombinationsfeld zur Eingabe oder Auswahl des Nachnamens eines Kontaktes Schaltfläche zum Schließen des Formulars Schaltfläche zum Abbrechen und damit zum Verwerfen der am aktuellen Datensatz vorgenommenen Änderungen Schaltfläche zum Hinzufügen eines neuen Datensatzes beziehungsweise zum Springen zu einem neuen Datensatz Schaltfläche zum Löschen des aktuellen Datensatzes Validierung der gebundenen Steuerelemente Mit dem Anlegen der hier aufgelisteten Elemente und dem notwendigen Code sind Sie je Formular schon einige Minuten beschäftigt – besonders das Anlegen der Validierungsfunktionen und ihr Test kosten Zeit. Das Formular aus Abbildung 14.1 besitzt die meisten der soeben aufgelisteten Eigenschaften eines Formulars zur Datenerfassung und –bearbeitung. Der einzige Unterschied ist, dass dieses Formular keine Funktionalität beinhaltet – sowohl das Kombinationsfeld als auch die Schaltflächen lösen keine Ereignisse aus. Das soll auch weitgehend so bleiben, denn Sie lernen nun eine Möglichkeit kennen, die Anwendungslogik hinter Formularen mit gleichem oder ähnlichem Aufbau wie diesem nur einmal zu erstellen und beliebig oft wieder zu verwenden.
Abbildung 14.1: Dieses Formular hat keine eigenen Funktionen
Standardfunktionen von Formularen auslagern
625
14.1.1 Codeauslagerung am Beispiel der OK-Schaltfläche Die OK-Schaltfläche ist ein einfaches Beispiel für die nachfolgend vorgestellte Technik. Sie enthält in der Regel nur eine einzige Anweisung. Das folgende Listing zeigt, wie die Ereignisprozedur aussieht, die beim Klicken auf diese Schaltfläche ausgelöst wird: Private Sub cmdOK_Click() DoCmd.Close acForm, Me.Name End Sub Listing 14.1: Prozedur zum Schließen eines Formulars
Es kostet zwar nicht viel Mühe, diese Ereignisprozedur zu erstellen und die einzige Zeile hinzuzufügen. Aber wenn Sie in einer Anwendung einige dieser Prozeduren anlegen, kostet das schon Zeit – und diese Prozedur ist nicht die einzige, die Sie benötigen. Versuchen Sie also, einen einfacheren Weg zu finden. Ein erster Ansatz wäre, eine öffentliche Prozedur in einem Standardmodul unterzubringen und diese von der Ereigniseigenschaft cmdOK_Click aus aufzurufen. Sie könnte dann auch von den entsprechenden Prozeduren anderer Formulare aus aufrufen werden. Gewonnen haben Sie damit allerdings nichts – Sie ersetzen einfach nur die eigentliche Anweisung durch eine, die eine weitere Prozedur aufruft. Das mag ein bisschen Zeit sparen, wenn die eigentliche Prozedur mehrere Zeilen enthält, bringt aber keine wirklichen Vorteile. Gegebenenfalls müssen Sie den Prozeduraufruf auch noch parametrisieren – etwa wenn es um den Code für das Kombinationsfeld geht, der je nach Datenherkunft des Formulars anders aussieht. Allein die Idee, die Funktion der Prozedur zentral verfügbar zu machen, fließt in die folgenden Schritte ein. In Kapitel 13, »Objektorientierte Programmierung«, haben Sie erfahren, wie Sie Ereignisse abfangen und eigenen Code dafür ausführen lassen können. Diese Technik machen Sie sich nun zu Nutze. Ziel ist also – am konkreten Beispiel der OK-Schaltfläche betrachtet – das Ereignis Beim Klicken der Schaltfläche abzufangen und bei dessen Aufruf eigenen Code auszuführen, der an völlig anderer Stelle steht. Um die Ereignisse der Schaltfläche abzufangen, benötigen Sie eine Objektvariable, die Sie mit folgender Zeile deklarieren: Private WithEvents mOkButton As CommandButton
Diese Anweisung fügen Sie in ein neues Klassenmodul namens clsFormCode ein. Damit das hier deklarierte Objekt die Ereignisse der Schaltfläche abfangen kann, muss es zunächst einmal existieren – und dazu ist eine Instanz der Klasse erforderlich, in der es deklariert wird.
626
14
Objektorientierung im Praxiseinsatz
Wo erwecken Sie das Klassenmodul nun zum Leben? Natürlich in dem Formular, dessen Ereignisse es abfangen soll. Und dabei übergeben Sie möglichst auch noch je eine Objektvariable, die auf das Formular und die betroffene Schaltfläche zeigt. Der günstigste Zeitpunkt für diese Aktion ist das Öffnen des Formulars. In folgendem Listing finden Sie das komplette Klassenmodul des Formulars frmKontakte. Die Objektvariable objFormCode wird modulweit deklariert, da sie für die komplette Lebensdauer dieser Formularinstanz benötigt wird. Die Ereignisprozedur Form_Open wird beim Öffnen des Formulars ausgelöst. Die bisher einzige Prozedur weist der Objektvariablen objFormCode eine neue Instanz der Klasse clsFormCode zu. Option Compare Database Option Explicit 'Deklarieren der Objektvariablen für die Codeklasse Dim objFormCode As clsFormCode Private Sub Form_Open(Cancel As Integer) 'Instanzieren des Codeklasse-Objekts Set objFormCode = New clsFormCode End Sub Listing 14.2: Instanzieren und vorbereiten der Codeklasse
Das Formular erstellt nun beim Öffnen ein Objekt, das auf Ereignisse eines CommandButtonObjekts lauschen soll. Damit das auch funktioniert, müssen Sie dem Objekt einen Verweis auf diese Schaltfläche übergeben – den die Klasse wiederum entgegennehmen muss. Daher fügen Sie dem Klassenmodul clsFormCode eine Property Set-Prozedur hinzu, um den schreibenden Zugriff auf die bereits deklarierte Objektvariable mOkButton zu erlauben: Public Property Set OkButton(cmb As CommandButton) Set mOkButton = cmb End Property Listing 14.3: Property Set-Prozedur für die Membervariable mOKButton
Im gleichen Zuge erweitern Sie die Ereignisprozedur Form_Open des Formulars. Die neue Anweisung erzeugt einen Verweis auf die Schaltfläche cmdOK. Private Sub Form_Open(Cancel As Integer) 'Instanzieren des Codeklasse-Objekts
Standardfunktionen von Formularen auslagern
627
Set objFormCode = New clsFormCode 'Zuweisen der OK-Schaltfläche Set objFormCode.OkButton = Me!cmdOK
End Sub Listing 14.4: Zuweisen der OK-Schaltfläche an die entsprechende Eigenschaft der Klasse clsFormCode
In der Klasse clsFormCode fehlt nun noch die Prozedur, die beim Abfangen des Beim Klicken-Ereignisses der Schaltfläche ausgeführt werden soll. Um schnell den Prozedurrumpf anzulegen, wählen Sie aus dem linken Kombinationsfeld des Codefensters den Eintrag OKButton aus, woraufhin der Prozedurrumpf automatisch erscheint. Anschließend brauchen Sie nur noch eine Zeile zum Testen hinzuzufügen – etwa eine MsgBoxAnweisung: Private Sub mOKButton_Click() MsgBox "Ereignis abgefangen" End Sub Listing 14.5: Prozedur zum Abfangen des Click-Ereignisses
Wenn Sie das Formular nun öffnen und auf die OK-Schaltfläche klicken, sollte eigentlich das Meldungsfenster erscheinen – es lässt sich aber nicht blicken! Bei genauer Betrachtung des Eigenschaftsfensters der Schaltfläche fällt auf, dass das auch gar nicht funktionieren kann, denn die Schaltfläche löst gar kein Ereignis aus (siehe Abbildung 14.2). Wählen Sie also für die Eigenschaft Beim Klicken den Eintrag [Ereignisprozedur] aus, öffnen Sie das Formular erneut und klicken Sie noch einmal auf die OK-Schaltfläche und – das Meldungsfenster erscheint. Zum Glück lässt sich auch dieser Vorgang automatisieren. Dazu passen Sie erneut das Klassenmodul clsFormCode an, indem Sie der Property Get-Prozedur OkButton eine Zeile wie folgt hinzufügen: Public Property Set OkButton(cmb As CommandButton) 'Objektvariable auf Ok-Schaltfläche einstellen Set mOkButton = cmb 'Beim Klicken-Ereigniseigenschaft hinzufügen mOkButton.OnClick = "[Event Procedure]" End Property Listing 14.6: Anpassen der Property Set-Prozedur OKButton
628
14
Objektorientierung im Praxiseinsatz
Abbildung 14.2: Die OK-Schaltfläche löst offensichtlich kein Ereignis aus.
Die Beim Klicken-Eigenschaft der Schaltfläche im Formular können Sie wieder leeren, da sie später ohnehin automatisch eingestellt wird. Fügen Sie nun die eigentliche Funktion hinzu – schließlich soll die Schaltfläche das Formular schließen und kein Meldungsfenster anzeigen. Dazu sind insgesamt vier Schritte erforderlich: 1. Erstellen einer Objektvariablen namens mForm für das betroffene Formular im Klassenmodul clsFormCode 2. Anlegen einer Property Set-Prozedur zum Setzen der Objektvariablen mForm (ebenfalls im Klassenmodul clsFormCode) 3. Setzen der Objektvariablen auf das aktuelle Formular (vom Beim Öffnen-Ereignis des Formulars aus) 4. Anpassen der Ereignisprozedur im Klassenmodul clsFormCode Das Klassenmodul clsFormCode sieht anschließend wie folgt aus (geänderte Zeilen fett gedruckt): Option Compare Database Private WithEvents mOkButton As CommandButton Private WithEvents mForm As Form
Public Property Set OkButton(cmb As CommandButton)
Standardfunktionen von Formularen auslagern
629
'Objektvariable auf Ok-Schaltfläche einstellen Set mOkButton = cmb 'Beim Klicken-Ereigniseigenschaft hinzufügen mOkButton.OnClick = "[Event Procedure]" End Property Private Sub mOKButton_Click() 'Schließen des Formulars DoCmd.Close acForm, mForm.Name
End Sub Public Property Set ThisForm(frm As Form)
'mForm auf das mit frm referenzierte Formular setzen Set mForm = frm End Property
Listing 14.7: Diese Version des Klassenmoduls fängt das Beim Schließen-Ereignis des instanzierenden Formulars ab und führt den entsprechenden Code aus.
Das Formular-Klassenmodul hat nun diesen Stand: Option Compare Database Option Explicit 'Deklarieren der Objektvariablen für die Codeklasse Dim objFormCode As clsFormCode Private Sub Form_Open(Cancel As Integer) 'Instanzieren des Codeklasse-Objekts Set objFormCode = New clsFormCode 'Zuweisen des aktuellen Formulars Set objFormCode.ThisForm = Me
'Zuweisen der OK-Schaltfläche Set objFormCode.OkButton = cmdOK End Sub Listing 14.8: Das Beim Öffnen-Ereignis instanziert die Klasse clsFormCode und stellt deren Eigenschaften so ein, dass es die Ausführung des Beim Klicken-Ereignisses der OK-Schaltfläche übernimmt.
630
14
Objektorientierung im Praxiseinsatz
Lohnt sich der Aufwand? Nachdem nun schon für die Erläuterung des Auslagerns einer einzigen Prozedur einige Seiten notwendig waren und die Menge des Codes sich vervielfacht hat, fragen Sie sich vermutlich, ob sich der Aufwand lohnt. Die Antwort lautet ganz klar: Ja! Denn jetzt haben Sie die Vorgehensweise einmal erfasst. Das Erstellen der übrigen Ereignisprozeduren ist reine Fleißarbeit und das Vorbereiten weiterer Formulare für die Verwendung der Klasse clsFormCode nur eine Sache weniger Zeilen.
14.1.2 Auslagern weiterer Ereignisprozeduren Natürlich soll nicht nur die Funktion der OK-Schaltfläche in eine eigene Klasse ausgelagert werden, sondern auch die der übrigen Ereignisprozeduren.
Abbrechen der Bearbeitung Die Abbrechen-Schaltfläche führt in der Regel zum Durchführen der Undo-Methode des Formulars und zum anschließenden Schließen. Um diese Anweisungen in der Klasse clsFormCode zu implementieren, fügen Sie im Deklarationsbereich zunächst folgende Zeile hinzu: Private WithEvents mCancelButton As CommandButton
Außerdem erstellen Sie noch eine Property Set-Prozedur, damit das instanzierende Formular eine Referenz auf die Abbrechen-Schaltfläche übergeben kann, und legen die Prozedur mit der eigentlichen Funktionalität an: Public Property Set CancelButton(cmb As CommandButton) Set mCancelButton = cmb mCancelButton.OnClick = "[Event Procedure]" End Property Private Sub mCancelButton_Click() mForm.Undo DoCmd.Close acForm, mForm.Name End Sub Listing 14.9: Prozeduren für die ausgelagerte Funktion der Abbrechen-Schaltfläche
Löschen von Datensätzen Auch für die Löschen-Schaltfläche legen Sie zunächst eine private Objektvariable an: Private WithEvents mDeleteButton As CommandButton
Standardfunktionen von Formularen auslagern
631
Der Löschvorgang soll außerdem eine benutzerdefinierte Rückfrage verwenden können. Diese wird in folgender Variablen gespeichert: Private mDeleteMessage As String
Die weitere Vorgehensweise ist ein wenig aufwändiger als bei den vorherigen Schaltflächen. Bereits die Property Set-Prozedur legt im Formular neben der Ereigniseigenschaft für die Schaltfläche eine weitere Ereigniseigenschaft für das Formular selbst an: Public Property Set DeleteButton(cmd As CommandButton) 'Referenz auf die Löschen-Schaltfläche setzen Set mDeleteButton = cmd 'Ereigniseigenschaft für die Schaltfläche anlegen mDeleteButton.OnClick = "[Event Procedure]" 'Ereigniseigenschaft "Beim Löschen" für das Formular anlegen mForm.OnDelete = "[Event Procedure]" End Property Listing 14.10: Das Löschen eines Datensatzes erfordert zwei Ereigniseigenschaften.
Natürlich ist auch für die Variable mDeleteMessage für die je nach Formular individuelle Rückfrage eine Property Let-Prozedur zum Einstellen der Eigenschaft vorzusehen: Public Property Let DeleteMessage(strDeleteMessage As String) mDeleteMessage = strDeleteMessage End Property Listing 14.11: Property Let-Eigenschaft für die Lösch-Rückfrage
Die erste Ereigniseigenschaft wird beim Klicken auf die Löschen-Schaltfläche ausgelöst. Die entsprechende Ereignisprozedur sieht folgendermaßen aus: Private Sub mDeleteButton_Click() On Error GoTo mDeleteButton_Click_Err With DoCmd 'Systemmeldungen ausschalten .SetWarnings False 'Aktuellen Datensatz löschen .RunCommand acCmdDeleteRecord 'Systemmeldungen wieder einschalten .SetWarnings True
632
14
Objektorientierung im Praxiseinsatz
End With mDeleteButton_Click_Exit: Exit Sub mDeleteButton_Click_Err: If Err.Number = 2501 Then 'Dieser Fehler tritt beim Abbrechen eines durch die Docmd.RunCommand 'accmdDeleteRecord ausgelösten Löschvorgangs auf. Er soll nicht 'behandelt werden. Else MsgBox "Fehler: " & Err.Description & vbCrLf & "Fehlernummer: " _ & Err.Number, vbOKOnly + vbCritical, "Fehler" End If Resume mDeleteButton_Click_Exit End Sub Listing 14.12: Ereignisprozedur beim Löschen eines Datensatzes per Schaltfläche
Diese zweite Ereigniseigenschaft wird immer beim Löschen eines Datensatzes ausgelöst. Sie kann beispielsweise Code für die Anzeige einer Rückfrage vor dem Löschen in Form eines Meldungsfensters enthalten. Private Sub mForm_Delete(Cancel As Integer) Dim strDeleteMessage As String 'Prüfen, ob individuelle Rückfrage vorhanden ist If Len(mDeleteMessage)> 0 Then 'Falls ja, diese verwenden... strDeleteMessage = mDeleteMessage Else '... oder die Standardmeldung anzeigen. strDeleteMessage = "Soll der aktuelle Datensatz gelöscht werden?" End If 'Rückfrage, ob der Datensatz wirklich gelöscht werden soll If MsgBox(strDeleteMessage, vbYesNo + vbExclamation + _ vbDefaultButton1, _ "Datensatz löschen") = vbCancel Then 'Falls nein, Abbruch einleiten Cancel = True End If End Sub Listing 14.13: Diese Prozedur wird bei allen Löschvorgängen aufgerufen.
Standardfunktionen von Formularen auslagern
633
In der Prozedur aus Listing 14.13 kommt nun auch die Variable mDeleteMessage zum Zuge. Sie kann genau wie die anderen Elemente über die entsprechende Property SetProzedur von der Beim Öffnen-Prozedur des Formulars eingestellt werden. Zum Einstellen der beiden neuen Member der Klasse clsFormCode fügen Sie in der Beim ÖffnenProzedur die folgenden beiden Codezeilen ein: Set objFormCode.DeleteButton = Me.cmdLoeschen objFormCode.DeleteMessage = "Möchten Sie diesen Kontakt löschen?"
Hinzufügen von Datensätzen Mit der Neu-Schaltfläche springen Sie auf einen neuen, leeren Datensatz. Damit die Klasse clsFormCode diese Operation übernimmt, legen Sie dort die folgenden Zeilen an: Private WithEvents mAddButton As CommandButton Public Property Set AddButton(cmd As CommandButton) Set mAddButton = cmd mAddButton.OnClick = "[Event Procedure]" End Property Private Sub mAddButton_Click() mForm.Recordset.AddNew End Sub Listing 14.14: Code zum Abfangen der Beim Klicken-Prozedur der Neu-Schaltfläche
Schließlich benötigen Sie noch eine Zeile in der Beim Öffnen-Prozedur des Formulars, damit die Schaltfläche cmdNeu auch im objFormCode-Objekt bekannt gemacht wird: Set objFormCode.AddButton = Me.cmdNeu
14.1.3 Einstellen des Kombinationsfeldes für die Schnellauswahl Das Kombinationsfeld im oberen Bereich des Formulars dient der Schnellauswahl von Kontakten nach dem Namen. Das Formular sollte beim Öffnen des Formulars initialisiert und mit den entsprechenden Daten gefüllt werden. Außerdem muss man den Inhalt bei jeder Änderung des Datenbestandes aktualisieren, also nach dem Bearbeiten, Löschen oder Hinzufügen eines Datensatzes. Dazu gehört auch das Aktualisieren des im Kombinationsfeld angezeigten Eintrags beim Blättern in den Datensätzen.
634
14
Objektorientierung im Praxiseinsatz
Sie benötigen also die folgenden Elemente: Ereignisprozeduren des Formulars, die durch Änderungen am Datenbestand ausgelöst werden: Löschen eines Datensatzes: Nach Löschbestätigung (AfterDelConfirm) Anlegen eines neuen Datensatzes: Nach Eingabe (AfterInsert) Bearbeiten des aktuellen Datensatzes: Nach Aktualisierung (AfterUpdate) Ereignisprozedur des Formulars, die beim Wechseln des Datensatzes ausgelöst wird: Beim Anzeigen (Current) Ereignisprozedur des Kombinationsfeldes, das bei Auswahl eines neuen Eintrags ausgelöst wird: Nach Aktualisierung (AfterUpdate) Für den Objektverweis auf das Formular haben Sie bereits eine Variable angelegt; fehlt also noch eine für das Kombinationsfeld. Fügen Sie diese Zeile im Kopf der Klasse clsFormCode hinzu: Private WithEvents mSearchComboBox As ComboBox
Außerdem benötigen Sie noch eine Eigenschaft zum Übergeben der Bezeichnung des Primärschlüsselfeldes der Datenherkunft des Formulars sowie eine weitere Eigenschaft, um anzugeben, ob das Primärschlüsselfeld den Datentyp String oder einen anderen Datentyp hat: Private mPrimaryKey As String Private mPrimaryKeyString As Boolean
Zum Setzen der letzten beiden Eigenschaften verwenden Sie die folgenden Property Let-Prozeduren: Public Property Let PrimaryKey(strPrimaryKey As String) mPrimaryKey = strPrimaryKey End Property Public Property Let PrimaryKeyString(bolPrimaryKeyString As Boolean) mPrimaryKeyString = bolPrimaryKeyString End Property Listing 14.15: Property Let-Prozeduren für die Verwendung des Kombinationsfeldes
Standardfunktionen von Formularen auslagern
635
Die Property Set-Prozedur zum Referenzieren des Kombinationsfeldes ist etwas umfangreicher: Public Property Set SearchComboBox(cbo As ComboBox) 'Referenz auf das Kombinationsfeld setzen Set mSearchComboBox = cbo 'Ereigniseigenschaft Nach Aktualisieren für das Kombinationsfeld anlegen mSearchComboBox.AfterUpdate = "[Event Procedure]" 'Ereigniseigenschaften für die Formular-Ereignisse '"Beim Anzeigen", "Nach Eingabe" und "Nach Löschbestätigung" anlegen mForm.AfterInsert = "[Event Procedure]" mForm.AfterDelConfirm = "[Event Procedure]" mForm.AfterUpdate = "[Event Procedure]" 'Ereigniseigenschaft für das Formular-Ereignis "Beim Anzeigen" anlegen mForm.OnCurrent = "[Event Procedure]" End Property Listing 14.16: Property Set-Prozedur zum Referenzieren des Kombinationsfeldes
Aktualisieren des Kombinationsfeldes Das Aktualisieren des Kombinationsfeldes besteht aus zwei Aktionen: Die erste tritt bei Änderungen am Datenbestand auf und aktualisiert die Datensatzherkunft des Kombinationsfeldes. Die zweite sorgt dafür, dass das Kombinationsfeld immer den Datensatz anzeigt, den auch das Formular darstellt. Die Datensatzherkunft des Kombinationsfeldes soll in mehreren Fällen aktualisiert werden – beim Einfügen, Ändern und Löschen von Datensätzen. Für die drei Ereignisse gibt es auch drei Ereignisprozeduren. Alle drei enthalten lediglich eine Anweisung, die immer die gleiche Prozedur aufruft (auf diese Weise brauchen Sie Änderungen nur an einer Stelle durchzuführen): Private Sub mForm_AfterDelConfirm(Status As Integer) RequeryComboBox End Sub Private Sub mForm_AfterInsert() RequeryComboBox End Sub
636
14
Objektorientierung im Praxiseinsatz
Private Sub mForm_AfterUpdate() RequeryComboBox End Sub Listing 14.17: Ereignisprozeduren zum Löschen, Einfügen und Ändern von Daten
Die von den drei Routinen aufgerufene Prozedur enthält ebenfalls nur eine Anweisung. Diese führt die die Requery-Methode des Kombinationsfeldes aus. Private Sub RequeryComboBox() 'Kombinationsfeld aktualisieren mSearchComboBox.Requery End Sub Listing 14.18: Aktualisieren der Datensatzherkunft des Kombinationsfeldes
Der zweite Teil der Aktualisierung stellt im Kombinationsfeld den Datensatz ein, der dem im Formular angezeigten Datensatz entspricht. Diese Aktion soll bei jedem Datensatzwechsel ausgelöst werden, was ein typischer Einsatzfall für die Ereigniseigenschaft Beim Anzeigen des Formulars ist. Die entsprechende Ereignisprozedur hat folgendes Aussehen: Private Sub mForm_Current() 'Wenn ein Kombinationsfeld referenziert wurde If Not (mSearchComboBox Is Nothing) Then 'Kombinationsfeld aktualisieren UpdateCombobox End If End Sub Listing 14.19: Beim Wechsel des im Formular angezeigten Datensatzes wird das Schnellsuche-Kombinationsfeld ebenfalls aktualisiert.
Die Ereignisprozedur aus Listing 14.19 prüft vor dem Aufrufen der Prozedur UpdateComboBox noch, ob überhaupt ein Schnellsuche-Kombinationsfeld referenziert ist, und unterlässt gegebenenfalls die Aktualisierung. Die Prozedur UpdateComboBox prüft, ob das Formular überhaupt einen Datensatz anzeigt, und ermittelt in diesem Fall den Wert des Primärschlüsselfeldes des aktuellen Datensatzes. Anderenfalls – entweder weil kein oder ein noch nicht gespeicherter Datensatz angezeigt wird – leert die Prozedur das Kombinationsfeld.
Standardfunktionen von Formularen auslagern
637
Private Sub UpdateCombobox() Dim rst As DAO.Recordset 'Kopie des Formular-Recordset erzeugen Set rst = mForm.RecordsetClone 'Wenn kein Datensatz markiert oder Datensatz neu ist If rst.NoMatch Or mForm.NewRecord Then 'Kombinationsfeld leeren mSearchComboBox.Value = 0 Else 'sonst Recordset-Kopie auf den gleichen Datensatz wie im 'Formular einstellen rst.Bookmark = mForm.Bookmark 'Kombinationsfeld auf den Primärindex dieses Datensatzes setzen mSearchComboBox.Value = rst.Fields(mPrimaryKey) End If Set rst = Nothing End Sub Listing 14.20: Aktualisieren des Kombinationsfeldes nach Änderungen im Datenbestand
Anzeige des im Kombinationsfeld ausgewählten Datensatzes Andersherum soll es natürlich möglich sein, mit dem Kombinationsfeld einen Datensatz auszuwählen, der anschließend im Formular angezeigt wird. Das durch das Kombinationsfeld ausgelöste Ereignis heißt Nach Aktualisierung; der auszuführende Code wird in der Ereignisprozedur mSearchComboBox_AfterUpdate untergebracht. Die Prozedur erstellt eine Kopie der Datensatzgruppe des Formulars und verweist mit einer Objektvariablen vom Typ Recordset darauf. Mit der bereits weiter oben vorgestellten Membervariablen mPrimaryKeyString überprüft die Routine, ob das Primärschlüsselfeld den Datentyp String hat, und setzt den Vergleichsausdruck des nachfolgend zusammengesetzten Kriteriumsausdrucks gegebenenfalls in Hochkommata. Der erste Teil des Kriteriums ist übrigens der in der Membervariablen mPrimaryKey gespeicherte Wert – dieser entspricht dem Primärschlüsselfeld des Formulars. Mit diesem Kriteriumsausdruck ausgerüstet sucht die Routine in der Kopie des Formular-Recordsets nach dem entsprechenden Datensatz und stellt bei Erfolg das Originalrecordset und damit den im Formular angezeigten Datensatz auf den gesuchten Datensatz ein.
638
14
Objektorientierung im Praxiseinsatz
Private Sub mSearchComboBox_AfterUpdate() Dim strCriteria As String 'Wenn Primärschlüsselfeld den Typ "String" hat, dann ... If mPrimaryKeyString = True Then 'Vergleichswert in Hochkomma einschließen strCriteria = mPrimaryKey & " = '" & mSearchComboBox & "'" Else '... sonst ohne Hochkomma angeben strCriteria = mPrimaryKey & " = " & mSearchComboBox End If 'Datensatz mit dem im Kombinationsfeld angegebenen 'Primärschlüsselwert suchen mForm.Recordset.FindFirst strCriteria End Sub Listing 14.21: Anzeigen des im Kombinationsfeld ausgewählten Datensatzes
14.1.4 Weitere Möglichkeiten Damit wären alle sichtbaren Funktionen des Formulars in die Klasse clsFormCode ausgelagert. Eine Fleißaufgabe wird allerdings noch nicht abgedeckt – das Validieren von Daten. Auch dies lässt sich aber mit der hier vorgestellten Technik realisieren.
14.2 Mehrere Formularinstanzen anzeigen Access weigert sich beharrlich, durch einfaches Öffnen mehrere Instanzen eines Formulars anzuzeigen. Dabei wäre dies aber gerade zur gleichzeitigen Anzeige von Detailansichten von Artikeln, Mitarbeitern und ähnlichen Objekten sehr interessant – zwar könnte man diese auch einfach in Tabellenform untereinander anzeigen, aber bei Objekten mit vielen Eigenschaften beziehungsweise Feldern scrollt man mehr hin und her als man Daten vergleichen kann. In den folgenden Abschnitten erfahren Sie, wie Sie mehrere Instanzen eines Formulars gleichzeitig öffnen und diese auch noch vernünftig verwalten.
14.2.1 Beispielformulare Als Beispielformulare dienen die Formulare frmKontaktuebersicht und frmKontaktdetails. Das Formular frmKontaktuebersicht enthält ein Listenfeld namens lstKontakte sowie drei Schaltflächen zum Öffnen und Schließen des aktuell markierten Kontakts sowie zum Schließen aller offenen Instanzen des Formulars frmKontaktdetails (siehe Abbildung 14.3).
Mehrere Formularinstanzen anzeigen
639
Beispiele auf CD: Die Tabelle tblKontakte und die Formulare frmKontaktuebersicht und frmKontaktdetails finden Sie auf der Buch-CD unter Kap_14/ObjektorientierteTechniken.mdb. Das Listenfeld hat als Datensatzherkunft folgenden SQL-Ausdruck: SELECT tblKontakte.KontaktID, [Nachname] & ", " & [Vorname] AS Kontakt FROM tblKontakte;
Stellen Sie außerdem die Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 2 und 0cm ein, damit das erste Feld der Datensatzherkunft nicht angezeigt wird, aber als gebundene Spalte verwendet werden kann.
Abbildung 14.3: Formular zum Öffnen eines oder mehrerer Detailformulare
Für die Anzeige der Details zu jedem Kontakt ist das Formular frmKontaktdetails verantwortlich. Das Formular ist relativ einfach aufgebaut. Es verwendet die Tabelle tblKontakte als Datenherkunft. Sämtliche Felder der Tabelle werden im Formular angezeigt (siehe Abbildung 14.4).
14.2.2 Erzeugen einer neuen Instanz Der übliche Weg zum Öffnen eines Formulars per DoCmd.OpenForm-Methode funktioniert hier nicht. Das können Sie ganz einfach im Testfenster nachvollziehen: Geben Sie dort zunächst einmal die folgende Anweisung ein: DoCmd.OpenForm "frmKontaktdetails"
640
14
Objektorientierung im Praxiseinsatz
Abbildung 14.4: Dieses Formular soll mehrfach instanziert werden können.
Die Anweisung öffnet das angegebene Formular wie gewünscht. Wenn Sie die gleiche Anweisung nun ein zweites Mal ausführen, erscheint kein weiteres Formular. DoCmd.OpenForm öffnet lediglich die Standardinstanz eines Formulars. Das Öffnen mehrerer Instanzen eines Formulars setzt Folgendes voraus: Das zu öffnende Formular hat ein Klassenmodul. Jede Instanz wird durch eine eigene Objektvariable referenziert. Schauen Sie sich folgendes Beispiel an. Es beschreibt, wie Sie mit der Öffnen-Schaltfläche des Formulars frmKontaktuebersicht eine neue Instanz des Formulars frmKontaktdetails mit dem aktuell in der Liste ausgewählten Kontakt erzeugen. Das Formular frmKontaktdetails hat bisher noch kein Modul, da Sie noch keine Ereignisprozedur angelegt oder anderweitig ein Modul erstellt haben. Das ist auch nicht notwendig; stellen Sie einfach die Eigenschaft Enthält Modul in der Entwurfsansicht des Formulars auf den Wert Ja ein. Der folgende Code zeigt die Vorgehensweise für eine einzelne Instanz des Formulars. Er enthält die Deklaration einer Objektvariablen, die später auf die neue Instanz verweisen wird, und eine Ereignisprozedur, die beim Klicken auf die Schaltfläche zur Anzeige der Details ausgelöst wird. Die Deklaration der Objektvariablen frm erfolgt modulweit und nicht innerhalb der Prozedur cmdDetails_Click. Fände die Deklaration in der Prozedur statt, würde die Variable mit Beenden der Prozedur gelöscht. Durch die Deklaration als Modulvariable bleibt die neue Instanz des Formulars geöffnet, bis Sie das aufrufende Formular schließen.
Mehrere Formularinstanzen anzeigen
641
Die Prozedur selbst erstellt die Instanz des Formulars und stellt die Eigenschaften Filter und FilterOn so ein, dass der im Listenfeld des aufrufenden Formulars ausgewählte Kontakt angezeigt wird. Option Compare Database Option Explicit 'Objektvariable deklarieren Dim frm As Form_frmKontaktdetails Private Sub cmdDetails_Click() 'Instanz von frmKontaktdetails erstellen und 'der Objektvariablen zuweisen Set frm = New Form_frmKontaktdetails 'Filter auf den gewünschten Kontakt setzen frm.Filter = "KontaktID = " & Nz(Me!lstKontakte) frm.FilterOn = True 'Formular sichtbar machen frm.Visible = True End Sub Listing 14.22: Code zum Öffnen einer Formularinstanz
14.2.3 Öffnen mehrerer Instanzen eines Formulars Das eigentliche Ziel haben Sie mit der oben gezeigten Vorgehensweise noch nicht erreicht, aber auch nicht aus den Augen verloren: Der Ablauf beim Instanzieren ist auch beim Erzeugen mehrerer Instanzen des Formulars gleich; die notwendige Erweiterung besteht darin, die einzelnen Instanzen durch die entsprechende Anzahl Objektvariablen zu referenzieren und diese zu verwalten.
Formularinstanz-Sammlung Das Zauberwort für die Verwaltung mehrerer Instanzen von Objekten der gleichen Klasse heißt Collection. Eine Collection kann Objekte beliebigen Typs aufnehmen – also auch verschiedene Instanzen eines Formulars. Statt der einsamen Objektvariablen frm zum Speichern einer einzigen Instanz, verwenden Sie nun eine Collection, die Sie folgendermaßen deklarieren: Dim colForms As Collection
642
14
Objektorientierung im Praxiseinsatz
Bevor Sie Formular-Objekte in dieser Collection speichern können, müssen Sie diese zunächst instanzieren. Das ginge theoretisch, indem Sie der obigen Deklaration einfach das Schlüsselwort New hinzufügen: Dim colForms As New Collection
Programmiertechnisch sauberer ist allerdings die Aufteilung der Deklaration und der Instanzierung. Ein besserer Ansatz wäre, Letzteres in der Ereignisprozedur durchzuführen, die beim Öffnen des Formulars ausgelöst wird: Private Sub Form_Open(Cancel As Integer) Set colForms = New Collection End Sub Listing 14.23: Instanzieren der Collection beim Öffnen des Formulars
Neue Formularinstanz erzeugen und zur Collection hinzufügen Nach der Auswahl eines Kontaktes aus dem Listenfeld soll per Knopfdruck eine Instanz des Formulars frmKontaktdetails geöffnet werden, ohne dass die bereits vorhandenen Instanzen wieder gelöscht werden (siehe Abbildung 14.5).
Abbildung 14.5: Öffnen mehrerer Formularinstanzen per Mausklick
Dabei soll natürlich nur eine neue Formularinstanz erzeugt werden, wenn noch kein Formular für diesen Kontakt angezeigt wird. Wird der Kontakt bereits angezeigt, soll das jeweilige Formular einfach aktiviert werden. Außerdem sollen natürlich nicht alle Instanzen des Formulars direkt übereinander angezeigt werden.
Mehrere Formularinstanzen anzeigen
643
Die folgende Prozedur wird diesen Anforderungen gerecht: Sie ermittelt zunächst die KontaktID und den im Listenfeld angezeigten Namen des aktuellen Kontaktes und speichert diese in entsprechenden Variablen. Dann prüft die Prozedur, ob bereits eine Formularinstanz existiert, die genau diesen Kontakt enthält. Die Anzahl der in der Collection bereits enthaltenen Elemente wird dabei aus zwei Gründen in einer Variablen gespeichert: Erstens lässt sich an diesem Wert überprüfen, ob überhaupt schon Instanzen des Formulars existieren, und falls ja, dient dieser Wert als Endpunkt der For…Next-Schleife, die alle enthaltenen Instanzen auf den angezeigten Inhalt hin überprüft. Innerhalb dieser Schleife kommt die Tag-Eigenschaft der Formularinstanz zum Tragen: Beim Anlegen einer neuen Instanz weist die Prozedur dieser Eigenschaft einen Wert zu, der aus der Zeichenkette »Kontakt« und dem Wert des Feldes KontaktID des Kontaktes besteht, also beispielsweise »Kontakt12« – dazu später mehr. Während die Prozedur alle Elemente der Collection durchläuft, vergleicht sie jeweils den Wert der TagEigenschaft des enthaltenen Formulars mit dem Wert, den die neue Instanz erhalten würde. Trifft die Prozedur beim Durchlaufen der Schleife auf ein Formular, das genau diesen Tag-Wert hat, bedeutet dies, dass bereits ein Formular für den entsprechenden Kontakt erzeugt wurde. Dieses wird dann mit der SetFocus-Methode aktiviert und die Prozedur beendet. Wenn die Prozedur in diesem Bereich feststellt, dass die Collection entweder noch gar kein Element enthält oder das betroffene Element sich nicht unter den vorhandenen Elementen befindet, erstellt es eine neue Instanz des Formulars für den ausgewählten Kontakt. Nach der Deklaration und Instanzierung eines neuen Objekts des Typs Form_ frmKontaktdetails erhält dieses die notwendigen Eigenschaftswerte – der Filter wird auf den gewünschten Kontakt-Datensatz eingestellt, die Tag-Eigenschaft enthält einen Wert, der aus der Zeichenkette »Kontakt« und dem Wert des Feldes KontaktID des betroffenen Kontaktes besteht (den Zweck haben Sie ja bereits weiter oben erfahren), die Überschrift des Formulars wird auf den Namen des Kontaktes eingestellt und schließlich das Formular sichtbar gemacht. Und dann folgt der wichtigste Schritt: Die Objektvariable, die auf die Formularinstanz verweist, wird der Collection colForms hinzugefügt. Da diese modulweit gültig ist, bleiben die enthaltenen Verweise bis zum Schließen des Formulars bestehen. Damit nicht alle Instanzen übereinander angezeigt werden, ermittelt die Prozedur noch die Anzahl der vorhandenen Formulare und versetzt das aktuelle Formular um einige Pixel nach rechts unten. Private Sub cmdDetails_Click() Dim i As Integer Dim intFormCount As Integer
644
14
Objektorientierung im Praxiseinsatz
Dim lngKontaktID As Long Dim strKontakt As String
If IsNull(Me!lstKontakte) Then Exit Sub End If lngKontaktID = Me!lstKontakte strKontakt = Me!lstKontakte.Column(1) 'Kontrollieren, ob Kontakt schon angezeigt wird...: 'Anzahl ermitteln intFormCount = colForms.Count 'Prüfen, ob überhaupt schon ein Element vorhanden ist If intFormCount > 0 Then '... und falls ja, prüfen, ob bereits 'eines den gewünschten Kontakt enthält For i = 1 To intFormCount If colForms.Item(i).Form.Tag = "Kontakt" & lngKontaktID Then colForms.Item(i).Form.SetFocus Exit Sub End If Next i End If '... und falls nicht, eine neue Formularinstanz mit dem Artikel erzeugen Dim frm As Form_frmKontaktdetails Set frm = New Form_frmKontaktdetails 'Eigenschaften wie anzuzeigender Kontakt, Überschrift und Tag festlegen With frm .Filter = "KontaktID = " & lngKontaktID .FilterOn = True .Tag = "Kontakt" & lngKontaktID .Caption = "Kontakt: " & strKontakt 'Formularinstanz sichtbar machen .Visible = True End With 'Formularobjekt zur Collection hinzufügen colForms.Add frm 'Aktuelle Anzahl ermitteln intFormCount = colForms.Count
Mehrere Formularinstanzen anzeigen
645
'Formulare entsprechend der Anzahl nach unten rechts verschieben DoCmd.MoveSize (intFormCount * 500) + 1000, (intFormCount * 500) + 1000 End Sub Listing 14.24: Per Mausklick eine weitere Formularinstanz öffnen
Gelöst ist das Problem noch lange nicht: Sie müssen noch zwei Schaltflächen zum Schließen eines bestimmten Formulars beziehungsweise zum Schließen aller geöffneten Formulare mit Leben füllen.
Schließen aller Instanzen des Formulars Letztere Aufgabe ist offensichtlich schneller erledigt: Wenn Sie sich einer Instanz eines Formulars durch Leeren der Objektvariablen entledigen können, dann lassen sich durch die gleiche Vorgehensweise mit der Collection, die alle Objektvariablen enthält, vermutlich auch alle Formulare schließen: Private Sub cmdAlleSchliessen_Click() Set colFrmArtikel = Nothing End Sub Listing 14.25: Schließen aller Formularinstanzen durch Leeren der kompletten Sammlung
Das funktioniert auch – solange sich keines der Formulare wehrt. Formulare zur Datenbearbeitung besitzen in der Regel Mechanismen zum Validieren der enthaltenen Daten. Das Formular frmKontaktdetail könnte etwa folgende Prozedur zum Überprüfen des Vornamens besitzen: Private Sub Form_BeforeUpdate(Cancel As Integer) If Nz(Me!Vorname, "") = "" Then MsgBox "Bitte geben Sie einen Vorname ein." Me!Vorname.SetFocus Cancel = True Exit Sub End If End Sub Listing 14.26: Validierung eines Formularfeldes
Wenn Sie nun den Vornamen in einer Instanz des Formulars frmKontaktdetails leeren und dann mit der Prozedur aus Listing 14.25 schließen möchten, zeigt Access zwar die für diesen Fall vorbereitete Meldung an, schließt aber anschließend das Formular, ohne dem Benutzer Gelegenheit zum Korrigieren der Eingabe zu geben – und ohne irgendeine Änderung in diesem Formular zu übernehmen.
646
14
Objektorientierung im Praxiseinsatz
Die folgende Prozedur ist zwar wesentlich umfangreicher als der Vorschlag aus Listing 14.25, dafür lässt sie Formulare, deren Inhalt »dirty«, also seit dem letzten Speichern verändert ist, außen vor und schließt nur die übrigen Formulare, indem sie diese aus der Collection colForms entfernt. Wenn die Collection nach dem kollektiven Schließen noch Elemente enthält und dementsprechend noch ein oder mehrere Formular geöffnet sind, gibt die Prozedur eine Meldung aus. Private Dim Dim Dim
Sub cmdAlleSchliessen_Click() intFormCount As Integer i As Integer frm As Form_frmKontaktdetails
'Anzahl der Instanzen ermitteln intFormCount = colForms.Count 'Alle Instanzen durchlaufen - rückwärts wegen Löschvorgang For i = intFormCount To 1 Step -1 'Aktuelles Element per Objektvariable referenzieren Set frm = colForms.Item(i) 'Prüfen, ob Datensatz gespeichert ist If frm.Dirty = False Then 'Falls gespeichert: Aus Collection entfernen. colForms.Remove i End If Next i 'Anzahl der Instanzen erneut ermitteln intFormCount = colForms.Count 'Wenn noch Instanzen vorhanden sind, Meldung ausgeben If intFormCount = 0 Then Set colForms = Nothing Else MsgBox "Es konnten nicht alle Formulare mit " _ & "Kontaktdetails geschlossen werden." End If End Sub Listing 14.27: Schließen aller Instanzen
Mehrere Formularinstanzen anzeigen
647
Schließen einer bestimmten Instanz Die Möglichkeit, mehrere Instanzen eines Formulars mit verschiedenen Datensätzen zu erzeugen, ist in vielen Fällen vermutlich die ergonomischste Lösung für die gleichzeitige Ansicht mehrerer Datensätze. Das gilt vor allem, wenn die durch die Datensätze repräsentierten Objekte so viele Daten enthalten, dass diese nicht nebeneinander auf einer Bildschirmbreite angezeigt werden können. Außerdem kann der Benutzer mit dieser Methode die Formulare mit den interessanten Daten auch noch so anordnen, wie es für den jeweiligen Fall am sinnvollsten ist. Es fehlen noch einige kleine Schritte, um die ergonomischen Vorteile dieser Lösung zu vollkommnen: Die Formularinstanzen sollten auch per Doppelklick in das Listenfeld geöffnet werden können. Der Benutzer soll auch einzelne Formulare vom Übersichtsformular aus schließen können. Die Formulare sollen auch mit der dafür vorgesehenen Schaltfläche geschlossen werden können. Der erste Wunsch ist reine Code-Optimierung. Fügen Sie den gesamten Code der Ereignisprozedur cmdDetails_Click in eine neue Prozedur namens InstanzOeffnen ein. Die beiden Variablen lngKontaktID und strKontakt sollen weiterhin von der aufrufenden Routine ermittelt und an die Prozedur InstanzOeffnen übergeben werden. Die Ereignisprozedur cmdDetails_Click sieht nunmehr wie folgt aus: Private Sub cmdDetails_Click() InstanzOeffnen Me!lstKontakte, Me!lstKontakte.Column(1) End Sub Listing 14.28: Aufruf der Prozedur InstanzOeffnen beim Klick auf die Schaltfläche cmdDetails …
Die gleiche Anweisung sorgt beim Doppelklick ins Listenfeld lstKontakte für den Aufruf der Prozedur InstanzOeffnen: Private Sub lstKontakte_DblClick(Cancel As Integer) InstanzOeffnen Me!lstKontakte, Me!lstKontakte.Column(1) End Sub Listing 14.29: … und beim Doppelklick auf den gewünschten Eintrag im Listenfeld
648
14
Objektorientierung im Praxiseinsatz
Fehlt noch die in die neue Prozedur »extrahierte« Funktionalität (Erläuterungen siehe Listing 14.24): Private Sub InstanzOeffnen(lngKontaktID As Long, strKontakt As String) Dim i As Integer Dim intFormCount As Integer Dim frm As Form_frmKontaktdetails intFormCount = colForms.Count If intFormCount > 0 Then For i = 1 To intFormCount If colForms.Item(i).Form.Tag = "Kontakt" & lngKontaktID Then colForms.Item(i).Form.SetFocus Exit Sub End If Next i End If Set frm = New Form_frmKontaktdetails With frm .Filter = "KontaktID = " & lngKontaktID .FilterOn = True .Tag = "Kontakt" & lngKontaktID .Caption = "Kontakt: " & strKontakt .Visible = True End With colForms.Add frm, "Kontakt" & lngKontaktID intFormCount = colForms.Count DoCmd.MoveSize (intFormCount * 500) + 1000, (intFormCount * 500) + 1000 End Sub Listing 14.30: Extrahierte Fassung der Funktionalität zum Anlegen einer neuen Formularinstanz
Der Wunsch nach dem Schließen einzelner Formularinstanzen wahlweise vom Übersichtsfenster aus oder direkt per Schließen-Schaltfläche des Formulars schreit ebenso wie im obigen Fall nach Bereitstellung einer Routine, die von den entsprechenden Stellen aus aufgerufen werden kann. Warum aber kann man das Formular nicht einfach mit seiner Schließen-Schaltfläche verschwinden lassen? Ganz einfach: Weil es dann nicht aus der Collection entfernt wird. Zugriffe auf den zurückgelassenen Eintrag in der Collection würden in der Folge zu Fehlern führen.
Mehrere Formularinstanzen anzeigen
649
Daher wird die InstanzSchliessen-Prozedur aus folgendem Listing auch vom zu schließenden Formular aus aufgerufen und deshalb als Public deklariert. Die Prozedur überprüft genau wie die Prozedur zum Schließen aller Formulare, ob das Formular noch zu speichernde Daten enthält, und bricht den Vorgang gegebenenfalls ab. Wenn dem Schließen aber nichts mehr im Wege steht, entfernt die Prozedur das durch den Eingangsparameter strTag charakterisierte Formular aus der Collection und schließt es damit. Public Sub InstanzSchliessen(strTag As String) Dim frm As Form_frmKontaktdetails On Error GoTo InstanzSchliessen_Err 'Per Objektvariable auf die Formularinstanz verweisen Set frm = colForms.Item(strTag) 'Nur wenn der Inhalt des Formulars seit dem letzten 'Speichern nicht geändert wurde: If frm.Dirty = False Then 'Tag des Formulars leeren frm.Tag = "" 'Formular aus Collection entfernen und damit schließen colForms.Remove strTag End If InstanzSchliessen_Exit: Exit Sub InstanzSchliessen_Err: 'Formular nicht mehr vorhanden If Err.Number = 5 Or Err.Number = 9 Then GoTo InstanzSchliessen_Exit Else MsgBox Err.Number & " " & Err.Description GoTo InstanzSchliessen_Exit End If End Sub Listing 14.31: Funktion zum Schließen eines Formulars
Im Vergleich zur Prozedur aus Listing 14.27 fällt die zusätzliche Anweisung frm.Tag = "" auf: Ihre Bedeutung wird nachfolgend erläutert.
650
14
Objektorientierung im Praxiseinsatz
Schließen-Vorgang des Formulars anpassen Wie bereits erwähnt, soll die Funktion InstanzSchliessen auch beim Schließen einer Formularinstanz über deren Schließen-Schaltfläche ausgelöst werden. Den Prozeduraufruf bringen Sie in der Ereignisprozedur Beim Schließen des Formulars unter: Private Sub Form_Close() 'Prüfen, ob die Formularinstanz noch in der Collection enthalten ist If Not Nz(Me.Tag, "") = "" Then 'Prozedur zum Entfernen der Instanz aus der Collection aufrufen Forms!frmKontaktuebersicht.InstanzSchliessen Me.Tag End If End Sub Listing 14.32: Prozedur, die beim Schließen des Detailformulars ausgelöst wird
Hier klärt sich auch die zusätzliche Zeile in der Prozedur zum Entfernen der Instanz aus der Collection (Listing 14.31). Wenn man das Formular mit seinen Bordmitteln schließen und dabei einfach nur die Funktion zum Entfernen der Instanz aufrufen würde, hätte man es mit einem typischen »Die Katze beißt sich in den Schwanz«-Problem zu tun: Das Beim Schließen-Ereignis ruft kurz vor dem Exitus des Formulars noch die Prozedur InstanzSchliessen auf. Durch das dortige Entfernen aus der Collection fliegt das Formular aus seinem Gültigkeitsbereich und löst wiederum das Ereignis Beim Schließen aus – und so beginnt das Spiel von vorne. Die Tag-Eigenschaft ist die Rettung: Beim Schließen vom Formular aus enthält diese Eigenschaft noch einen Wert wie »Kontakt12«. Dann wird die InstanzSchliessen-Prozedur aufgerufen, die diese Eigenschaft leert. Damit führt der durch das Entfernen der Instanz aus der Collection verursachte Schließen-Vorgang endgültig zum Exitus des Formulars.
14.3 Mehrschichtige Anwendungen Im Gegensatz zu objektorientierten Programmiersprachen wie C#, VB.NET oder Java gibt es in Access-Anwendungen keine einzelnen Dateien, die unterschiedliche Klassen repräsentieren. Ganz im Gegenteil: Sämtliche Objekte wie Tabellendefinitionen, Abfragen, Formulare, Berichte, Module und selbst die Daten stecken alle in einer einzigen Datei. Da wird sich der eine oder andere fragen: Mehrschichtige Anwendungen mit diesem aus einem Haufen einzelner Objekte und wirr verteiltem VBA-Code bestehenden Klotz? Wie soll das funktionieren?
Mehrschichtige Anwendungen
651
Letztendlich funktioniert das genau wie in anderen objektorientierten Programmiersprachen – nur dass diese natürlich einige Features mehr liefern wie etwa Vererbung und Polymorphie. Und die Tatsache, dass Access alle Klassen und Objekte intern speichert, spielt letzten Endes nur eine Rolle, wenn Sie den Code örtlich verteilen möchten. Welche Vorteile bringen mehrschichtige Anwendungen nun konkret? Nun, schauen Sie sich erstmal die Nachteile herkömmlicher Access-Anwendungen an. Benutzungsoberfläche und Anwendungslogik sind zu einem sehr hohen Anteil in den Formularen und Berichten konzentriert. Das ist an sich kein Nachteil, wenn nicht verschiedene Funktionen auch noch miteinander vermengt wären: Der Zugriff auf die Daten erfolgt direkt über die Bindung von Formularen und die enthaltenen Steuerelemente auf die Abfragen beziehungsweise Tabellen. Manchmal sorgt eine Gültigkeitsregel oder ein anderer Integritätsmechanismus innerhalb der Tabellen für die Konsistenz der Daten, in vielen Fällen ist diese Funktion jedoch in den Formularen selbst enthalten und wird etwa durch die Ereignisse Vor Aktualisierung des Formulars oder Steuerelements ausgelöst. Andere Felder sind nicht direkt an die Datenherkunft gebunden, sondern beziehen ihre Daten beispielsweise per VBA über Domänenfunktionen. Sprich: Die Anwendungslogik verteilt sich über mehrere verschiedene Objektarten wie Tabellen, Formulare und VBA-Module – vielleicht gibt es sogar noch ein paar Makros. Die Pflege solcher Anwendungen kann sehr zeitintensiv werden: Wenn Sie – als einfaches Beispiel – eine Meldung, die auf einen falschen Datentyp bei der Eingabe in ein Formularfeld hinweist, ändern oder entfernen möchten, sind Sie unter Umständen lange unterwegs, da sich der Auslöser in verschiedenen Ereignissen des Formulars oder auch im Tabellenentwurf befinden kann. Das alles muss man natürlich in soweit relativieren, dass man mit ein wenig Sorgfalt und konsistenter Vorgehensweise durchaus seine eigenen Anwendungen und die Orte, an denen man die Anwendungslogik unterbringt, im Griff hat. Und Access ist natürlich zuerst einmal dafür ausgelegt, auf schnellstem Wege Anwendungen für den Zugriff auf und die Verwaltung von Daten zu entwickeln. Wenn eine Anwendung allerdings ein gewisses Maß an Komplexität überschritten hat und Sie für kleine Änderungen beinahe genauso lange brauchen, als wenn Sie die halbe Anwendung neu programmieren, sollten Sie über alternative Vorgehensweisen nachdenken. Diese liegen beispielsweise in der Verwendung eines mehrschichtigen Datenmodells. Solche Modelle gibt es in mehreren Varianten mit unterschiedlicher Interpretation. Im folgenden lernen Sie ein Modell kennen, das je nach Sichtweise aus drei oder vier Schichten besteht: Der Benutzungsoberfläche (GUI-Schicht), der Business-Schicht, der Datenzugriffsschicht und den Daten. Manch einer betrachtet die Datenzugriffsschicht und die Daten als Einheit, andere sehen zwei Schichten darin. Im Rahmen dieses Buches werden Datenzugriffsschicht und Daten als zwei Schichten betrachtet.
652
14
Objektorientierung im Praxiseinsatz
Nachteile hat die Verwendung einer mehrschichtigen Architektur natürlich auch: Das Schichtmodell bringt einen immensen zusätzlichen programmatorischen Aufwand mit sich, der sich eindeutig in geringerer Performance niederschlägt – vor allem, wenn die Daten einer oder mehrerer Tabellen komplett in Form von Objekten vorliegen. Das nachfolgende Beispiel kann auch nur einen Eindruck vom Aufbau einer mehrschichtigen Anwendung vermitteln – für den Einsatz in der Praxis müsste man in einige Stellen noch weit mehr Arbeit investieren. So wäre zum Beispiel ein Mechanismus zu schaffen, der sicherstellt, dass die Objekte regelmäßig mit den aktuellen Daten aus der Datenbank gefüttert werden, da auch andere Benutzer auf die enthaltenen Daten zugreifen können. Die Beispiele zu diesem Abschnitt finden Sie auf der Buch-CD unter Kap_14\ObjektorientierteTechniken.mdb. Es handelt sich dabei um die Tabelle tblPersonen, das Formular frmPersonen sowie die Module clsPerson, clsController und clsPersonDAO_ DAO.
14.3.1 Beispiel Als Beispiel für den mehrschichtigen Datenzugriff dient eine Tabelle namens tblPersonen (siehe Abbildung 14.6) und ein Formular namens frmPersonen (siehe Abbildung 14.7). Das Formular ist komplett ungebunden und enthält lediglich die folgenden Steuerelemente: cboSchnellsuche: dient der Auswahl von Personen txtPersonID: schreibgeschützt txtVorname, txtNachname, txtStrasse, txtPLZ, txtOrt: Textfelder der Tabelle cmdSpeichern: Schaltfläche zum Speichern des aktuellen Inhalts cmdLoeschen: Schaltfläche zum Löschen des aktuellen Datensatzes cmdNeu: Schaltfläche zum Anlegen eines neuen Datensatzes
Abbildung 14.6: Entwurfansicht der Tabelle tblPersonen
Mehrschichtige Anwendungen
653
Abbildung 14.7: Das Formular frmPersonen ist ungebunden.
14.3.2 Die GUI-Schicht Die GUI-Schicht – also die Benutzungsoberfläche – bildet für das nachfolgende Beispiel das Formular frmPersonen aus Abbildung 14.7. In diesem Fall wird die GUISchicht also in Form eines Access-Frontends realisiert. Die GUI-Schicht enthält nur Methoden für den Zugriff auf die Business-Schicht, auf keinen Fall kann sie direkt auf eine darunter liegende Schicht zugreifen. Das ist bei herkömmlichen Access-Anwendungen der Fall: Hier werden Datenherkunft und Steuerelemente direkt an die Datenschicht gebunden. Andersherum kann keine der anderen Schichten auf die GUI-Schicht zugreifen – das ist eine der Hauptprämissen bei der Entwicklung mehrschichtiger Anwendungen. Sie minimieren damit die Abhängigkeit, indem Sie dafür sorgen, dass diese lediglich einseitig ist.
14.3.3 Die Business-Schicht Die Business-Schicht enthält zwei Typen von Objekten: Die erste repräsentiert die in den Datensätzen der Tabellen enthaltenen Daten (Daten-Objekte), die zweite enthält die Steuermechanismen für den Transfer der Daten zwischen der Datenzugriffsschicht und der GUI-Schicht (Controller-Objekte). Genau genommen ist das nicht ganz richtig: Die Controller-Objekte steuern zwar die Objekte der Datenzugriffsschicht und andere Objekte der Business-Schicht, aber die Kooperation zwischen der GUI-Schicht und den Controller-Objekten geht immer von der GUI-Schicht aus. Wie bereits erwähnt – die oberen Schichten können zwar auf die unteren zugreifen, aber niemals umgekehrt. Und da auch nie eine Schicht übersprungen werden darf, muss die GUI-Schicht immer über die Business-Schicht auf die Datenzugriffsschicht zugreifen, die dann die gewünschten Daten nach oben reicht.
654
14
Objektorientierung im Praxiseinsatz
Wie viele und welche Objekte Sie in der Business-Schicht ansiedeln, hängt von der Art der zugrunde liegenden Daten und der Benutzungsoberfläche ab. Sie werden vermutlich für jede Tabelle, die objektartige Daten enthält, ein eigenes Objekt erstellen. Außerdem müssen Sie entscheiden, ob Sie ein Controller-Objekt pro Element der Benutzungsoberfläche oder vielleicht sogar ein großes Controller-Objekt verwenden. Übersichtlicher dürfte etwa ein Objekt pro Formular sein.
14.3.4 Die Datenzugriffsschicht Die Datenzugriffschicht enthält Datenzugriffsobjekte. Zu jedem Daten-Objekt gibt es ein Datenzugriffsobjekt, das verschiedene Operationen ausführen kann: Erzeugen eines Datenobjekts auf der Basis eines Datensatzes der zugrunde liegenden Tabelle Erzeugen eines Recordsets mit Datensätzen als Suchergebnis mit vorgegebenen Kriterien Aktualisieren eines Datensatzes in der Datenbank auf Basis der in einem Datenobjekt enthaltenen Daten Anlegen eines neuen Datensatzes in der Datenbank Löschen eines Datensatzes aus der Datenbank Damit entkoppelt die Datenzugriffsschicht die Business-Schicht von den Daten. Der Vorteil liegt darin, dass Sie ohne Probleme die Datenquelle wechseln können – etwa um von einem Access-Backend auf einen SQL-Server umzusteigen oder vielleicht sogar, um eine XML-Datei als Datenquelle zu verwenden. Sie müssen lediglich die Klassen der Datenzugriffsschicht anpassen – die GUI-Schicht und die Business-Schicht bleiben vom Wechsel der Datenquelle unberührt. Im nachfolgend beschriebenen Beispiel erfahren Sie, wie die fünf Operationen eines Datenzugriffobjekts aussehen. Man benötigt je ein Datenzugriffsobjekt pro BusinessObjekt der Business-Schicht. Das Beispiel verwendet lediglich ein Datenzugriffsobjekt für den Zugriff auf die Datenbank per DAO. Sie können alternativ ein Datenzugriffsobjekt mit den gleichen Methoden, aber anderen Anweisungen für den Zugriff auf die Daten verwenden, um etwa die ADODB-Bibliothek statt der DAO-Bibliothek einzusetzen. Oder Sie erstellen ein drittes Datenzugriffsobjekt, das den Zugriff auf eine XML-Datei über das mit der Bibliothek MSXML gelieferte Document Object Model ermöglicht. Wenn Sie bezüglich des Datenzugriffs derart flexibel sein möchten, empfiehlt sich die Verwendung einer Schnittstelle – mehr dazu haben Sie bereits in Kapitel 13, Abschnitt 13.9, »Schnittstellen und Vererbung«, gelesen.
Mehrschichtige Anwendungen
655
14.3.5 Die Datenschicht Die Datenschicht enthält die eigentlichen Daten. Im vorliegenden Beispiel ist das eine Tabelle in einer Access-Datenbank. Es kann sich aber auch um eine Tabelle in einer SQL-Server-Datenbank oder um eine XML-Datei handeln. Diese Flexibilität erhalten Sie durch die Aufteilung der Anwendung auf verschiedene Schichten – um etwa auf eine XML-Datei statt auf eine Access-Datenbank zuzugreifen, müssten Sie nur die Objekte der Datenzugriffsschicht anpassen. Benutzungsoberfläche und BusinessSchicht bleiben unangetastet.
14.3.6 Zusammenhänge der Objekte und Schichten Abbildung 14.8 zeigt die Aufteilung der Objekte auf die einzelnen Schichten. Es gibt eine Menge Zusammenhänge, die Sie in den folgenden Abschnitten detailliert und mit Code versehen näher kennen lernen.
GUI-Schicht
Businessschicht
clsPerson
clsController
Datenzugriffsschicht
clsPerson_DAO
Datenschicht
tblPersonen
Abbildung 14.8: Aufteilung der Objekte auf die einzelnen Schichten
656
14
Objektorientierung im Praxiseinsatz
14.3.7 Initialisieren des Formulars Direkt nach dem Öffnen soll das Formular keinen Datensatz anzeigen. Lediglich das Kombinationsfeld cboSchnellsuche soll alle enthaltenen Personen zur Auswahl anbieten. Das Füllen dieses Steuerelements ist dann auch die erste Funktion, die programmiert und auf mehrere Schichten aufgeteilt werden soll. Der Beginn sieht unspektakulär aus: Die beim Öffnen des Formulars ausgelöste Routine initialisiert das im Kopf des Moduls deklarierte Controller-Objekt und ruft die Prozedur cboSchnellsucheAktualisieren auf. Dim objController As clsController Private Sub Form_Open(Cancel As Integer) Set objController = New clsController cboSchnellsucheAktualisieren End Sub Listing 14.33: Initialisieren des Formulars
Die Prozedur cboSchnellsucheAktualisieren ruft die Routine GetPersons des Controller-Objekts auf. Diese Methode liefert ein Collection-Objekt zurück, das in dem Objekt objPersonen gespeichert wird. Bevor Sie die restlichen Zeilen der Prozedur betrachten, schauen Sie sich zunächst den weiteren Verlauf an. Private Sub cboSchnellsucheAktualisieren() Dim objPerson As clsPerson Dim objPersonen As Collection Dim str As String Set objPersonen = objController.GetPersons If Not objPersonen Is Nothing Then For Each objPerson In objPersonen Me.cboSchnellsuche.AddItem objPerson.PersonID & ";" _ & objPerson.Nachname & ", " & objPerson.Vorname Next objPerson Else MsgBox "Personenliste konnte nicht geladen werden." End If End Sub Listing 14.34: Zuweisen einer Datensatzgruppe mit allen Personen an das Kombinationsfeld zur Schnellsuche
Initialisieren des Controller-Objekts In Listing 14.33 wurde eine Instanz der Klasse clsController erzeugt. Diese löst das Initialize-Ereignis dieser Klasse aus, die folgendermaßen aussieht und das modulweit deklarierte Objekt objPersonDAO instanziert:
Mehrschichtige Anwendungen
657
Dim objPersonDAO As clsPersonDAO_DAO Dim objPersonen As Collection
Private Sub Class_Initialize() Set objPersonDAO = New clsPersonDAO_DAO End Sub Listing 14.35: Initialisieren der Klasse clsController der Business-Schicht
Damit hat die Business-Schicht direkt die nächste Schicht – die Datenzugriffsschicht – ins Spiel gebracht. Diese enthält die Methoden für den Zugriff auf die Datenschicht und kommt gleich zum Einsatz.
Aufruf der Methode GetPersons der Business-Schicht Nach dem Initialisieren des Controller-Objekts kann die Prozedur aus Listing 14.34 endlich die GetPersons-Methode aufrufen. Diese ist wiederum recht kurz und enthält lediglich den Aufruf der Find-Methode des Objekts objPersonDAO der Datenzugriffsklasse sowie die Zuweisung des erhaltenen Objekts an den Rückgabewert der Funktion. Der Zugriff auf die Daten wird also nach unten weitergereicht: Public Function GetPersons() As Collection Set objPersonen = objPersonDAO.Find Set GetPersons = objPersonen End Function Listing 14.36: Die Methode GetPersons der Klasse clsController
Zugriff des Datenzugriffobjekts auf die Datenschicht Die Find-Methode stellt nach einigem Weiterreichen den ersten Zugriff auf die Daten dar. Sie verwendet Objekte, Methoden und Eigenschaften des DAO-Objektmodells für den Zugriff auf die Tabelle tblPersonen. Die Methode hat einen optionalen Parameter namens varSearch – hier können Sie eine beliebige WHERE-Bedingung übergeben. Dieser Parameter wird im vorliegenden Beispiel nicht verwendet. Deshalb setzt die Prozedur die Zeichenkette strSQL lediglich aus dem SQL-Ausdruck SELECT * FROM tblPersonen zusammen. Neben dem Database- und dem Recordset-Objekt für den Zugriff auf die Daten deklariert die Methode noch ein Personen-Objekt und ein Collection-Objekt, das später die eingelesenen Personen-Objekte enthalten wird. Nach dem Öffnen der Datensatzgruppe rst durchläuft die Routine alle enthaltenen Datensätze und legt jeweils ein neues Objekt des Typs clsPerson an (Beschreibung
658
14
Objektorientierung im Praxiseinsatz
siehe weiter unten) und füllt dessen Eigenschaften mit den Inhalten der entsprechenden Tabellenfelder. Nach dem Einlesen der Daten wird das fertige Objekt mit dem Wert der Eigenschaft PersonID als Schlüssel an die Auflistung objPersonen angehängt. Diesen Schlüssel verwenden Sie später, um auf einzelne Personen-Objekte der Collection zugreifen zu können. Nachdem auf diese Weise alle Datensätze der Auflistung objPersonen in Form eines Personen-Objekts zugewiesen wurden, gibt die Funktion das Collection-Objekt mit den Personen-Objekten an die aufrufende Prozedur zurück. Public Function Find(Optional varSearch As Variant) As Collection On Error GoTo Find_Err Dim db As DAO.Database Dim rst As DAO.Recordset Dim strSQL As String Dim objPerson As clsPerson Dim objPersonen As Collection Set db = CurrentDb strSQL = "SELECT * FROM tblPersonen" If Not IsMissing(varSearch) Then strSQL = strSQL & " WHERE " & varSearch End If Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) Set objPersonen = New Collection Do While Not rst.EOF Set objPerson = New clsPerson With objPerson .PersonID = rst!PersonID .Vorname = rst!Vorname .Nachname = rst!Nachname .Strasse = rst!Strasse .PLZ = rst!PLZ .Ort = rst!Ort End With objPersonen.Add objPerson, CStr(objPerson.PersonID) rst.MoveNext Loop Set Find = objPersonen Find_Exit: On Error Resume Next Set db = Nothing Exit Function Find_Err: GoTo Find_Exit End Function Listing 14.37: Die Find-Methode des Datenzugriffsobjekts clsPersonDAO_DAO
Mehrschichtige Anwendungen
659
Ab nach oben Als Rückgabewert der Funktion Find wird das Collection-Objekt zunächst an die aufrufende Prozedur GetPersons der Business-Schicht weitergegeben, die es für weitere Zugriffe zwischenspeichert und es ihrerseits an die Routine cboSchnellsucheAktualisieren des Formulars zurückgibt. Diese wiederum wertet die Collection mit den Personen-Objekten derart aus, dass jeweils der Wert der Eigenschaft PersonID und Vor- und Nachname in der Form , im Kombinationsfeld cboSchnellsuche angezeigt werden können. Dazu müssen Sie noch die Eigenschaft Herkunftsart auf Wertliste und die Eigenschaften Spaltenanzahl und Spaltenbreite auf die Werte 2 und 0cm einstellen (siehe Abbildung 14.9).
Abbildung 14.9: Kombinationsfeld mit Daten aus einer Collection von Personen-Objekten
Die Klasse clsPerson Die Find-Routine hat Objekte des Typs clsPerson verwendet, um die Daten aus der Tabelle tblPersonen in einer Collection aller enthaltenen Datensätze zu speichern. Solche Objekte zum Verwenden von in Tabellen gespeicherten Objekten heißen ValueObjekte. Sie speichern lediglich die Daten der in der Tabelle enthaltenen Datensätze. Dabei sind alle Felder als private Variablen vorhanden, die mit Property Get- und Property Let-Prozeduren von außen gelesen und geschrieben werden können. Die Klasse clsPerson sieht wie folgt aus: Dim Dim Dim Dim Dim Dim
mPersonID As Long mVorname As String mNachname As String mStrasse As String mPLZ As String mOrt As String
660
14 Public Property Get PersonID() As Long PersonID = mPersonID End Property Public Property Let PersonID(lngPersonID As Long) mPersonID = lngPersonID End Property Public Property Get Vorname() As String Vorname = mVorname End Property Public Property Let Vorname(strVorname As String) mVorname = strVorname End Property Public Property Get Nachname() As String Nachname = mNachname End Property Public Property Let Nachname(strNachname As String) mNachname = strNachname End Property Public Property Get Strasse() As String Strasse = mStrasse End Property Public Property Let Strasse(strStrasse As String) mStrasse = strStrasse End Property Public Property Get PLZ() As String PLZ = mPLZ End Property Public Property Let PLZ(strPLZ As String) mPLZ = strPLZ End Property Public Property Get Ort() As String Ort = mOrt End Property Public Property Let Ort(strOrt As String) mOrt = strOrt End Property
Listing 14.38: Code der Klasse clsPerson
Objektorientierung im Praxiseinsatz
Mehrschichtige Anwendungen
661
14.3.8 Auswählen und Anzeigen eines Datensatzes Nach dem Füllen des Kombinationsfeldes zur Schnellauswahl von Personen kümmern Sie sich nun um die Funktionalität des Kombinationsfeldes. Dieses soll nach der Auswahl den gewählten Datensatz im Formular anzeigen. Hier kommt wiederum die Klasse clsPerson ins Spiel. Sie dient als Transportmittel der Daten aus der immer noch in der Klasse objController befindlichen Collection objPersonen. Ja, genau: Nach der Auswahl des anzuzeigenden Datensatzes aus dem Kombinationsfeld greift die Anwendung nicht etwa über die Zwischenschichten auf die Datenschicht zu, sondern bezieht die Informationen aus der im Controller zwischengespeicherten Collection, die alle im Kombinationsfeld auswählbaren Personen in Form von Objekten des Typs clsPerson enthält. Und das sieht so aus: Nach dem Deklarieren der Objektvariable objPerson wird diese mit Hilfe der Methode LoadPerson des Controller-Objekts gefüllt. Als Parameter wird dabei das gebundene Feld des Kombinationsfeldes übergeben, das die PersonID der anzuzeigenden Person enthält. Private Sub cboSchnellsuche_AfterUpdate() Dim objPerson As clsPerson Set objPerson = objController.LoadPerson(Me.cboSchnellsuche) With objPerson Me!txtPersonID = .PersonID Me!txtVorname = .Vorname Me!txtNachname = .Nachname Me!txtStrasse = .Strasse Me!txtPLZ = .PLZ Me!txtOrt = .Ort End With End Sub Listing 14.39: Diese Routine wird nach der Auswahl eines Eintrags des Kombinationsfelds aufgerufen.
Für das Füllen der Eigenschaften mit den Inhalten der Felder des Datensatzes mit der gesuchten PersonID ist die Methode LoadPerson des Controller-Objekts zuständig. Diese Methode deklariert zunächst ein Objekt des Typs clsPersonen und weist diesem das Objekt aus der Collection objPersonen mit dem passenden Wert der Eigenschaft PersonID zu, der in der Collection als Schlüsselwert eines jeden Elements gespeichert ist. Sollte ein solches Objekt einmal nicht in der Collection zu finden sein, greift die Funktion über die Methode Read des Objekts objPersonDAO auf die in der Datenschicht beziehungsweise der Datenbank enthaltenen Daten zu. Anderenfalls dient die in der Auflistung vorgefundene Instanz des gesuchten Personen-Objekts als Rückgabewert der Methode.
662
14
Objektorientierung im Praxiseinsatz
Public Function LoadPerson(lngPersonID As Long) As clsPerson Dim objPerson As clsPerson Set objPerson = objPersonen(CStr(lngPersonID)) If objPerson Is Nothing Then Set LoadPerson = objPersonDAO.Read(lngPersonID) Else Set LoadPerson = objPerson End If End Function Listing 14.40: Weiterdelegieren des Ladens von Personendaten in das entsprechende Objekt
Einlesen von Personen, die nicht in der Collection enthalten sind Für den in diesem Beispiel eigentlich nicht vorgesehenen Fall, dass ein PersonenObjekt angezeigt werden soll, das nicht in der Collection objPersonen enthalten ist, greift die Methode LoadPerson des Controller-Objekts mit der Read-Methode der Datenzugriffsklasse auf den in der Datenbank gespeicherten Personendatensatz zu. An dieser Stelle wird natürlich offensichtlich, dass die hier beschriebene Vorgehensweise keinerlei Feedback von der Datenschicht beinhaltet, wenn Daten durch andere Benutzer geändert, gelöscht oder hinzugefügt werden. Eine Möglichkeit, dieses Feedback zu realisieren, wäre die Verwendung von ADO. ADO-Recordsets enthalten Ereigniseigenschaften, mit denen sich Prozeduren zum Benachrichtigen übergeordneter Instanzen erstellen lassen. Dieses zusätzliche Feature soll aber aus Gründen der Übersichtlichkeit nicht eingebunden werden. Diese öffnet zunächst eine Datensatzgruppe aller Datensätze mit der übergebenen PersonID, wobei die Anzahl logischerweise 1 ist. Die dort enthaltenen Informationen werden nun in ein frisch instanziertes Personen-Objekt eingetragen, das anschließend als Rückgabewert der Funktion festgelegt wird. Public Function Read(PersonID As Long) As clsPerson On Error GoTo Read_Err Dim db As DAO.Database Dim rst As DAO.Recordset Dim objPerson As clsPerson Set db = CurrentDb Set rst = db.OpenRecordset("SELECT * FROM tblPersonen " _ & "WHERE [PersonID] = " & PersonID, dbOpenDynaset) Set objPerson = New clsPerson With objPerson .PersonID = rst![PersonID] .Vorname = rst![Vorname] .Nachname = rst![Nachname] .Strasse = rst![Strasse]
Mehrschichtige Anwendungen
663
.PLZ = rst![PLZ] .Ort = rst![Ort] End With Set Read = objPerson Read_Exit: On Error Resume Next Set objPerson = Nothing rst.Close Set rst = Nothing Set db = Nothing Exit Function Read_Err: GoTo Read_Exit End Function Listing 14.41: Einlesen eines Datensatzes in ein Objekt der Business-Schicht
Von hier aus geht es dann über die Business-Schicht direkt in die GUI-Schicht. Dort wartet die Routine aus Listing 14.39 bereits und trägt die Eigenschaften der gewünschten Person in die entsprechenden Textfelder des Formulars ein.
14.3.9 Neuer Datensatz Das Anlegen neuer Datensätze verläuft in ungebundenen Formularen erstaunlich ruhig. Damit ist natürlich vor allem die Interaktion mit der Datenbank gemeint, denn wenn man nicht die Speichern-Schaltfläche anklickt, passiert gar nichts. Falls noch ein Datensatz im Formular angezeigt wird, müssen Sie dieses allerdings erst einmal leeren. Dazu verwenden Sie die Schaltfläche mit der Beschriftung Neu. Diese ruft eine weitere Funktion auf, die alle Textfelder des Formulars leert. Private Sub cmdNeu_Click() FormularLeeren End Sub Listing 14.42: Zum Anlegen eines neuen Datensatzes …
Private Sub FormularLeeren() With Me !txtPersonID = Null !txtVorname = "" !txtNachname = "" !txtStrasse = "" !txtPLZ = "" !txtOrt = "" End With End Sub Listing 14.43: … sind lediglich die Textfelder zu leeren.
664
14
Objektorientierung im Praxiseinsatz
14.3.10 Speichern eines Datensatzes Nach der Eingabe der Daten ist es sinnvoll, diese zu speichern. Dazu klicken Sie auf die Schaltfläche cmdSpeichern. Diese löst die folgende Prozedur aus: Private Sub cmdSpeichern_Click() Me!txtPersonID = objController.SavePerson(Nz(Me!txtPersonID, 0), _ Me!txtVorname, Me!txtNachname, Me!txtStrasse, Me!txtPLZ, Me!txtOrt) cboSchnellsucheAktualisieren End Sub Listing 14.44: Auslösen des Speicher-Vorgangs im Formular
Die Routine prüft, ob der Datensatz bereits einen Wert im Feld PersonID hat oder nicht. Falls nicht, handelt es sich um einen neuen Datensatz und die Funktion CreatePerson des Controller-Objekts wird aufgerufen. Anderenfalls ist der Datensatz vorhanden und es wird nur die neue Version gespeichert. Dafür ist die Funktion UpdatePerson zuständig.
Datensatz neu anlegen oder aktualisieren? Der beim Betätigen der Speichern-Schaltfläche im Formular befindliche Datensatz kann bereits in der Datenbank vorhanden sein oder auch nicht. Ein eindeutiges Kennzeichen dafür ist das Vorhandensein eines Wertes in der Eigenschaft PersonID. Diese ID wird nur beim Anlegen eines Objekts in der Datenbank erstellt. Die Methode SavePerson prüft dies und reicht die zu speichernden Daten entweder an die Routine CreatePerson oder UpdatePerson weiter. Public Function SavePerson(lngPersonID As Long, strVorname As String, _ strNachname As String, strStrasse As String, strPLZ As String, _ strOrt As String) As Long If lngPersonID = 0 Then lngPersonID = CreatePerson(strVorname, strNachname, strStrasse, _ strPLZ, strOrt) Else UpdatePerson lngPersonID, strVorname, strNachname, strStrasse, _ strPLZ, strOrt End If SavePerson = lngPersonID End Function Listing 14.45: Diese Methode entscheidet, ob ein Objekt in der Datenbank gespeichert oder nur aktualisiert werden soll.
Mehrschichtige Anwendungen
665
Neuen Datensatz anlegen Die Funktion CreatePerson gibt die Daten des anzulegenden Objekts an die Methode Create des Datenzugriffsobjekts weiter. Dies geschieht in der Form, dass zunächst ein Personen-Objekt mit den Eigenschaften der Person erstellt und diese dann an das Datenzugriffsobjekt übergeben wird. Public Function CreatePerson(strVorname As String, strNachname As String, _ strStrasse As String, strPLZ As String, strOrt As String) As Long Dim objPerson As clsPerson Set objPerson = New clsPerson With objPerson .Vorname = strVorname .Nachname = strNachname .Strasse = strStrasse .PLZ = strPLZ .Ort = strOrt End With If objPersonDAO.Create(objPerson) = True Then CreatePerson = objPerson.PersonID Else MsgBox "Die Person konnte nicht angelegt werden.", _ vbOKOnly + vbExclamation, "Fehler beim Anlegen von Daten" End If Set objPerson = Nothing End Function Listing 14.46: Die Funktion CreatePerson des Controller-Objekts erwartet die zu speichernden Eigenschaften des Person-Objekts als Parameter.
Die Create-Methode kümmert sich nun um das Anlegen des Datensatzes in der Tabelle tblPersonen. Dabei wird neben dem Anlegen des Datensatzes auch die Eigenschaft PersonID mit dem in der Tabelle angelegten Wert gefüllt. Wenn das Anlegen erfolgreich war, liefert die Methode den Wert True zurück. Die Methode aus Listing 14.46 kann dann aus dem per Referenz übergebenen Objekt den neuen Wert der Eigenschaft PersonID auslesen. Public Function Create(objPerson As clsPerson) As Long On Error GoTo Create_Err Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset _ ("SELECT * FROM tblPersonen", dbOpenDynaset) With objPerson rst.AddNew
666
14
Objektorientierung im Praxiseinsatz
rst![Vorname] = .Vorname rst![Nachname] = .Nachname rst![Strasse] = .Strasse rst![PLZ] = .PLZ rst![Ort] = .Ort .PersonID = rst![PersonID] rst.Update End With Create = True Create_Exit: On Error Resume Next Set rst = Nothing Set db = Nothing Exit Function Create_Err: Create = False GoTo Create_Exit End Function Listing 14.47: Die Create-Methode des Datenzugriffsobjekts legt einen neuen Datensatz auf Basis des übergebenen Objekts an.
Aktualisieren eines Datensatzes Das Aktualisieren bestehender Datensätze erfolgt analog. Diesmal ruft die Methode SavePerson die Funktion UpdatePerson auf, wobei im Vergleich zum Anlegen des Datensatzes der Wert der Eigenschaft PersonID mit übergeben wird. Private Function UpdatePerson(lngPersonID As Long, strVorname As String, _ strNachname As String, strStrasse As String, strPLZ As String, _ strOrt As String) Dim objPerson As clsPerson Set objPerson = New clsPerson With objPerson .PersonID = lngPersonID .Vorname = strVorname .Nachname = strNachname .Strasse = strStrasse .PLZ = strPLZ .Ort = strOrt End With If Not objPersonDAO.Update(objPerson) = True Then MsgBox "Die Person konnte nicht aktualisiert werden.", _ vbOKOnly + vbExclamation, "Fehler beim Aktualisieren" End If End Function Listing 14.48: Vorbereitung der Aktualisierung eines Datensatzes im Controller-Objekt
Mehrschichtige Anwendungen
667
Die Methode Update des Datenzugriffsobjekts öffnet eine Datensatzgruppe, die lediglich einen Datensatz enthält – den mit der übergebenen PersonID. Public Function Update(objPerson As clsPerson) As boolean On Error GoTo Update_Err Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset _ ("SELECT * FROM tblPersonen WHERE [PersonID] = " _ & objPerson.PersonID, dbOpenDynaset) With objPerson rst.Edit rst![Vorname] = .Vorname rst![Nachname] = .Nachname rst![Strasse] = .Strasse rst![PLZ] = .PLZ rst![Ort] = .Ort rst.Update End With Update = True Update_Exit: On Error Resume Next Set rst = Nothing Set db = Nothing Exit Function Update_Err: Update = False GoTo Update_Exit End Function Listing 14.49: Aktualisieren eines Datensatzes auf Basis des passenden Objekts
14.3.11 Löschen eines Datensatzes Um den aktuell im Formular angezeigten Datensatz zu löschen, klicken Sie auf die Löschen-Schaltfläche. Diese ruft wie gehabt die Business-Schicht auf und übergibt den Wert des Feldes PersonID an die dortige Methode DeletePerson. Nach erfolgreichem Löschvorgang leert die Routine das Formular, aktualisiert das Kombinationsfeld zur Schnellsuche und leert auch dieses. Private Sub cmdLoeschen_Click() If objController.DeletePerson(Me!txtPersonID) = True Then FormularLeeren cboSchnellsucheAktualisieren
668
14
Objektorientierung im Praxiseinsatz
Me!cboSchnellsuche = Null End If End Sub Listing 14.50: Starten des Löschvorgangs
Die Methode DeletePerson des Controller-Objekts reicht die ID des zu löschenden Datensatzes direkt an die Methode Delete der Datenzugriffsklasse weiter. Public Function DeletePerson(lngPersonID As Long) As Boolean If objPersonDAO.Delete(lngPersonID) = True Then DeletePerson = True Else DeletePerson = False MsgBox "Die Person konnte nicht gelöscht werden.", _ vbOKOnly + vbExclamation, "Fehler beim Löschen" End If End Function Listing 14.51: Von der Business-Schicht zur Datenzugriffsschicht: Löschen einer Person
Diese löscht den Datensatz mit einem DELETE-Statement und gibt bei Gelingen den Wert True zurück. Public Function Delete(PersonID As Long) As Boolean On Error GoTo Delete_Err Dim db As DAO.Database Set db = CurrentDb db.Execute "DELETE FROM tblPersonen WHERE [PersonID] = " & PersonID, dbFailOnError Delete = True Delete_Exit: On Error Resume Next Set db = Nothing Exit Function Delete_Err: Delete = False GoTo Delete_Exit End Function Listing 14.52: Entfernen eines Datensatzes aus der Tabelle tblPersonen
14.3.12 Businesslogik und mehr Dieses Beispiel zeigt, wie Sie zwei Pattern der objektorientierten Welt mit VBA und Access einsetzen – das Model View Controller-Pattern und das DAO-Pattern (wobei DAO hier auch Data Access Objects bedeutet, aber nichts mit der DAO-Bibliothek von
Mehrschichtige Anwendungen
669
Access zu tun hat) – diese Stichworte nur für diejenigen, die sich genauer mit der Materie auseinandersetzen möchten. Was bringt das Ganze nun? Immerhin geht hier eine Menge Code für eine Aufgabe drauf, die sonst mit wenigen Zeilen zu lösen wäre. Dafür erhalten Sie aber auch mehr Flexibilität und Übersicht. Sie können die Businessregeln komplett in der BusinessSchicht versenken, was sich natürlich erst dann auszahlt, wenn Sie nicht nur mit einem, sondern mit mehreren Objekten, einer umfangreicheren Benutzungsoberfläche und einem dementsprechenden Datenmodell arbeiten. Die Validierung der Daten können Sie je nach Anforderung in die GUI-Schicht verfrachten oder in die Business-Schicht integrieren. Wenn Werte direkt nach der Eingabe in ein Textfeld auf ihre Gültigkeit geprüft werden sollen, macht eine entsprechende Validierung in der GUI-Schicht sicher Sinn. Geschäftsregeln, die sich auf einen größeren Zusammenhang beziehen und gegebenenfalls mehrere Projekte betreffen, lassen sich bestens in der Business-Schicht unterbringen.
Objektklassen und Datenzugriffsobjekte automatisch erstellen Die objektorientierte Entwicklung in der Form, wie Sie in den vorhergehenden Abschnitten dargestellt wurde, erfordert natürlich eine Menge Code-Einsatz. Eines dürfen Sie aber dabei nicht vergessen: Ein großer Teil ist reine Fleißarbeit. Während die vorgestellten Routinen alle einen recht individuellen Eindruck machten, benötigen Sie für eine eventuelle weitere Klasse zum Verwalten anderer Objekte wie etwa Unternehmen nur noch ganz wenig neuen Code. Die Datenzugriffsklassen clsPersonDAO_DAO und die Objektklasse clsPerson etwa enthalten nur Code, der automatisch auf Basis der entsprechenden Tabelle tblPersonen erstellt wurde. Damit Sie beim Ausbauen des vorliegenden Beispiels oder beim Umsetzen auf eine eigene Datenbankanwendung keine wunden Finger bekommen, finden Sie auf der Buch-CD ein Tool, das einige Funktionen zum automatischen Generieren von Objektund Datenzugriffsklassen zur Verfügung stellt (»accessVBATools«, siehe weiter unten). Interessant sind in diesem Zusammenhang die beiden Funktionen Objekt aus Tabelle erstellen und DAO-Objekt für Tabelle erstellen (siehe Abbildung 14.10). Die erste der beiden Funktionen legt ein Objekt für eine in einem Dialog festgelegte Tabelle an. Dazu wählen Sie im Dialog aus Abbildung 14.11 die Tabelle aus, zu der eine Klasse erstellt werden soll, und fügen das Primärschlüsselfeld dieser Tabelle sowie den Objektnamen ein. Der Objektname sollte im Singular stehen und das Objekt bestmöglich umschreiben. Er wird unter anderem zusammen mit dem Präfix »cls« als Klassenname verwendet (hier beispielsweise clsPerson).
670
14
Objektorientierung im Praxiseinsatz
Abbildung 14.10: Funktionen zum Erstellen von Tabellen-Objekten und DAO-Objekten
Abbildung 14.11: Festlegen der Parameter zum Erstellen einer Tabellen-Klasse
Die zweite Funktion DAO-Objekt für Tabelle erstellen erwartet die gleichen Parameter und legt eine Datenzugriffsklasse an, die per DAO auf eine Access-Tabelle zugreift. Die Klasse enthält folgende Methoden (in Klammern das entsprechende Listing aus obigem Beispiel): Create (Listing 14.47) Delete (Listing 14.52) Find (Listing 14.37) Read (Listing 14.41) Update (Listing 14.49)
Mehrschichtige Anwendungen
671
Das Tool liegt auf der Buch-CD in Form einer .dll-Datei unter dem Dateinamen Kap_11\accessVBATools.dll vor. Diese .dll-Datei kopieren Sie in ein Verzeichnis Ihrer Wahl (vorzugsweise c:\Windows\System32) und registrieren diese über den Ausführen…-Dialog mit der Anweisung regsvr32.exe c:\Windows\System32\accessVBATools.dll. Anschließend öffnen Sie die VBA-Entwicklungsumgebung neu und finden die neue Symbolleiste sowie die Einträge im Kontextmenü vor. Achtung: Das Tool befand sich bei Drucklegung dieses Buchs noch in der BetaPhase. Eine aktuelle Version finden Sie unter http://www.access-entwicklerbuch.de.
15 Anpassen der Entwicklungsumgebung Die VBA-Entwicklungsoberfläche enthält eine Reihe Elemente, von denen das wichtigste zweifellos das Codefenster ist, das der Anzeige und Bearbeitung des in den Modulen enthaltenen VBA-Codes dient. Jedes geöffnete Modul wird in einem eigenen Codefenster angezeigt. Neben dem Codefenster gibt es noch ein weiteres »reguläres« Fenster – den Objektkatalog. Die »Steuerzentrale« des VBA-Editors befindet sich wie in Windows-Anwendungen üblich in den Menü- und Symbolleisten. Die dritte Gruppe der Bedienelemente der VBA-Entwicklungsumgebung sind die so genannten Toolwindows. Das sind Fenster, die sich am rechten, linken, oberen oder unteren Rand des Hauptfensters oder an anderen bereits vorhandenen Bedienelementen »andocken« lassen oder einfach frei im Hauptfenster »schweben«. Beispiele für oft verwendete Toolwindows sind der Projektbrowser und der Direktbereich. Abbildung 15.1 zeigt die VBA-Entwicklungsumgebung mit den genannten Bedienelementen. Die VBA-Entwicklungsumgebung scheint für die meisten Anwendungsfälle ausreichend zu sein, aber wenn man sich die Möglichkeiten von Entwicklungsumgebungen wie Eclipse oder Microsoft Visual Studio .NET vor Augen führt, wird man schnell neidisch wegen der Vielfalt der verfügbaren und leicht integrierbaren Erweiterungen – das gilt vor allem für die aus dem Java-Umfeld stammende Eclipse-Plattform. Wenn Sie eine dieser Entwicklungsumgebungen und die eine oder andere Erweiterung für die VBA-Entwicklungsumgebung herbeisehnen oder einfach eine zündende Idee für deren Ausbau haben, gibt es gute Nachrichten: Sie können – das richtige Werkzeug und die technischen Fähigkeiten vorausgesetzt – selbst für die gewünschten Funktionen sorgen und sowohl die Menüs erweitern als auch eigene Toolwindows hinzufügen.
674
15
Anpassen der Entwicklungsumgebung
Abbildung 15.1: Die VBA-Entwicklungsumgebung mit dem Projektbrowser, dem Codefenster und dem Direktbereich
Beispiele für bestehende Erweiterungen sind etwa der Prozedurbrowser von Sascha Trowitzsch (auf der Buch-CD im Verzeichnis Kap_15/Prozedurbrowser.zip, aktuelle Version unter http://www.moss-soft.de/public/procbrowser) oder eine Menüleiste mit Funktionen zum Hinzufügen und Entfernen der Zeilennummerierung im aktuellen Modul und zum Hinzufügen einer Fehlerbehandlung zur aktuell markierten Routine. Abbildung 15.2 zeigt die VBA-Entwicklungsumgebung mit den beiden Erweiterungen. Der Prozedurbrowser ist gerade bei der Arbeit mit Modulen mit vielen Codezeilen hilfreich, denn er stellt alle enthaltenen Deklarationen, Funktionen und Sub-Prozeduren übersichtlich dar. Per Mausklick auf den gewünschten Eintrag im Prozedurbrowser zeigt das Codefenster die entsprechende Stelle an. Außerdem gibt es zu jedem Eintrag ein Kontextmenü mit Funktionen zum Ausführen, Kopieren oder Löschen einer Prozedur und einige weitere Optionen. In Abschnitt 15.6, »Toolwindows«, und 15.7, »COM-Add-Ins per Menübefehl«, finden Sie detaillierte Informationen zum Erstellen benutzerdefinierter Toolwindows und Menüleisten für die VBA-Entwicklungsumgebung. Außerdem erfahren Sie dort, wie Sie die Erweiterungen in die Entwicklungsumgebung integrieren.
675
Abbildung 15.2: VBA-Entwicklungsumgebung mit benutzerdefinierten Erweiterungen
Zusätzlich benötigte Software: Microsoft Visual Studio 6.0 Für die Entwicklung der nachfolgend beschriebenen Toolwindows und Menüleisten-Tools benötigen Sie Microsoft Visual Studio 6.0. Auch wenn Sie nicht über dieses Werkzeug verfügen, können Sie die nachfolgend beschriebenen Tools bei der Arbeit mit der VBA-Entwicklungsumgebung verwenden. Sie können auch ein alternatives Werkzeug wie Visual Studio .NET, eine der aktuellen Ausgaben der Office-Entwickler-Versionen oder jede andere Entwicklungsplattform verwenden, die mit OLE-Schnittstellen umgehen kann. Da für die Verwendung eines .NET-Toolwindows aber das .NET-Framework auf allen Zielrechnern installiert sein muss, was zum Zeitpunkt der Drucklegung dieses Buchs noch nicht gewährleistet ist, beschränken sich die folgenden Abschnitte auf die Erstellung mit dem »klassischen« Visual Basic.
676
15
Anpassen der Entwicklungsumgebung
15.1 Gründe für die Erweiterung der Entwicklungsumgebung Möglicherweise reicht die VBA-Entwicklungsumgebung in der derzeitigen Form für Sie völlig aus. Das kann eigentlich nur zwei Gründe haben: Entweder Sie beschäftigen sich so wenig mit diesem Werkzeug, dass es für Ihre Ansprüche ausreicht, oder Sie haben vielleicht noch keine Anregungen gefunden, die sich positiv auf Ihre Arbeit auswirken könnten. Wenn Sie die vorhergehenden Kapitel zum Thema Objektorientierung gelesen haben, ist Ihnen aufgefallen, dass die dort beschriebenen Vorteile auch Mehrarbeit erfordern (zumindest im ersten Schritt). Wenn Sie nicht mehr direkt mit gebundenen Formularen oder Berichten auf die gewünschten Daten zugreifen, bedeutet der notwendige Code für die einzufügenden Schichten natürlich zusätzliche Arbeit.
Automatische Codegenerierung Diese zusätzliche Arbeit ist in den meisten Fällen allerdings reine Fleißarbeit. Das Rad werden Sie dabei vermutlich nicht neu erfinden müssen. Die Objektklassen zu den in Tabellen gespeicherten Daten sowie die Klassen, die die Methoden und Eigenschaften für die Übertragung der Daten zwischen Objektklassen und Tabellen (die Datenzugriffsobjekte) bereitstellen, sind in der Regel nach dem gleichen System aufgebaut. Der Unterschied liegt lediglich in den Namen der betroffenen Tabelle und der enthaltenen Felder. Damit haben Sie – vorausgesetzt Sie möchten die objektorientierten Entwicklungstechniken einsetzen – bereits einen interessanten Anwendungsfall für eine Erweiterung der Entwicklungsumgebung gefunden: eine Funktion, die für eine angegebene Tabelle eine Objektklasse und/oder eine entsprechende Datenzugriffsklasse erstellt. Dieser Funktion müssten Sie auf geeignete Weise den Namen der Tabelle angeben, für die entsprechende Klassen erstellt werden sollen. Außerdem müssten Sie einige Parameter vorsehen, mit denen man etwa einstellen kann, ob eine Objekteigenschaft lesbar und/ oder schreibbar sein soll.
Fehlerbehandlung per Knopfdruck Es gibt aber auch Beispiele für Vereinfachungen bei der Quellcodeerstellung, die Sie verwenden können, wenn Sie die prozedurale Entwicklung bevorzugen (natürlich lässt sich folgendes Beispiel auch in Klassen einsetzen): Jede Prozedur sollte eine Fehlerbehandlung enthalten. Diese ist immer wie im folgenden Beispiel aufgebaut: Public Function () On Error GoTo Beispielfunktion_Err _Exit:
Gründe für die Erweiterung der Entwicklungsumgebung
677
'Restarbeiten Exit Function _Err: 'Fehlerbehandlung Call Fehlerbehandlung("<Modulname>", "", Erl, _ "Bemerkungen: ./.") Resume _Exit End Function Listing 15.1: Aufbau einer Fehlerbehandlung
Mit wachsender Anzahl Prozeduren wird die manuelle Erstellung von Fehlerbehandlungsroutinen mitunter etwas nervig. Wie schön wäre es doch, wenn man die entsprechenden Zeilen einfach per Mausklick hinzufügen könnte! Natürlich lässt sich das bewerkstelligen. In Abbildung 15.3 sehen Sie eine zusätzliche Symbolleiste, die unter anderem einen Befehl namens Fehlerbehandlung hinzufügen enthält. Um die Funktion zu verwenden, platzieren Sie einfach die Einfügemarke innerhalb der Zielprozedur und klicken auf den entsprechenden Menüeintrag. Die Funktion ermittelt den Namen des aktuellen Moduls und der Prozedur, in der sich die Einfügemarke befindet, und fügt die der Prozedur angepasste Fehlerbehandlung hinzu. Die Fehlerbehandlung ruft eine globale Fehlerbehandlungsroutine auf und übergibt einige Parameter wie den Modulnamen, den Prozedurnamen, die Funktion Erl, die – falls vorhanden – die Nummer der fehlerhaften Zeile enthält, und eventuell notwendige Bemerkungen. Die Fehlerbehandlungsroutine könnte dann beispielsweise eine aussagekräftige Fehlermeldung ausgeben oder auch die Fehlermeldung in einer Datei speichern, die von den Benutzern der Anwendung zur Auswertung an den Entwickler weitergeleitet werden kann.
Nummerieren von Codezeilen Sie haben soeben richtig gelesen: Die Funktion Erl ist eine nicht dokumentierte Funktion von VBA, die beim Auftreten eines Fehlers die Nummer der betroffenen Zeile zurückgibt. Möglicherweise fühlen Sie sich nun in die guten alten C64-Zeiten zurückversetzt, als Zeilennummern wesentlicher Bestandteil eines Basic-Programms waren. Sie können aber tatsächlich vor fast jede Zeile einer Prozedur Nummern setzen. Sie können dann beim Auftreten eines Fehlers über die Funktion Erl die entsprechende Zeilennummer ermitteln. Der Nutzen dieser Funktion ist phänomenal: Setzt man sie konsequent ein, sind die Zeiten vorbei, da die armen Benutzer Ihrer Anwendungen bei jeder Fehlermeldung erst einmal Screenshots vom Debug-Fenster und von der Fehlermeldung erstellen mussten, damit der Entwickler Informationen über die Herkunft des Fehlers erhielt.
678
15
Anpassen der Entwicklungsumgebung
Abbildung 15.3: Zusatzfunktionen in der VBA-Entwicklungsumgebung
Unter http://www.access-entwicklerbuch.de finden Sie die aktuelle Version der accessVBATools zu Download. Der Download enthält die Datei accessVBATools.dll, die alle in Abbildung 15.3 gezeigten Funktionen beinhaltet. Sie können diese .dll-Datei ganz einfach in ein beliebiges Verzeichnis kopieren (am besten in c:\Windows\System32) und mit der Anwendung regsvr32.exe unter Angabe des Dateinamens registrieren. Anschließend können Sie die Funktionen nach einem Neustart von Access in der VBA-Entwicklungsumgebung einsetzen. Die Frage, wie man denn nun schnell mal mehrere hundert, tausend oder mehr Zeilen Code durchnummeriert, beantwortet sich praktisch von selbst: Natürlich mit einer Erweiterung der Entwicklungsumgebung. Diese kann auf Knopfdruck die Prozeduren des aktuellen Moduls durchnummerieren und die Nummerierung ebenso schnell wieder entfernen.
15.2 Programmieren der Entwicklungsumgebung Sie sehen, dass sich sehr schnell sinnvolle Anwendungen zur Erweiterung der Entwicklungsumgebung finden lassen. Erwartungsgemäß befassen sich alle genannten Erweiterungsmöglichkeiten mit dem Manipulieren von Modulen und Quellcode. Das ist natürlich kein Zufall, denn Tools zum Erstellen oder Bearbeiten von Tabellen, Abfragen, Berichten oder Formularen gehören zweifellos zur Benutzungsoberfläche von Access und sind dort in Form geeigneter Add-Ins zu integrieren.
Programmieren der Entwicklungsumgebung
679
Die Erweiterung der Entwicklungsumgebung steht nicht gerade im Mittelpunkt des Interesses der Anwendungsentwickler, da kurze Entwicklungszeiten in der Regel keine Zeit lassen, Tools zu erstellen, die dem Entwickler regelmäßig anfallende Aufgaben abnehmen. Das ist zwar ein gutes Argument, aber wenn man immer wieder manuell die gleichen Schritte durchführt, ist zu überlegen, ob die Erstellung eines entsprechenden Tools nicht auf Dauer viel Zeit spart. Nun sind aber die zur Erstellung von Tools für die Entwicklungsumgebung benötigten Grundlagen im Internet nicht gerade leicht zu finden und wenn man sich nicht mit englischsprachigen Quellen auseinander setzen möchte, wird es noch schwieriger. Deshalb soll dieses Thema im vorliegenden Entwicklerhandbuch etwas ausführlicher besprochen werden. Die Entwicklung eines Tools, wie das in Abbildung 15.2 abgebildete Toolwindow oder die ebenfalls in dieser Abbildung gezeigte Symbolleiste zum Anlegen von Fehlerbehandlungen, erfolgt in zwei Schritten: 1. Entwickeln der eigentlichen Funktionalität zum Manipulieren von Modulen und ihrem Inhalt 2. Erstellen einer Benutzerschnittstelle zum Aufrufen der Funktionalität, etwa per Toolwindow oder in Form einer speziellen Symbolleiste Sie finden die benötigten Grundlagen in dieser Reihenfolge in den folgenden Abschnitten. Der Abschnitt 15.3, »Das Objektmodell der VBA-Entwicklungsumgebung«, behandelt Objekte, Methoden und Eigenschaften des Objektmodells der Benutzungsoberfläche. Sie finden dort die Grundlagen, um Module und Quellcode zu manipulieren. Die weiteren Abschnitte stellen Möglichkeiten vor, um die Funktionalität verfügbar zu machen. In Abschnitt 15.6, »Toolwindows«, erfahren Sie, wie Sie eigene Formulare erstellen, die Sie in die VBA-Entwicklungsumgebung integrieren können. Damit können Sie Steuerelemente wie Textfelder, Kombinations- oder Listenfelder und Schaltflächen zur Verfügung stellen, um für das Durchführen der gewünschten Funktionalität benötigte Parameter anzugeben oder um bestimmte Informationen über das aktuelle Projekt und seine Objekte anzuzeigen. Für einfachere Funktionen, die der Benutzer ohne weitere Informationen einfach per Knopfdruck starten können soll, reicht normalerweise die Bereitstellung einfacher Schaltflächen in speziellen Symbolleisten aus. Mehr dazu erfahren Sie in Abschnitt 15.7, »COM-AddIns per Menübefehl«. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbank Kap_15\VBAIDE.mdb.
680
15
Anpassen der Entwicklungsumgebung
15.3 Das Objektmodell der VBA-Entwicklungsumgebung Für die Programmierung der VBA-Entwicklungsumgebung steht eine eigene Bibliothek mit entsprechendem Objektmodell für den Zugriff auf die enthaltenen Objekte bereit. Die enthaltenen Objekte und ihre Methoden und Eigenschaften lassen sich grob in zwei Bereiche gliedern: 1. Die erste Kategorie dient der Programmierung der Entwicklungsumgebung selbst. Mit den enthaltenen Objekten, Methoden und Eigenschaften lassen sich beispielsweise die Menüleisten, Fenster, Verweise oder Add-Ins steuern. Einige Elemente dieser Kategorie lernen Sie später kennen, wenn es um das Anpassen der VBA-Entwicklungsumgebung geht. 2. In der zweiten Kategorie finden Sie Objekte, Methoden und Eigenschaften zum Manipulieren der Module und des darin enthaltenen Quellcodes. In den folgenden Abschnitten erfahren Sie, wie Sie die enthaltenen Elemente für die Automatisierung oft wiederkehrender Arbeitsschritte einsetzen. Um die enthaltenen Objekte verwenden zu können, müssen Sie zunächst einen Verweis auf die Objektbibliothek Visual Basic for Applications Extensibility 5.3 einrichten. Dazu verwenden Sie wie üblich den Dialog Verweise (siehe Abbildung 15.4).
Abbildung 15.4: Einrichten eines Verweises auf die VBA-IDE-Objektbibliothek
Um ein wenig in den Objekten dieser Bibliothek zu stöbern, nehmen Sie den Objektkatalog aus Abbildung 15.5 zu Hilfe (anzuzeigen mit (F2)). Im Kontextmenü der einzelnen Einträge finden Sie die Möglichkeit, die Onlinehilfe zu einem Objekt anzuzeigen.
Das Objektmodell der VBA-Entwicklungsumgebung
681
Dieses erreichen Sie durch Markieren des gewünschten Eintrags und Betätigen von (F1). Die Onlinehilfe hilft weiter, wenn Sie detaillierte Informationen zu einzelnen Elementen der Entwicklungsoberfläche benötigen. Im Folgenden finden Sie daher keine Auflistung der Objekte, Eigenschaften und Methoden der Entwicklungsoberfläche, sondern lernen anhand einiger Beispiele ihren Einsatz kennen.
Abbildung 15.5: Durchstöbern der Objektbibliothek mit dem Objektkatalog
Aufbau des Objektmodells Zum Nachvollziehen der folgenden Beispiele werden Kenntnisse in den Grundzügen des Objektmodells vorausgesetzt. Daher finden Sie nachfolgend eine kurze Zusammenfassung der wichtigsten Elemente. Ganz oben in der Hierarchie steht die VBE-Klasse (Visual Basic Environment). Sie enthält die folgenden für die nachfolgenden Beispiele relevanten Elemente: ActiveCodePane: Verweis auf das aktuelle oder zuletzt verwendete Code-Fenster ActiveVBProject: Verweis auf das aktuelle VBA-Projekt ActiveWindow: Verweis auf das aktive Fenster in der Entwicklungsumgebung Addins: Auflistung aller geladenen Add-Ins CodePanes: Auflistung aller geöffneten Code-Fenster
682
15
Anpassen der Entwicklungsumgebung
CommandBars: Auflistung aller verfügbaren Menüleisten (einschließlich der durch COM-Add-Ins bereitgestellten Menüleisten) SelectedVBComponent: Enthält einen Verweis auf die im Projekt-Explorer enthaltene Komponente (das sind alle Objekte, die sich in den Ordnern Microsoft Office Klassenobjekte, Module, Klassenmodule, Formulare und Verweise befinden). VBProjects: Auflistung aller geöffneten Projekte. Normalerweise enthält eine Access-Anwendung nur ein einziges Projekt. Wenn Sie aber etwa eine andere Datenbank per Verweis einbinden, enthält VBProjects auch dieses Projekt. Sie können mit dieser Auflistung auf alle Projekte zugreifen, die auch der Projekt-Explorer anzeigt (siehe Abbildung 15.6).
Abbildung 15.6: Projekt-Explorer mit mehreren Projekten
Version: Gibt die VBA-Version an. Windows: Auflistung aller Window-Objekte. Enthält die sichtbaren Code-Fenster sowie alle verfügbaren Toolwindows – das bezieht sich nicht auf die sichtbaren, sondern auf die eingebauten dockenden Fenster und die geladenen COM-Add-Ins, die als dockende Fenster ausgeführt sind.
15.4 Mit Modulen arbeiten Was man unter Access Standardmodul, Klassenmodul oder Formular-/Berichtsmodul nennt, fasst das VBE-Objektmodell unter VBComponents zusammen. Das VBE-Objektmodell unterscheidet die für Access interessanten Arten im Wesentlichen durch die Eigenschaft Type. Die Liste der Module lässt sich ausgeben und – was noch interessanter ist – es lassen sich auch neue Elemente hinzufügen.
Mit Modulen arbeiten
683
15.4.1 Auflisten aller enthaltenen Module Ein Überblick über die Module eines Projekts bietet zum Beispiel die Möglichkeit, das gewünschte Modul in der VBA-Entwicklungsumgebung anzuzeigen oder zu löschen. Die folgende Prozedur gibt eine Liste aller Module mit Modulnamen und Typ des aktuellen Projekts im Testfenster aus (siehe Abbildung 15.7). Dazu ermittelt sie zunächst die Anzahl der Module über die Count-Eigenschaft der Auflistung VBComponents für das betreffende Projekt. In einer For Next-Schleife durchläuft die Prozedur dann alle Elemente der Auflistung und ermittelt den Namen und den Typ des Moduls. Letzterer wird durch einen der jeweiligen Konstanten entsprechenden Text repräsentiert. Public Sub ListAllModules() Dim i As Integer Dim intVBComponentsCount As Integer Dim objVBComponent As VBComponent Dim strModulename As String Dim strModuletype As String intVBComponentsCount = VBE.ActiveVBProject.VBComponents.Count For i = 1 To intVBComponentsCount Set objVBComponent = VBE.ActiveVBProject.VBComponents.Item(i) strModulename = objVBComponent.Name Select Case objVBComponent.Type Case vbext_ct_StdModule strModuletype = "Standardmodul" Case vbext_ct_ClassModule strModuletype = "Klassenmodul" Case vbext_ct_Document strModuletype = "Formular- oder Berichtsmodul" Case vbext_ct_MSForm strModuletype = "MSForms Userform-Modul" End Select Debug.Print strModulename, strModuletype Next i Set objVBComponent = Nothing End Sub Listing 15.2: Prozedur zur Ausgabe aller Module eines Projekts
Abbildung 15.7: Ausgabe aller Module im Testfenster
684
15
Anpassen der Entwicklungsumgebung
15.4.2 Anlegen eines neuen Moduls Die automatisierte Quellcode-Erstellung umfasst natürlich auch das Anlegen der entsprechenden Module. Die Prozedur aus Abbildung 15.8 fügt der Auflistung der vorhandenen VBComponents ein weiteres Element des Typs Klassenmodul hinzu. Bei der Angabe der kryptischen Konstanten hilft IntelliSense. Die Konstante vbext_ct_Document entspricht übrigens einem Formular- oder Berichtsmodul.
Abbildung 15.8: Anlegen der Prozedur zum Erstellen einer neuen Klasse
Die Ausgabe der Eigenschaften zeigt, dass genau die gleiche Vorgehensweise zum Erstellen des neuen Moduls angewendet wird: Als Modulname kommt »Klasse1« zum Zuge (sofern noch nicht vergeben) und … halt: Die Eigenschaft Saved zeigt den Wert True an. Normalerweise muss man neu erstellte Module doch erst noch speichern! Ein Blick auf die Definition dieser Eigenschaft klärt die Sache auf: Der Wert True bedeutet, dass das Objekt seit dem letzten Speichern nicht mehr geändert wurde. Nehmen Sie also mit folgender Anweisung im Testfenster eine Änderung – beispielsweise des Modulnamens – vor: VBE.ActiveVBProject.VBComponents("Klasse1").Name = "clsBeispielklasse"
Im Projekt-Explorer können Sie beobachten, dass diese Anweisung das gewünschte Ergebnis bringt. Prüfen Sie nun erneut die Eigenschaft Saved: Debug.Print VBE.ActiveVBProject.VBComponents("clsBeispielklasse").Saved Falsch
Mit Prozeduren arbeiten
685
Das bedeutet, dass das Modul seit dem letzten Speichern bearbeitet wurde. Eine Methode zum Speichern stellt das Objektmodell nicht zur Verfügung; hier schafft die Save-Methode des DoCmd-Objekts Abhilfe: DoCmd.Save acModule, "clsBeispielklasse"
Beachten Sie, dass das DoCmd-Objekt Bestandteil der Access-Bibliothek ist und Sie einen Verweis auf die Bibliothek Microsoft Access x.y Object Library anlegen müssen, wenn Sie das Objekt etwa in einem COM-Add-In verwenden möchten.
15.4.3 Entfernen eines Moduls Das Entfernen eines Moduls erfolgt über die Remove-Methode der VBComponents-Auflistung. Ihr übergeben Sie eine Objektvariable mit einem Verweis auf das zu entfernende Modul. Die Prozedur des folgenden Beispiels erwartet den Namen der Prozedur als String-Variable und ermittelt damit das entsprechende Objekt. Public Sub RemoveModule(strModulename As String) Dim objVBComponent As VBComponent Set objVBComponent = VBE.ActiveVBProject.VBComponents.Item(strModulename) VBE.ActiveVBProject.VBComponents.Remove objVBComponent Set objVBComponent = Nothing End Sub Listing 15.3: Prozedur zum Entfernen eines Moduls
15.5 Mit Prozeduren arbeiten Mit den Methoden und Eigenschaften des CodeModule-Objekts, das den Zugriff auf den Inhalt eines Moduls erlaubt, erhalten Sie eine Vielfalt von Möglichkeiten. Ihre Beschreibung wird daher in den lesenden und den schreibenden Zugriff gegliedert (siehe Abschnitt 15.5.1, »Lesender Zugriff auf den Quellcode«, und 15.5.3, »Manipulieren des Quellcodes«). Zusätzlich finden Sie zu jedem Bereich ein Beispiel (siehe Abschnitt 15.5.2, »Beispielanwendung: Codeviewer«, und 15.5.4, »Beispielanwendung: Nummerieren von Codezeilen in einem Modul«).
15.5.1 Lesender Zugriff auf den Quellcode Für den Zugriff auf den Code eines Moduls benötigen Sie einen Verweis auf das betreffende Modul. Dazu gibt es mehrere Möglichkeiten: Sie kennen den Namen des Moduls, dessen Code Sie manipulieren möchten. In diesem Fall können Sie über die bereits in Zusammenhang mit den obigen Beispielprozeduren vorgestellte VBComponents-Auflistung das Modul referenzieren und über
686
15
Anpassen der Entwicklungsumgebung
das CodeModule-Objekt auf den enthaltenen Code zugreifen. Das ist zum Beispiel beim automatisierten Anlegen neuer Klassen und dem anschließenden Hinzufügen von Quellcode sinnvoll. Sie möchten den Code im aktiven Fenster manipulieren. Dann verwenden Sie einfach die CodeModule-Eigenschaft des ActiveCodePane-Objekts der VBE. Damit können Sie beispielsweise Quellcode an der Stelle der Einfügemarke hinzufügen oder einen markierten Bereich des Quellcodes kopieren. Auf welche der beiden Arten Sie einen Verweis auf das zu manipulierende Modul erstellen, ist für die nachfolgend vorgestellten Beispiele unwichtig. Daher werden beide Möglichkeiten angewendet. Die folgenden Beschreibungen der Eigenschaften und Methoden des CodeModuleObjekts beziehen sich auf das Modul mdlZeilenZaehlen in Abbildung 15.9.
Abbildung 15.9: Beispielmodul für die Anwendung der Methoden und Eigenschaften des CodeModule-Objekts
Zählen der Codezeilen des Moduls Leider speichert die VBA-Entwicklungsumgebung die einzelnen Elemente wie Deklarationen, Funktionen und Sub-Prozeduren nicht in einer Auflistung wie beispielsweise die Module. Es steht lediglich das komplette Modul zur Verfügung. Die gewünschten Inhalte müssen Sie selbst einkreisen. Dazu bietet das Objektmodell wiederum ausreichende Möglichkeiten.
Mit Prozeduren arbeiten
687
Die Eigenschaft CountOfLines gibt beispielsweise die Anzahl der Zeilen des Moduls zurück. Wenn Sie die folgende Anweisung im Direktbereich ausführen, erhalten Sie die Anzahl der Zeilen des aktuellen Moduls: Debug.Print VBE.ActiveCodePane.CodeModule.CountOfLines
Für das Beispielmodul aus Abbildung 15.9 gibt dieser Ausdruck den Wert 17 zurück. Manchmal befinden sich noch einige Leerzeilen hinter der letzten Prozedur, sodass die Funktion eine scheinbar größere Zeilenanzahl als vorhanden zurückgibt.
Zählen der Zeilen des Deklarationsbereichs eines Moduls Etwas differenzierter ist die Eigenschaft CountOfDeclarationLines. Sie gibt die Anzahl der Zeilen des Deklarationsbereichs des Moduls zurück. Dabei ist Folgendes zu beachten: Wenn das Modul lediglich Deklarationen und keine Prozeduren enthält, stimmt der Wert von CountOfDeclarationLines mit dem von CountOfLines überein. Wenn das Modul mindestens eine Prozedur enthält, gibt CountOfDeclarationLines die Anzahl der Zeilen bis zur letzten Deklarationszeile zurück. Die Ausgabe der Zeilenanzahl des Deklarationsbereichs für das aktuelle Modul sieht folgendermaßen aus und liefert den Wert 4 zurück: Debug.Print VBE.ActiveCodePane.CodeModule.CountOfDeclarationLines
Erste Zeile und Deklarationszeile einer Prozedur Die Entwickler des Objektmodells haben sich vermutlich die Frage gestellt, wie man mit den Leerzeilen zwischen zwei Prozeduren umgeht – gehören diese zu einer Prozedur? Und wenn ja – zu welcher? Gelöst haben sie das Problem auf die folgende Art: Die erste Zeile einer Prozedur ist die erste Zeile nach dem vorherigen Bereich, wobei mit Bereich der Deklarationsbereich eines Moduls oder die vorherige Prozedur gemeint ist. Wenn sich keine Elemente vor der ersten Prozedur befinden, ist die erste Zeile des Moduls gleichzeitig die erste Zeile der Prozedur. Die Funktion ProcStartLine erwartet den Prozedurnamen und die dem Prozedurtyp entsprechende Konstante als Eingangsparameter und gibt die Nummer der ersten Zeile der Prozedur gemäß obiger Beschreibung zurück (in diesem Fall die 5): Debug.Print VBE.ActiveCodePane.CodeModule.ProcStartLine("SampleFunction",vbext_pk_Proc)
Für den zweiten Parameter gibt es vier gültige Werte: vbext_pk_Proc: Sub- und Function-Prozeduren vbext_pk_Get, vbext_pk_Set, vbext_pk_Let: unterschiedliche Property-Prozeduren
688
15
Anpassen der Entwicklungsumgebung
Nun sind die Zeilen zwischen zwei Prozeduren eigentlich nur interessant, wenn sich dort wichtige Kommentare befinden – und das auch nur, wenn Sie auf diese zugreifen möchten. In allen anderen Fällen beginnt der spannende Teil mit der Zeile der Prozedurdeklaration (beispielsweise »Public Sub …«). Diese Zeile entlocken Sie dem CodeModule-Objekt mit der ProcBodyLine-Funktion, die genau die gleichen Parameter wie die ProcStartLine-Funktion erwartet: Debug.Print VBE.ActiveCodePane.CodeModule.ProcBodyLine("SampleFunction",vbext_pk_Proc)
Zeilenanzahl einer Prozedur Um die Anzahl Zeilen einer Prozedur zu ermitteln, verwenden Sie die Funktion ProcCountLines. Die Funktion erwartet die üblichen zwei Parameter. Bei der Anwendung der Funktion ist zu beachten, dass sie die Anzahl der Codezeilen von der ersten Zeile nach dem vorherigen Element (also der mit ProcStartLine zu ermittelnden Zeilennummer) bis zur letzten Zeile der Prozedur (also der Zeile mit dem Schlüsselwort »End«) zählt. Ein Beispielaufruf sieht folgendermaßen aus: Debug.Print VBE.ActiveCodePane.CodeModule.ProcCountLines("SampleFunction", vbext_pk_Proc)
Codezeilen einer Prozedur Wenn Sie die tatsächliche Anzahl Zeilen einer Prozedur von der Prozedurdeklaration bis zum End-Statement ermitteln möchten, gehen Sie folgendermaßen vor: Ermitteln Sie die Nummer der Zeile mit der Prozedurdeklaration: VBE.ActiveCodePane.CodeModule.ProcBodyLine("SampleFunction",vbext_pk_Proc)
Ermitteln Sie die Nummer der letzten Zeile der Prozedur: VBE.ActiveCodePane.CodeModule.ProcStartLine("SampleFunction",vbext_pk_Proc ) + VBE.ActiveCodePane.CodeModule.ProcCountLines("SampleFunction", vbext_pk_Proc) -1
Bilden Sie die Differenz der beiden Werte und addieren Sie den Wert 1 hinzu. Zusammengefasst heben sich die subtrahierte und die addierte 1 auf. Die Ermittlung der Codezeilen einer Prozedur lässt sich in einer Funktion zusammenfassen, die den Modulnamen, den Prozedurnamen sowie den Typ erwartet:
Mit Prozeduren arbeiten
689
Public Function GetRealProcLineCount(strModule As String, _ strProcName As String, lngProcType As Long) Dim objCodeModule As CodeModule Dim lngProcLineCount As Long Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule With objCodeModule 'Berechnung der Zeilenanzahl lngProcLineCount = .ProcStartLine(strProcName, lngProcType) _ + .ProcCountLines(strProcName, lngProcType) _ - .ProcBodyLine(strProcName, lngProcType) End With GetRealProcLineCount = lngProcLineCount Set objCodeModule = Nothing End Function Listing 15.4: Ermitteln der Zeilen von der Deklarations- bis zur End-Zeile einer Prozedur
Zu welcher Prozedur gehört eine Zeile? Wenn Sie genau wissen möchten, welche Zeile eines Moduls zu welcher Prozedur gehört, probieren Sie einfach einmal folgende Prozedur aus. Die Prozedur erwartet als Parameter den Namen des zu untersuchenden Moduls. Sie durchläuft alle Zeilen des Moduls und gibt zu jeder Zeile die Zeilennummer und den Namen der Prozedur aus, zu der die aktuelle Zeile gehört. Dabei werden sowohl der Deklarationsbereich als auch Property Get-/Let-/Set-Prozeduren außer Acht gelassen. Für die Ausgabe der Property-Prozeduren ersetzen Sie die Konstante vbext_pk_Proc durch die der PropertyProzedur entsprechende Konstante. Public Sub LinesAndProcedures(strModule As String) Dim objCodeModule As CodeModule Dim intLines As Integer Dim i As Integer Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule intLines = objCodeModule.CountOfLines For i = 1 To intLines Debug.Print Format(i, "000"), _
690
15
Anpassen der Entwicklungsumgebung
objCodeModule.ProcOfLine(i, vbext_pk_Proc) Next i End Sub Listing 15.5: Zeilennummer und Prozedur ausgeben
Ausgabe des kompletten Codes eines Moduls Um den kompletten Quellcode eines Moduls auszugeben oder ihn in einer Variablen zu speichern, verwenden Sie die Lines-Eigenschaft in Verbindung mit der CountOfLines-Eigenschaft. Die CountOfLines-Eigenschaft gibt die Anzahl Zeilen des Moduls zurück. Die LinesFunktion erwartet die Nummern der ersten und der letzten auszugebenden Zeile und gibt den entsprechenden Inhalt des Moduls zurück. Folgendes Beispiel zeigt, wie Sie den Inhalt des aktuellen Codefensters im Testfenster ausgeben: Debug.Print VBE.ActiveCodePane.CodeModule.Lines(1,VBE.ActiveCodePane.CodeModule. CountOfLines)
Die folgende Funktion zeigt, wie Sie mit der Lines-Funktion und der CountOfLinesFunktion den Inhalt eines per Parameter übergebenen Moduls ermitteln: Public Function GetCompleteCode(strModule As String) Dim objCodeModule As CodeModule Dim lngLineCount As Long Dim strCompleteCode As String Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule With objCodeModule 'Ermitteln der Zeilenanzahl lngLineCount = .CountOfLines 'Einlesen der Zeilen von der ersten bis zur letzten Zeile strCompleteCode = .Lines(1, lngLineCount) End With GetCompleteCode = strCompleteCode Set objCodeModule = Nothing End Function Listing 15.6: Diese Funktion ermittelt den kompletten Code eines Moduls.
Mit Prozeduren arbeiten
691
Ermitteln der Position der aktuellen Markierung Weiter oben wurde eine Funktion zum Hinzufügen einer Fehlerbehandlung zur aktuell markierten Prozedur per Mausklick erwähnt. Eine Voraussetzung für die Umsetzung dieser Funktion ist die aktuelle Position der Einfügemarke. Die für diese Ermittlung notwendige Funktion liefert direkt Informationen über den kompletten markierten Bereich – sollte kein Bereich markiert sein und sich nur die Einfügemarke an einer bestimmten Position befinden, handelt es sich um einen Sonderfall: Die Markierung erstreckt sich dann über 0 Zeichen und 0 Zeilen. Für die Kennzeichnung des markierten Bereichs sind vier Werte erforderlich, daher wird das Ergebnis nicht als Funktionswert, sondern per Parameter zurückgegeben. Diese müssen Sie vorher als Long-Variablen deklarieren. Die folgende Prozedur zeigt, wie Sie die Parameter der aktuellen Markierung ermitteln (siehe auch Abbildung 15.10). Public Function GetSelectionParameters() Dim Dim Dim Dim Dim
objCodePane As CodePane lngStartLine As Long lngEndLine As Long lngStartColumn As Long lngEndColumn As Long
Set objCodePane = VBE.ActiveCodePane 'Lesen der Parameter des markierten Bereichs objCodePane.GetSelection lngStartLine, lngStartColumn, _ lngEndLine, lngEndColumn Debug.Print Debug.Print Debug.Print Debug.Print
"Erste Zeile: " & lngStartLine "Erstes Zeichen: " & lngStartColumn "Letzte Zeile: " & lngEndLine "Letztes Zeichen: " & lngEndColumn
Set objCodePane = Nothing End Function Listing 15.7: Ausgabe der Parameter des aktuell markierten Bereichs im Codefenster
Ermitteln des Inhalts der aktuellen Markierung Für die Rückgabe des Inhalts des aktuell markierten Bereichs gibt es keine eingebaute Funktion, sodass Sie selbst Hand anlegen müssen. Diesmal benötigen Sie zwei Objekte: eines, das auf das aktuelle Codefenster verweist, und eines für das entsprechende Codemodul.
692
15
Anpassen der Entwicklungsumgebung
Abbildung 15.10: Ausgabe der Parameter der aktuellen Markierung im Direktbereich
Mit der Funktion GetSelection des aktuellen CodePane-Objekts ermittelt die Funktion GetSelectedText (eigentlich passt die Bezeichnung GetSelection besser, aber so heißt halt schon die darin verwendete CodePane-Methode) die Parameter des markierten Bereichs und speichert diese in vier entsprechenden Long-Variablen. Mit der Lines-Methode des CodeModule-Objekts liest die Funktion dann alle Zeilen aus, die vom markierten Bereich geschnitten werden. Dann kommt der knifflige Teil: Natürlich kann sich eine Markierung beispielsweise auch von der Mitte der einen bis zur Mitte der übernächsten Codezeile erstrecken. Die nicht markierten Teile der ersten und der letzten Zeile muss die Funktion dann natürlich noch entfernen. Für die erste Zeile reicht der einfache Einsatz der Mid-Funktion aus: Diese erhält als Startposition einfach den Wert des Parameters lngStartColumn der Markierung und gibt alles zurück, was sich rechts davon befindet. Eine nicht vollständige letzte Zeile erfordert zusätzlichen Aufwand: Erst ermitteln Sie mit der Len-Funktion die Länge der letzten Zeile. Die Differenz zwischen dem Wert des Parameters lngLastColumn und der Länge der letzten Zeile gibt an, um wie viele Zeichen Sie die komplette Zeichenkette kürzen müssen. Public Function GetSelectedText() Dim objCodePane As CodePane
Mit Prozeduren arbeiten Dim Dim Dim Dim Dim Dim Dim
693
objCodeModule As CodeModule lngStartLine As Long lngStartColumn As Long lngEndLine As Long lngEndColumn As Long strSelection As String lngLenLastLine As Long
Set objCodePane = VBE.ActiveCodePane Set objCodeModule = VBE.ActiveCodePane.CodeModule 'Parameter des markierten Bereichs ermitteln objCodePane.GetSelection lngStartLine, lngStartColumn, _ lngEndLine, lngEndColumn#
'Markierte Zeilen komplett einlesen strSelection = objCodeModule.Lines(lngStartLine, _ lngEndLine - lngStartLine + 1) 'Falls erste Zeile nicht komplett, entsprechenden linken Teil abschneiden strSelection = Mid(strSelection, lngStartColumn) 'Länge der letzten Zeile ermitteln lngLenLastLine = Len(objCodeModule.Lines(lngEndLine, 1)) 'Falls letzte Zeile nicht komplett, 'entsprechenden rechten Teil abschneiden strSelection = Left(strSelection, _ Len(strSelection) - lngLenLastLine + lngEndColumn - 1) GetSelection = strSelection Set objCodePane = Nothing Set objCodeModule = Nothing End Function Listing 15.8: Funktion zum Ermitteln des Inhalts des markierten Bereichs
In Modulen suchen Natürlich bietet das CodeModule-Objekt auch eine Methode zur Suche von Ausdrücken in einem Modul. Die Find-Methode hat acht Parameter – fünf Pflichtparameter und drei optionale. Die folgende Prozedur zeigt, wie Sie die Find-Methode zum Ermitteln des Ortes einer bestimmten Zeichenfolge einsetzen.
694
15
Anpassen der Entwicklungsumgebung
Public Function FindString(strModule As String, strSearch As String) Dim Dim Dim Dim Dim
objCodeModule As CodeModule lngStartLine As Long lngStartColumn As Long lngEndLine As Long lngEndColumn As Long
'Referenz auf angegebenes CodeModul anlegen Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule 'Suche durchführen FindString = objCodeModule.Find(strSearch, lngStartLine, _ lngStartColumn, lngEndLine, lngEndColumn) 'Ausgabe des Ergebnisses Debug.Print "Erste Zeile: " & lngStartLine Debug.Print "Erstes Zeichen: " & lngStartColumn Debug.Print "Letzte Zeile: " & lngEndLine Debug.Print "Letztes Zeichen: " & lngEndColumn Set objCodeModule = Nothing End Function Listing 15.9: Ausgabe der Position einer zu suchenden Zeichenkette
Zusätzlich zu den fünf in der Prozedur verwendeten Parametern besitzt die FindMethode noch drei Boolean-Parameter: WholeWord: Sucht bei True nach kompletten übereinstimmenden Wörtern. MatchCase: Beachtet bei True die Groß-/Kleinschreibung. PatternSearch: Wertet den Suchausdruck bei True als regulären Ausdruck aus.
15.5.2 Beispielanwendung: Codeviewer Der CodeViewer aus Abbildung 15.11 fasst die Funktionen zum lesenden Zugriff auf Module und die enthaltenen Prozeduren zusammen. Als Benutzungsoberfläche dient ein Access-Formular mit drei Steuerelementen: zwei Kombinationsfelder zum Auswählen des Moduls und der Prozedur und ein Textfeld zur Anzeige des Codes der ausgewählten Prozedur. Das zweite Kombinationsfeld hängt vom ersten Kombinationsfeld ab und zeigt nur die Prozeduren des jeweils ausgewählten Moduls an. Die Funktionen zum Ermitteln der durch die Kombinationsfelder anzuzeigenden Werte befinden sich im Klassenmodul clsVBE.
Mit Prozeduren arbeiten
695
Abbildung 15.11: Der Codeviewer dient dem Betrachten von Prozeduren
Anzeige der Module Das Kombinationsfeld zur Anzeige der Module besitzt als Datensatzherkunft eine Wertliste, die aus einer durch Semikolons getrennten Auflistung des Index und des Namens der einzelnen Module einschließlich Angabe der Modulart besteht. Die Datensatzherkunft sieht also etwa wie folgt aus: 1;'Modul1 (Standardmodul)'; 2;'Modul2 (Klassenmodul)'; 3;'Modul3 (Formular-/Berichtsmodul)'
Das Kombinationsfeld soll nur die Modulnamen, nicht aber den Index anzeigen; daher stellen Sie die Werte der Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 2 beziehungsweise 0cm ein. Letzterer sorgt dafür, dass der jeweils erste Wert der Zeile mit der Spaltenbreite 0cm, also nicht sichtbar, und der zweite Wert über die volle Spaltenbreite angezeigt wird. Das Füllen des Kombinationsfeldes erfolgt in der Ereigniseigenschaft Beim Anzeigen des Formulars. Die entsprechende Prozedur enthält folgenden Code: Private Sub Form_Current() Dim strModules As String Dim objVBE As clsVBE Set objVBE = New clsVBE 'Module einlesen strModules = objVBE.GetModulesCSV
696
15
Anpassen der Entwicklungsumgebung
'Herkunftsart des Kombinationsfeldes auf Wertliste einstellen Me.cboModules.RowSourceType = "Value List" 'Zuweisen der Wertliste an die Datensatzherkunft Me.cboModules.RowSource = strModules End Sub Listing 15.10: Diese Prozedur füllt das erste Kombinationsfeld mit der Modulliste
Die Prozedur instanziert ein Objekt der Klasse clsVBE und verwendet deren Funktion GetModulesCSV, um die Liste der Module des Codeprojekts einzulesen Die Funktion ermittelt die Anzahl der enthaltenen Module und durchläuft diese anschließend nacheinander. Dabei erstellt es eine Objektvariable mit einem Verweis auf das entsprechende Element der VBComponents-Auflistung des aktuellen Projekts. Das Ermitteln des Namens erfolgt über die Name-Eigenschaft des Objekts, und eine dem Modultyp entsprechende Konstante lässt sich mit Hilfe der Type-Eigenschaft herausfinden. Da diese Konstanten nicht besonders benutzerfreundlich sind, weist die Funktion der StringVariablen strModuleType einen der Ausdrücke Standardmodul, Klassenmodul oder Formular-/Berichtmodul zu. Schließlich fügt die Funktion den Index, den Namen und den Typ an die String-Variable strModules an. Public Function GetModulesCSV() Dim Dim Dim Dim Dim Dim
intVBComponentsCount As Integer i As Integer objVBComponent As VBComponent strModuleName As String strModuleType As String strModules As String
'Anzahl der Module ermitteln intVBComponentsCount = VBE.ActiveVBProject.VBComponents.Count 'Alle Module durchlaufen und ... For i = 1 To intVBComponentsCount '... per Objektvariable auf das aktuelle Modul verweisen ... Set objVBComponent = VBE.ActiveVBProject.VBComponents.Item(i) '... den Modulnamen lesen ... strModuleName = objVBComponent.Name '... und den Typ ermitteln und übersetzen Select Case objVBComponent.Type Case vbext_ct_StdModule strModuleType = "Standardmodul" Case vbext_ct_ClassModule strModuleType = "Klassenmodul" Case vbext_ct_Document strModuleType = "Formular-/Berichtsmodul"
Mit Prozeduren arbeiten
697
Case vbext_ct_MSForm strModuletype = "MSForms Userform-Modul" End Select 'Informationen des aktuellen Moduls zur Liste hinzufügen strModules = strModules & i & ";" strModules = strModules & "'" & strModuleName _ & " (" & strModuleType & ")';" Next i 'Zuweisen des Ergebnisses an den Rückgabewert GetModulesCSV = strModules End Function Listing 15.11: Erzeugen einer Liste aller Module
Anzeige der Prozedurliste Die Anzeige der Prozeduren ist um einiges komplizierter, da Sie nicht per Auflistung auf diese zugreifen können. Das ist aber nicht weiter schlimm, da Sie auf diese Weise einige Methoden und Eigenschaften des CodeModule-Objekts im Praxiseinsatz kennen lernen. Das Füllen des Kombinationsfeldes zur Anzeige der Prozeduren erfolgt beim Auslösen des Ereignisses Nach Aktualisieren des Kombinationsfeldes cboModule, also nach der Auswahl eines Moduls. Private Sub cboModules_AfterUpdate() Dim strProcedures As String Dim objVBE As clsVBE Set objVBE = New clsVBE 'Einlesen der Prozeduren strProcedures = objVBE.GetProceduresCSV(Me.cboModules) 'Zuweisen der Prozedurliste und Leeren des Kombinationsfelds With Me.cboProcedures .RowSourceType = "Value List" .RowSource = strProcedures .Value = Null End With 'Leeren des Codefensters Me.txtCode = Null
698
15
Anpassen der Entwicklungsumgebung
Set objVBE = Nothing End Sub Listing 15.12: Aktualisieren des Kombinationsfeldes zur Anzeige der Prozeduren
Das Einlesen der Prozeduren des ausgewählten Moduls erfolgt wiederum über eine Funktion der Klasse clsVBE. Die Funktion heißt GetProceduresCSV und erwartet den Index des auszuwertenden Moduls als Parameter. Die aufrufende Prozedur hält den Indexwert in der gebundenen Spalte des Kombinationsfeldes cboModule vor und übergibt ihn mit dem Aufruf an die Funktion GetProceduresCSV. Die Funktion erstellt zunächst einen Objektverweis auf das CodeModule-Objekt des Moduls mit dem übergebenen Index. Dann ermittelt es mit der Eigenschaft CountOfLines die Gesamtanzahl der Zeilen dieses Moduls. Die folgende For Next-Schleife durchläuft alle Zeilen des Moduls – mit folgender Ausnahme: Die Funktion überprüft mit der ProcOfLine-Funktion, ob die aktuelle Zeile zu einer Prozedur gehört. Ist das der Fall, liest sie aus der Eigenschaft ProcCountLines die Zeilenanzahl dieser Prozedur und erhöht nach der Ermittlung der gewünschten Informationen die Laufvariable so, dass im nächsten Durchlauf der For Next-Schleife die erste Zeile nach der aktuellen Prozedur geprüft wird. Dazwischen liest die Funktion die Informationen zu jeder einzelnen Prozedur ein. Der Prozedurname stammt dabei aus der Funktion ProcOfLine, die als Parameter die zu untersuchende Zeilennummer und eine Variable für die Rückgabe des Prozedurtyps erwartet. Diese müssen Sie unbedingt vorher als Long-Variable deklarieren. Die Funktion ProcCountLines enthält den gleichen Parameter zur Angabe des Prozedurtyps. Hier muss dieser allerdings konkret angegeben werden und mit dem tatsächlichen Typ der Prozedur mit dem angegebenen Namen übereinstimmen. Das ist hier gegeben, denn die Variable lngProcType wird in der Funktion ProcOfLine mit der entsprechenden Typkonstante belegt und kann diesen Wert in der Funktion ProcCountLines bereitstellen. Zusätzlich zum Prozedurnamen soll die Funktion GetProceduresCSV den Prozedurtyp zurückgeben – und zwar in Form einer aussagekräftigen Zeichenkette und nicht als Konstante wie etwa vbext_pk_Proc. Dazu werden die Konstanten in einem Select CaseStatement ausgewertet und durch eine entsprechende Zeichenkette ersetzt. Damit lassen sich »normale« Prozeduren und Property Get-/Let-/Set-Prozeduren unterscheiden. Das ist etwas unbefriedigend; die Unterscheidung von Sub- und FunctionProzeduren wäre schon sinnvoll. Daher forscht die Funktion GetProceduresCSV im Falle einer »normalen« Prozedur noch etwas weiter. Ein erster Ansatz wäre, die erste Zeile der Prozedur nach dem Schlüsselwort »Sub« oder »Function« zu durchsuchen, doch ist erstens die Position dieser Schlüsselwörter nicht immer gleich, da noch die Wörter Private oder Public vorangestellt sein könnten, und zweitens könnten diese Schlüssel-
Mit Prozeduren arbeiten
699
wörter auch noch im Prozedurnamen vorkommen. Doch warum mit verschieden ausgeprägten Ausdrücken herumplagen, wenn es auch einfacher geht? Die letzte Zeile einer Prozedur enthält garantiert einen der beiden Ausdrücke »End Sub« oder »End Function« – hier brauchen Sie also nur die letzte Zeile ausfindig zu machen und mit einem dieser beiden Ausdrücke zu vergleichen. Den Namen, den Typ und den Zahlenwert der dem Typ entsprechenden Konstante der einzelnen Prozeduren setzt die Funktion schließlich in der String-Variablen strProcs zusammen und gibt deren Inhalt an die aufrufende Prozedur zurück. Die Liste sieht beispielsweise wie folgt aus: 'Beispiel1';'Sub-Prozedur';0; 'Beispiel2';'Property Get-Prozedur';3; 'Beispiel3';'Property Let-Prozedur';1;
Das Kombinationsfeld soll die ersten beiden Werte der Liste anzeigen und den dritten Wert verbergen. Daher stellen Sie die Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 3 beziehungsweise 5cm;5cm;0cm ein. Public Function GetProceduresCSV(intModuleID As Integer) Dim Dim Dim Dim Dim Dim Dim Dim Dim
objCodeModule As CodeModule intModLineCount As Integer intProcLineCount As Integer i As Integer strProcName As String strProcs As String lngProcType As Long strProcType As String strLastLine As String
Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(intModuleID).CodeModule With objCodeModule 'Zeilenanzahl des Moduls ermitteln intModLineCount = .CountOfLines 'Alle Zeilen untersuchen For i = 1 To intModLineCount 'Wenn die aktuelle Zeile nicht leer ist ... If Not .ProcOfLine(i, lngProcType) = "" Then 'Prozedur der aktuellen Zeile ermitteln strProcName = .ProcOfLine(i, lngProcType)
700
15
Anpassen der Entwicklungsumgebung
'Zeilen der aktuellen Prozedur ermitteln intProcLineCount = .ProcCountLines(strProcName, lngProcType) 'Ermitteln des Prozedurtyps Select Case lngProcType Case vbext_pk_Proc 'Prozedur: kann Sub oder Function sein, 'daher Untersuchung der letzten Zeile strLastLine = .Lines(i + intProcLineCount - 1, 1) If strLastLine = "End Sub" Then strProcType = "Sub-Prozedur" Else strProcType = "Function-Prozedur" End If Case vbext_pk_Let strProcType = "Property Let-Prozedur" Case vbext_pk_Set strProcType = "Property Set-Prozedur" Case vbext_pk_Get strProcType = "Property Get-Prozedur" End Select 'Anfügen von Name, Typbezeichnung und Typkonstante 'eine String-Variable strProcs = strProcs & "'" & strProcName & "';" strProcs = strProcs & "'" & strProcType & "';" strProcs = strProcs & lngProcType & ";" 'Laufvariable auf erste Zeile hinter der 'aktuellen Prozedur setzen i = i + intProcLineCount - 1 End If Next i End With 'Liste an Rückgabeparameter übergeben GetProceduresCSV = strProcs Set objCodeModule = Nothing End Function Listing 15.13: Einlesen von Name und Typ der Prozeduren eines Moduls
Mit Prozeduren arbeiten
701
Anzeige des Codes einer Prozedur Nach der Auswahl des Moduls und der Prozedur bleibt nur noch ihre Anzeige im dafür vorgesehenen Textfeld. Die Ausgabe wird durch das Auswählen einer Prozedur aus dem Kombinationsfeld cboProcedures angestoßen. Die verantwortliche Ereignisprozedur aus folgendem Listing ermittelt den Quellcode der Prozedur mit der Funktion GetCode, die drei Parameter erwartet: den Modulindex, den Prozedurnamen sowie den Prozedurtyp. Alle Werte lassen sich aus den beiden Kombinationsfeldern ermitteln. Private Sub cboProcedures_AfterUpdate() Dim objVBE As clsVBE Dim strCode As String Set objVBE = New clsVBE 'Einlesen des Codes der ausgewählten Prozedur strCode = objVBE.GetCode(Me.cboModules, _ Me.cboProcedures, Me.cboProcedures.Column(2)) 'Zuweisen des Codes an das Textfeld txtCode Me.txtCode = strCode Set objVBE = Nothing End Sub Listing 15.14: Anzeigen des Codes einer Prozedur
Die Funktion GetCode verarbeitet ihre Eingangsparameter folgendermaßen: Zunächst erstellt die Funktion eine Objektvariable mit einem Verweis auf das CodeModule-Objekt des Elements der VBComponents-Auflistung mit dem im ersten Parameter angegebenen Index. Mit dem Prozedurnamen und -typ, die mit den übrigen Eingangsparametern übergeben wurden, ermittelt die Funktion dann drei Zeilennummern: die Nummer der Zeile, in der die angegebene Prozedur beginnt (das ist, wie bereits weiter oben beschrieben, die erste Zeile nach der vorherigen Prozedur beziehungsweise des Deklarationsbereichs) die Nummer der Zeile, die die Prozedurdeklaration enthält die Nummer der letzten Zeile der Prozedur (das ist die Zeile mit dem End-Schlüsselwort) Mit diesen Informationen und der Lines-Funktion lässt sich der komplette Prozedurtext leicht auslesen.
702
15
Anpassen der Entwicklungsumgebung
Public Function GetCode(intModuleID As Integer, strProcedure As String, _ lngProcedureType As Long) Dim Dim Dim Dim Dim
objCodeModule As CodeModule intFirstLine As Integer intStartLine As Integer intLastLine As Integer strCode As String
'Objektvariable mit Verweis auf das Codemodul erstellen Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(intModuleID).CodeModule 'Erste Zeile der Prozedur ermitteln intStartLine = objCodeModule.ProcStartLine(strProcedure, _ lngProcedureType) 'Zeile der Prozedurdeklaration ermitteln intFirstLine = objCodeModule.ProcBodyLine(strProcedure, lngProcedureType) 'Letzte Zeile der Prozedur ermitteln intLastLine = intStartLine + objCodeModule.ProcCountLines(strProcedure, _ lngProcedureType) - 1 'Prozedur auslesen ... strCode = objCodeModule.Lines(intFirstLine, _ intLastLine - intFirstLine + 1) '... und zurückgeben GetCode = strCode End Function Listing 15.15: Auslesen des Codes einer konkreten Prozedur
15.5.3 Manipulieren des Quellcodes Die in den Abschnitten 15.5.1, »Lesender Zugriff auf den Quellcode«, und 15.5.2, »Beispielanwendung: Codeviewer«, beschriebenen Techniken bilden die Grundlage für einige Manipulationen am Quellcode. Wenn Sie beispielsweise eine Fehlerbehandlung zur Prozedur hinzufügen möchten, in der sich die Einfügemarke gerade befindet, müssen Sie zunächst einmal deren Position herausfinden. Andere Operationen lassen sich auch ohne vorheriges Positionieren ausführen – das Einfügen von Prozeduren erfolgt standardmäßig beispielsweise immer direkt hinter dem Deklarationsteil des Moduls.
Mit Prozeduren arbeiten
703
In den folgenden Abschnitten lernen Sie die Techniken für das Manipulieren des Inhalts von Modulen kennen – dazu gehören das Löschen, Suchen und Ersetzen, Hinzufügen von Prozeduren oder Hinzufügen einzelner Codezeilen in bestehende Prozeduren.
Code hinzufügen Neuer Code lässt sich auf verschiedene Art in ein Modul einfügen. Dabei kommen die folgenden Methoden des CodeModule-Objekts zum Zuge: AddFromFile: Fügt den Inhalt einer Textdatei direkt hinter dem Deklarationsbereich des Moduls ein. AddFromText: Fügt die angegebene Zeichenkette direkt hinter dem Deklarationsbereich des Moduls ein. InsertLines: Fügt die angegebene Zeichenkette in der ebenfalls angegebenen Zeile ein und verschiebt den restlichen Code nach unten. ReplaceLine: Fügt die angegebene Zeichenkette anstelle der ebenfalls angegebenen Zeile ein. CreateEventProc: Fügt den Rumpf einer Ereignisprozedur ein (siehe weiter unten in Abschnitt »Ereignisprozeduren hinzufügen«). Die AddFromFile- und die AddFromText-Methode fügen beide den gewünschten Text direkt hinter dem Deklarationsteil des Moduls ein. Sie sind einfach zu bedienen, aber auch nur dann sinnvoll, wenn komplette Prozeduren oder zumindest Prozedurrümpfe eingesetzt werden. Die folgende Anweisung fügt beispielsweise eine kleine Prozedur wie in Abbildung 15.12 in das Modul im aktuellen Codefenster ein: VBE.ActiveCodePane.CodeModule.AddFromString(vbcrlf & "Public Sub Test()" _ & vbcrlf & " MsgBox ""Hallo""" & vbcrlf & "End Sub")
Abbildung 15.12: Diese Prozedur lässt sich mit einem Einzeiler automatisch hinzufügen …
704
15
Anpassen der Entwicklungsumgebung
Die InsertLines-Methode erwartet zwei Parameter: den einzufügenden Text und die Zielzeile. Nach der Behandlung mit der folgenden Zeile sieht die Prozedur aus Abbildung 15.12 wie in Abbildung 15.13 aus: VBE.ActiveCodePane.CodeModule.InsertLines 4, "
On Error Resume Next"
Abbildung 15.13: … und sie lässt sich mit einem weiteren Einzeiler erweitern.
Wenn Sie einzelne Zeilen ersetzen möchten, verwenden Sie dazu die ReplaceLineMethode. Damit können Sie allerdings nur jeweils eine Zeile ersetzen: VBE.ActiveCodePane.CodeModule.ReplaceLine 4, "
On Error Goto Test_Err"
Ereignisprozeduren hinzufügen Während das Hinzufügen herkömmlicher Prozeduren nicht unterstützt wird, ist das bei Ereignisprozeduren sehr wohl der Fall. Die dazu verwendete Methode CreateEvent erwartet zwei Parameter: die englische Bezeichnung der Methode (etwa Open, OnCurrent, BeforeUpdate) und den Namen des Objekts (Form, Report, txtText, lstListenfeld). Die folgende Anweisung fügt beispielsweise eine Prozedur namens Form_Open in das Klassenmodul eines Formulars ein: VBE.ActiveCodePane.CodeModule.CreateEventProc "Open", "Form"
Mit dieser Methode lassen sich beispielsweise automatisch Ereignisprozeduren für größere Mengen Steuerelemente anlegen. Der Vorteil dieser Prozedur ist, dass sie automatisch überprüft, ob das angegebene Steuerelement überhaupt vorhanden ist und das gewünschte Ereignis zur Verfügung stellt.
Löschen von Zeilen Für das Löschen von Zeilen ist die Methode DeleteLines des CodeModule-Objekts verantwortlich. Die Methode erwartet die Nummer der ersten und die Gesamtanzahl der zu löschenden Zeilen.
Mit Prozeduren arbeiten
705
15.5.4 Beispielanwendung: Nummerieren von Codezeilen in einem Modul Die folgende Routine nummeriert die Zeilen des im Übergabeparameter angegebenen Moduls. Dabei durchläuft sie alle Zeilen des Moduls und prüft, ob sich die aktuelle Zeile innerhalb einer Routine befindet, ob es sich um die erste Zeile einer Select CaseAnweisung handelt und ob die Zeile gegebenenfalls die Fortsetzung einer bestehenden Zeile ist (gekennzeichnet durch den einleitenden Unterstrich). Die Routine können Sie zum Ausprobieren vom Direktfenster aus aufrufen. Das Pendant zum Entnummerieren eines Moduls finden Sie auf der Buch-CD im Modul mdlModulNummerieren der Datenbank Kap_15\VBAIDE.mdb. Die Routine findet übrigens auch in dem in Abbildung 15.3 gezeigten Tool Verwendung und ist hier nur so weit angepasst, dass Sie es vom Direktfenster aus aufrufen und dort den Namen des zu nummerierenden Moduls übergeben können. Im Tool öffnen Sie einfach das gewünschte Modul und wählen den Menüeintrag Nummerieren beziehungsweise Entnummerieren aus. Public Sub Nummerieren(strModulname As String) Dim Dim Dim Dim Dim Dim Dim Dim Dim
objVBE As VBE cdPane As VBIDE.CodePane mdl As VBIDE.CodeModule bolNummerieren As Boolean bolJetztNicht As Boolean bolNaechsteNicht As Boolean strZeile As String i As Integer j As Integer
Entnummerieren strModulname Set objVBE = Application.VBE j = 1 Set mdl = objVBE.ActiveVBProject.VBComponents(strModulname).CodeModule For i = 1 To mdl.CountOfLines strZeile = Trim(mdl.Lines(i, 1)) If Left(strZeile, 10) Or Left(strZeile, Or Left(strZeile, Or Left(strZeile, Or Left(strZeile, Or Left(strZeile,
= "Public Sub" _ 11) = "Private Sub" _ 3) = "Sub" _ 15) = "Public Function" _ 16) = "Private Function" _ 8) = "Function" _
706
15
Anpassen der Entwicklungsumgebung
Or Left(strZeile, 15) = "Public Property" _ Or Left(strZeile, 16) = "Private Property" Then bolNummerieren = True bolJetztNicht = True Else bolJetztNicht = False End If If bolNaechsteNicht = True Then bolJetztNicht = True End If If Left(strZeile, 12) = "End Function" _ Or Left(strZeile, 7) = "End Sub" _ Or Left(strZeile, 12) = "End Property" Then bolNummerieren = False bolJetztNicht = True End If bolNaechsteNicht = Right(strZeile, 1) = "_" _ Or Left(strZeile, 11) = "Select Case" If bolNummerieren = True And bolJetztNicht = False Then mdl.ReplaceLine i, j & "0 " & mdl.Lines(i, 1) j = j + 1 End If Next i End Sub Listing 15.16: Diese Routine nummeriert die Zeilen eines Moduls.
15.6 Toolwindows Die VBA-Entwicklungsumgebung liefert bereits einige andockbare Toolwindows mit. Nachfolgend finden Sie eine Auflistung der vorhandenen Toolwindows und ihre Funktion: Projektbrowser: Navigation in den Klassenmodulen von Formularen und Berichten, Standardmodulen und eigenständigen Klassenmodulen Direktbereich: Schnelles Testen von Anweisungen und Funktionsaufrufen; Ausgabe von Debug.Print-Anweisungen im Quellcode Lokalfenster: Überwachung der Werte der Variablen der aktuellen Prozedur Eigenschaften: Anzeige der Eigenschaften des aktuellen Moduls Überwachungsausdrücke: Überwachung benutzerdefinierter Ausdrücke
Toolwindows
707
Sie können die eingebauten Toolwindows über das Untermenü Ansicht aufrufen (siehe Abbildung 15.14). Diese werden dann an einer voreingestellten Stelle eingeklinkt. Um ein Toolwindow auszublenden, klicken Sie einfach auf die Schließen-Schaltfläche.
Abbildung 15.14: Anzeigen der eingebauten Toolwindows per Menüaufruf
15.6.1 Benutzerdefiniertes Toolwindow = COM-Add-In Toolwindows können Sie auch selbst erstellen. Die dazu verwendete Technik ist jedoch wenig verbreitet. Sowohl in der Fachliteratur als auch im Internet finden sich dazu kaum Informationen. Tatsächlich gibt es nur wenige Anbieter, die überhaupt als Toolwindow ausgelegte COM-Add-Ins vertreiben. Das ist aber weiter nicht schlimm, da Sie nach dem Studium der folgenden Abschnitte selbst in der Lage sein werden, Toolwindows zu erstellen. Und nicht nur das: In Abschnitt 15.7, »COM-Add-Ins per Menübefehl aufrufen«, erfahren Sie auch noch, wie Sie COM-Add-Ins erstellen, die per benutzerdefinierter Symbolleiste oder Kontextmenü aufgerufen werden können. Sie finden auf der Buch-CD ein VB-Projekt, dass Sie als Grundlage für eigene Toolwindows/COM-Add-Ins verwenden können. Die notwendigen Dateien befinden sich im Verzeichnis Kap_15\COMAddIn_Toolwindow_Basis.
708
15
Anpassen der Entwicklungsumgebung
Die nachfolgenden Ausführungen beziehen sich auf die Erstellung von COM-Add-Ins mit Visual Basic und dem Visual Studio 6.0. Es ist auch möglich, diese mit der Developer Edition von Access 2000 oder höher oder mit .NET im Visual Studio .NET zu erstellen. Grundlage für die Verwendung von COM-Add-Ins ist eine Schnittstelle namens IDTExtensibility2. Ist ein COM-Add-In einmal als solches registriert, sorgt diese Schnittstelle dafür, dass das Add-In beim Laden oder Entladen der Zielanwendung – in diesem Fall der VBA-Entwicklungsumgebung – gestartet wird und Informationen über die Zielanwendung erhält. Ist ein COM-Add-In ordnungsgemäß registriert, erscheint es im Add-In-Manager der Zielanwendung – hier im VBA-Editor. Abbildung 15.15 zeigt den entsprechenden Dialog der VBA-Entwicklungsumgebung mit einigen COM-Add-Ins. Den Dialog öffnen Sie über den Menüeintrag Add-Ins/Add-In-Manager … In diesem Dialog können Sie das Ladeverhalten der Add-Ins festlegen. Wenn Sie ein COM-Add-In für die Access-Entwicklungsumgebung (also nicht für die VBA-Entwicklungsumgebung!) entwickeln, benötigen Sie noch die Information, dass Sie COM-Add-Ins dort mit einem anderen Dialog als in der VBA-Entwicklungsumgebung verwalten. Den Menüeintrag, um diesen Dialog zu öffnen, müssen Sie zunächst zu einem der Menüs hinzufügen: Dazu verwenden Sie den Anpassen-Dialog für Menüs und wählen auf der Registerseite Befehle die Kategorie Extras und den Befehl COM-Add-Ins… aus.
Abbildung 15.15: Verwalten der Add-Ins der VBA-Entwicklungsumgebung
Toolwindows
709
15.6.2 Anlegen eines leeren Toolwindows In den folgenden Schritten legen Sie ein erstes eigenes COM-Add-In an, das Sie anschließend als Toolwindow in die VBA-Entwicklungsumgebung integrieren können. Den Weg dahin macht das Microsoft Visual Studio 6.0 Ihnen relativ leicht – richtig interessant wird das Hinzufügen der eigentlichen Funktionalität. Da der Weg dorthin aber nur über das hier im Folgenden vorgestellte Grundgerüst führt, ist damit nun erst einmal der Pflichtteil an der Reihe.
Hinweise zum Entwickeln mit Microsoft Visual Studio 6.0 Falls Sie noch keine Erfahrung im Umgang mit dem Visual Studio besitzen, werden Ihnen die folgenden Tipps an einigen Stellen weiterhelfen: Das Kompilieren eines COM-Add-Ins erfolgt wie bei allen anderen Projektarten über den Menübefehl Datei/ erstellen. Erst wenn Sie ein COM-AddIn kompiliert haben, wird ein Eintrag in der Registry vorgenommen und das COMAdd-In ist im Dialog Add-In-Manager verfügbar. Das Erstellen der .dll-Datei entspricht dem Kompilieren eines VBA-Moduls. Dementsprechend werden Kompilierfehler erst beim Erstellen der .dll-Datei aufgedeckt. Sie können das Projekt natürlich auch debuggen. Dazu starten Sie es mit (F5). Im Fall der hier vorliegenden COM-Add-Ins tut sich natürlich nichts, bis Sie Access und die VBA-Entwicklungsumgebung starten und das zu debuggende Add-In aktivieren. Das Debuggen läuft ansonsten wie in der VBA-Entwicklungsumgebung ab – Sie können Haltepunkte in den Code-Fenstern des VB6-Editors setzen, Überwachungen hinzufügen und so weiter. Wichtig ist, dass Sie mit dem Debuggen die Registrierung der eventuell schon kompilierten .dll-Datei wieder aufheben. Das können Sie am besten beobachten, indem Sie alle Optionen des Ladeverhaltens der fertigen .dll-Datei im Add-In-Manager aktivieren. Wenn Sie das Projekt anschließend debuggen und erneut die Optionen des Ladeverhaltens betrachten, stellen Sie fest, dass diese zurückgesetzt sind. Sie müssen also nach dem Debuggen des Projekts die .dll-Datei wieder neu registrieren oder das Projekt komplett neu erstellen.
15.6.3 Anlegen eines neuen Projekts Nach dem Öffnen von Microsoft Visual Basic 6.0 folgen Sie dem Vorschlag, ein neues Projekt anzulegen, und wählen im Dialog aus Abbildung 15.16 den Eintrag Add-In aus. Falls dieser Dialog nicht automatisch beim Start angezeigt wird, verwenden Sie den Menübefehl Datei/Neues Projekt zum Anlegen des neuen Projekts.
710
15
Anpassen der Entwicklungsumgebung
Abbildung 15.16: Anlegen eines neuen COM-Add-Ins
Durch die Auswahl dieses Projekttyps nimmt das Visual Studio Ihnen einige Aufgaben ab. So legt es etwa schon die benötigten Verweise auf die Bibliotheken Microsoft Office x.y Objekt Library und Microsoft Visual Basic 6.0 Extensibility und Microsoft Add-In Designer an (siehe Abbildung 15.17). Die Bibliothek Microsoft Visual Basic 6.0 Extensibility müssen Sie in diesem Fall allerdings durch die Bibliothek Microsoft Visual Basic for Applications Extensibility 5.3 ersetzen. Die Office-Bibliothek benötigen Sie für den Zugriff auf die Menü-Objekte, und die Microsoft Visual Basic for Applications 5.3 Extensibilty-Bibliothek enthält das Objektmodell für den Zugriff auf das in der VBA-Entwicklungsumgebung angezeigte Projekt einschließlich seiner Module und enthaltener Codes. Wenn Sie den Projekt-Explorer aktivieren, bietet sich nun das Bild aus Abbildung 15.18. Von den beiden vorhandenen Elementen benötigen Sie nur eines – den Add-InDesigner. Das Formular würde – wenn Sie ein Add-In mit einem Formular erstellen wollten – als herkömmliches Fenster in der VBA-Entwicklungsumgebung angezeigt. Es weist nicht die gleichen Eigenschaften des Toolwindows auf und lässt sich dementsprechend nicht an einer bestimmten Stelle andocken.
Toolwindows
711
Abbildung 15.17: Visual Studio legt für das Add-In-Projekt automatisch einige Verweise an.
Falls Sie ein Add-In planen, das zusätzlich zum Toolwindow weitere Formulare benötigt, lassen Sie das automatisch erstellte Formular zunächst im Projekt, ansonsten sollten Sie es entfernen.
Abbildung 15.18: Der Projekt-Explorer des neuen Projekts
15.6.4 Der COM-Add-In-Designer Schauen Sie sich nun das verbliebene Objekt des Projekts an – den Add-In-Designer. Mit einem Doppelklick auf den entsprechenden Eintrag im Projekt-Explorer öffnen Sie das Eigenschaftsfenster aus Abbildung 15.19. Neben dem Namen, der dann später im Add-In-Manager aufgelistet wird, und der Beschreibung des Add-Ins ist vor allem die Einstellung der Anwendung interessant. Für die VBA-Entwicklungsumgebung als Zielanwendung wählen Sie den Eintrag Visual Basic For Applications IDE. Wenn Sie das anfängliche Ladeverhalten auf Startup einstellen, wird das COM-Add-In direkt beim Start der VBA-Entwicklungsumgebung im Menü Add-Ins angezeigt, ansonsten müssen Sie dieses erst mit dem Add-In-Manager starten.
712
15
Anpassen der Entwicklungsumgebung
Abbildung 15.19: Eigenschaftsfenster des COM-Add-In-Designers
Der Add-In-Designer enthält außerdem noch ein Code-Modul, in dem Sie die benötigten Ereigniseigenschaften anlegen. Das Code-Modul zeigen Sie an, indem Sie aus dem Kontextmenü des Eintrags Connect (Connect.Dsr) des Projekt-Explorers den Befehl Code anzeigen auswählen. Im nun erscheinenden Code-Fenster finden Sie bereits einigen Quellcode, der beim Erstellen des Projekts automatisch angelegt wurde (siehe Abbildung 15.20). Um das handelsübliche COM-AddIn in eines mit Toolwindow umzuwandeln, sind einige Schritte notwendig.
15.6.5 Das Userdocument als Toolwindow Anstelle eines normalen Formulars verwenden Sie ein Objekt namens Benutzerdokument zur Realisierung des Toolwindows. Das Benutzerdokument ist einem herkömmlichen VB-Formular sehr ähnlich. Um ein solches Objekt zum Projekt hinzuzufügen, verwenden Sie den Eintrag Hinzufügen/Benutzerdokument des Kontextmenüs des Projekt-Explorers. Wählen Sie im Dialog Benutzerdokument hinzufügen den Eintrag Benutzerdokument aus. Der VB-Editor legt das neue Benutzerdokument an und öffnet es in der Entwurfsansicht (siehe Abbildung 15.21).
Toolwindows
713
Abbildung 15.20: Ereignisprozeduren des Add-In-Designers
Das Toolwindow enthält noch keine Steuerelemente und auch keinen Quellcode. Das soll auch vorerst so bleiben, denn zunächst soll nur das leere Toolwindow in der VBAEntwicklungsumgebung angezeigt werden.
Abbildung 15.21: Dieses Benutzerdokument dient in der VBA-Entwicklungsumgebung als Toolwindow.
15.6.6 Ereignisprozeduren des COM-Add-Ins mit Leben füllen Selbstverständlich bekommen Sie in der VBA-Entwicklungsumgebung kein Toolwindow zu sehen, ohne es mit der grundlegenden Funktionalität zu bestücken. Die folgenden Prozeduren gehören alle ins Codemodul des Add-In-Designers. Den Start machen einige modulweite Konstanten und Deklarationen (siehe Listing 15.17). Die String-Konstanten werden in mehreren Ereignisprozeduren benötigt und sind daher modulweit erreichbar.
714
15
Anpassen der Entwicklungsumgebung
Die Variable evtMenu hat den Typ CommandbarEvents und wird mit dem Schlüsselwort WithEvents deklariert. Später wird die Variable auf Ereignisse des Menüelements für das neue COM-Add-In im Menü Add-Ins reagieren. Die Deklaration der Variable für dieses Menüelement namens cbc befindet sich in der nächsten Zeile. Die letzten drei Zeilen enthalten die Deklarationen von Objektvariablen für die VBAEntwicklungsumgebung selbst (objApplication), für das Toolwindow (objToolwindow) und das Benutzerdokument (objUserDocument), das zur Laufzeit zum Toolwindow wird. Option Explicit Const Const Const Const Const Const
strGUID$ = "{E3A0FF80-720A-4AB2-BAAC-0BB233E7526E}" strAppTitle = "COMAddin" 'Applikations-Titel strTitle = "Toolwindow (COM-Add-In)" 'Titelzeile des Addin-Fensters strMenu = "Add-&Ins" 'Menü für Eintrag des Addins strSubMenu = "Beispiel-Toolwindow-Add-In" 'Bezeichnung des Menüeintrags strUserDocument = "Toolwindow" 'Name des Userdocuments
Public WithEvents evtMenu As VBIDE.CommandBarEvents Private cbc As Office.CommandBarControl Dim strMenubar As String Public objApplication As VBIDE.VBE Public objToolwndow As VBIDE.Window Public objUserDocument As Object Listing 15.17: Deklarationsteil des Add-In-Designer-Moduls
Als Nächstes folgt die Prozedur, die beim Aufrufen des COM-Add-Ins durch die VBAEntwicklungsumgebung gestartet wird. Voraussetzung für diesen Aufruf ist, dass Sie im Dialog Add-In-Manager die Eigenschaft Geladen/Entladen aktivieren und den Dialog schließen oder dass die VBA-Entwicklungsumgebung bei aktivierter Beim Start ladenEigenschaft des jeweiligen COM-Add-Ins geöffnet wird. Diese Ereignisprozedur heißt AddinInstance_OnConnection und enthält folgenden Code: Private Sub AddinInstance_OnConnection( _ ByVal Application As Object, _ ByVal ConnectMode As AddInDesignerObjects.ext_ConnectMode, _ ByVal AddInInst As Object, _ custom() As Variant) Dim cbcTemp As Office.CommandBarControl Dim i As Long
Toolwindows
715
On Error GoTo AddinInstance_OnConnection_Err Set objApplication = Application App.Title = strAppTitle On Error Resume Next strMenubar = "Menüleiste" strMenubar = objApplication.CommandBars(strMenubar).Name If Err.Number <> 0 Then strMenubar = "Menu Bar" End If On Error GoTo AddinInstance_OnConnection_Err For i = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls.Count To 1 Step -1 Set cbcTemp = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls(i) If cbcTemp.Caption = strSubMenu Then cbcTemp.Delete End If Next i If Not objToolWindow Is Nothing Then objToolWindow.Visible = True Else Set objToolWindow = objApplication.Windows.CreateToolWindow( _ AddInInst, strAppTitle & "." & strUserDocument, strTitle, _ strGUID$, objUserDocument) End If AddToCommandBar AddinInstance_OnConnection_Exit: Set cbcTemp = Nothing Exit Sub AddinInstance_OnConnection_Err: MsgBox "...in Zeile " & Erl & " Fehlernummer " & Err.Number & vbNewLine _ & "- " & Err.Description _ & " in Prozedur: AddinInstance_OnConnection in Connect", _ vbCritical, "Fehler in " & strTitle Resume AddinInstance_OnConnection_Exit End Sub Listing 15.18: Die Ereignisprozedur AddinInstance_OnConnection
716
15
Anpassen der Entwicklungsumgebung
Die Eingangsparameter der Prozedur übergeben automatisch die benötigten Informationen über die aufrufende VBA-Entwicklungsumgebung. Die Prozedur führt im Wesentlich die folgenden Schritte aus: Entfernen des Eintrags für das Toolwindow aus dem Menü Add-Ins, sofern noch vorhanden – was zum Beispiel durch einen früheren Absturz der Umgebung der Fall sein könnte. Überprüfen, ob das Toolwindow schon instanziert ist, und es gegebenenfalls in den Vordergrund holen oder es mit der Methode CreateToolWindow neu erzeugen. Aufruf der Prozedur AddToCommandBar zum Hinzufügen eines Eintrags für das Toolwindow in das Menü Add-Ins und Funktion zur Verarbeitung der Menü-Ereignisse dieses Eintrags hinzufügen. Die Prozedur AddToCommandBar macht zunächst die Hauptmenüleiste der VBA-Entwicklungsumgebung sichtbar, sofern das noch nicht der Fall war. Anschließend fügt sie dem Menü Add-Ins einen Eintrag zum Aktivieren des Toolwindows hinzu und stellt dessen Beschriftung, Symbol und das beim Klicken auszulösende Ereignis ein. Schließlich überprüft die Routine, ob das Toolwindow beim letzten Schließen sichtbar war oder nicht, und stellt diesen Zustand wieder her. Dabei greift sie mit der Funktion GetSetting auf die Registry zu. Weitere Informationen zum Erstellen von Menüs per VBA erhalten Sie in Kapitel 10, »Menüleisten«. Sub AddToCommandBar() On Error GoTo AddToCommandBar_Err objApplication.CommandBars(strMenubar).Visible = True Set cbc = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls.Add(1 , , , , True) cbc.Caption = strSubMenu cbc.Style = msoButtonIconAndCaption cbc.FaceId = 611 Set Me.evtMenu = objApplication.Events.CommandBarEvents(cbc) If GetSetting("VBA AddIns", App.Title, "DisplayOnConnect", "0") _ = "1" Then objToolWindow.Visible = True End If AddToCommandBar_Exit: Exit Sub AddToCommandBar_Err:
Toolwindows
717
MsgBox "...in Zeile " & Erl & " Fehlernummer " & Err.Number & vbNewLine _ & "- " & Err.Description _ & " in Prozedur: AddToCommandBar in Connect", vbCritical, _ "Fehler in " & strTitle Resume AddToCommandBar_Exit End Sub Listing 15.19: Hinzufügen eines Menüleisteneintrags zum Aktivieren des Toolwindows
Damit der Menüeintrag beim Klicken auch das Toolwindow aktiviert, benötigt das Modul noch einen Event-Handler. Dieser enthält lediglich eine Anweisung, die das Toolwindow sichtbar macht: Private Sub evtMenu_Click(ByVal CommandBarControl As Object, handled As Boolean, CancelDefault As Boolean) On Error Resume Next objToolWindow.Visible = True End Sub Listing 15.20: Event-Handler für den Toolwindow-Menüeintrag
Zu guter Letzt bleibt noch die Ereignisprozedur, die beim Entladen des COM-Add-Ins ausgelöst wird. Die Prozedur sorgt für das Entfernen des Menüeintrags zum Anzeigen des Toolwindows, speichert den aktuellen Zustand bezüglich der Sichtbarkeit des Toolwindows in der Registry und gibt die Objektvariablen wieder frei: Private Sub AddinInstance_OnDisconnection(ByVal RemoveMode As AddInDesignerObjects.ext_DisconnectMode, custom() As Variant) Dim cbcTemp As Office.CommandBarControl Dim i As Long On Error Resume Next For i = objApplication.CommandBars(strMenubar).Controls(strMenu). _ Controls.Count To 1 Step -1 Set cbcTemp = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls(i) If cbcTemp.Caption = strSubMenu Then cbcTemp.Delete End If Next i If objToolWindow.Visible Then SaveSetting "VBA AddIns", App.Title, "DisplayOnConnect", "1" objToolWindow.Visible = False Else SaveSetting "VBA AddIns", App.Title, "DisplayOnConnect", "0"
718
15
Anpassen der Entwicklungsumgebung
End If objToolWindow.Close Set Set Set Set Set Set
Me.evtMenu = Nothing cbcTemp = Nothing cbc = Nothing objToolWindow = Nothing objUserDocument = Nothing objApplication = Nothing
End Sub Listing 15.21: Die Ereignisprozedur AddinInstance_OnDisconnection wird beim Entladen des COM-Add-Ins ausgelöst.
15.6.7 Anpassen der Eigenschaften des COM-Add-Ins Sicher möchten Sie das COM-Add-In und damit das Toolwindow Ihren eigenen Bedürfnissen entsprechend zurechtschneiden. Dazu gibt es folgende Ansatzpunkte: Passen Sie die Eigenschaften Angezeigter Name des Add-Ins und Add-In-Beschreibung im Designer an (siehe auch Abbildung 15.19). Diese Informationen werden im Add-In-Manager angezeigt. Ändern Sie die Bezeichnungen der String-Konstanten im Codemodul des Designers. Hier ist vor allem der Fenstertitel interessant, der in der Konstanten strTitle gespeichert wird. Sehr wichtig ist, dass Sie – wenn Sie die oben aufgeführten Listings aus dem Buch oder von der Buch-CD übernehmen – für die Eindeutigkeit der verwendeten GUID sorgen. Um eine solche neue GUID zu ermitteln, fügen Sie den Code aus Listing 15.22 in ein neues Modul ein (entweder im Visual Studio oder in der VBA-Entwicklungsumgebung) und verwenden die Funktion CreateGUID. Sie können diese Funktion beispielsweise im Direktfenster beziehungsweise Direktbereich mit dem Aufruf Debug.Print CreateGUID starten und erhalten die gewünschte Ausgabe im gleichen Bereich. Anschließend kopieren Sie diesen GUID-String in die Konstante strGUID$ des Designer-Moduls. Public Type TYP_GUID bytes(15) As Byte End Type Public Declare Function CoCreateGuid Lib "OLE32.dll" (Guid As TYP_GUID) As Long Public Declare Function StringFromGUID2 Lib "OLE32.dll" _ (Guid As TYP_GUID, _
Toolwindows
719
ByVal lpszString As String, _ ByVal iMax As Long) As Long Public Function CreateGUID() As String Dim uGuid As TYP_GUID, sBuffer As String, lResult As Long sBuffer = VBA.Space(78) CoCreateGuid uGuid lResult = StringFromGUID2(uGuid, sBuffer, Len(sBuffer)) CreateGUID = Left$(StrConv(sBuffer, vbFromUnicode), lResult - 1) End Function Listing 15.22: Funktion zum Ermitteln einer GUID
15.6.8 Anzeige des Toolwindows beim Starten der VBAEntwicklungsumgebung Wenn Sie die VBA-Entwicklungsumgebung starten, werden Abläufe in Gang gesetzt, die für das Einbinden eines COM-Add-Ins interessant sind: 1. Die VBA-Entwicklungsumgebung sucht in der Registry von Windows nach den vorhandenen Add-Ins. Die entsprechenden Einträge legt Windows unter HKEY_CURRENT_USER\Software\Microsoft\VBA\VBE6.0\Addins an (siehe Abbildung 15.22). 2. Sind dort Einträge vorhanden, wird der Wert der Eigenschaft LoadBehaviour geprüft. Der Wert gibt an, ob das Add-In beim Anwendungsstart geladen wird oder nicht. Die Eigenschaft kann folgende Werte annehmen und gibt damit die Einstellungen aus Abbildung 15.15 wieder: 0: Geladen/Entladen: Falsch; Beim Start laden: Falsch 1: Geladen/Entladen: Falsch; Beim Start laden: Wahr 2: Geladen/Entladen: Wahr; Beim Start laden: Falsch 3: Geladen/Entladen: Wahr; Beim Start laden: Wahr
15.6.9 Testen des neuen Toolwindows Nun wird es Zeit, das neue COM-Add-In zu testen. Kompilieren Sie das Projekt im Visual Studio über den Menüeintrag Datei/.dll. Wenn keine Fehler auftreten, starten Sie Access und öffnen die VBA-Entwicklungsumgebung. Ein Blick in den Add-In-Manager (Menüpunkt Add-Ins/Add-In-Manager) schafft Gewissheit: Wenn alles funktioniert hat, befindet sich dort der neue Eintrag – noch mit jungfräulichen Einstellungen für das Ladeverhalten (siehe Abbildung 15.23).
720
Abbildung 15.22: Registrierungsort der COM-Add-Ins
Abbildung 15.23: Das COM-Add-In im jungfräulichen Zustand
15
Anpassen der Entwicklungsumgebung
Toolwindows
721
Das soll allerdings nicht lange so bleiben. Aktivieren Sie die Optionen Geladen/Entladen und Beim Start laden und schließen Sie den Add-In-Manager. Öffnen Sie erneut das Menü Add-Ins und machen Sie die Probe aufs Exempel (siehe Abbildung 15.24). Das fertige Toolwindow zeigt Abbildung 15.25. Das Toolwindow präsentiert sich ein wenig schmucklos und es enthält auch noch keine Steuerelemente geschweige denn Funktionalität. Außerdem schwebt es noch frei in der VBA-Entwicklungsumgebung. Das Andocken müssen Sie also per Maus selbst übernehmen. Sie haben aber immerhin den Grundstein für die Erstellung vieler nützlicher Helfer geschaffen.
Abbildung 15.24: Das neue COM-Add-In ist einsatzbereit.
Abbildung 15.25: Das Toolwindow im Einsatz
15.6.10 Das Toolwindow füllen Das Toolwindow brauchen Sie nur noch zu füllen. Dazu stehen prinzipiell die gleichen Möglichkeiten zur Verfügung wie in herkömmlichen VB-Formularen. Leider ist die Funktionalität von Toolwindows je nach Anwendung relativ umfangreich, sodass Sie an dieser Stelle auf Ihre eigene Geschicklichkeit angewiesen sind. Wer sich bis hierher durchgekämpft hat und ambitioniert ist, die VBA-Entwicklungsumgebung um nützliche Funktionen zu erweitern, der wird auch diesen letzten Schritt in Angriff nehmen. Außerdem wissen Sie selbst am besten, welche Funktionen Sie in der VBA-Entwicklungsumgebung am meisten vermissen – die Basisausrüstung für nützliche Helfer haben Sie nun vor sich liegen. Und falls Ihnen Ideen fehlen, hier einige Vorschläge: Toolwindow zum Speichern, Auswählen und Suchen von VBA-Code: Damit sichern Sie oft verwendete Code-Fragmente oder Routinen und machen diese dank der Suchfunktion bei Bedarf verfügbar
722
15
Anpassen der Entwicklungsumgebung
ToDo-Manager: Toolwindow mit einer Auflistung aller im aktuellen Modul oder auch im kompletten Projekt enthaltenen Kommentare, die mit »ToDo« oder »Fix« beginnen und offene Aufgaben enthalten. Per Knopfdruck auf einen der Einträge wird die entsprechende Stelle im Code angezeigt. Wenn Sie eigene Vorschläge haben oder anderen Lesern dieses Buchs ein selbst entwickeltes Tool zur Verfügung stellen möchten, schreiben Sie einfach eine Mail an [email protected]. Ihre Vorschläge und Tools werden dann nach Prüfung auf http://www.access-entwicklerbuch.de veröffentlicht.
15.7 COM-Add-Ins per Menübefehl aufrufen Neben Toolwindows lassen sich auch Erweiterungen der VBA-Entwicklungsumgebung erstellen, die Sie über benutzerdefinierte Menüs aufrufen können. Beispiele sind die Nummerierung von Modulen, das automatische Einfügen von Fehlerbehandlungen oder die automatische Generierung von Datenzugriffsklassen auf Basis einer ausgewählten Tabelle. Ein Beispiel für ein menügesteuertes COM-Add-In finden Sie zu Beginn dieses Kapitels in Abbildung 15.3. Dort sehen Sie auch einen Hinweis, wo Sie das Tool finden und wie Sie es installieren können.
15.7.1 Vorbereitungen Die Erstellung eines COM-Add-Ins, das zusätzliche, über Menüleisten erreichbare Funktionen zur Verfügung stellt, erfolgt genau wie beim Toolwindow mit dem Microsoft Visual Studio 6.0. Diesmal verwenden Sie eine ActiveX-DLL als Vorlage für das neue Projekt (siehe Abbildung 15.26). Auch menügesteuerte COM-Add-Ins können Sie mit den Developer-Editionen von Access oder mit .NET erstellen. Hier soll jedoch nur die Erstellung mit Visual Basic 6 beschrieben werden. Auf der Buch-CD finden Sie ein VB-Projekt, das Sie als Grundlage für eigene COMAdd-Ins mit Menüs verwenden können. Die notwendigen Dateien befinden sich im Verzeichnis Kap_15\COMAddIn_Menu_Basis.
COM-Add-Ins per Menübefehl aufrufen
723
Abbildung 15.26: Anlegen einer ActiveX-DLL
Objekte hinzufügen Das im Projekt-Explorer angezeigte Klassenmodul können Sie entfernen. Es wird im weiteren Verlauf nicht benötigt. Damit es im Projekt-Explorer nicht allzu trist aussieht, fügen Sie die benötigten Komponenten direkt hinzu: Zum Hinzufügen der AddIn Class wählen Sie im Kontextmenü des Projekts im ProjektExplorer den Eintrag Hinzufügen/Add-In-Class aus. Ist dieser Eintrag nicht vorhanden, müssen Sie ihn zunächst aktivieren: Öffnen Sie den Dialog Komponenten über den Menübefehl Projekt/Komponenten und wechseln Sie dort auf die Registerseite Designer. Aktivieren Sie den Eintrag Addin Class (siehe Abbildung 15.27) und schließen Sie den Dialog wieder. Wiederholen Sie dann den Vorgang zum Hinzufügen der AddIn Class (siehe Abbildung 15.28). Neben diesem Designer benötigen Sie lediglich noch ein Standardformular für globale Variablen. Legen Sie dieses mit dem Eintrag Hinzufügen/Modul des Kontextmenüs des Projekts im Projekt-Explorer an.
Eigenschaften der AddIn Class anpassen Die AddIn Class stellt zwei Eigenschaftsfenster zur Verfügung: Das erste ist das Eigenschaftsfenster, das jedes Objekt besitzt (anzuzeigen über (F4)), das zweite lässt sich durch einen Doppelklick auf das Objekt im Projektexplorer anzeigen. Zusätzlich besitzt es natürlich ein Codemodul, das Sie über das Kontextmenü öffnen. Sie müssen in beiden Eigenschaftsfenstern Änderungen vornehmen. Die »speziellen« Eigenschaften passen Sie wie in Abbildung 15.29 an. Im herkömmlichen Eigenschaftsfenster stellen Sie die Eigenschaft Public auf True ein und bestätigen die anschließend erscheinende Meldung.
724
Abbildung 15.27: Bereitstellen der Addin Class
Abbildung 15.28: Hinzufügen der Addin Class zum Projekt
15
Anpassen der Entwicklungsumgebung
COM-Add-Ins per Menübefehl aufrufen
725
Abbildung 15.29: Eigenschaften der AddIn Class
Anpassen des Standardmoduls Das Standardmodul statten Sie mit einer einzigen Anweisung aus. Diese stellt eine öffentlich zugängliche Objektvariable für den Zugriff auf das Objektmodell der VBAEntwicklungsumgebung zur Verfügung: Public objVBE As VBIDE.VBE
Weitere Einstellungen Da Sie mit den in der VBA-Entwicklungsumgebung angelegten Menüs vermutlich Funktionen zur Automatisierung dieser Entwicklungsumgebung durchführen möchten, benötigten Sie noch einen entsprechenden Verweis. Öffnen Sie den Dialog Verweise über den Menüeintrag Projekt/Verweise und aktivieren Sie dort den Eintrag Microsoft Visual Basic for Applications Extensibility 5.3. Zum Anlegen von Menüleisten benötigen Sie ebenfalls einen Verweis; die entsprechende Bibliothek heißt Microsoft Office x.y Object Library.
726
15
Anpassen der Entwicklungsumgebung
Projekt speichern Damit Sie später noch wissen, welches Objekt welche Aufgabe hat, vergeben Sie nun sinnvolle Namen und speichern das komplette Projekt. Nennen Sie das Standardmodul mdlGlobal, die AddIn Class AddInDesigner und vergeben Sie für das Projekt den Namen VBEMenus (wobei VBE für Visual Basic Editor steht). Anschließend speichern Sie das Projekt in einem Verzeichnis Ihrer Wahl. Die Speichernamen der einzelnen Objekte werden von den Bezeichnungen abgeleitet, die Sie vergeben haben – diese können Sie einfach übernehmen. Die Vorarbeiten sind damit abgeschlossen. Sie können den Zwischenstand testen, indem Sie dem Modul des Add-In-Designers eine Anweisung zum Anzeigen eines Meldungsfensters hinzufügen, das Projekt und damit die .dll-Datei erstellen und die VBA-Entwicklungsumgebung aufrufen: 1. Öffnen Sie das Codemodul des Add-In-Designers. 2. Wählen Sie im linken Kombinationsfeld des Codefensters den Eintrag AddinInstance aus. Die Prozedur AddinInstance_OnConnection wird automatisch angelegt. 3. Fügen Sie dieser Prozedur lediglich eine MsgBox-Anweisung mit dem gewünschten Text hinzu. 4. Kompilieren Sie das Projekt mit dem Menübefehl Datei/VBMenus.dll erstellen… (nach Murphy’s Gesetz ist die Wahrscheinlichkeit hoch, dass das Projekt sich nicht kompilieren lässt – gehen Sie in diesem Fall einfach noch einmal die Beschreibung durch). 5. Wenn die .dll-Datei erstellt wurde, öffnen Sie Access und rufen die VBA-Entwicklungsumgebung auf. Wenn das Meldungsfenster wie geplant erscheint, ist bis hierhin alles in Ordnung – nun geht es mit der eigentlichen Funktionalität weiter.
15.7.2 Hinzufügen der Funktionen und Menüs Die folgenden Beschreibungen beziehen sich alle auf das Codemodul des Add-In-Designers. Die prinzipielle Funktionstüchtigkeit haben Sie ja bereits mit dem Meldungsfenster in der Prozedur AddinInstance_OnConnection bewiesen – die entsprechenden Anweisungen können Sie daher nun entfernen und durch wirklich nützlichen Code ersetzen.
Grundgerüst erstellen Das Programmieren benutzerdefinierter Menüleisten und Schaltflächen mit entsprechenden Funktionen erfordert ein kleines Grundgerüst und für jede Schaltfläche ein wenig Code. Der größte Teil der Arbeit liegt im Entwickeln der eigentlichen Funktionen, die durch die einzelnen Schaltflächen ausgelöst werden.
COM-Add-Ins per Menübefehl aufrufen
727
Grundlage für die Verwendung der Schaltflächen der Menüleiste und das Auslösen der entsprechenden Ereignisprozeduren sind einige modulweit deklarierte Variablen. Sie benötigen für jede Schaltfläche eine Objektvariable des Typs CommandBarButton und für das entsprechende Ereignis eine Objektvariable des Typs CommandBarEvents. Der Kopf des Moduls sieht, wenn Sie nur eine Schaltfläche verwenden, folgendermaßen aus: Option Explicit Dim cbb As CommandBarButton Dim WithEvents evt As CommandBarEvents
Da hier zunächst die Struktur der AddinInstance_OnConnection-Prozedur und der Schaltflächenereignisse beschrieben werden soll, heißen die Variablen einfach dem Typ entsprechend »cbb« für CommandBarButton und »evt« für CommandBarEvents. Wenn Sie später mehrere Schaltflächen einfügen, benötigen Sie für jede Schaltfläche je eine dieser Objektvariablen. Am besten verwenden Sie als Variablennamen die beiden Präfixe »cbb« und »evt« zuzüglich eines Namens, der die enthaltene Funktion beschreibt – etwa cbbZeilenNummerieren und evtZeilenNummerieren. Die folgende Ereignisprozedur kennen Sie bereits – sie wird von der VBA-Entwicklungsumgebung aufgerufen, wenn diese geladen wird. Da Sie in den Eigenschaften des Add-In-Designers das anfängliche Ladeverhalten auf Startup eingestellt haben, wird die Prozedur beim nächsten Start der VBA-Entwicklungsumgebung auf jeden Fall aufgerufen (weiter oben beim Erstellen des Toolwindows wurde das Startverhalten auf None eingestellt – dort musste der Anwender das COM-Add-In zunächst über den Add-In-Manager aktivieren). Die Prozedur erstellt zunächst einen Verweis auf das Objektmodell der VBA-Entwicklungsumgebung. Diesen Verweis benötigen Sie, um dort eine Symbolleiste und die Schaltflächen anlegen zu können; außerdem stellt das Objektmodell die Methoden und Eigenschaften für den Zugriff auf die Module und den enthaltenen Code zur Verfügung. Wenn Sie mehr als eine Symbolleiste anlegen möchten, müssen Sie die entsprechenden Zeilen der Prozedur mehrere Male wiederholen. Zum Anlegen mehrerer Schaltflächen wiederholen Sie lediglich die letzten fünf Zeilen der Prozedur für jede Schaltfläche einmal – genau wie die beiden weiter oben beschriebenen Deklarationszeilen. Private Sub AddinInstance_OnConnection(ByVal Application As Object, _ ByVal ConnectMode As AddInDesignerObjects.ext_ConnectMode, _ ByVal AddInInst As Object, custom() As Variant)
728
15
Anpassen der Entwicklungsumgebung
Dim cbr As CommandBar 'Verweis auf die aufrufende Anwendung setzen; 'in diesem Fall die VBA-Entwicklungsumgebung Set objVBE = Application 'Hinzufügen einer temporären Menüleiste Set cbr = objVBE.CommandBars.Add("VBETools", msoBarTop, , True) 'Sichtbarmachen der Menüleiste cbr.Visible = True 'Hinzufügen einer Schaltfläche zur Menüleiste Set cbb = cbr.Controls.Add(msoControlButton, , , , True) 'Anpassen der Eigenschaften der Schaltflächen cbb.Style = msoButtonIconAndCaption cbb.FaceId = 2950 cbb.Caption = "Beispielbutton" 'Ereignis für die Schaltfläche festlegen Set evt = objVBE.Events.CommandBarEvents(cbb) End Sub Listing 15.23: Diese Prozedur wird beim Öffnen der VBA-Entwicklungsumgebung ausgeführt – vorausgesetzt, die .dll-Datei ist ordnungsgemäß registriert.
Fehlt noch die Ereignisprozedur, die beim Anklicken der Schaltfläche ausgeführt werden soll. Das Anlegen des Prozedurrumpfs ist ein Kinderspiel: Dazu wählen Sie einfach aus dem linken Kombinationsfeld des Codefensters den Eintrag evt aus – Visual Studio legt dann automatisch die Click-Ereignisprozedur an. Eine solche Prozedur benötigen Sie später für jede anzulegende Schaltfläche. Sobald Sie eine neue Objektvariable des Typs CommandBarEvents erstellt haben, steht der Eintrag im entsprechenden Kombinationsfeld des Codefensters zur Verfügung. Private Sub evt_Click(ByVal CommandBarControl As Object, _ handled As Boolean, CancelDefault As Boolean) 'Testanweisung für das Ereignis MsgBox "Die Menü-Schaltfläche funktioniert!" End Sub
COM-Add-Ins per Menübefehl aufrufen
729
Zugriff auf die Objekte der VBA-Entwicklungsumgebung Wenn Sie obiges Grundgerüst mit den in den vorherigen Abschnitten vorstellten Prozeduren ausstatten, können Sie beliebige Manipulationen der Module und des enthaltenen Codes durchführen. Die meisten zu automatisierenden Vorgänge werden sich dabei auf den Inhalt des aktuellen Codefensters beziehen, das sich sehr leicht referenzieren lässt. Ein wenig interessanter wird es, wenn weitere Informationen für die Operationen notwendig sind, also wenn Sie beispielsweise einen Assistenten zum Anlegen der MsgBox-Anweisung benötigen oder eine Objektklasse für die Daten einer Tabelle automatisch erstellen lassen möchten – Sie müssen dann eine Benutzungsschnittstelle für die Eingabe der Daten zur Verfügung stellen. Für einfache Ansprüche reicht möglicherweise die InputBox-Anweisung aus, mit der sich jeweils ein Wert abfragen lässt. In den meisten Fällen ist aber die Verwendung eines Formulars erforderlich. Natürlich bietet VB auch die Möglichkeit, Formulare zu erstellen und diese in ComAdd-Ins zu verwenden. Leider würde eine Beschreibung der entsprechenden Vorgehensweise nicht nur den Umfang, sondern auch den thematischen Rahmen des Buchs sprengen. Weitere diesbezügliche Informationen erhalten Sie in Fachbüchern zum Thema Visual Basic und auf den entsprechenden Internetseiten.
16 Sicherheit von Access-Datenbanken Wenn bei Access-Datenbanken von »Schutz« oder »Sicherheit« die Rede ist, kann dies ganz unterschiedliche Gründe haben. Der Entwickler beispielsweise möchte seinen in langen Nächten produzierten Code vor den Blicken des Kunden verbergen, um sich Exklusivrechte an eventuellen Erweiterungen zu sichern, der Lottospieler schützt die Datenbank zum Ermitteln des sicheren Sechsers per Kennwort vor seinen Kollegen und der Geschäftsführer einer Firma möchte selbst auf alle Daten zugreifen, aber den Mitarbeitern nur die jeweils relevanten Daten zeigen. Die Ihren Bedürfnissen entsprechende Lösung finden Sie in den nächsten Abschnitten, die sich folgenden Aufgabenstellungen widmen: Verbergen des Codes durch Umwandlung in eine .mde-Datenbank Schützen des Codes durch ein Kennwort Schützen der Datenbank durch Aktivieren des Kennwortschutzes Verschlüsseln einer Datenbank Anwenden des Sicherheitssystems von Access und Jet Die Beispieldateien zu diesem Kapitel finden Sie auf der Buch-CD im Verzeichnis Kap_16. Die Benutzerdaten für die mit dem Sicherheitssystem geschützte Datenbankdatei Nordwind_Geschuetzt.mdb lauten NordwindAdmin (Benutzername) und entwicklerbuch (Kennwort). Die zugehörige .mdw-Datei heißt Nordwind.mdw.
16.1 Code schützen per .mde-Datenbank Wer viel Zeit und Hirnschmalz in die Entwicklung einer Anwendung gesteckt hat, möchte diese möglicherweise nicht mit frei zugänglichem Quellcode weitergeben. Access bietet hier einen sehr zuverlässigen Schutz: Dabei wandeln Sie die Datenbank einfach in eine so genannte .mde-Datei um und verhindern so den Zugriff auf den in Formular-, Berichts-, Klassen- und Standardmodulen enthaltenen Quellcode.
732
16
Sicherheit von Access-Datenbanken
Die Umwandlung ist extrem einfach: Sie rufen einfach nur den Menüeintrag Extras/ Datenbank-Dienstprogramme/MDE-Datei erstellen… auf und geben dann den Namen der zu erstellenden .mde-Datei ein. Einen kleinen Haken hat die Sache allerdings, wenn Sie mit Access 2003 arbeiten, aber als Standarddateiformat Access 2000 eingestellt haben, um Anwendungen zu entwickeln, die mit älteren Access-Versionen kompatibel sind. Access 2003 erlaubt nur die Umwandlung von Datenbanken im Access 2003-Format. Wenn Sie die Datenbank im Access 2000-Format weitergeben möchten, benötigen Sie die entsprechende AccessVersion für die Konvertierung in eine .mde-Datenbank. Nach dem Konvertieren ist die Entwurfsansicht von Formularen, Berichten, Seiten, Makros und Modulen gesperrt und außer Makros lässt sich auch keines der genannten Objekte neu anlegen. Lediglich Tabellen und Abfragen entziehen sich der Sperrung der Entwurfsansicht – sie sind offen zugänglich und können auch neu angelegt werden. Um keine falschen Hoffnungen zu wecken, deaktiviert Access auch alle Möglichkeiten zum Kopieren oder Exportieren (siehe Abbildung 16.1).
Abbildung 16.1: In .mde-Datenbanken sind die Änderungsmöglichkeiten einiger Objekte stark eingeschränkt.
Code schützen per Kennwort
733
Löschen Sie nach der Erstellung der .mde-Datenbank auf gar keinen Fall die Originaldatei. Die .mde-Datenbank bietet keine Möglichkeit zur Rückumwandlung in eine normale Datenbankdatei. Wenn Sie nur Teile Ihres Codes schützen möchten – etwa die Sammlung Standardfunktionen, die Sie im Laufe der Jahre zusammengetragen haben –, gibt es eine verfeinerte Variante: Speichern Sie die relevanten Objekte in einer separaten Datenbank und binden Sie diese per Verweis in die eigentliche Datenbank ein. Natürlich müssen Sie die separate Datenbank zuvor in eine .mde-Datenbank umwandeln. Auf diese Weise kann der Benutzer zumindest die anwendungsspezifischen Objekte wie Formulare und Berichte bearbeiten beziehungsweise eigene Objekte hinzufügen.
16.2 Code schützen per Kennwort Wenn Sie das VBA-Projekt nicht direkt als .mde-Datei weitergeben wollen, sondern die Möglichkeit offen halten möchten, noch einmal Änderungen daran durchzuführen, können Sie dem VBA-Projekt und den enthaltenen Modulen auch ein Kennwort zuweisen. Dazu öffnen Sie den Dialog – Projekteigenschaften mit dem Menüeintrag Extras/Eigenschaften von …, wobei Sie für jeweils den entsprechenden Ausdruck eintragen. Im Dialog wechseln Sie zur Registerseite Schutz, haken das Kontrollkästchen Projekt für die Anzeige sperren an und geben zweimal das gleiche Kennwort ein (siehe Abbildung 16.2). Nach dem nächsten Öffnen von Access müssen Sie das Kennwort eingeben, bevor Sie auf die enthaltenen Module zugreifen können.
16.3 Einfacher Kennwortschutz Für den schnellen Rundumschutz bietet Access einen einfachen Kennwortschutz. Damit verhindern Sie das Öffnen einer Datenbank durch Unbefugte. Um ein Kennwort für eine Datenbank anzulegen, verwenden Sie den Menüeintrag Extras/Sicherheit/ Datenbankkennwort. Im nun erscheinenden Dialog geben Sie das Kennwort zweimal ein (siehe Abbildung 16.3). Beim nächsten Öffnen der Datenbank erscheint ein Dialog, der den Benutzer zur Eingabe des Datenbankkennworts auffordert und bei Eingabe eines falschen Kennworts den Zugang verwehrt.
734
16
Sicherheit von Access-Datenbanken
Abbildung 16.2: Dialog zum Anlegen eines Kennworts für ein VBA-Projekt
Abbildung 16.3: Zuweisen eines Datenbankkennworts
Das Zuweisen eines Datenbankkennwortes erfordert, dass die Datenbank im ExklusivModus geöffnet ist. Der durch das Datenbankkennwort gewährte Schutz ist nicht besonders zuverlässig.
16.4 Verschlüsseln einer Datenbank Findige Menschen können Hex-Editoren und ähnliche Werkzeuge verwenden, um sich Informationen aus geschützten Datenbanken zu verschaffen. Auch hier gibt es ein Mittel: Sie können eine Datenbankdatei verschlüsseln, um den externen Zugriff auf die gespeicherten Daten zu verhindern. Dazu verwenden Sie den Menüeintrag Extras/ Sicherheit/Datenbank ver-/entschlüsseln. Zusätzlich müssen Sie die Daten mit einem Kennwort oder mit dem Sicherheitssystem von Jet schützen. Der Grund ist, dass jeder, der vollen Zugriff auf die Datenbank hat, diese leicht per Menübefehl wieder entschlüsseln kann.
Das Sicherheitssystem von Access
735
16.5 Das Sicherheitssystem von Access Wer mehr möchte, als einfach nur den Zugriff auf Daten zu verhindern – etwa um verschiedenen Benutzern und Benutzergruppen unterschiedliche Berechtigungen für die einzelnen Datenbankobjekte und die enthaltenen Daten zu gewähren, muss das Sicherheitssystem von Access verwenden.
16.5.1 Leistungen des Sicherheitssystems Das Sicherheitssystem bietet eine skalierbare Zuweisung von Zugriffsrechten auf Objektebene. Sie können Benutzer und Benutzergruppen einrichten, die unterschiedliche Zugriffsrechte auf Objekte wie Tabellen, Abfragen, Formulare, Berichte und Module erhalten. Die Zuteilung der Berechtigungen für einen Benutzer kann über das Benutzerkonto oder über die jeweiligen Benutzergruppen erfolgen, denen der Benutzer angehört.
Priorität der Berechtigungen Ein Benutzer kann verschiedene Berechtigungen für ein und dasselbe Objekt erhalten. Er könnte als Benutzer Müller nur lesende Berechtigungen für eine Tabelle tblMitarbeiter besitzen, als Mitglied der Gruppe Personalabteilung schreibende Berechtigungen und als Mitglied der Gruppe Administratoren komplette Zugriffsrechte inklusive Vollzugriff auf den Tabellenentwurf. Beachten Sie unbedingt, dass der Benutzer immer die jeweils höchsten Zugriffsrechte erhält, die in seinem eigenen Konto beziehungsweise in seinen Gruppenkonten festgelegt sind!
Vorteil durch benutzergruppen-orientierte Rechtevergabe Wegen dieser unter Umständen unübersichtlichen Situation sollten Sie zumindest dem individuellen Benutzerkonto jegliche Rechte entziehen. Legen Sie die Berechtigungen, die Sie an einzelne Benutzer zu vergeben gedenken, für unterschiedliche Gruppenkonten fest und weisen Sie die Benutzer den entsprechenden Konten zu. Die Benutzer »erben« dann die Berechtigungen der Gruppenkonten.
Aufteilung der Sicherheitsinformationen Die für die Sicherheit einer Datenbank notwendigen Informationen sind auf zwei Schultern verteilt. Die Arbeitsgruppen-Informationsdatei enthält Informationen über Benutzer und Benutzergruppen. Die Berechtigungen haben ihren Platz in der Datenbankdatei selbst.
736
16
Sicherheit von Access-Datenbanken
16.5.2 Die Arbeitsgruppen-Informationsdatei Der Name der Arbeitsgruppen-Informationsdatei endet mit .mdw. Jede Installation von Access erstellt beim ersten Öffnen von Access eine frische Arbeitsgruppen-Informationsdatei namens SYSTEM.MDW, die zwei vordefinierte Benutzergruppen und ein Benutzerkonto mitbringt, damit sich die Access-Datenbanken ohne Probleme öffnen lassen – mehr dazu in Abschnitt 16.5.3, »Aktivieren des Sicherheitssystem«. Der Arbeitsgruppen-Informationsdatei können Sie beliebig viele Benutzerkonten und Benutzergruppen hinzufügen. Access bietet dazu ausreichend komfortable Dialoge an. Darüber hinaus lässt sich die Verwaltung von Benutzerkonten und Benutzergruppen auch mit VBA (DAO und ADO) sowie mit SQL lösen. Diese Möglichkeiten werden in diesem Buch allerdings nicht besprochen.
Welche Arbeitsgruppen-Informationsdatei ist meiner Datenbank zugeordnet? Die Beantwortung dieser Frage hilft, viele Probleme und Unsicherheiten zu vermeiden. Die Arbeitsgruppen-Informationsdatei SYSTEM.MDW ist zu Beginn als Standarddatei eingestellt. Neue Access-Datenbanken werden immer mit der als Standard eingestellten Arbeitsgruppen-Informationsdatei verknüpft. Wenn Sie nicht sicher sind, welche die aktuelle Arbeitsgruppen-Informationsdatei ist, können Sie den Arbeitsgruppenadministrator verwenden, den Sie mit dem Menübefehl Extras/Sicherheit/Arbeitsgruppenadministrator… öffnen. Mit Access-Versionen älter als Access 2002 konnte man den Arbeitsgruppenadministrator noch nicht per Menübefehl öffnen. Statt dessen stand dort die Datei wrkgadm.exe zum manuellen Aufruf bereit.
Der Arbeitsgruppenadministrator Abbildung 16.4 zeigt den Arbeitsgruppenadministrator mit seinen wenigen Funktionen. Damit lassen sich jedoch alle wichtigen Aufgaben erledigen. Der Arbeitsgruppenadministrator zeigt die aktuelle Arbeitsgruppen-Informationsdatei direkt auf dem Hauptdialog an und bietet die Möglichkeit, eine neue Arbeitsgruppen-Informationsdatei zu erstellen oder eine bestehende Datei zu verwenden.
Erstellen einer neuen .mdw-Datei Zum Erstellen einer neuen Arbeitsgruppen-Informationsdatei klicken Sie auf die Schaltfläche Erstellen… und geben die im folgenden Dialog geforderten Informationen an. Sie sollten sich die hier eingetragenen Informationen notieren, da Sie diese später nicht mehr abrufen können. Besonders wichtig ist hier die Arbeitsgruppen-ID, die Jet zum Verschlüsseln der Kennwörter verwendet (siehe Abbildung 16.5).
Das Sicherheitssystem von Access
737
Abbildung 16.4: Der Arbeitsgruppenadministrator
Sie benötigen diese Informationen möglicherweise noch einmal – etwa wenn Sie die Arbeitsgruppen-Informationsdatei löschen und diese rekonstruieren möchten. Dazu sind nicht nur die hier gespeicherten Daten, sondern auch die Informationen, die Sie beim Anlegen der Benutzerkonten und der Benutzergruppen angegeben haben, erforderlich. Mehr dazu erfahren Sie in Abschnitt 16.5.4, »Rekonstruieren einer .mdwDatei«.
Abbildung 16.5: Anlegen einer neuen Arbeitsgruppen-Informationsdatei
Verwenden einer bestehenden .mdw-Datei als Standard-ArbeitsgruppenInformationsdatei Wenn Sie eine andere .mdw-Datei als die automatisch erzeugte SYSTEM.MDW festlegen und dem Administrator-Konto ein Kennwort zuweisen (was dies bewirkt, erfahren Sie in Abschnitt 16.5.3, »Aktivieren des Sicherheitssystems«), werden Sie künftig schneller als erwartet wieder zu der gewohnten .mdw-Datei zurückkehren wollen: Von nun an werden Sie nämlich beim Öffnen jeder beliebigen Datenbank mit dem
738
16
Sicherheit von Access-Datenbanken
Anmeldedialog begrüßt. Und das Beste ist: Wenn Sie dabei eine andere Datenbank öffnen möchten als die, mit der Sie die Benutzerkonten und Benutzergruppen angelegt haben, kommen Sie in diese Datenbanken gar nicht hinein, denn dort sind in der Regel keine Rechte für den entsprechenden Benutzer definiert. Zum Glück ist der Arbeitsgruppenadministrator auch ohne geöffnete Datenbank von Access aus erreichbar und so werden Sie sich mit Hilfe des Dialogs aus Abbildung 16.6 wieder der Standarddatei SYSTEM.MDW zuwenden.
Abbildung 16.6: Auswählen einer alternativen .mdw-Datei
Wie Sie sich auch entscheiden, beim nächsten Start einer Datenbank in Access wird automatisch die auf dem Startdialog des Arbeitsgruppenadministrators angezeigte Arbeitsgruppen-Informationsdatei verwendet – zumindest wenn Sie keine anderweitigen Maßnahmen treffen.
Manuelles Festlegen der .mdw-Datei beim Öffnen einer Datenbank per Verknüpfung Wie nun soll man eine geschützte Datenbank mit der passenden .mdw-Datei öffnen und alle anderen mit der Datei SYSTEM.MDW? Ganz einfach: Sie verwenden die SYSTEM.MDW als Standard-Arbeitsgruppen-Informationsdatei und geben spezielle .mdw-Dateien explizit beim Aufruf der jeweiligen Datenbank an. Dazu können Sie je Datenbankdatei eine Verknüpfung einrichten. Klicken Sie dazu im Explorer mit der rechten Maustaste auf die zu schützende .mdbDatei und wählen Sie im Kontextmenü den Eintrag Verknüpfung erstellen. Das Ziel der Verknüpfung sieht etwa folgendermaßen aus: "C:\Programme\Microsoft Office\OFFICE11\MSACCESS.EXE" "c:\Datenbanken\Beispiel.mdb" /wrkgrp "c:\Datenbanken\Beispiel.mdw"
Dabei geben Sie als Erstes die Anwendung (Access), dann die zu öffnende Datenbankdatei (Beispiel.mdb) und zuletzt den Parameter /wrkgrp mit der zu verwendenden .mdw-Datei (Beispiel.mdw) an. Natürlich müssen Sie auch noch die Pfadangaben anpassen.
Das Sicherheitssystem von Access
739
Hinweis: Setzen Sie sicherheitshalber alle Pfadangaben in Anführungszeichen, damit Windows mit eventuellen Leerzeichen darin klarkommt. Beim Öffnen einer Datenbank mit dieser Anweisung erscheint – soweit das Administrator-Konto der .mdw-Datei mit einem Kennwort versehen ist – der Anmeldedialog mit dem Standardbenutzer Administrator. Wenn der Dialog standardmäßig mit einem anderen Benutzernamen angezeigt werden soll, verwenden Sie in der Verknüpfung zusätzlich den folgenden Parameter: /user
Und für Tippfaule geeignet, aber sehr unsicher ist die folgende Variante. Mit diesem Anhängsel wird der Anmeldedialog übersprungen und direkt die Datenbank mit den angegebenen Benutzerdaten geöffnet: /pwd
Dummerweise ist das Kennwort so für jeden, der auf die Verknüpfungs-Datei zugreifen kann, leicht einsehbar.
16.5.3 Aktivieren des Sicherheitssystems Nachdem Sie nun eine Menge über die .mdw-Datei und den Arbeitsgruppenadministrator erfahren haben, finden Sie in den nächsten Abschnitten alle notwendigen Informationen zur eigentlichen Anwendung des Sicherheitssystems. Das Sicherheitssystem brauchen Sie nicht zu aktivieren, da es bei jeder Datenbank unbemerkt im Hintergrund mitläuft. Unbemerkt deshalb, weil es bei jedem Start einer Access-Datenbank zunächst versucht, den aktuellen Benutzer als Admin mit leerem Kennwort anzumelden, und dies bei der Verwendung einer jungfräulichen Arbeitsgruppen-Informationsdatei auch funktioniert. Statt von Aktivierung sollte man also eher von »Scharfschalten« sprechen. Wie Sie weiter oben bereits gelesen haben, sollten Sie die Datei SYSTEM.MDW als Standard-Arbeitsgruppen-Informationsdatei beibehalten, um jederzeit problemlos auf ungeschützte Datenbanken zugreifen zu können. Für Beispielzwecke werden Sie also gleich eine neue .mdw-Datei erstellen. Als Beispieldatenbank mit allen notwendigen Objekten ist die Nordwind-Datenbank immer gut zu gebrauchen. Erstellen Sie eine Kopie dieser Datenbank, starten Sie diese und legen Sie anschließend mit dem Anlegen einer neuen Arbeitsgruppen-Informationsdatei los:
740
16
Sicherheit von Access-Datenbanken
1. Starten Sie den Arbeitsgruppenadministrator über den Menübefehl Extras/Sicherheit/Arbeitsgruppenadministrator… 2. Füllen Sie die Felder Name, Firma und Arbeitsgruppen-ID aus (nur Letzteres ist ein Pflichtfeld mit vier bis zwanzig Buchstaben oder Zahlen). 3. Legen Sie den Dateinamen für die neue .mdw-Datei fest. Anschließend erhalten Sie eine Bestätigung mit den wichtigsten Daten. Hier bietet sich im Übrigen die letzte Gelegenheit, die Daten nochmals zu ändern oder zu notieren (siehe Abbildung 16.7).
Abbildung 16.7: Informationen zur neuen Arbeitsgruppen-Informationsdatei
Achtung: Die neue .mdw-Datei ist nun als Standard für das aktuelle System festgelegt. Vergessen Sie nicht, diesen Schritt wieder rückgängig zu machen. Warten Sie allerdings damit, bis Sie die Beispieldatenbank abgesichert haben, und legen Sie für Datenbanken, die eine andere .mdw-Datei verwenden, eine Verknüpfung an, die explizit die zu verwendende Datenbank angibt.
Datenbank absichern Nun sorgen Sie dafür, dass nicht – wie bisher – jeder beliebige Benutzer die Datenbank öffnen und direkt mit Administratorrechten ausgestattet darin arbeiten kann. Das ist faktisch der Stand – solange das Administrator-Konto kein Kennwort hat, meldet Access alle Benutzer automatisch als Administrator an. Erst mit Kennwort wird das Sicherheitssystem »scharf geschaltet« – bei jedem Öffnen von Access erscheint dann ein Anmeldedialog. Die folgenden Schritte erläutern, wie Sie vorgehen müssen, um eine Datenbank zu sichern.
Das Sicherheitssystem von Access
741
Kennwort für das Administrator-Konto zuweisen Öffnen Sie den Dialog Benutzer- und Gruppenkonten, indem Sie aus der Menüleiste den Eintrag Extras/Sicherheit/Benutzer- und Gruppenkonten… auswählen. Der Dialog zeigt den Benutzer Administrator an. Dieser hat alle Rechte, also sorgen Sie zunächst dafür, dass sich keiner mehr ohne Kennwort unter diesem Konto anmelden kann. Legen Sie dazu auf der Registerseite Anmeldungskennwort ändern ein neues Kennwort an. Das Feld Altes Kennwort lassen Sie leer (siehe Abbildung 16.8).
Abbildung 16.8: Setzen eines Administrator-Kennworts
Testen Sie die Wirksamkeit, indem Sie die Datenbank schließen und erneut öffnen. Dort erscheint nun der Anmeldedialog des Sicherheitssystems.
Neuen Benutzer als zukünftigen Administrator anlegen Erstellen Sie einen neuen Benutzer, der von nun an als Administrator dient. Öffnen Sie dazu erneut den Dialog Benutzer- und Gruppenkonten und klicken Sie auf der Registerseite Benutzer auf die Schaltfläche Neu. Geben Sie dort einen Namen wie NordwindAdmin ein und füllen Sie das Feld Persönliche ID aus (siehe Abbildung 16.9). Fügen Sie den neuen Benutzer zur Gruppe Administratoren hinzu (siehe Abbildung 16.10). Entfernen Sie außerdem den Benutzer Administrator aus dieser Gruppe.
742
16
Sicherheit von Access-Datenbanken
Abbildung 16.9: Anlegen eines neuen Benutzers
Abbildung 16.10: Hinzufügen des neuen Benutzers zur Gruppe Administratoren
Beenden Sie Access (Schließen der Datenbank reicht nicht aus) und öffnen Sie die Datenbank erneut. Melden Sie sich mit dem neuen Benutzernamen NordwindAdmin an. Beachten Sie: Sie haben diesem Benutzerkonto noch kein Kennwort zugewiesen! Das holen Sie in diesem Schritt nach.
Erste Anmeldung als neuer Administrator Dazu rufen Sie erneut den Dialog Benutzer- und Gruppenkonten auf und wechseln direkt zur Registerseite Anmeldungskennwort ändern. Dort wird automatisch der aktuell angemeldete Benutzer angezeigt. Fügen Sie dem Benutzer NordwindAdmin dort ein Kennwort hinzu.
Das Sicherheitssystem von Access
743
Neuen Administrator zum Besitzer aller Objekte machen Nun wird es Zeit, den Benutzer NordwindAdmin zum Besitzer der Datenbank und der enthaltenen Objekte zu machen. Schließen Sie die Datenbank, erstellen Sie eine neue, leere Datenbank unter dem Benutzer NordwindAdmin und importieren Sie alle Objekte der Ausgangsdatenbank (Datei/Externe Daten/Importieren…) – siehe Abbildung 16.11.
Abbildung 16.11: Importieren der Datenbankobjekte mit allem Drum und Dran
Rechte aller anderen Benutzergruppen und Benutzer entziehen Im letzten Schritt sorgen Sie für die Alleinherrschaft des neuen Administrators: Entziehen Sie der Benutzergruppe Benutzer alle Rechte. Dieser Schritt ist notwendig, weil jeder neue Benutzer automatisch dieser Gruppe zugeordnet wird. Öffnen Sie den Dialog Benutzer- und Gruppenberechtigungen, wählen Sie dort unter Liste den Eintrag Gruppen aus, markieren Sie in der Liste Benutzer/Gruppenname den Eintrag Benutzer und selektieren Sie alle Einträge des Listenfeldes Objektname. Deaktivieren Sie dann alle Häkchen im Bereich Berechtigungen (siehe Abbildung 16.12). Wenn Sie den Vorgang für alle Einträge des Kombinationsfeldes Objekttyp wiederholt haben, ist die Datenbank endgültig gesichert. Sicherheitshalber sollten Sie auch die Berechtigungen für das Administrator-Konto entziehen – zwar haben Sie dieses Konto auch mit einem Kennwort versehen, doch Kennwörter können geknackt werden. Sie können nun noch die Berechtigungen für die Gruppe Administratoren vergeben, der Jetzt nur noch der aktuelle Benutzer NordwindAdmin angehört. Dies können Sie aber auch zu einem späteren Zeitpunkt erledigen – die Rechte dazu kann Ihnen nun niemand mehr entziehen.
744
16
Sicherheit von Access-Datenbanken
Abbildung 16.12: Entfernen sämtlicher Berechtigungen der Gruppe »Benutzer«
Die Datenbank ist nun geschützt. Noch einmal die wichtigsten Punkte: Der Benutzer Administrator gehört nicht mehr zur Gruppe Administratoren. Der Benutzer Administrator hat ein Kennwort. Der Benutzer Administrator hat keine Rechte mehr. Die Gruppe Benutzer hat keinerlei Zugriffsrechte mehr. Das einzige Mitglied der Administratoren-Gruppe ist der Benutzer NordwindAdmin, welcher gleichzeitig Besitzer aller Datenbank-Objekte ist. Wenn Sie nun wieder die Datei SYSTEM.MDW als Standard-Benutzergruppen-Informationsdatei festlegen, können Sie die Datenbank Nordwind_Geschuetzt.mdb nicht mehr öffnen, ohne die .mdw-Datei Nordwind.mdw explizit anzugeben. Wenn Sie so vorgehen, versucht Access die Datei mit dem Standardbenutzer Administrator zu öffnen, der in der .mdw-Datei SYSTEM.MDW auch kein Kennwort hat, zu öffnen. Da dieser aber in der Datenbank Nordwind_Geschuetzt.mdb gar keine Rechte mehr hat – auch nicht die zum Öffnen der Datenbank – geht das natürlich schief ... allerdings anders, als erwartet. Abbildung 16.13 zeigt die Meldung, die Access bei diesem Anmeldeversuch seltsamerweise ausgibt.
Sichern von Access-Datenbanken per Assistent
745
Abbildung 16.13: Diese Meldung erscheint beim Öffnen einer Datenbank mit einem Benutzer ohne jegliche Rechte.
16.5.4 Rekonstruieren einer .mdw-Datei Weiter oben wurde verschiedentlich darauf hingewiesen, dass Sie sich die beim Anlegen der .mdw-Datei, der Benutzerkonten und der Benutzergruppen verwendeten Informationen notieren sollen. Damit schaffen Sie sich eine Rückversicherung, falls die .mdw-Datei einmal gelöscht oder zerstört werden sollte. Um die .mdw-Datei und die enthaltenen Informationen zu rekonstruieren, legen Sie einfach alle Elemente mit genau den gleichen Angaben wie beim Einrichten der vorherigen .mdw-Datei an.
16.6 Sichern von Access-Datenbanken per Assistent Am leichtesten schützen Sie eine Access-Datenbank mit dem Datensicherheits-Assistenten, den Sie mit dem Menübefehl Extras/Sicherheit/Benutzerdatensicherheits-Assistent aufrufen. Der Assistent unterstützt Sie bei allen Schritten, die Sie zuvor selbst durchgeführt haben. Dennoch ist es wichtig, dass Sie wissen, wie Sie das Sicherheitssystem manuell »scharf schalten« können. Der Assistent hilft Ihnen zwar beim Einrichten des Sicherheitssystems, aber er kann Sie nicht vor Fehlern bei der weiteren Pflege bewahren. Der erste Dialog des Assistenten bietet zwei Optionen an, von denen eine deaktiviert ist – hier haben Sie leichtes Spiel. Im zweiten Dialog geben Sie Informationen zu der zu erstellenden Arbeitsgruppen-Informationsdatei an (siehe Abbildung 16.14). Wichtig ist hier die Angabe der Arbeitsgruppen-ID (AID), die der Assistent mit einem gut zu merkenden Wert vorbelegt. Zu merken brauchen Sie sich die Werte beim Assistenten zum Glück nicht – er öffnet im letzten Schritt einen Bericht mit allen Informationen wie Arbeitsgruppen-ID, Gruppen-IDs und persönlichen IDs sowie den übrigen Daten. In diesem Dialog entscheiden Sie außerdem, ob die neue Arbeitsgruppen-Informationsdatei auch für zukünftige neue Datenbanken verwendet werden soll oder nur für die aktuelle – im letzteren Fall erstellt der Assistent automatisch eine Verknüpfung für die Datenbank auf dem Desktop, die einen Verweis auf die zu benutzende Arbeitsgruppen-Informationsdatei enthält – ein weiterer Pluspunkt für den Assistenten.
746
16
Sicherheit von Access-Datenbanken
Abbildung 16.14: Anlegen einer neuen Arbeitsgruppen-Informationsdatei
Folgt Schritt Nummer Drei: Hier wählen Sie alle Objekte aus, die gesichert werden sollen – das sind standardmäßig alle (siehe Abbildung 16.15). Sie können also auch die Einstellungen dieses Dialogs übernehmen und zum nächsten Schritt übergehen.
Abbildung 16.15: Auswählen der zu schützenden Datenbankobjekte
Sichern von Access-Datenbanken per Assistent
747
Im nächsten Schritt bringt der Assistent wiederum eine interessante Arbeitserleichterung: Er bietet einige vordefinierte Benutzergruppen mit recht sinnvollen Zusammenstellungen der Berechtigungen an (siehe Abbildung 16.16). Klicken Sie auf einen der Einträge des Listenfeldes, um eine Beschreibung der Gruppenberechtigungen zu lesen, und setzen Sie einen Haken an die Gruppen, die Sie für Ihre Datenbankanwendungen übernehmen möchten. Leider bietet der Dialog keine Möglichkeiten, eigene Benutzergruppen anzulegen oder die Namen oder Berechtigungen der vordefinierten Benutzergruppen zu ändern. Wenn Sie weitere Benutzergruppen anlegen möchten, müssen Sie dies im Anschluss an die Verwendung des Assistenten erledigen.
Abbildung 16.16: Der Datensicherheits-Assistent bietet Standardgruppen zur Übernahme in die ArbeitsgruppenInformationsdatei an.
Als Nächstes folgt ein Schritt, den Sie auch beim manuellen Schützen der Datenbank durchgeführt haben: Sie entziehen der Gruppe Benutzer, der automatisch alle Benutzer angehören, jegliche Berechtigungen (siehe Abbildung 16.17). Der Assistent bietet hier zwar auch die Möglichkeit, dieser Benutzergruppe dennoch Rechte zuzuweisen – das macht aber keinen Sinn, wenn Sie die Datenbank zuverlässig schützen möchten. Das macht der Assistent auch deutlich – er weist sehr gewissenhaft darauf hin, dass jeder Benutzer die hier zugewiesenen Berechtigungen erhalten wird (siehe Abbildung 16.18).
748
16
Sicherheit von Access-Datenbanken
Abbildung 16.17: Festlegen von Berechtigungen für die Standardgruppe Benutzer
Abbildung 16.18: Der Assistent passt auf – und Berechtigungen für die Gruppe »Benutzer« sieht er nicht gerne …
Sichern von Access-Datenbanken per Assistent
749
Als Nächstes geht’s an das Anlegen der Benutzer. Der Dialog zeigt eine Liste der vorhandenen Benutzer und bietet die Möglichkeit, weitere Benutzer hinzuzufügen (siehe Abbildung 16.19). Davon sollten Sie auch dringend Gebrauch machen – zumindest wenn Sie nach dem Schließen der Datenbank noch einmal an diese herankommen möchten. Der Assistent entzieht nämlich dem ansonsten einzigen Benutzer namens »Administrator« alle Berechtigungen und weist diesem vor allem ein geheimes Kennwort zu – der Zugang zur Datenbank ist damit verriegelt. Die Daten, die Sie hier für die einzelnen Benutzer eintragen, brauchen Sie sich ebenfalls nicht zu merken – auch diese werden inklusive Kennwörtern im Klartext im abschließenden Bericht ausgegeben.
Abbildung 16.19: Hinzufügen von Benutzern zur Arbeitsgruppen-Informationsdatei
Der nächste Schritt zeigt wieder einmal einen guten Ansatz: Hier legen Sie fest, zu welchen Benutzergruppen jeder einzelne Benutzer gehören soll (siehe Abbildung 16.20). Dazu wählen Sie zunächst den betroffenen Benutzer oder die Gruppe aus dem Kombinationsfeld aus und weisen dann im unteren Listenfeld die Gruppen beziehungsweise Benutzer zu. Positiv ist, dass die Gruppe Benutzer hier gar nicht angeboten wird, um keine unnötigen Sicherheitslücken heraufzubeschwören. Außerdem passt der Assistent wieder auf: Sie können zwar alle Benutzer aus der Gruppe Administratoren entfernen, aber beim Übernehmen der Einstellungen zeigt der Assistent bei leerer AdministratorenGruppe eine entsprechende Meldung an (siehe Abbildung 16.21).
750
16
Sicherheit von Access-Datenbanken
Abbildung 16.20: Zuordnen von Benutzern zu Benutzergruppen und umgekehrt
Abbildung 16.21: Der Assistent lässt keine leere Administratoren-Gruppe zu.
Schließlich kommt der letzte Dialog des Assistenten: Hier geben Sie noch an, unter welchem Dateinamen die Sicherheitskopie der zu schützenden Datenbank gespeichert werden soll (siehe Abbildung 16.22). Nach Betätigen der Schaltfläche Fertig stellen erscheinen der Bericht mit den Informationen über die angelegte .mdw-Datei sowie die Gruppen und Benutzer. An dieser Stelle sollten Sie den Bericht entweder ausdrucken oder über den Eintrag Exportieren des Kontextmenüs im Snapshot-Format (.snp) exportieren. Der Assistent bietet zwar im nächsten Schritt die (vermeintliche) Möglichkeit an, den Bericht auf diese Weise zu speichern (siehe Abbildung 16.23). Die Ernüchterung folgt aber auf dem Fuße, denn laut anschließend angezeigtem Dialog ist dieses Format nicht verfügbar (siehe Abbildung 16.24).
Sichern von Access-Datenbanken per Assistent
751
Abbildung 16.22: Backup einer ungesicherten Version der Datenbank
Abbildung 16.23: Der Datensicherheitsassistent bietet zwar die Möglichkeit des Exports der Zusammenfassung an …
Abbildung 16.24: … zeigt aber dann eine lange Nase.
17 Installation, Betrieb und Wartung Mit der Fertigstellung ist die Arbeit an einer Access-Anwendung noch lange nicht beendet. Die Anwendung muss installiert, komprimiert oder gewartet werden, wobei gerade der erste Schritt manche Schikane birgt. Soll die Anwendung im Mehrbenutzerbetrieb als Einzelplatzlösung eingesetzt werden oder kommt vielleicht sogar Replikation zum Einsatz? Wie sorgen Sie während des Betriebs für das Erstellen von Backups und was hat es mit den oft erwähnten Problemen mit den Verweisen auf sich? Antworten auf diese und weitere Fragen finden Sie in diesem Kapitel. Die Beispiele zu den einzelnen Abschnitten dieses Kapitels sind auf mehrere Datenbankdateien auf der Buch-CD verteilt. Wenn keine andere Datei angegeben ist, finden Sie die Objekte und Quellcodes unter Kap_17\InstallationBetriebWartung.mdb.
17.1 Verschiedene Access-Versionen auf demselben Rechner Wenn Sie mehrere Access-Versionen auf demselben Rechner verwenden müssen, sollten Sie einige Punkte bei der Installation beachten. Die beiden wichtigsten sind, dass Sie immer zuerst die ältere Version von Access installieren (das gilt insbesondere, wenn es sich dabei um Access 97 handelt). Außerdem geben Sie bei der Installation der jüngeren Version an, dass Sie diese erstens in ein eigenes Verzeichnis installieren und dass Sie die ältere Version nicht durch die neue Version aktualisieren möchten. In der Regel liegt obige Situation vor, wenn Sie zu einer der Versionen Access 2000, Access 2002 oder Access 2003 zusätzlich noch Access 97 benötigen. Ob Sie aber gegebenenfalls zwei Access-Versionen jünger als Access 97 benötigen, ist fraglich, denn sowohl mit Access 2002 als auch mit Access 2003 lassen sich Datenbanken im Format Access 2000 ohne Probleme öffnen und auch bearbeiten. Wenn Sie keine spezifischen Funktionen der neueren Version verwenden, können Sie die Datenbanken jederzeit wieder mit Access 2000 öffnen und auch bearbeiten.
754
17
Installation, Betrieb und Wartung
Starten von Access-Datenbanken unterschiedlicher Versionen Wenn Sie beispielsweise Access 97 und Access 2003 auf einem Rechner installiert haben, gibt es beim Aufruf von Datenbankdateien per Doppelklick manchmal das Problem, dass die falsche Version von Access geöffnet wird. Das liegt daran, dass die aktuell verwendete Access-Version sich beim Öffnen einer Datenbankdatei als Verknüpfung für alle Dateien mit den Endungen .mdb, .mde und so weiter einträgt. Und die dort eingestellte Version wird auch zukünftig beim Doppelklick auf eine AccessDatenbank gestartet. So kann es vorkommen, dass Sie eigentlich eine Access 2003Datenbank öffnen möchten und sich dann wundern, dass Access 97 auf den Plan tritt und Probleme mit dem Datenbankformat hervorruft. Meist geht man dann dazu über, erst die richtige Anwendung zu starten und dann die gewünschte Datenbank zu öffnen. Das ist aber eigentlich ein Schritt mehr als notwendig. Einfacher geht es, wenn Sie dem Kontextmenü der Dateien im Windows Explorer zwei zusätzliche Einträge zum Öffnen der Datenbanken mit der richtigen Anwendung hinzufügen. Das erledigen Sie durch einige neue Einträge in der Registry. Zur Vereinfachung schreiben Sie diese in eine Textdatei, speichern sie unter dem Namen Access.reg und rufen sie per Doppelklick auf: REGEDIT4 [HKEY_CLASSES_ROOT\Access.Application.11\shell\Starten mit Access 97] [HKEY_CLASSES_ROOT\Access.Application.11\shell\Starten mit Access 97\command] @="\"c:\\Programme\\Microsoft Office\\Office97\\MSACCESS.EXE\" \"%1\"" [HKEY_CLASSES_ROOT\Access.Application.11\shell\Starten mit Access 2003] [HKEY_CLASSES_ROOT\Access.Application.11\shell\Starten mit Access 2003\command] @="\"c:\\Programme\\Microsoft Office\\Office11\\MSACCESS.EXE\" \"%1\"" Listing 17.1: Registry-Einträge zum Starten von Datenbanken mit verschiedenen Access-Versionen
Die in Tabelle 17.1 aufgeführten Einträge stellen nur einen Teil der benötigten Einträge dar, da diese nur verwendet werden, wenn Access 2003 (»Access.Application.11«) die aktuelle Access-Version ist. Wenn Sie mit dem oben angelegten Menüeintrag eine Datenbank mit Access 97 öffnen, ist Access 97 die aktuelle Access-Version. Damit die Kontextmenü-Einträge aus Abbildung 17.1 nun angezeigt werden, müssen Sie die obigen Einträge auch noch für Access 97 (»Access.Application.8«) anlegen. Mit einem Tool von Sascha Trowitzsch können Sie die passenden Einträge in Abhängigkeit von den auf dem System verfügbaren Access-Versionen automatisch erstellen lassen. Das Tool bietet noch einige andere Funktionen.
Weitergabe von Access-Datenbanken
755
Sie finden die Datei Access.reg auf der Buch-CD unter dem Dateinamen Kap_17\Access.reg. Im gleichen Verzeichnis finden Sie das oben genannte Tool. Die Setup-Datei hat den Dateinamen setup_mdbetool.exe. Im Kontextmenü des Windows Explorers sieht das dann wie in Abbildung 17.1 aus.
Abbildung 17.1: Starten verschiedener Access-Anwendungen per Kontextmenü
17.2 Weitergabe von Access-Datenbanken Nicht jeder Access-Entwickler erstellt mit Access ausschließlich Anwendungen für den Eigenbedarf. Die meisten entwickeln Anwendungen für Kunden oder – wenn sie in einem Unternehmen arbeiten – auch für die eigenen Mitarbeiter. Wenn Sie Glück haben, verfügt der künftige Benutzer der neuen Access-Anwendung über Microsoft Access in der einen oder anderen Version. Wenn nicht, gibt es zwei Möglichkeiten: Entweder der Benutzer legt sich eine Lizenz zu oder Sie erstellen ihm eine Runtime-Version.
17.2.1 Weitergabe mit Runtime Runtime-Versionen einer Access-Anwendung enthalten nicht nur die pure .mdb-Datei plus gegebenenfalls weitere Dateien wie ein Backend oder eine Arbeitsgruppen-Informationsdatei, sondern liefern Access direkt mit. Dabei handelt es sich zwar um eine eingeschränkte Version, mit der man beispielsweise keine Datenbankanwendung administrieren kann, aber immerhin wird der Benutzer ohne weitere Kosten für eine eigene Access-Lizenz in der Lage sein, mit der Access-Anwendung zu arbeiten.
756
17
Installation, Betrieb und Wartung
Das Problem ist nur, dass dem Entwickler in diesem Fall zusätzliche Kosten entstehen, da er zusätzliche Entwicklerwerkzeuge anschaffen muss. Unter Access 2003 heißt das entsprechende Paket »Visual Studio Tools für Microsoft Office System«. Im Gegensatz zu vorherigen Access-Versionen sind die für die Runtime-Version benötigten Daten bereits auf der Office2003-Installations-CD enthalten. Die Entwicklerwerkzeuge liefern nur noch die notwendige Lizenz, um die Runtime-Datenbanken auch legal weitergeben zu können, und einen Verpackungsassistenten, mit dem Sie die benötigten Dateien zu einer Setup-Datei zusammenstellen. Wie bei der Verwendung von verschiedenen Access-Versionen auf demselben Rechner (siehe auch Abschnitt 17.1, »Verschiedene Access-Versionen auf demselben Rechner«) gibt es auch hier eine Menge Vorgänge, die schief laufen können, wenn Sie eine AccessRuntime auf einem Rechner mit einer anderen Access-Version installieren. Deshalb gibt es einige Anbieter, die umfassende konfigurierbare Setups herstellen. Die meisten haben ihren Preis, aber falls Sie regelmäßig Runtimes für Kunden erstellen, sollten Sie die Investition in Erwägung ziehen und die Setups unterschiedlicher Anbieter prüfen. Eine ausführliche Diskussion der Möglichkeiten und Probleme beim Erstellen von Installationsroutinen für die Runtime-Version von Access ist in diesem Buch aus Platzgründen leider nicht möglich. Die folgenden Tipps sollen aber helfen, Probleme mit der Access-Anwendung selbst zu verhindern.
Benutzerdefinierte Menüs In Runtimes gibt es kein Datenbankfenster und auch die Menüs sind stark eingeschränkt. Letzteres liegt in der Natur der Sache, denn die Runtime-Version von Access liefert nur die Funktionen, die zum Laufen der Datenbankanwendung selbst – also der Benutzeroberfläche einschließlich Formularen, Berichten und Menüs – und von VBA notwendig sind. Die wichtigste Konsequenz hieraus ist: Sie müssen selbst Sorge tragen, dass der Benutzer auf irgendeinem Wege die benötigten Formulare und Berichte öffnen beziehungsweise die VBA-Routinen aufrufen kann. Das kann durch ein Formular geschehen, das beim Start der Anwendung angezeigt wird und die möglichen Optionen enthält. Sinnvoller, da ständig sichtbar, sind allerdings benutzerdefinierte Menüleisten zum Aufrufen von Formularen und Berichten und anderen Funktionen wie Drucken oder Beenden der Datenbankanwendung. Weitere Informationen über benutzerdefinierte Menüleisten erhalten Sie in Kapitel 10, »Menüleisten«.
Weitergabe von Access-Datenbanken
757
Fehlerbehandlung Access-Runtimes sind mit einer äußerst sparsamen Fehlerbehandlung ausgestattet. Das bedeutet, dass im Falle eines Fehlers höchstens eine wenig aussagekräftige Fehlermeldung erscheint (»Die Ausführung dieser Anwendung wurde wegen eines Laufzeitfehlers angehalten. Die Anwendung wird beendet.«) und die Anwendung sich daraufhin rasch verabschiedet. Das ist nicht nur für den Benutzer unbefriedigend, sondern auch für den Entwickler: Der Benutzer kann dem Entwickler lediglich mitteilen, an welcher Stelle der Fehler aufgetreten ist und was dazu geführt hat. Es gibt aber weder eine Fehlermeldung noch Informationen über den fehlerhaften Code. Deshalb sollten Sie gerade in Datenbanken, die Sie als Runtime weitergeben, unbedingt eine umfassende Fehlerbehandlung integrieren. Das gilt natürlich auch für »normale« zur Weitergabe bestimmte Datenbankanwendungen, aber hier ist es besonders wichtig. Weitere Informationen zum Thema Fehlerbehandlung finden Sie in Kapitel 11, »Debugging, Fehlerbehandlung und Fehlerdokumentation«.
Runtime-Simulation Wer eine Anwendung zur Weitergabe als Runtime-Version entwickelt, muss nicht jedes Mal eine Runtime-Version erstellen und diese neu installieren, um zu prüfen, ob alles läuft wie geplant. Mit der Befehlszeilenoption /runtime können Sie den Start einer Datenbankanwendung unter Runtime-Bedingungen simulieren: "C:\Programme\Microsoft Office\OFFICE11\MSACCESS.EXE" "C:\Beispieldatenbank.mdb" /runtime
17.2.2 Weitergabe ohne Runtime Die Weitergabe von Datenbankanwendungen an Benutzer, die bereits über die benötigte Access-Version verfügen, ist in den meisten Fällen völlig unproblematisch. Am einfachsten ist dies natürlich, wenn Sie nur eine einzige .mdb-Datei oder .mde-Datei weitergeben müssen – hier spielt es nicht einmal eine Rolle, wo der Benutzer die Anwendung speichert. Einige Anwendungen sind aber etwas aufwändiger: So könnte es beispielsweise sein, dass die Datenbank aus Frontend und Backend besteht und im Frontend noch der neue Standort des Backends angegeben werden muss (weitere Informationen zu Frontend- und Backend-Datenbanken in Abschnitt 17.5). Oder die Datenbank verweist auf Bilddateien oder Ähnliches, wozu ein entsprechender Ordner einzurichten ist. Und bei
758
17
Installation, Betrieb und Wartung
einer Datenbank mit aktiviertem Sicherheitssystem kommt auch noch die .mdw-Datei hinzu. Spätestens in diesem Fall machen Verknüpfungen im Startmenü von Windows oder auf dem Desktop Sinn, die die Anwendung im Kontext der richtigen .mdw-Datei öffnen. Wenn Sie die Installation nicht selbst vor Ort vornehmen, können Sie natürlich eine Dokumentation erstellen, die alle notwendigen Informationen zu den Speicherorten und Verknüpfungen enthält. Das ist aber nicht besonders kundenfreundlich. Alternativ können Sie ein Setup erstellen, das den Benutzer beim Einrichten der Anwendung unterstützt, indem es beispielsweise das Verzeichnis abfragt, in dem die Dateien installiert werden sollen, oder ob im Startmenü oder auf dem Desktop Verknüpfungen anzulegen sind. Für diese Aufgaben gibt es sogar kostenlose Tools wie etwa Inno Setup. Es bietet praktisch alle Möglichkeiten professioneller Setup-Tools. Sie finden das Tool unter folgender Internetadresse: http://www.jrsoftware.org. In Access-Kreisen hat sich Inno Setup einen Namen gemacht, weil es die Erstellung professioneller Runtime-Setups mit Access 97 möglich gemacht hat. Für Access 97 gibt es im Internet ausreichend Beispielskripte. Leider ist die Installation von Runtimes der Access-Versionen 2000, 2002 und 2003 zu komplex geworden, sodass bisher keine als zuverlässig geltenden Skripte zur Erstellung von Runtimes mit neueren Access-Versionen existieren. Auch der Windows Installer ist eine interessante Alternative. Einige Informationen finden Sie unter folgender Internetadresse: http://www.installsite.org.
17.3 Aktionen beim Starten oder Beenden der Datenbank durchführen Einige Aktionen wie etwa Komprimieren der Datenbank, Einbinden von Tabellen aus Backend-Datenbanken oder Sichern einer Datenbank sollten regelmäßig durchgeführt werden. Dazu bieten sich der Start oder das Beenden einer Datenbank an. Während das Ausführen von Code oder das Aufrufen eines Formulars beim Starten einer Datenbank recht einfach zu realisieren sind, ist für Aktionen, die beim Beenden der Datenbankanwendung durchgeführt werden sollen, schon ein kleiner Trick notwendig.
17.3.1 Code beim Starten einer Datenbank ausführen Um eine Routine beim Starten einer Datenbankanwendung auszuführen, benötigen Sie ein Makro namens Autoexec. Ein Makro mit diesem Namen wird von Access automatisch beim Start einer Anwendung ausgeführt – außer natürlich, der Benutzer hält beim Start die Umschalt-Taste gedrückt.
Aktionen beim Starten oder Beenden der Datenbank durchführen
759
Das Autoexec-Makro aus Abbildung 17.2 ruft die eingebaute Funktion Meldung auf. Statt dieser können Sie jede beliebige öffentliche Funktion innerhalb der Datenbank angeben.
Abbildung 17.2: Dieses Autoexec-Makro zeigt beim Starten einer Anwendung ein Meldungsfenster an.
17.3.2 Formular beim Starten einer Datenbank anzeigen Wenn Sie den Benutzer Ihrer Datenbank mit einem Begrüßungsformular beglücken möchten, können Sie beim Öffnen dieses Formulars die beim Start der Anwendung auszuführenden Routinen aufrufen. Dazu fügen Sie die gewünschten Aufrufe einfach der Prozedur hinzu, die durch die Ereigniseigenschaft Beim Anzeigen ausgelöst wird. Dass Access das Formular beim Starten der Anwendung anzeigen soll, teilen Sie ihm in den Start-Optionen von Access mit. Diese können Sie mit dem Menüeintrag Extras/ Start… anzeigen. Hier stellen Sie das Kombinationsfeld mit der Beschriftung Formular/ Seite anzeigen: auf den entsprechenden Wert ein (siehe Abbildung 17.3).
Abbildung 17.3: Einstellen des beim Start von Access anzuzeigenden Startformulars
760
17
Installation, Betrieb und Wartung
17.3.3 Aktion beim Schließen einer Datenbank ausführen Access bietet nur Ereignisse für Formulare, Berichte, Steuerelemente und andere Objekte, aber keine globalen Ereignisse an. Während dieser Mangel sich beim Start einer Datenbank mit den oben genannten und für diesen Zweck vorgesehenen Methoden umgehen lässt, ist für das Ausführen von Aktionen beim Schließen einer Datenbank ein wenig Fantasie gefordert. Der Clou ist, dass Formulare ein Ereignis bieten, das beim Entladen eines Formulars ausgelöst wird, und dass Access alle Formulare, die beim Schließen der Anwendung noch geöffnet sind (ob sichtbar oder unsichtbar) entlädt. Das heißt: Sie brauchen nur dafür zu sorgen, dass die beim Beenden der Datenbank auszuführende Routine in der Ereignisprozedur Beim Entladen eines Formulars enthalten ist und dass dieses Formular beim Schließen der Anwendung sicher noch geöffnet ist. Das lässt sich einfach realisieren: Öffnen Sie beim Starten der Anwendung ein solches Formular und machen Sie es direkt unsichtbar. Sofern der Benutzer dieses Formular nicht explizit öffnet und dann wieder schließt beziehungsweise in der Entwurfsansicht anzeigt, ist das Formular beim Schließen der Anwendung noch geöffnet. Damit der Benutzer keinen Unsinn mit dem benötigten Formular anstellt, zeigen Sie ihm einfach das Datenbankfenster nicht – dies sollte übrigens in professionellen Anwendungen ohnehin nie zu sehen sein. Sie können natürlich auch das Start-Formular für das Auslösen einer Prozedur beim Schließen der Datenbank verwenden. Das Formular aus Abbildung 17.4 ist in der Beispieldatenbank InstallationBetriebWartung.mdb als beim Start anzuzeigendes Formular angegeben.
Abbildung 17.4: Startformular mit Pep
Die Schaltfläche OK löst die folgende Routine aus und macht das Formular damit praktisch unsichtbar: Private Sub cmdOK_Click() Me.Visible = False End Sub Listing 17.2: Hokus Pokus – Formular Verschwindibus
Datenbanken komprimieren und reparieren
761
Alternativ können Sie ein Formular auch direkt unsichtbar öffnen. Dazu verwenden Sie wiederum das Autoexec-Makro und legen dort einen Eintrag mit der Aktion ÖffnenFormular mit dem gewünschten Formularnamen und dem Fenstermodus Ausgeblendet an. Es verharrt dann im unsichtbaren Zustand, bis die Anwendung geschlossen wird. Dies löst nämlich das Ereignis BEIM ENTLADEN des Formulars aus, was in der Ausführung der folgenden Routine resultiert. In diesem Fall zeigt die Routine einfach nur ein Meldungsfenster an; Sie können dort aber beliebige Funktionen etwa zum Erstellen einer Sicherheitskopie anlegen: Private Sub Form_Unload(Cancel As Integer) MsgBox "Das Formular frmStart wird nun entladen." End Sub Listing 17.3: Das Formular wird beim Schließen der Anwendung entladen und das löst diese Prozedur aus.
17.4 Datenbanken komprimieren und reparieren Datenbanken wachsen mit der Zeit – zumindest wenn Sie regelmäßig Daten anlegen. Falls Sie Daten im gleichen Maße löschen, sollte der Umfang der Datenbank eigentlich abnehmen – was aber nicht der Fall ist. Der Grund ist, dass Access gelöschte Daten erst beim Komprimieren einer Datenbank endgültig aus der Datenbank entfernt. Dabei ist mit Komprimieren nicht das Packen in ein anderes Format wie .zip, .tar, .arj oder Ähnliches mit einem externen Komprimierungsprogramm gemeint, sondern die Verwendung der Access-eigenen Funktion. Diese lösen Sie mit dem Menüeintrag Extras/Datenbank-Dienstprogramme/Datenbank komprimieren und reparieren… aus. Das Komprimieren löscht nicht nur alle noch vorhandenen Datenfragmente, sondern initialisiert auch die Autowert-Felder der in der Datenbank enthaltenen Tabellen. Das bedeutet, dass der Zähler beim nächsten Aufruf beim Folgewert des größten enthaltenen Wertes ansetzt. Leere Tabellen, die zwischenzeitlich einmal Daten enthalten haben, werden somit – rein autowert-technisch betrachtet – in einen jungfräulichen Zustand zurückversetzt. Zwischenräume in Tabellen mit bestehenden Daten werden dadurch allerdings nicht gefüllt. Zusätzlich sorgt das Komprimieren für eine Neuberechnung der Statistiken, auf deren Basis die Abfragen optimiert werden. Außerdem werden die Indizes der Tabellen neu geordnet und die Abfragen in den unkompilierten Zustand versetzt – wenn Sie die Datenbank weitergeben und von Beginn an eine gute Abfrageperformance erreichen möchten, sollten Sie alle Abfragen zuvor neu kompilieren. Mehr zu diesem Thema erfahren Sie in Kapitel 12 in Abschnit 12.2.2, »Datenbank mit kompilierten Abfragen ausliefern«.
762
17
Installation, Betrieb und Wartung
Den seit einigen Access-Versionen mit dem Reparieren zusammengefassten Komprimiervorgang können Sie automatisch beim Beenden einer Datenbank durchführen lassen. Dazu aktivieren Sie einfach die Option Beim Schließen komprimieren im OptionenDialog (siehe Abbildung 17.5).
Abbildung 17.5: Option zum automatischen Komprimieren einer Datenbank beim Schließen
17.5 Mehrbenutzerbetrieb mit Access-Datenbanken Der Mehrbenutzerbetrieb von reinen Access-Datenbanken setzt voraus, dass Sie die enthaltenen Daten auf der einen und die Benutzungsoberfläche und die Anwendungslogik auf der anderen Seite in einzelnen Access-Dateien speichern. Das hört sich wilder an, als es ist – selbst wenn Sie nicht die Dienste des Assistenten zur Datenbankaufteilung in Anspruch nehmen, ist eine Datenbank im Nu gegliedert. Sie finden die aufgeteilten Datenbanken auf der Buch-CD unter Nordwind_ Frontend.mdb und Nordwind_Backend.mdb im Verzeichnis Kap_17.
Mehrbenutzerbetrieb mit Access-Datenbanken
763
17.5.1 Aufteilen einer Access-Datenbank Dazu sind nur die folgenden drei Schritte erforderlich: Importieren aller Tabellen in eine neue, leere Datenbank Löschen der Tabellen aus der ursprünglichen Datenbank Exportierte Tabellen als verknüpfte Tabellen einbinden Die daraus entstandenen Datenbanken heißen Frontend und Backend. Das Frontend können Sie beliebig oft kopieren und an alle Benutzer verteilen, die auf die im Backend enthaltenen Daten zugreifen sollen. Im Einzelnen sieht das folgendermaßen aus.
Tabellen in neue Datenbank importieren Zum Importieren der Tabellen in das zukünftige Backend haben Sie zwei Möglichkeiten: Sie können eine neue, leere Datenbank anlegen und dort aus der Menüleiste den Eintrag Datei/Externe Daten/Importieren… auswählen, um die Ausgangsdatenbank auszuwählen und anschließend im Objekte Importieren-Dialog alle zu importierenden Tabellen anzugeben. Die zweite Variante ist ein wenig brachialer: Kopieren Sie einfach die Ausgangsdatenbank und löschen Sie alle Objekte außer den Tabellen.
Tabellen aus der Ausgangsdatenbank löschen Dieser Schritt ist noch einfacher als der vorhergehende und der folgende: Löschen Sie einfach alle Tabellen aus der Ausgangsdatenbank. Vielleicht hakt es hier und da, weil zusätzlich Verknüpfungen gelöscht werden müssen, aber davon sollten Sie sich nicht beirren lassen.
Tabellen als Verknüpfung einbinden Im letzten Schritt rufen Sie in der Ausgangsdatenbank den Menüeintrag Datei/Externe Daten/Tabellen Verknüpfen… auf, wählen die neue Backend-Datenbank aus und erstellen Verknüpfungen zu allen dort enthaltenen Tabellen. Am Beispiel der NordwindDatenbank sieht das Datenbankfenster anschließend wie in Abbildung 17.6 aus.
764
17
Installation, Betrieb und Wartung
Abbildung 17.6: Datenbankfrontend mit verknüpften Tabellen aus dem Backend
17.5.2 Erneutes Einbinden der Tabellen nach Umbenennen oder Verschieben des Backends Solange Sie das Backend immer an der gleichen Stelle aufbewahren, ist das alles auch kein Problem: Der Pfad des Backends ist absolut in der Datenbank gespeichert. Interessant wird es, wenn Sie die Datenbank weitergeben. Wenn beim Endanwender nicht gerade genau die gleichen Rechner- und Verzeichnisnamen wie in der Entwicklungsumgebung vorliegen, sind Änderungen an den Pfaden der eingebundenen Tabellen notwendig. Am besten versorgen Sie die Datenbank mit einer Funktion, die selbst überprüft, ob sich das Backend an der geplanten Stelle befindet, und die ansonsten einen Dateidialog anzeigt, mit dem der Benutzer den neuen Standort des Backends eingeben kann. Im Internet kursieren einige Lösungen, die zwar dynamisch alle Verknüpfungen aktualisieren, aber keine Absicherung dagegen bieten, dass der Benutzer einmal die falsche .mdb-Datei als Ziel der Verknüpfungen auswählt. Infolgedessen werden dort zwar die bestehenden Verknüpfungen gelöscht, aber mangels passender Tabellen im Backend nicht wieder hergestellt. Beim nächsten Versuch fehlt dann zumindest eine Verknüpfung, die bis zum Bemerken des Fehlers gelöscht wurde; das komplette Datenmodell kann dann ohne manuellen Eingriff nicht wieder hergestellt werden. Die folgende Lösung für das automatische Wiedereinbinden von Backend-Tabellen erfordert zwar einmalig die Angabe aller zu verknüpfenden Tabellen, ist aber unempfindlicher gegenüber dem Einbinden falscher Backends.
Mehrbenutzerbetrieb mit Access-Datenbanken
765
Die Routine erwartet zwei Parameter: Den kompletten Pfad inklusive Dateiname der Backend-Datenbank sowie ein Array mit den einzubindenden Tabellen. Sie erfordert außerdem das Vorhandensein der FileDialog-Klasse (mehr Informationen im Anhang zu diesem Buch). Public Function BackendEinbinden(strBackendPfad As String, _ strTabellenliste() As String) As String Dim Dim Dim Dim Dim
lngAnswer As Long db As Database tdf As TableDef objFiledialog As FileDialog strBackendName As String
strBackendName = Mid(strBackendPfad, InStrRev(strBackendPfad, "\") + 1) CheckPath: 'Prüfen, ob angegebenes Backend vorhanden If Dir(strBackendPfad) = "" Then 'Falls nicht gefunden: 'Fragen, ob ein anderes Backend ausgewählt werden soll If MsgBox("Die Backend-Datenbankdatei kann nicht im Pfad '" _ & strBackendPfad & "' gefunden werden. Klicken Sie auf 'Ja', " _ & "um die Datei auszuwählen und auf 'Nein', um die Anwendung " _ & "zu beenden.", vbYesNo) = vbYes Then 'Filedialog-Objekt zum Auswählen des Backend verwenden Set objFiledialog = New FileDialog With objFiledialog 'Einstellungen des Dialogs vornehmen .DefaultDir = strBackendPath .DefaultFileName = strBackendName .DialogTitle = "Datenbankbackend auswählen" 'Dialog anzeigen und ausgewählte Datei in Variable schreiben strBackendPfad = .ShowOpen 'Wenn keine Datei ausgewählt oder Dialog abgebrochen: 'Anwendung beenden. If strBackendPfad = "" Then MsgBox "Sie haben die Aktion abgebrochen. " _ & "Die Anwendung wird geschlossen." DoCmd.Quit End If End With Set db = CurrentDb
766
17
Installation, Betrieb und Wartung
'Prüfen, ob eine vorhandene Datei ausgewählt wurde 'und gegebenenfalls Auswahl neu starten GoTo CheckPath Else DoCmd.Quit End If Else Set db = CurrentDb 'Alle Tabellen durchlaufen... For Each tdf In db.TableDefs '... und mit der übergebenen Tabellenliste abgleichen For i = LBound(strTabellenliste()) To UBound(strTabellenliste()) 'Wo Übereinstimmungen sind: If tdf.Name = strTabellenliste(i) Then 'Versuchen, die Tabelle neu zu verknüpfen 'und gegebenenfalls Fehlerliste aktualisieren tdf.Connect = ";database=" & strBackendPfad On Error Resume Next tdf.RefreshLink If Err.Number > 0 Then strFehler = strFehler & "- Die Tabelle '" _ & strTabellenliste(i) _ & "' konnte nicht eingebunden werden." & vbCrLf End If On Error GoTo 0 End If Next i Next tdf End If 'Falls es Fehler gab: Meldung ausgeben If Len(strFehler) > 0 Then MsgBox "Es sind Fehler beim Einbinden der " _ & "Backend-Datenbank aufgetreten: " & vbCrLf & strFehler, _ vbOKOnly + vbExclamation, "Fehler beim Einbinden" End If End Function Listing 17.4: Routine zum Wiedereinbinden von Tabellen aus einer Backend-Datenbank
Mehrbenutzerbetrieb mit Access-Datenbanken
767
Die Routine wird beispielsweise wie in folgender Prozedur aufgerufen. Die Prozedur erstellt ein Array mit der gewünschten Anzahl Felder und trägt die einzubindenden Tabellen ein. Dann ruft die Prozedur die Routine BackendEinbinden auf. Der Vorteil bei der Übergabe der einzubindenden Tabellen per Parameter ist, dass Sie auch mit Anwendungen arbeiten können, die ihre Tabellen aus mehr als einem Backend beziehen. In diesem Fall rufen Sie die Routine BackendEinbinden einfach mehrmals mit den unterschiedlichen Parametern auf. Die Funktion VerknuepfungenPruefen testet zuvor, ob die Tabellen überhaupt neu eingebunden werden müssen. Den Code dieser Routine finden Sie im nächsten Abschnitt. Public Function BackendEinbindenBeispielaufruf() Dim strTabellen(8) As String strTabellen(1) = "Artikel" strTabellen(2) = "Bestelldetails" strTabellen(3) = "Bestellungen" strTabellen(4) = "Kategorien" strTabellen(5) = "Kunden" strTabellen(6) = "Lieferanten" strTabellen(7) = "Personal" strTabellen(8) = "Versandfirmen" If VerknuepfungenPruefen(strTabellen()) = False Then BackendEinbinden CurrentProject.Path & "\Nordwind_Backend.mdb", _ strTabellen End If End Function Listing 17.5: Beispielaufruf der Funktion zum Einbinden von Tabellen des Backends
Zeitpunkt zum Wiedereinbinden von Tabellen Zwischen zwei Arbeitstagen oder auch zwischen zwei Datenbanksitzungen kann eine Menge passieren: Ein Benutzer löscht zwischendurch die Backend-Datenbank, benennt sie um oder entschließt sich, die Verzeichnisstruktur auf dem Server ein wenig zu überarbeiten. Daher ist es sinnvoll, die Routine BackendEinbinden regelmäßig auszuführen – am besten bei jedem Start der Anwendung. Wie Sie einen geeigneten Aufruf der Prozedur beim Start ausführen können, erfahren Sie in Abschnitt 17.3, »Aktionen beim Starten oder Beenden der Datenbank durchführen«. Sie sollten in diesem Fall allerdings zuvor prüfen, ob die Verknüpfungen nicht bereits funktionieren – bei größeren Anwendungen mit vielen Tabellen würde das ständige Neuverknüpfen beim Start sonst zu lange dauern. Das erledigen Sie mit der folgenden kleinen Prozedur:
768
17
Installation, Betrieb und Wartung
Public Function VerknuepfungenPruefen(strTabellen() As String) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb On Error Resume Next For i = LBound(strTabellen()) To UBound(strTabellen()) Set rst = db.OpenRecordset(strTabellen(i)) Next i If Err.Number = 3024 Then VerknuepfungenPruefen = False End If End Function Listing 17.6: Prüfen, ob alle verknüpften Tabellen vorhanden sind
17.6 Replikation von Datenbanken Eine Alternative zum Mehrbenutzerbetrieb mit aufgeteilten Datenbanken ist die Replikation. Diese Variante dient dazu, mehrere Replikate einer Datenbank zu erstellen, in denen verschiedene Benutzer Daten bearbeiten können, und die geänderten Daten der verschiedenen Replikate anschließend durch eine so genannte Synchronisation wieder zusammenzuführen. Was hat dies für einen Sinn, wenn doch über ein Netzwerk ohnehin mehrere Benutzer gleichzeitig auf die Datenbasis zugreifen können? Man muss einfach davon ausgehen, dass es trotz steigender Mobilität auch bei Computern und Anwendungen Grenzen gibt. Da ist auf der einen Seite die Technik, die zwar eine ständige Verbindung zum Datenbankserver möglich macht, aber noch zu unsicher ist – was erstens Sicherheitsaspekte und zweitens die notwendige Qualität der Verbindung angeht – und auf der anderen Seite die Aufgabenstellung selbst. Mobile Verbindungen vom Außendienstler zur Zentrale zur Übertragung von Kundendaten, Bestellungen und dergleichen bringen eventuell nur ein paar Stunden Zeitgewinn. Dagegen ist es sicher sinnvoll, dass der Kellner in einem Groß-Restaurant mehrere Bestellungen in sein mobiles Endgerät eintippt und per Funk zur Küche überträgt, damit die bestellten Menüs dort zum Abholen bereitstehen. Aber ob der Versicherungsvertreter den Antrag seines neuen Kunden umgehend zur Bearbeitung zur Zentrale sendet oder dies einige Stunden später erledigt, macht sicher keinen großen Unterschied in der gesamten Bearbeitungszeit. Für den genannten Versicherungsvertreter ist es eher interessant, abends in der Geschäftsstelle die Daten auf seinem Notebook mit dem Hauptrechner abzugleichen. Und hierbei kann – wenn eine Access-Datenbank bei dieser oder einer ähnlichen Aufgabe zum Einsatz kommt – die Replikation und Synchronisation durchaus hilfreich sein.
Replikation von Datenbanken
769
Die in diesem Abschnitt replizierten Datenbanken finden Sie unter den Dateinamen Nordwind.mdb und Replikat von Nordwind.mdb im Verzeichnis Kap_17.
17.6.1 Funktionsweise der Replikation Wenn Sie eine Datenbank verwenden möchten, um davon mehrere Kopien – nachfolgend Replikate genannt – zu erzeugen, damit ein oder mehrere Benutzer ohne ständige Verbindung dort Daten bearbeiten können, um diese anschließend wieder in einer Datenbank zusammenzuführen, gehen Sie folgendermaßen vor: Öffnen Sie die Datenbank, die Sie replizieren möchten. Wählen Sie den Menüeintrag Extras/Replikation/Datenbank in Replikat konvertieren… aus. Es erscheint die Meldung aus Abbildung 17.7, die über die folgenden Schritte aufklärt. Nach dem Klicken auf die Schaltfläche Ja erscheint noch eine Meldung, die Gelegenheit bietet, automatisch eine Sicherungskopie zu erstellen (siehe Abbildung 17.8).
Abbildung 17.7: Rückfrage vor dem Konvertieren einer Datenbank in eine Designmaster-Datenbank
Abbildung 17.8: Vor dem Erstellen der Designmaster-Datenbank sollten Sie die Originaldatenbank sichern.
Anschließend legen Sie den neuen Speicherort des ersten Replikats der DesignmasterDatenbank fest. Access erstellt dann das Designmaster und öffnet dieses, was sich im neuen Outfit des Datenbankfensters bemerkbar macht (siehe Abbildung 17.9). Die Systemtabellen sind in dieser Abbildung eingeblendet, um zu zeigen, dass hier einige
770
17
Installation, Betrieb und Wartung
Tabellen hinzugekommen sind. Übrigens sind nicht nur die Tabellen, sondern auch alle anderen Datenbankobjekte mit dem Replikat-Symbol versehen. Zusätzlich erstellt Access an der gewünschten Stelle das erste Replikat des Designmasters. Sie haben statt einer also nun drei Datenbanken: Die Sicherungskopie mit der Endung .bak, die in ein Designmaster umgewandelte Datenbank sowie ein erstes Replikat.
Abbildung 17.9: Das Datenbankfenster enthält einige neue Objekte. Außerdem machen neue Symbole deutlich, dass sich hier einiges verändert hat.
17.6.2 Erzeugen weiterer Replikate Wenn Sie mehrere Replikate benötigen, können Sie diese von der Designmaster-Datenbank aus anlegen. Dazu wählen Sie erneut den Eintrag Extras/Replikation/Datenbank in Replikat konvertieren… aus der Menüleiste und geben den Speichernamen des neuen Replikats an. Da es bereits eine Designmaster-Datenbank gibt, sind hier keine weiteren Schritte erforderlich.
17.6.3 Replikation und Synchronisation im Einsatz Mit der Designmaster-Datenbank und dem ersten Replikat haben Sie nun zwei Datenbanken, die Sie unabhängig voneinander ändern und anschließend synchronisieren können. Probieren Sie es einfach einmal aus: Ändern Sie einen Datensatz in der Tabelle Artikel in der Designmaster-Datenbank und einen anderen im Replikat. Am einfachs-
Replikation von Datenbanken
771
ten ist dies, wenn Sie in der ersten Datenbank den Artikelnamen Chang in Chang1 und in der zweiten Chai in Chai1 ändern. Wo sich die Datenbanken befinden, spielt bei der Datenänderung keine Rolle. Im Praxiseinsatz wird sich die Designmaster-Datenbank vermutlich an zentraler Stelle befinden beziehungsweise dort, wo auch der oder die Entwickler sitzen – Änderungen am Datenbankentwurf lassen sich nämlich nur an der Designmaster-Datenbank durchführen. Diese werden beim nächsten Synchronisieren auf die Replikate übertragen. Wichtig ist, dass sich die zu synchronisierenden Datenbanken möglichst auf dem gleichen Rechner befinden – das ist allerdings nur für eine gute Performance bei der Synchronisation notwendig. Die Synchronisation kann auch über das Netzwerk erfolgen. Es sollte sich dabei allerdings schon um eine sichere Verbindung handeln. Um die beiden Datenbanken mit den Änderungen zu synchronisieren, öffnen Sie eine der beiden Datenbanken – hier die Designmaster-Datenbank – und wählen den Eintrag Extras/Replikation/Jetzt synchronisieren… aus der Menüleiste. Access fragt dann nach, ob es die Datenbank zum Zwecke der Synchronisation schließen darf, und führt nach der Bestätigung die Synchronisation durch. Wenn Sie anschließend die Tabelle Artikel der beiden Datenbanken ansehen, stellen Sie fest, dass beide Datenbanken die geänderten Daten enthalten.
Synchronisation auf Feldebene Mit dem vorherigen Beispiel haben Sie gezeigt, dass verschiedene Datensätze geändert und synchronisiert werden können. Nun gehen Sie ein wenig ins Detail: Ändern Sie in der ersten Datenbank den Artikelnamen des ersten Artikels von Chai1 (wie soeben geändert) in Chai12 und in der zweiten Datenbank den Wert des Feldes Einzelpreis von 9,00 € in 10,00 €. Synchronisieren Sie die Datenbanken erneut und stellen Sie fest, dass Access die Daten sogar bei Änderungen im selben Datensatz synchronisiert. Nun ändern Sie in beiden Tabellen den Wert des Feldes Artikelname – einmal in Chai1 und einmal in Chai2. Führen Sie erneut eine Synchronisation durch. Die Meldung von Access verheißt zunächst nur Gutes (siehe Abbildung 17.10).
Abbildung 17.10: Erfolgreicher Abschluss auch beim gleichen geänderten Feld?
772
17
Installation, Betrieb und Wartung
Die anschließende Meldung zeigt, dass der Erfolg allerdings doch nicht so umwerfend ist (siehe Abbildung 17.11).
Abbildung 17.11: Access hat einen Konflikt bei der Synchronisation bemerkt.
Natürlich lösen Sie nun diesen Konflikt. Access bietet dazu ausreichend Hilfe an – zum Beispiel mit der Übersicht aller aufgetauchten Konflikte in Abbildung 17.12. Ein einziger Konflikt – das ist überschaubar. Zeigen Sie mit einem Klick auf die Schaltfläche Anzeigen… weitere Informationen zu diesem Konflikt an.
Abbildung 17.12: Liste der aufgetretenen Konflikte
Der Dialog aus Abbildung 17.13 bietet alle Möglichkeiten, um den Konflikt aufzulösen. Dazu zeigt er die Konfliktursache sowie die in Konflikt stehenden Daten an. In diesem Fall wurde in zwei Datenbanken der Inhalt des gleichen Felds des gleichen Datensatzes auf unterschiedliche Werte geändert. Access liefert gleich auch einen Vorschlag für einen Konfliktgewinner und einen Konfliktverlierer mit – dazu später mehr.
Replikation von Datenbanken
773
Sie haben nun die Möglichkeit, den Vorschlag von Access zu übernehmen, sich für den Konfliktverlierer als Gewinner zu entscheiden oder gar einen ganz anderen Wert in das konfliktbehaftete Feld einzutragen. Wenn Sie unentschieden sind, können Sie die Lösung des Konflikts auch einfach aufschieben – etwa, weil Sie sich dazu mit den beteiligten Benutzern abstimmen müssen. Wenn Sie sich für eine der ersten beiden Varianten entschieden haben, zeigt Access nachfolgend im Dialog aus Abbildung 17.12 an, dass keine Konflikte mehr vorliegen.
Abbildung 17.13: Dialog mit Funktionen zum Auflösen eines Konflikts
Gewinner und Verlierer von Konflikten Nach welchen Kriterien hat Access nun entschieden, wer Gewinner und wer Verlierer des Konflikts ist? Access setzt beim Anlegen jeder neuen Replikation einen Wert für die Priorität fest. Diesen Wert können Sie beim Anlegen eines Replikats selbst bestimmen, indem Sie im Dialog zur Auswahl des Speicherorts auf die Schaltfläche Priorität klicken und im dann erscheinenden Dialog einen Wert zwischen 0 und 100 eintragen.
774
17
Installation, Betrieb und Wartung
Standardmäßig erhält die Designmaster-Datenbank den Wert 90 und weitere einen Wert, der 90% des Prioritätswertes der Datenbank entspricht, die das Replikat erstellt. Wenn Sie selbst die Priorität festlegen möchten, ersetzen Sie einfach den Eintrag STANDARD im Dialog in Abbildung 17.14 durch den gewünschten Wert.
Abbildung 17.14: Festlegen der Priorität eines Replikats
17.6.4 Weitere Informationen Unter folgendem Link steht eine Replikations-FAQ von Microsoft im Word-Format zum Download bereit: http://support.microsoft.com/?kbid=282977
17.6.5 Wann sollten Sie Replikation verwenden? Die Replikation von Datenbanken bringt nicht nur Vorteile, deshalb sollten Sie diese Technik nur einsetzen, wenn es erforderlich ist. Folgende Nachteile replizierter Datenbanken sind zu berücksichtigen: Der Umfang der Datenbanken wird etwa verdoppelt. Nachträgliche Änderungen an der Datenbankstruktur können aufwändiger werden als bei einer herkömmlichen Datenbank. Die Performance verschlechtert sich deutlich.
Sichern von Access-Datenbanken
775
17.7 Sichern von Access-Datenbanken Ein für Access-Entwickler sehr heikles Thema ist das Sichern von Access-Datenbanken. Das Problem ist, dass sich wie bei vielen anderen Datenbanksystemen eine Sicherung im laufenden Betrieb nicht realisieren lässt – zumindest nicht mit der Sicherheit, dass Ihnen im Fall der Fälle ein hundertprozentig funktionsfähiges Backup der Datenbankanwendung zur Verfügung steht. In vielen Unternehmen werden die Backend-Datenbanken oder gar die kompletten Datenbanken auf den Server gelegt, weil die dortigen Daten mit dem täglichen Sicherungslauf auf die sichere Seite gebracht werden. Greift jedoch jemand zum Zeitpunkt der Sicherung auf die Datenbank zu, kann es passieren, dass die im Backup enthaltenen Daten oder gar der im VBA-Projekt enthaltene Code beschädigt wird – denn auch dieser wird in einer internen Tabelle gespeichert. Die nachfolgend vorgestellten Listings finden Sie in der Datenbankdatei Kap_17\ InstallationBetriebWartung.mdb im Modul mdlBackup.
17.7.1 Voraussetzungen und Vorbereitungen Der optimale Zeitpunkt zum Sichern einer Datenbank ist der, an dem diese geschlossen ist. Sollte dies im Fall Ihrer Anwendung zu fest definierten Zeitpunkten der Fall sein – zum Beispiel nachts – steht einer automatischen Sicherung etwa durch einen geplanten Task nichts im Wege. In allen anderen Fällen können die folgenden VBA-Routinen große Hilfe leisten – das Backup lässt sich hiermit auch zur Laufzeit durchführen. Voraussetzung für die nachfolgend beschriebene Vorgehensweise zum Erstellen eines Backups ist, dass die zu sichernden Daten getrennt vom Frontend in einer Backend-Datenbank gehalten werden. Wie Sie dies realisieren, haben Sie bereits in Abschnitt 17.5, »Mehrbenutzerbetrieb mit Access-Datenbanken« dieses Kapitels erfahren. Die erste Routine durchläuft alle geöffneten Objekte der aktuellen Datenbank und schließt sie gegebenenfalls. Dabei werden nur Tabellen, Abfragen, Formulare und Berichte sowie geöffnete Datensatzgruppen berücksichtigt. Public Sub AlleObjekteSchliessen() Dim obj As Object Dim strTemp As String On Error Resume Next
776
17
Installation, Betrieb und Wartung
'Alle Formulare schließen For Each obj In Forms DoCmd.Close acForm, obj.Name, acSaveYes Next obj 'Alle Berichte schließen For Each obj In Reports DoCmd.Close acReport, obj.Name, acSaveYes Next obj 'Eventuell offene Tabellen oder Abfragen schließen Do strTemp = vbNullString strTemp = Screen.ActiveDatasheet.Name DoCmd.Close acTable, strTemp DoCmd.Close acQuery, strTemp DoEvents Loop Until Len(strTemp) = 0 'Eventuell offene Recordsets schließen For Each obj In DBEngine(0)(0).Recordsets obj.Close Next obj Set obj = Nothing End Sub Listing 17.7: Schließen aller offenen Datenbankobjekte
Die zweite Routine beendet alle gegebenenfalls noch offenen Schreibvorgänge. Dazu startet sie eine Transaktion (weitere Informationen in Kapitel 8, Abschnitt 8.11, »Transaktionen«), um direkt im Anschluss alle begonnenen Transaktionen mit CommitTrans und der Option dbForceOSFlush zu beenden. Das vorherige Starten einer Transaktion ist notwendig, weil CommitTrans einen Fehler erzeugt, wenn gar keine Transaktion läuft. Der anschließende Aufruf der Idle-Methode bewirkt, dass noch offene Hintergrundprozesse ausgeführt werden. Private Sub SchreibvorgaengeBeenden() '"Nulltransaktion": 'Eventuell ausstehende Schreibvorgänge erzwingen DBEngine.BeginTrans DBEngine.CommitTrans dbForceOSFlush 'Datenbank-Engine "resetten":
Sichern von Access-Datenbanken
777
DBEngine.Idle End Sub Listing 17.8: Beenden der Schreibvorgänge
Schließlich muss geprüft werden, ob das Backend geschlossen ist – die vorhergehenden Routinen haben das für das aktuelle Frontend bereits erledigt, es könnten aber noch andere Benutzer das Backend im Zugriff haben. Das ist am einfachsten möglich, indem Sie die Existenz einer entsprechenden .ldb-Datei nachweisen. Sie wird immer angelegt, wenn man eine Datenbank öffnet. Ist die Datei vorhanden, ist die BackendDatenbank nicht geschlossen und kann dementsprechend auch nicht risikolos kopiert werden. Außerdem muss die Backend-Datenbank für den exklusiven Zugriff vorbereitet sein, was Sie durch entsprechendes Öffnen prüfen. Private Function BackendBereit(strBackend As String) As Boolean Dim strLDB If Len(Dir(strBackend)) = 0 Then Exit Function 'Existenz des zugehörigen LDB-Files ermitteln strLDB = Left(strBackend, InStrRev(strBackend, ".") - 1) & ".LDB" If Len(Dir(strLDB)) = 0 Then BackendBereit = True On Error Resume Next 'Zur Sicherheit Möglichkeit des Vollzugriff auf Backend ermitteln Open strBackend For Binary Access Read Lock Read Write As #1 If Err.Number <> 0 Then BackendBereit = False Close #1 End Function Listing 17.9: Prüfen, ob das Backend geschlossen und für den Vollzugriff vorbereitet ist
Public Function BackupErstellen(strBackend As String) As Boolean AlleObjekteSchliessen SchreibvorgaengeBeenden If BackendBereit(strBackend) Then 'BackupErstellen = BackendKopieren(strBackend, strBackendKopie) '... BackupErstellen = True Else MsgBox "Backend kann momentan nicht ausgeführt werden." & vbCrLf & _ "Versuchen Sie es zu einem späteren Zeitpunkt."
778
17
Installation, Betrieb und Wartung
End If End Function Listing 17.10: Aufruf der vorherigen drei Routinen und Start des Backup-Vorgangs, wenn das Backend geschlossen und zugreifbar ist
17.7.2 Sichern des Datenbank-Backends Das eigentliche Sichern der Datenbank erfordert nicht mehr als einen simplen Kopiervorgang. Dafür gibt es verschiedene Möglichkeiten.
Einfaches Kopieren mit FileCopy Die einfachste Variante verwendet die FileCopy-Methode von VBA. Diese Methode kann allerdings nur Dateien kopieren, auf die gerade nicht zugegriffen wird. Die folgende Funktion platzieren Sie im gleichen Modul wie die oben beschriebenen Funktionen. Der Aufruf erfolgt durch einen in der Funktion BackupErstellen befindlichen, bisher noch auskommentierten Aufruf: Private Function BackendKopieren(strBackend As String, _ strBackendKopie As String) As Boolean On Error GoTo BackendKopieren_Err 'Durchführen des Kopiervorgangs DBEngine.CompactDatabase strBackend, strBackendKopie BackendKopieren = True 'Fehlerbehandlung BackendKopieren_Exit: 'Restarbeiten Exit Function BackendKopieren_Err: BackendKopieren = False GoTo BackendKopieren_Exit End Function Listing 17.11: Funktion zum Kopieren einer Datenbankdatei
Kopieren per API-Funktion Eine weitere Möglichkeit zum Kopieren bietet die API-Funktion SHFileOperation. Die Deklaration der Funktion und der verwendeten Konstanten finden Sie im Modul mdlFileCopy in der Beispieldatenbank Kap_17\InstallationBetriebWartung.mdb. Sie bietet
Sichern von Access-Datenbanken
779
einige Optionen wie etwa eine Fortschrittsanzeige bei länger dauernden Kopiervorgängen, beim Kopieren von Dateiberechtigungen und mehr. Der Aufruf der ebenfalls in diesem Modul befindlichen Wrapper-Funktion sieht etwa folgendermaßen aus: BackendKopieren CurrentDb.Name, _ "c:\Sicherung\Sicherung.mdb", _ eFC_DateiNichtUeberschreiben Or eFC_OhneBerechtigungen _ Or eFC_OhneFehlermeldungen
Die einzelnen Optionen und ihre Beschreibung finden Sie ebenfalls im Modul mdlFileCopy.
Kopieren und komprimieren Wenn Sie die Datenbank anschließend komprimieren möchten, können Sie auch die CompactDatabase-Methode des DBEngine-Objekts verwenden. Dabei setzen Sie einfach die folgende Anweisung für die jeweilige Kopieranweisung der oben vorgestellten Funktion BackendKopieren ein. DBEngine.CompactDatabase strBackend, strBackendKopie
Kopieren und zippen Natürlich können Sie die kopierte Datenbankdatei auch direkt mit WinZip oder ähnlichen Tools packen. Die meisten Zip-Tools bieten die Möglichkeit der Steuerung per Kommandozeile. Unter WinZip würde die notwendige Anweisung etwa wie folgt aussehen: Shell "c:\programme\winzip\wzzip -a " & strBackendKopie & ".zip " _ & strBackend
17.7.3 Sicherungsstrategie Ein Punkt ist noch offen: Welche Strategie wird beim Sichern verfolgt, das heißt, wann wird gesichert, wie heißen die Dateinamen der Sicherungen und in welchem Verzeichnis befinden sich diese, werden Daten nach einem bestimmten Zeitraum überschrieben und vor allem: Wie teilen Sie dem oder den Benutzern mit, dass in Kürze ein Sicherungsvorgang ansteht? Letzteres ist natürlich nur relevant, wenn die Sicherung tatsächlich während des laufenden Betriebs stattfinden muss und nicht nachts erfolgen kann. Wenn sichergestellt ist, dass nachts niemand auf die Datenbank zugreift, kann man den Vorgang mit einem VB-Skript ausführen, das mit dem Tool Geplante Tasks von Windows aufgerufen wird.
780
17
Installation, Betrieb und Wartung
Ein einfaches Skript könnte wie folgt aussehen und wird mit der Datei-Endung .vbs gespeichert: Set objFSO = CreateObject("Scripting.FileSystemObject") strZeit = Year(Date()) & Right("0" & Month(date()), 2) _ & Right("0" & Day(date()), 2) & "_" & Right("0" & Hour(Time()),2) _ & Right("0" & Minute(Time),2) & Right("0" & Second(Time),2) objFSO.CopyFile "c:\Nordwind_Backend.mdb", "c:\Nordwind_Backend" & strZeit & ".mdb"
Es kopiert die Datei und fügt dem neuen Dateinamen vor dem Suffix das aktuelle Datum und die Uhrzeit im Format yyyymmdd_hhmmss hinzu. Der resultierende Dateiname würde also beispielsweise Nordwind_Backend20050723_005830.mdb lauten. Das Tool Geplante Tasks starten Sie mit dem gleichnamigen Eintrag der Systemsteuerung. Im Originalzustand enthält das Tool keine Einträge (siehe Abbildung 17.15).
Abbildung 17.15: Das Tool »Geplante Tasks«
Um einen Aufruf eines Skripts anzulegen, klicken Sie doppelt auf den Eintrag Geplanten Task hinzufügen. Ein Assistent wird gestartet und fragt Sie zunächst nach der auszuführenden Datei. Hier geben Sie die gewünschte .vbs-Datei an und im nächsten Schritt einen Tasknamen und die Frequenz der Ausführung – in diesem Fall »Täglich« (siehe Abbildung 17.16).
Sichern von Access-Datenbanken
781
Abbildung 17.16: Name des Tasks und Frequenz festlegen
Der folgende Dialog fragt ab, zu welcher Zeit der Task gestartet werden soll und ob dies täglich, nur werktags oder alle x Tage zu erfolgen hat. Außerdem geben Sie hier das Startdatum ein (siehe Abbildung 17.17).
Abbildung 17.17: Angabe der Uhrzeit und der Tage
Schließlich geben Sie den Benutzer samt Kennwort ein, in dessen Kontext der Task durchgeführt werden soll. Gegebenenfalls sollen die Dateien in einem Verzeichnis gespeichert werden, auf das nur bestimmte Benutzer Zugriff haben – beachten Sie dies beim Anlegen des Tasks.
782
17
Installation, Betrieb und Wartung
Nach dem Beenden des Assistenten können Sie die Eigenschaften des Tasks mit einem Doppelklick auf den entsprechenden Eintrag in der Liste der geplanten Tasks ansehen.
Dateiname der Sicherung Im Beispiel des Taskmanagers mit VB Skript haben Sie bereits eine Möglichkeit für den Dateinamen einer Backup-Datei kennen gelernt – dabei wird der Dateiname einfach um die genaue Zeit des Backups erweitert.
Dateien regelmäßig überschreiben Wenn Sie beispielsweise nur den aktuellen Stand der letzten sieben einzelnen Tage sichern möchten, können Sie am aktuellen Tag jeweils die vor einer Woche angelegte Kopie überschreiben. Sie benötigen dann nur sieben verschiedene Dateinamen, die entweder die Wochentage oder eine entsprechende Zahl im Dateinamen tragen.
Benutzer vor dem Backup benachrichtigen Unter Umständen möchten Sie die Daten vielleicht mehrmals am Tag sichern, weil es sich um sehr wichtige Daten handelt, die auch noch regelmäßig geändert werden. In diesem Fall kann das ausschließlich nächtliche Speichern durchaus fatale Folgen haben, beispeilsweise wenn kurz vor Feierabend die Datenbank durch einen Netzwerkfehler oder Ähnliches zerstört wird. Der erste Tipp für diesen Fall ist: Verwenden Sie keine Access-Datenbank, sondern den Microsoft SQL Server oder die MSDE als Backend. Diese loggen die Datenänderungen auch abseits von Backups mit und können die zum Zeitpunkt des Absturzes enthaltenen Daten fast völlig wieder herstellen. Falls es dennoch eine Access-Datenbank sein muss, gibt es verschiedene Möglichkeiten, die hier nur angerissen werden können: Sorgen Sie dafür, dass alle geöffneten Frontends zur gleichen Zeit eine Meldung ausgeben, dass beispielsweise in fünf Minuten eine Sicherung durchgeführt wird und dazu alle Tabellen, Abfragen, Formulare und Berichte geschlossen sein müssen. Nun stellt sich erstens die Frage, wie Sie die Frontends dazu bringen, zur gleichen Zeit diese Meldung anzuzeigen und dann auch möglichst gleichzeitig alle nicht geöffneten Objekte zu schließen. Eine Rolle wird dabei ein ständig im Hintergrund geöffnetes Formular spielen, das regelmäßig die Zeit abfragt und beim Erreichen eines bestimmten Zeitpunktes die Meldungen anzeigt. Bleibt das Problem mit der synchronen Anzeige der Meldungen: Nicht alle Rechner haben zwingend die gleiche Systemzeit. Sie müssen bei dieser Lösung also dafür sorgen, dass die Zeit zentral ermittelt wird.
Sicheres Ausführen von Access-Anwendungen
783
Die zweite Variante ist, dass Sie den Sicherungsvorgang starten, wenn kein Benutzer aktiv ist. Um zu ermitteln, wann das der Fall ist, könnten Sie immer, wenn ein Benutzer den Fokus auf ein anderes Formular, Steuerelement oder sonstiges Objekt verschiebt, einen Datensatz in einer speziell für diesen Fall angelegten Tabelle im Backend auf die aktuelle Zeit einstellen. Wiederum per ständig geöffnetem, aber am besten nicht sichtbarem Formular wird per OnTimer-Ereignis regelmäßig abgefragt, wann der letzte Benutzer mit der Datenbank gearbeitet hat. Nach einer bestimmten Zeit ohne Aktionen werden dann automatisch die Objekte aller Frontends geschlossen, um Zugriffe auf das Backend auszuschließen, und das Backup erstellt.
17.8 Sicheres Ausführen von Access-Anwendungen Mit steigender Nutzung des Internets erhöht sich auch die Wahrscheinlichkeit, Opfer eines Angriffs zu werden. Es gibt kaum noch Anwendungen, die nicht durch eine unsichere Stelle aufgefallen sind. Hersteller und Hacker wetteifern um das Ausnutzen, Aufdecken und Schließen von Lücken in den Anwendungen, die es etwa ermöglichen, durch die Ausführung von Code auf einem fremden Rechner auf diesen zuzugreifen und die Kontrolle über diesen zu erlangen. Auch Microsoft Access bleibt hier nicht verschont und so hat Microsoft in die Version 2003 einige Vorsichtsmaßnahmen integriert.
17.8.1 Schutz vor bösartigem Code Die erste Maßnahme ist bereits von anderen Office-Anwendungen wie Word oder Excel her bekannt und sorgt dafür, dass Sie keine Anwendung öffnen können, ohne entsprechende Sicherheitsmeldungen über sich ergehen zu lassen. Dies ist nur die Standardeinstellung. Es geht noch eine Stufe schärfer, in der Access nur noch Dateien öffnet, die mit einer digitalen Signatur versehen sind. Mit der Standardeinstellung warnt Access Sie jeweils, wenn Sie eine Datenbank öffnen, die nicht aus einer vertrauenswürdigen Quelle stammt (siehe Abbildung 17.18) – nicht dass Sie anschließend mit formatierter Festplatte dastehen und behaupten, Sie seien nicht gewarnt worden … Diese Warnung erscheint also immer, wenn Sie eine Datenbank öffnen, deren VBA-Projekt nicht signiert wurde. Enthält die Datenbank ein signiertes VBA-Projekt, fragt Sie Access, ob Sie VBA-Projekte mit dieser Signatur generell zulassen möchten oder nicht. Stimmen Sie dem zu, werden Datenbanken mit VBAProjekten mit dieser Signatur immer anstandslos geöffnet, während bei unsignierten Datenbanken weiterhin die Warnmeldung erscheint.
784
17
Installation, Betrieb und Wartung
Abbildung 17.18: Warnung beim Öffnen einer Datenbank mit nicht signiertem VBA-Projekt
17.8.2 Schutz vor bösartigen SQL-Statements Die zweite Maßnahme ist der Sandbox-Modus des Jet Expression Service. Unter Access bietet Jet die Möglichkeit, VBA-Funktionen in SQL-Ausdrücke einzubinden. Das kann sehr hilfreich sein, bietet aber auch jede Menge Möglichkeiten für Angreifer, ungewünschte Aktionen auf einem Rechner auszuführen. So lässt sich in einer SELECTAnweisung die Shell-Funktion aktivieren, die bekanntermaßen alle möglichen Anwendungen aufrufen und über die Anwendung cmd.exe auch DOS-Befehle absetzen kann. Beispiel: Mit folgender SQL-Anweisung können Sie bei deaktiviertem Sandbox-Modus leicht eine Datei löschen (hier die Datei c:\test.txt): SELECT (Shell("cmd.exe /c del c:\test.txt"));
Da ist es leicht zu erahnen, dass sich auch umfangreichere Datenbestände mit einer solchen oder ähnlichen Anweisung entfernen lassen. Unter der Internetadresse http://support.microsoft.com/kb/294698/en-us finden Sie Informationen über die gesperrten und die freigegebenen VBA-Anweisungen. Wenn der Sandbox-Modus aktiviert ist und Sie versuchen, eine nicht zulässige Funktion via Jet aufzurufen, erscheint eine Meldung wie in Abbildung 17.19.
Abbildung 17.19: Meldungen wie diese erscheinen, wenn eine Funktion durch den Sandbox-Modus blockiert wird.
Sicheres Ausführen von Access-Anwendungen
785
Die beiden Schutzmechanismen sind zwar eigentlich unabhängig voneinander, da sie verschiedene Felder beackern, aber ihre Aktivierung ist eng miteinander verwoben – sofern Sie sich nicht der Registry zum Einstellen der entsprechenden Parameter bedienen – dazu später mehr. Wie Sie mit den beiden Mechanismen umgehen, sie aktivieren und deaktivieren und wie Sie Datenbankdateien mit einer digitalen Signatur versehen, erfahren Sie in den nächsten Abschnitten.
17.8.3 Deaktivieren der Sicherheitswarnungen Wer nichts mit fremden Datenbankanwendungen zu tun hat und nur mit eigenen Datenbanken arbeitet beziehungsweise diese entwickelt, ist mit der Zeit durch die ständigen Warnhinweise möglicherweise ein wenig genervt. Die Meldung aus Abbildung 17.18 lässt sich glücklicherweise relativ einfach abschalten. Dazu wählen Sie einfach den Menüeintrag Extras/Makros/Sicherheit… aus und stellen die Sicherheitsstufe im Dialog aus Abbildung 17.20 auf Niedrig – nur so bleiben Sie von weiteren Sicherheitsmeldungen verschont.
Abbildung 17.20: Einstellen der Sicherheitsstufe beim Öffnen von Access-Datenbanken
Der Dialog bietet noch eine weitere Sicherheitsstufe an, die lediglich das Ausführen signierter Makros zulässt – mehr dazu in Abschnitt 17.8.4.
786
17
Installation, Betrieb und Wartung
Niedrige Sicherheitsstufe Beim Wechsel der Sicherheitsstufe auf Niedrig zeigt Access noch einen weiteren Dialog an, in dem Sie gefragt werden, ob Sie unsichere Ausdrücke künftig zulassen möchten oder nicht. Hier entscheiden Sie, ob Sie den Sandbox-Modus von Jet deaktivieren oder beibehalten möchten – im Zweifelsfall kann dieses bisschen mehr Sicherheit nicht schaden.
Abbildung 17.21: Unsichere Ausdrücke blockieren oder nicht?
Hohe und mittlere Sicherheitsstufe Als Entwickler können Sie es sich leisten, auf Ihrem Entwicklungsrechner die Sicherheitsstufe Niedrig einzustellen, weil Sie wissen, worauf Sie achten müssen – zum Beispiel dass Sie Access-Datenbanken aus unbekannten Quellen nicht öffnen. Beim Benutzer Ihrer Datenbankanwendungen ist das möglicherweise anders – wenn Sie wissen, dass er beispielsweise nur Anwendungen aus Ihrer Schmiede verwendet, macht es Sinn, dass er die Sicherheitsstufe auf Mittel oder sogar auf Hoch einstellt. Das bedeutet wiederum, dass Sie Ihre Anwendungen mit einer digitalen Signatur versehen müssen und der Benutzer der Verwendung der von Ihnen signierten Anwendungen zustimmt. Auf diese Weise kann er die von Ihnen signierten Datenbanken problemund vor allem meldungslos öffnen, erhält aber bei nicht signierten Datenbanken eine Warnmeldung (mittlere Sicherheitsstufe) oder kann diese überhaupt nicht öffnen (hohe Sicherheitsstufe).
Wechseln zwischen den Sicherheitsstufen Beim Wechsel zwischen den Sicherheitsstufen prüft Access auch jeweils, ob der Sandbox-Modus aktiviert ist, und zeigt gegebenenfalls eine Meldung an, die eine Anpassung der durch die Sandbox gewährleisteten Sicherheit an die Makro-Sicherheitsstufe erlaubt. So wird beim Wechsel aus der mittleren in die niedrige Sicherheitsstufe die Meldung aus Abbildung 17.21 angezeigt. Wenn Sie hier die unsicheren Ausdrücke blockieren und dann wieder in die mittlere oder hohe Sicherheitsstufe wechseln, erscheint eine weitere Meldung, die die Sperrung unsicherer Ausdrücke anbietet.
Sicheres Ausführen von Access-Anwendungen
787
Ob Sie den Sandbox-Modus beim Benutzer Ihrer Datenbankanwendung aktivieren müssen oder nicht, hängt davon ab, ob die enthaltenen Abfragen Funktionen beinhalten, die im Sandbox-Modus gesperrt sind, aber die für die Benutzung der Anwendung Voraussetzung sind. In diesem Fall bleibt Ihnen nicht viel anderes übrig, als den Sandbox-Modus zu deaktivieren.
17.8.4 Digitale Signaturen Digitale Signaturen schützen Benutzer von Access-Anwendungen beziehungsweise der dahinter stehenden VBA-Projekte, indem sie diese als »echt« authentifizieren. Das funktioniert folgendermaßen: 1. Eine Signatursoftware bildet einen Hashwert auf Basis des Inhalts der zu signierenden Datei. 2. Der Hashwert wird mit einem privaten Schlüssel verschlüsselt – der resultierende Wert ist die Signatur. 3. Datei und Signatur inklusive öffentlichem Schlüssel werden an den Benutzer weitergegeben. 4. Aus der Signatur und dem öffentlichen Schlüssel wird der weiter oben erzeugte Hashwert gewonnen und mit dem Hashwert der signierten Datei verglichen. 5. Stimmen die Hashwerte überein, gilt die Datei als vom angegebenen Absender erzeugt und als seit dem Signieren unverändert übermittelt.
VBA-Projekte signieren in der Praxis Unter Access/Office läuft der Vorgang mit den genannten Schritten wie folgt ab: Starten Sie die Anwendung zum Signieren von VBA-Projekten über den Eintrag Alle Programme/Microsoft Office/Microsoft Office Tools/Digitale Signatur für VBA-Projekte des Start-Menüs von Windows. Geben Sie in den nun erscheinenden Dialog einen Zertifikatsnamen ein und bestätigen Sie die Eingabe mit OK (siehe Abbildung 17.22). Die anschließende Meldung bestätigt die erfolgreiche Erstellung des Zertifikats (siehe Abbildung 17.23). Das war nur der erste Schritt. Nun wenden Sie sich dem zu signierenden VBA-Projekt zu und wählen dort den Menüeintrag Extras/Digitale Signatur… aus. Wie erwartet, fehlt bisher eine Zertifizierung dieses VBA-Projekts (siehe Abbildung 17.24). Mit einem Klick auf die Schaltfläche Wählen… und in dem danach erscheinenden Dialog Zertifikat auswählen ändern Sie dies allerdings, indem Sie das soeben erstellte Zertifikat auswählen und mit OK übernehmen (siehe Abbildung 17.25).
788
Abbildung 17.22: Erstellen eines Zertifikats
Abbildung 17.23: Erfolgsmeldung der Zertifikats-Software
Abbildung 17.24: Das VBA-Projekt ist derzeit noch nicht signiert …
17
Installation, Betrieb und Wartung
Sicheres Ausführen von Access-Anwendungen
789
Abbildung 17.25: … was sich aber durch Zuweisen des soeben erzeugten Zertifikats ändern lässt.
Bei Interesse sollten Sie sich durchaus einmal anschauen, wie die Eigenschaften eines solchen Zertifikats aussehen.
Mit einer bestimmten Signatur versehene VBA-Projekte zulassen Im nächsten Schritt schauen Sie sich an, was Sie mit dem Signieren erreicht haben. Schließen Sie die Datenbank und öffnen Sie diese erneut. Vorausgesetzt, es ist die mittlere oder hohe Sicherheitsstufe aktiviert, erscheint nun die Meldung aus Abbildung 17.26. Hier können Sie sich Details zur Signatur ansehen oder das Kontrollkästchen Dateien von dieser Quelle… aktivieren und die Datei öffnen. Letzteres führt dazu, dass alle mit dieser Signatur signierten Dateien von nun an ohne Rückfrage geöffnet werden.
Abbildung 17.26: Dateien mit der gleichen Signatur öffnen Sie entweder immer oder gar nicht.
790
17
Installation, Betrieb und Wartung
Auf diese Weise signierte Dateien werden allerdings nur auf dem eigenen Rechner ohne Murren von Access geöffnet. Wenn Sie eine solche Datei weitergeben beziehungsweise diese unter einem anderen Benutzernamen oder auf einem anderen Rechner öffnen, erscheint wiederum eine Sicherheitswarnung – diesmal allerdings ohne die Möglichkeit, die Signatur dauerhaft zuzulassen. Dafür ist die Öffnen-Schaltfläche direkt aktiviert (siehe Abbildung 17.27). Andere Benutzer geraten also vom Regen in die Traufe – das heißt, von einer Sicherheitswarnung zur anderen.
Abbildung 17.27: Sicherheitswarnung beim Öffnen eines VBA-Projekts auf einem anderen als dem Zertifizierungsrechner
Um auch anderen Benutzern die Vorzüge der Sicherheit ohne Sicherheitsmeldungen zu Gute kommen zu lassen, muss der Benutzer das Zertifikat anerkennen und eine entsprechende Datei auf seinem Rechner speichern, um das dauerhafte Erscheinen der Sicherheitswarnung zu vermeiden. Der Weg dorthin lässt sich nicht unbedingt intuitiv finden. Um das Zertifikat anzuerkennen, klicken Sie zunächst auf die Schaltfläche Details… und im folgenden Dialog auf Zertifikat anzeigen. Im nächsten Dialog findet sich dann die Schaltfläche Zertifikat installieren…, mit der Sie den entsprechenden Assistenten starten (siehe Abbildung 17.28). Die in den einzelnen Schritten des Assistenten angebotenen Optionen übernehmen Sie alle und erhalten schließlich eine Meldung, der Sie den Fingerabdruck (Hash) des Zertifikats entnehmen können (siehe Abbildung 17.29). Wenn der Ersteller der Signatur die Richtigkeit des Fingerabdrucks bestätigt, können Sie davon ausgehen, dass die Datei vertrauenswürdig ist.
Sicheres Ausführen von Access-Anwendungen
791
Abbildung 17.28: Starten des Assistenten zum Installieren eines Zertifikats
Abbildung 17.29: Meldung mit dem Hashwert zur Prüfung der Herkunft der Signatur
Zur unkomplizierten Verteilung vertrauenswürdiger Datenbankanwendungen über das Internet empfiehlt sich das Hinzuziehen eines professionellen Dienstleisters für die Vergabe von Zertifizierungen.
792
17
Installation, Betrieb und Wartung
17.8.5 Sicherheitseinstellungen per Registry vornehmen Sowohl den Sandbox-Modus als auch die Makro-Sicherheitsstufe können Sie via Registry einstellen. Diese öffnen Sie über den Ausführen…-Dialog mit dem Befehl Regedit.exe.
17.8.6 Makro-Sicherheitsstufe einstellen Die Makro-Sicherheitsstufe lässt sich für jeden Benutzer einzeln einstellen. Zum passenden Registry-Schlüssel finden Sie auf dem in Abbildung 17.30 dargestellten Weg. Dort fügen Sie den der gewünschten Sicherheitsstufe entsprechenden Zahlenwert ein (siehe Tabelle 17.1).
Abbildung 17.30: Einstellen der Makro-Sicherheit per Registry
Einstellung (Sicherheitsstufe)
Beschreibung
1
Niedrige Sicherheitsstufe
2
Mittlere Sicherheitsstufe
3
Hohe Sicherheitsstufe
Tabelle 17.1: Einstellen der verschiedenen Makro-Sicherheitsstufen per Registry
17.8.7 Sandbox-Modus einstellen Der Sandbox-Modus erfordert eine computerweite Einstellung. Dementsprechend ist der passende Registry-Schlüssel im Pfad HKEY_LOCAL_MACHINE zu finden (siehe Abbildung 17.31). Die möglichen Einstellungen finden Sie in Tabelle 17.2.
Datenbank reparieren
793
Abbildung 17.31: Einstellen des Sandbox-Modus Einstellung
Beschreibung
0
Der Sandbox-Modus ist für alle Anwendungen, die auf die Jet Engine zugreifen, deaktiviert.
1
Der Sandbox-Modus ist nur für Access-Anwendungen aktiviert.
2
Der Sandbox-Modus ist nur für Nicht-Access-Anwendungen aktiviert.
3
Der Sandbox-Modus ist immer aktiviert.
Tabelle 17.2: Einstellungen für den Registry-Key SandBoxMode
17.9 Datenbank reparieren Es gibt verschiedene Möglichkeiten, wie Access-Datenbanken dem Benutzer mitteilen, dass sie defekt sind und eine Reparatur benötigen. Sie lassen sich einfach nicht mehr öffnen oder stürzen in bestimmten Situationen komplett ab.
794
17
Installation, Betrieb und Wartung
17.9.1 Symptome Typische Symptome einer beschädigten Datenbank sind folgende: Es erscheint die Fehlermeldung »Nicht erkennbares Datenbankformat: , Fehlernummer 3343. Access meldet, dass die Reparatur nicht erfolgreich durchgeführt werden konnte«. Es erscheint ein Dialog zur Eingabe eines Kennworts, obwohl die Datenbank nicht geschützt ist. Beschädigungen, die sich durch diese oder andere Symptome bemerkbar machen, entstehen zum Beispiel durch einen Absturz der Datenbank, ein Backend auf einem überlasteten Server, falsche Netzwerkeinstellungen am Server, Netzwerkausfälle, Fehler im Filesystem oder fehlerhaft wiederhergestellte Backups (etwa durch Lesefehler beim Kopieren von CD). Wenn Sie Glück haben, passiert so etwas bereits während der Entwicklungsphase, in der Sie hoffentlich immer über eine aktuelle Sicherungskopie verfügen. Wenn Sie aber Pech haben, tritt dieser Fall ein, wenn sich die Datenbank bereits beim Kunden im vollen Einsatz befindet. Hoffentlich haben Sie ihm bereits vorher mitgeteilt, dass AccessAnwendungen keine Wunderkisten sind und auch streiken können, und ihm empfohlen, seinerseits regelmäßig Sicherungen anzulegen. Wenn keine Sicherung existiert oder diese lange zurückliegt, ist der Versuch, die Datenbankanwendung und die enthaltenen Daten zu retten, sicher zu empfehlen. Sie müssen dabei allerdings einige Voraussetzungen beachten. Leider greifen die nachfolgend vorgestellten Möglichkeiten nur zum Teil. Wenn Sie selbst keine Chance mehr sehen, die Daten zu retten, dies aber unabdingbar ist, können Sie immer noch einen professionellen Dienstleister mit der Reparatur der Datenbank beauftragen. Eine erfolgreiche Wiederherstellung der Datenbank kann aber auch dieser nicht garantieren – es gibt Fälle, in denen sie irreparabel beschädigt ist.
17.9.2 Sicherung geht vor Nicht nur bei der Arbeit mit Access-Datenbanken, sondern auch beim Versuch, eine Datenbank zu reparieren, sollten Sie zuvor eine Sicherungskopie der scheinbar beschädigten Datenbank anlegen. Um auszuschließen, dass ein Festplattendefekt schuld an dem Schaden ist und gegebenenfalls auch die Sicherungskopie dadurch zerstört wird, speichern Sie diese auf einem externen Medium wie einer CD oder einem externen Laufwerk. Wenn Sie versuchen, eine defekte Access-Anwendung mit den eingebauten Befehlen zu komprimieren und zu reparieren, können Sie diese möglicherweise für Rettungsversuche durch entsprechende Dienstleister nicht mehr verwenden – das gilt prinzipiell für alle Reparaturversuche.
Datenbank reparieren
795
17.9.3 Allgemeine Reparaturversuche Wenn Sie die Datenbank gesichert haben, können Sie versuchen, die Datenbank selbst zu reparieren. Falls es möglich ist, sie mit Access zu öffnen, bevor sie abstürzt, können Sie die dort angebotene Funktion zum Komprimieren und Reparieren der Datenbank verwenden (Menüeintrag Extras/Datenbank-Dienstprogramme/Datenbank komprimieren und reparieren…). Sollten Sie nicht bis zu diesem Menüpunkt gelangen, können Sie es mit dem Tool JetComp.exe versuchen, das die Datenbank komprimiert und repariert, ohne diese zu öffnen. Dieses Tool steht unter www.microsoft.com zum Download bereit; suchen Sie dort nach dem Begriff Jet Compact Utility. In manchen Fällen hilft es auch, eine neue Datenbank anzulegen und alle Objekte aus der beschädigten Datenbank in die neue Datenbank zu importieren. Falls irgendetwas auf Probleme mit dem in der Datenbank enthaltenen Code hindeutet (etwa Formulare, die sich nicht mehr öffnen lassen, oder Meldungen wie »Es konnte auf den OLE-Server nicht zugegriffen werden« oder »Die Netzwerkverbindungen konnten nicht hergestellt werden«), können Sie versuchen, die Datenbank zu dekompilieren. Dabei hilft der Befehlszeilenschalter /decompile. Erstellen Sie entweder eine Verknüpfung mit folgendem Inhalt oder setzen Sie die folgende Anweisung im Ausführen…-Dialog von Windows ab, wobei Sie gegebenenfalls noch den Pfad zur Datei MSACCESS.EXE anpassen müssen: "C:\Programme\Microsoft Office\OFFICE11\MSACCESS.EXE" /decompile
17.9.4 Weitere Informationen Die hier genannten Rettungsmöglichkeiten umfassen nur allgemeine Lösungsansätze. Zu diesem Thema finden Sie zahlreiche Artikel in der Knowledge-Base von Microsoft; dort gibt es Lösungsansätze für spezielle Probleme. Auch die Internetseiten professioneller Dienstleister bieten weitere Informationen; dort können Sie auch Preise und Möglichkeiten für die Reparatur einer beschädigten Datenbank erfragen. Zusätzlich gibt es natürlich auch entsprechende Tools wie AccessRecovery oder EasyRecovery. Hinweise dazu finden Sie im Anhang zu diesem Buch. Damit Sie nicht in die Verlegenheit geraten, für die Reparatur einer Datenbankanwendung tief in die Tasche greifen zu müssen, sollten Sie mit entsprechenden Backups vorsorgen – Strategien und technische Möglichkeiten dazu finden Sie weiter oben in Abschnitt 17.6, »Replikation von Datenbanken«.
796
17
Installation, Betrieb und Wartung
17.10 Verweise und Probleme mit Verweisen Verweise werden unter Access verwendet, um externe Bibliotheken mit Klassenobjekten, Eigenschaften, Methoden und Ereignissen unter VBA verfügbar zu machen. Dabei handelt es sich um Type Libraries (.tlb), Object Libraries (.olb), Control Libraries (.ocx) oder auch Access-Datenbanken (*.mdb, *.mde). Es lassen sich aber auch ActiveX-DLLs (.dll-Dateien) verwenden. Standardmäßig sind unter Access 2003 fünf Bibliotheken eingestellt (siehe Abbildung 17.32).
Abbildung 17.32: Der Verweise-Dialog mit den Standardverweisen
Durch Setzen eines Verweises auf externe Bibliotheken wie etwa Office-Anwendungen wie Word, Excel oder Outlook machen Sie deren Objektbibliotheken für den Zugriff per VBA verfügbar. Sie können genauso auf das Objektmodell der Anwendungen zugreifen, als ob Sie direkt mit diesen Anwendungen arbeiteten. Das Gleiche gilt auch für andere Komponenten von Microsoft und auch von externen Anbietern. Beispiele für die Verwendung der Objektbibliotheken anderer Anwendungen finden Sie in Kapitel 6 im Abschnitt 6.8, »Zugriff auf andere Bibliotheken und Objekte«. Beim Öffnen prüft Access, ob die angegebenen Dateien geladen sind. Ist das nicht der Fall, sucht Access nach einer Datei mit der gleichen GUID, anschließend nach einer Datei mit dem gleichen Dateinamen wie die im Verweis enthaltene Datei. Schließlich
Verweise und Probleme mit Verweisen
797
gibt es noch die ProgID (etwa »Word.Application«) als möglichen Anhaltspunkt. Wenn auch hier nichts zu holen ist, wird der Verweis im Verweise-Dialog als NICHT VORHANDEN gekennzeichnet. Da man dort nicht nach jedem Start einer Access-Anwendung hineinschaut, wird man früher oder später auf andere dadurch bedingte Unregelmäßigkeiten stoßen. Dabei gibt es meist eine der beiden folgenden Ausprägungen: Es erscheint die Meldung »Fehler beim Kompilieren. Projekt oder Bibliothek nicht auffindbar«. Dies ist ein offensichtlicher Hinweis darauf, einmal die Verweise zu kontrollieren. Es erscheint eine Meldung wie »Funktion steht in Ausdrücken nicht zur Verfügung«. Dies weist nicht unmittelbar auf einen defekten Verweis hin, ist aber meist dadurch begründet. Die Meldung tritt in Zusammenhang mit Standardfunktionen wie Left(), Right() oder Format() auf.
Alles besser unter Access 2003? Access 2003 meldet fehlende Verweise direkt beim Öffnen der Anwendung (siehe Abbildung 17.33). Das ist immerhin eine Verbesserung gegenüber früheren Versionen, die den Anwender erst beim Auftreten eines konkreten Problems mehr oder weniger aussagekräftig informierten (siehe oben).
Abbildung 17.33: Access hat ein Problem mit einem Verweis entdeckt.
Nach dem Auftauchen der Meldung aus Abbildung 17.33 lohnt sich ein Blick in den Verweise-Dialog, den Sie mit dem Menüeintrag Extras/Verweise in der VBA-Entwicklungsumgebung öffnen. Dort wird ein fehlender Verweis auf eine externe Datenbank angezeigt (siehe Abbildung 17.34).
798
17
Installation, Betrieb und Wartung
Abbildung 17.34: Anzeige eines fehlenden Verweises
Ohne Verweise arbeiten? Fast alle Anweisungen, die man unter VBA verwendet, werden über so genannte OLEObjekte zur Verfügung gestellt. Diese OLE-Objekte lassen sich innerhalb von VBA unter anderem mit der Methode CreateObject erzeugen – falls Access sie nicht schon beim Start der Anwendung selbst erzeugt hat, wie etwa das DBEngine-Objekt der DAO-Bibliothek. Voraussetzung ist, dass Sie die ProgID des Objekts kennen. Meist ist diese aber offensichtlich: So greifen Sie auf Word mit Word.Application oder Excel mit Excel.Application zu. Der Vorteil ist: Access sucht sich so immer die Bibliothek mit der angegebenen ProgID heraus (also etwa Word.Application) und muss nicht auf die Verwendung der richtigen Version achten. Für die Abwärtskompatibilität haben Sie in diesem Fall allerdings selbst zu sorgen – wenn Sie eine Datenbank weitergeben, die Funktionen einer Office-Anwendung in der Version 2003 enthält, müssen Sie bei einem System mit Office 2000 mit dem Scheitern rechnen.
Late Binding und Early Binding Late Binding und Early Binding heißen die beiden Techniken, die Objekte unter VBA zur Verfügung stellen. Beide haben Sie in den vorherigen Abschnitten bereits kennen gelernt: Early Binding bedeutet, dass Sie einen Verweis auf das gewünschte Objekt erstellen und dann über dieses Objekt zugreifen; beim Late Binding erstellen Sie den Verweis erst über das Füllen einer entsprechenden Objektvariable mit der CreateObjectoder der GetObject-Methode – wobei man Letztere verwendet, wenn schon eine Instanz dieses Objekts läuft.
Verweise und Probleme mit Verweisen
799
Verweise und die Weitergabe von Anwendungen Bei weitergegebenen Anwendungen bringen Verweise auf Office-Anwendungen oft Probleme, weil sie auf dem Zielrechner andere Versionen der entsprechenden Bibliotheken vorfinden als erwartet. Befindet sich auf dem Zielrechner eine jüngere Version als angenommen, bedeutet dies in der Regel kein Problem, eine ältere Version dagegen schon. Daher ist es empfehlenswert, eine für die Weitergabe bestimmte Anwendung mit der ältesten Version von Access beziehungsweise Office zu erstellen, die auf dem Zielrechner laufen kann. Für Zielrechner mit Office 97 kommt da quasi nur die Version 97 in Betracht. Anders ist es ab Access 2000: Wenn Sie eine Anwendung mit Access 2000 und mit Verweisen auf Office 2000-Anwendungen erstellen, ist diese in der Regel auch unter Access 2002 und Access 2003 einsetzbar.
Auf Nummer Sicher Sie können programmatisch sicherstellen, dass auf dem Zielrechner die aktuellste Version eines Verweises verwendet wird, wenn Sie beim Start eine passende Routine aufrufen. Welche Probleme Sie damit umgehen können, sehen Sie an folgendem Beispiel: Legen Sie in einer mit Access 2003 erstellten Datenbank (Standarddateiformat Access 2000) die Verweise aus Abbildung 17.35 an.
Abbildung 17.35: Diese Office-Verweise sollen gleich in einer älteren Office-Version zum Einsatz kommen.
Öffnen Sie die Datenbank anschließend auf einem Rechner, der mit Office 2000 ausgestattet ist. Die Datenbank lässt sich zwar ohne Probleme öffnen, aber ein Blick in die Verweise bringt schlechte Nachricht, wie Abbildung 17.36 zeigt. Bis auf die Microsoft Office-Bibliothek konnte Access die Verweise nicht anpassen.
800
17
Installation, Betrieb und Wartung
Abbildung 17.36: Die meisten Verweise fehlen unter Office 2000.
Eine Lösung sieht so aus, dass Sie vor der Weitergabe der Datenbank alle Verweise, die kritisch sein könnten, manuell aus dem Verweise-Dialog entfernen. Zusätzlich fügen Sie der Anwendung eine Funktion hinzu, die alle notwendigen Verweise manuell anlegt – und zwar in der aktuellsten Fassung. Das erledigt die folgende Routine: Public Function VerweiseAnpassen() On Error Resume Next References.Remove References("Outlook") References.AddFromGuid "{00062FFF-0000-0000-C000-000000000046}", References.Remove References("Word") References.AddFromGuid "{00020905-0000-0000-C000-000000000046}", References.Remove References("Excel") References.AddFromGuid "{00020813-0000-0000-C000-000000000046}", References.Remove References("Office") References.AddFromGuid "{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}", End Function
0, 0 0, 0 0, 0 0, 0
Listing 17.12: Automatisches Anlegen von Verweisen
Sie finden die Funktion in der Datenbankdatei Kap_17\Verweise.mdb. Interessant ist dabei jeweils die AddFromGuid-Methode. Sie enthält die GUID, unter der die jeweiligen Bibliotheken im System registriert werden. Diese ist im Übrigen für alle Versionen gleich. Die übrigen beiden Parameter enthalten die Werte für die Eigenschaften Major und Minor, die stellvertretend für die Versionsnummer mit folgendem Format sind: <Major>.<Minor>
Verweise und Probleme mit Verweisen
801
Durch die Wahl von 0 und 0 wird automatisch die aktuellste Version verwendet. Auf einem Rechner mit Windows 2000 und Office 2000 sieht das Verweise-Fenster anschließend etwa wie in Abbildung 17.37 aus.
Abbildung 17.37: Access 2000 mit korrekt eingebundenen Verweisen
Gleichnamige Objekte, Eigenschaften und Methoden in Bibliotheken Gelegentlich kommt es vor, dass ein VBA-Projekt Verweise auf zwei oder mehr Bibliotheken enthält, die mit gleich lautenden Elementen bestückt sind. Das bekannteste Beispiel dafür sind die beiden Bibliotheken DAO und ADO. Leider haben gleich lautende Elemente nicht immer genau die gleiche Funktion, ja noch nicht einmal die gleiche Syntax, sodass Sie sicherstellen müssen, dass Sie mit der richtigen Version arbeiten. Unter Access 2003 sind beide Bibliotheken eingebunden. Wie stellen Sie also fest, mit welcher Fassung der OpenRecordset-Methode Sie gerade arbeiten? Die erste Regel lautet: Bei gleichnamigen Elementen verwendet VBA immer die aus der in der Verweisliste höher angeordnete Fassung. Daraus resultiert die zweite Regel: Verlassen Sie sich nicht darauf, wie die Bibliotheken angeordnet sind, und setzen Sie bei der Deklaration explizit den Bezug auf die entsprechende Bibliothek, beispielsweise folgendermaßen: Dim db As DAO.Database
oder Dim cnn As ADODB.Connection
Anhang Hilfsmittel für Access-Entwickler Für die Entwicklung von Access-Anwendungen benötigen Sie mehr als nur eine mehr oder weniger aktuelle Version von Microsoft Access. Was sonst noch nützlich ist, erfahren Sie in der folgenden Zusammenstellung. Folgende (freie) Tools können die Arbeit mit der VBA-Entwicklungsumgebung und die Entwicklung mit Access erleichtern.
MZTools Diese Toolsammlung bietet unter anderem folgende Funktionen: Suchen und Ersetzen in mehreren Projekten gleichzeitig Auffinden von Prozeduraufrufen Erstellen einer Favoritenliste mit häufig genutzten Prozeduren Assistent zum einfachen Erstellen von Prozedurrümpfen Hinzufügen von Prozedurköpfen und Fehlerbehandlungen Code nummerieren und entnummerieren Download: http://mztools.com
Indenter Wenn Sie wie in Kapitel 6, »VBA«, vorgeschlagen für ein ansehnliches Layout in Ihren Modulen sorgen möchten, können Sie den Smart Indenter verwenden. Er nistet sich nach der Installation im Bearbeiten-Menü der VBA-Entwicklungsumgebung ein und bietet einige Optionen und Funktionen zum Einrücken des Inhalts von VBA-Modulen. Download: http://www.bmsltd.ie/indenter/default.htm
804
Anhang
ProcBrowser Der ProcBrowser ist ein Toolwindow, das einen Überblick über alle im aktuellen Modul befindlichen Deklarationen und Routinen bietet. Nach einem Klick auf den Namen eines der Elemente scrollt das VBA-Fenster zu der ausgewählten Routine. Download: http://www.moss-soft.de/public/procbrowser
accessVBATools Die accessVBATools enthalten vor allem die in den Kapiteln 10, »Fehlerbehandlung«, und Kapitel 14, »Objektorientierung im Praxiseinsatz«, vorgestellten Funktionen. Dazu gehören die automatische Nummerierung von Zeilen und das Anlegen von Fehlerbehandlungen sowie die Funktionen zum automatischen Erstellen von Objektklassen und Datenzugriffsobjekten. Download: http://www.access-entwicklerhandbuch.de
accessUnit accessUnit ist ein Tool zum testgetriebenen Entwickeln von Access-Anwendungen. Eigentlich sollte das Buch noch ein Kapitel über die testgetriebene Entwicklung enthalten und die Anwendung dieses Tools beschreiben. Da andere Themen wichtiger erschienen, musste dieses Kapitel leider weichen. Sie finden es aber samt dem Tool auf der Internetseite zu diesem Buch. Download: http://www.access-entwicklerhandbuch.de
OLConnector Der OLConnector wurde bereits am Ende von Kapitel 6, »VBA«, vorgestellt. Er dient dazu, die Sicherheitsmeldungen von Outlook beim Zugriff auf die Adressen und beim Versenden von E-Mail via VBA zu unterdrücken. Download: http://www.moss-soft.de/public/olconnector/
FileDialog Hier handelt es sich nicht um das seit Office XP in der Office-Bibliothek enthaltene Objekt, sondern um eine alternative Klasse zur Kapselung der API-Aufrufe zur Verwendung verschiedener Dialoge zum Öffnen und Speichern von Dateien und zum Auswählen von Verzeichnissen. Die Klasse bietet in einigen Punkten mehr als das entsprechende Office-Objekt. Download: http://www.kpries.de/index.htm?Entwicklertools/filedialog.htm
Internetangebote zu Access und verwandten Themen
805
Diese Liste wird ständig erweitert. Die komplette Fassung finden Sie auf der Internetseite zu diesem Buch unter http://www.access-entwicklerbuch.de. Wenn Sie auch ein (möglichst freies) Tool kennen, das Ihrer Meinung nach in diese Liste gehört, schicken Sie einfach eine E-Mail mit Ihrem Vorschlag an [email protected]. Die Links werden nach Prüfung veröffentlicht.
Internetangebote zu Access und verwandten Themen Das Internet bietet einige sehr gute Seiten zum Thema Access. Hier finden Sie einige Beispiele. Die Access-FAQ von Karl Donaubauer enthält Antworten auf die in den deutschsprachigen Newsgroups meistgestellten Fragen (http://www.donkarl.com). Unter der gleichen Domain befindet sich die Seite der AEK, der Access Entwickler Konferenz. Hier finden Sie Informationen zur jährlich stattfindenden Konferenz, aber vor allem einige Skripte zu den Vorträgen der vergangenen Konferenzen mit vielen interessanten Themen (http://www.donkarl.com/aek). Access im Unternehmen ist eines der wenigen deutschen Access-Magazine. Auf der Internetseite zu diesem Magazin gibt es einige Know-how-Beiträge inklusive Beispieldatenbanken (http://www.access-im-unternehmen.de). Das MS-Office-Forum ist eine der größten Anlaufstellen für deutschsprachige AccessEntwickler. Hier erhalten Sie nicht nur Antworten zu Fragen rund um Access und VBA, sondern auch zu anderen Office-Themen (http://www.ms-officeforum.de). Google Groups ist das Archiv aller Usenet-Beiträge seit etwa 1981. Dort finden Sie zahllose Threads zu allen möglichen Themen – natürlich auch zu Access. Damit ist dies vermutlich die umfassendste Informationsquelle zu diesem Thema (http:// www.google.de/grphp?hl=de&tab=wg&q=). fullAccess bietet eine riesige Tipps-und-Tricks-Sammlung, die regelmäßig erweitert wird (http://www.fullaccess.de). Im Access-Paradies finden Sie eine Menge Tipps und Tricks und nützliche Tools (http://www.access-paradies.de). freeaccess.de stellt eine Suchfunktion zur Verfügung, mit der einige deutsche Access-Seiten durchsucht werden. Außerdem steht hier die knowhow.mdb zum Download bereit, die eine Riesenmenge Beispiele rund um Access enthält (http://www.freeaccess.de).
806
Anhang
Das selbst ernannte Zuhause von Access heißt Access-Home und bietet viele Beispieldatenbanken und Funktionen (http://www.access-home.de). Einen Überblick über (fast) alles, was im Internet zum Thema Access zu finden ist, finden Sie bei Yaccess (http://www.yaccess.de). Und wer sich nicht scheut, mal eine englischsprachige Seite zu besuchen, wird bei Stephen Lebans oder Dev Avish fündig:
http://www.lebans.com (Webseite von Stephen Lebans mit vielen API-Lösungen) http://www.mvps.org/access (Dev Avish’s »The Access Web«) Auch die Liste der Links rund um Access wird ständig erweitert. Die komplette Fassung finden Sie auf der Internetseite zu diesem Buch unter http:// www.access-entwicklerbuch.de. Wenn Sie auch einen interessanten Link kennen, der Ihrer Meinung nach in diese Liste gehört, schicken Sie einfach eine E-Mail mit Ihrem Vorschlag an [email protected]. Die Links werden nach Prüfung veröffentlicht.
Index ! .mdb-Datei Performance 562 .mde-Datei Performance 562 .mde-Datenbank 731 .mdw-Datei manuell zuweisen 738 rekonstruieren 745 1:1-Beziehung 78, 149 im Formular 190 1:n-Beziehung 71 im Formular 192 per Listenfeld 198 per Unterformular 193 A Abbrechen-Schaltfläche 179 Abfrage Aktualisierbarkeit 140 ermitteln von Extremwerten 154 kompilieren 533 mit reflexiver 1:n-Beziehung 162 nicht aktualisierbare 141 optimieren 533 per VBA zusammensetzen 131 Performance 541 prüfen, ob kompiliert 540 Verweis auf Steuerelemente 139 Abfrage-Entwurfsansicht 336 Abfragen 127 als Datenherkunft 128 als Datensatzherkunft 128 mit Rushmore optimieren 534 Performance 532 Abfragen mit Parameter unter ADO 443
Abfragestrategien 536 AbsolutePosition 408 Abstrakte Datentypen 576 Access verschiedene Versionen auf einem Rechner 753 Access-Datenbanken per Assistent sichern 745 sichern 775 unterschiedliche Versionen starten 754 Weitergabe 755 Access-SQL 335 accessVBATools 520 acCmdDeleteRecord 182, 189 acDataErrContinue 524 acDataErrDisplay 524 acDesign 249 acDetail 255 acDialog 166, 181, 219, 249 acFooter 256 acFormAdd 166 acFormEdit 184 acHeader 256 acNormal 249 acPageFooter 256 acPageHeader 256 acPreview 249 Active Data Objects 379 ActiveCodePane 681 ActiveConnection 430 ActiveVBProject 681 ActiveWindow 681 ActiveX Data Objects 427 acWindowNormal 249 adBigInt 432
808
adBinary 432 adBoolean 432 adChar 432 adCmdTableDirect 445 adCurrency 432 adDate 432 adDBTimeStamp 432 AddFromFile 703 AddFromText 703 Addins 681 AddItem 482 AddNew 420, 450 adDouble 432 adFilterNone 449 adForwardOnly 440 adGUID 432 adInteger 432 adLockBatchOptimistic 439 adLockOptimistic 439 adLockPessimistic 439 adLockReadonly 439 adLongVarBinary 432 adLongVarWChar 432 Administrator-Konto Kennwort zuweisen 741 adNumeric 432 ADO 379, 427 ActiveConnection 430 AddNew 450 Bookmark 450 Command 443 Connection 428 ConnectionString 428 CurrentProject 428 CursorType 439 Delete 452 Filter 448 Find 447 GetRows 442 GetString 442 Open 438 Parameters 443 referenzieren 380 Seek 445 Update 450 adOpenDynamic 439
Index
adOpenForwardOnly 439 adOpenKeyset 439 adOpenStatic 439 ADORecordset 438 ADOX Catalog 430 DeleteRule 435 Table 431 Tables 431 UpdateRule 435 adPersistXML 453 Adressenverwaltung Datenmodell 86 adRICascade 435 adSearchBackward 447 adSearchForward 447 adSeekFirstEQ 445 adSeekLastEQ 445 adSingle 432 adSmallInt 432 adUseClient 440 adVarWChar 432 adWChar 432 Ändern eines Feldes per SQL 376 Änderungsvorgang Abbrechen 171 Aggregatfunktionen in SQL 347 Aktionsabfragen Ausführen per DAO 405 Aktionsabfragen ausführen per ADO 452 Aktivitätsdiagramm 32 Aktualisierungsweitergabe 65 per SQL festlegen 373 aktuellen Markierung im Code-Fenster ermitteln 691 Inhalt lesen 691 ALL 357 AllForms 220 AllTables 437 ALTER TABLE 375 Arbeitsgruppenadministrator 736 Arbeitsgruppen-Informationsdatei 736 erstellen 736 manuell zuweisen 738
Index
Performance 563 zuordnen 736 Architektur 25 Arrays 310 Artikelverwaltung Datenmodell 91 AS 341 ASC 347 Attributintegrität 63 Aufgabenverwaltung Datenmodell 99 Auflistungen 580 unter DAO 385 Auflistungsklasse Nachbildung relationaler Beziehungen 607 Auflistungsklassen benutzerdefinierte 605 Aufrufliste 504 Aufteilen einer Access-Datenbank 763 Aufzählungstypen 309 Ausdrücke per VBA überwachen 505 überwachen 505 Ausrufezeichen zwischen Objektbezeichnern 386 Außer Berichtskopf 292 Automatische Codegenerierung 676 Autowert 85 Autowert anlegen per ADOX 432 per DAO 393 Avg 347 B Bedingungen in SQL 343 BeginGroup 475 BeginTrans 230, 423 Bei Änderung 174 Bei Aktivierung 253 Bei Deaktivierung 253 Bei Fehler 253 Bei Fokuserhalt 174 Bei Fokusverlust 174 Bei Geändert 170, 174, 175 Bei Größenänderung 170
809
Bei Löschbestätigung 172 Bei nicht in Liste 175 Bei Ohne Daten 253 Bei Rückgängig 171 Bei Rückname 254 Bei Seite 253 Beim Aktivieren 170 Beim Anzeigen 167, 170, 171, 172 Beim Deaktivieren 170 Beim Drucken 254, 263 Beim Entladen 170 Beim Formatieren 254 Beim Hingehen 174 Beim Klicken 175 Beim Laden 170 Beim Löschen 172 Beim Öffnen 170, 253 Beim Schließen 170, 253 Beim Verlassen 174 Benutzer Rechte entziehen 743 Benutzerdefinierte Auflistungen 601 Benutzerdefinierte Auflistungsklassen 605 Benutzerdefinierte Ereignisse 593 Benutzerdefinierte Menüs in Runtime-Versionen 756 Benutzerdefinierte Typen 311 Benutzerdefiniertes Toolwindow 707 Benutzergruppen Rechte entziehen 743 Benutzungsoberfläche 651 Berechnungen in Berichten 289 Bereich wiederholen 269, 284 Bericht 1:n-Beziehung 276 acDetail 255 acFooter 256 acHeader 256 acPageFooter 256 acPageHeader 256 anzeigen 249 Außer Berichtskopf 292 Bei Aktivierung 253, 260 Bei Deaktivierung 253, 260 Bei Fehler 253, 261
810
Bei Ohne Daten 253, 260 Bei Rückname 254 Bei Seite 253, 261 Beim Drucken 254, 263 Beim Formatieren 262 Beim Öffnen 253, 256 Beim Schließen 253 Bereich wiederholen 269, 284 Daten aus einfachen Tabellen darstellen 271 Datensätze durchstreichen 265 Detailbereich 252 filtern 250 ForceNewPage 267 FormatCount 262 Gitternetzlinien 263 Gruppenfuß 266 Gruppenkopf 266 Gruppieren nach 266 Height 263 Höhe von Steuerelementen einstellen 262 Intervall 266 KeepTogether 267 Layout anpassen 262 Line 261 m:n-Beziehung 280 mit Unterbericht 280 Neue Seite 267, 278 Neue Zeile oder Spalte 268 NewRowOrCol 268 Öffnungsargumente auswerten 256 Rechnungserstellung 284 ReportName 249 Section 255 Seite einrahmen 261 Seitenfuß 252 Seitenkopf 252 sortieren 250 Übertrag 290 Visible 293 WillContinue 279 zum Anzeigen externer Bilddateien 111 Zusammenhalten 267 Zwischensumme 290 Berichte 249 Performance 548
Index
Berichtsbereiche 252 Zugriff auf 255 Berichtsereignisse 253 Beschriftung von Steuerelementen verschieben 272 BETWEEN 137, 344 Beziehung 1:1 78 1:n 71 m:n 75 n:1 72 reflexiv 83 Beziehung erstellen per ADOX 435 Beziehung löschen per ADOX 436 per DAO 398 Beziehungen 66 automatisch festlegen 68 Beziehungsarten in Formularen abbilden 175 Bild als Binärstrom verfügbar machen 122 in OLE-Feld speichern 106 Bilder im Bericht anzeigen 111 im Formular anzeigen 108 in Tabelle speichern 105 Bildlaufleisten 176 Bildsteuerelement Alternative 113 Binärstrom zum Speichern von Daten 114 BINARY 368 BIT 368 BOF 408, 441 Bookmark 416, 450 Bug 501 Businesslogik 668 Business-Schicht 651, 653 BYTE 368 C Catalog 430 CD-Verwaltung Datenmodell 92 CHAR 368 CHECK 370
Index
cmd.exe 784 Code einrücken 299 Code ausführen beim Start 758 Code hinzufügen per VBA 703 Code schützen 731 per Kennwort 733 Codeauslagerung 625 Abbrechen-Schaltfläche 630 Datensatz hinzufügen 633 Datensatz löschen 630 Codegenerierung automatisch 676 Code-Layout Anweisungen zusammenfassen 303 Kommentare 304 Leerzeilen 301 Zeilenumbrüche 302 CodeModule 685, 692 CodePane 692 CodePanes 681 Codeviewer 694 Codezeilen einer Prozedur auslesen 688 eines Moduls zählen 686 nummerieren 677 Collection 601 ColumnWidth 193 COM-Add-In 707 Eigenschaften anpassen 718 Ereignisprozeduren 713 Funktionen hinzufügen 726 Grundgerüst 726 Menüs hinzufügen 726 per Menübefehl aufrufen 722 COM-Add-In-Designer 711 Command 443 CommandBar 470 CommandBarControl 462 CommandBarPopup 470 CommandBars 682 CommitTrans 233, 423 Connection 428 ConnectionString 428 ConnectionString ermitteln 429
811
CONSTRAINT 367, 370 Controller in mehrschichtigen Anwendungen 656 Controls 490 Count 347, 683 COUNTER 368 CountOfDeclarationLines 687 CountOfLines 687 CREATE TABLE 367 CreateDatabase 388, 391 CreateEvent 704 CreateEventProc 703 CreateField 391, 393 CreateIndex 391, 394 CreateProperty 391 CreateQueryDef 391 CreateRelation 391, 395 CreateTableDef 391 CreateToolWindow 716 CurrentDB 382, 388 CurrentProject 428 CurrentView 220 CursorPosition 440 CursorType 439, 440 Cursor-Typen unter ADO 439 D DAO 379 AbsolutePosition 408 AddNew 420 Auflistungen 385 BeginTrans 423 BOF 408 Bookmark 416 CommitTrans 423 CreateDatabase 388, 391 CreateField 391, 393 CreateIndex 391, 394 CreateProperty 391 CreateQueryDef 391 CreateRelation 391, 395 CreateTableDef 391 CurrentDB 382, 388 Database 382, 388, 390 Databases 388
812
Daten bearbeiten 406 DBEngine 382, 386 DBEngine(0)(0) 382 dbFailOnError 422 Delete 421 Edit 420 EOF 406, 408 Errors 387 Execute 422 Fields 411 FindFirst 414 FindLast 414 FindNext 414 FindPrevious 414 Groups 388 Index 417 MoveFirst 407 MoveLast 407 MoveNext 406 MovePrevious 407 NoMatch 413 Objekte deklarieren 383 Objekte instanzieren 383 Objekte referenzieren 386 Objektmodell 381 OpenDatabase 388 OpenRecordset 400 PercentPosition 408 QueryDef 404, 422 RecordCount 409 RecordsAffected 422 Recordset 384 referenzieren 380 Requery 410 Rollback 423 Seek 412 Sort 416 Update 420 Users 388 Workspace 382, 387 Workspaces 387 DAO-Pattern 668 Data Access Objects 379 Data Definition Language 335 Data Manipulation Language 335 Database 382, 388, 390 Databases 388
Index
DataErr 524 DataLinks 429 DataMode 166, 183 Datei aus OLE-Feld extrahieren 119 in Datenbank importieren 117 in OLE-Feld importieren 114 in Tabelle speichern 105 Daten aktualisieren 363 Daten an bestehende Tabelle anfügen 364 Daten ausgeben per DAO 411 Daten auswählen mit SQL 339 Daten bearbeiten per DAO 406, 419 Daten löschen 363 Daten manipulieren 363 Daten sperren 402 per ADO 439 Datenbank Kopieren mit FileCopy 778 Kopieren per API-Funktion 778 Kopieren und komprimieren 779 Kopieren und zippen 779 replizieren 768 verschlüsseln 734 Wiedereinbinden 764 Datenbank absichern 740 Datenbank erzeugen 388 Datenbank öffnen mit DAO 388 Datenbank referenzieren 388 Datenbank reparieren 793 Datenbankanwendung planen 23 Datenbank-Backend sichern 778 Datenbanken komprimieren 761 Datenbanken reparieren 761 Datenblatt im Unterformular 185 Standardansicht 194 Datenblattansicht anpassen 186 ColumnWidth 193
Index
im Unterformular 185 in Formularen 184 Datenherkunft 128 per VBA zuweisen 131 Datenmodell Adressenverwaltung 86 Artikelverwaltung 91 Aufgabenverwaltung 99 CD-Verwaltung 92 Fahrtenbuch 104 Kundenverwaltung 86, 102 Literaturverwaltung 95 Mitarbeiterverwaltung 95 Mitgliederverwaltung 97 Normalisieren für bessere Performance 527 Präsenteverwaltung 102 Projektverwaltung 94 Projektzeitverwaltung 100 Rezepteverwaltung 89 Urlaubsverwaltung 99 Datenmodell erstellen 366 Datenmodell manipulieren 366 Datensätze Anzahl ermitteln per DAO 409 durchstreichen im Bericht 265 Filtern per DAO 416 Löschen im Formular 182 mehrfach anzeigen 157 nummerieren 160 Sortieren per DAO 416 Datensätze bearbeiten per ADO 450 Datensätze durchlaufen per ADO 440 per DAO 406 Datensätze suchen per ADO 444 per DAO 412 Datensatz bearbeiten 170 Details anzeigen 183 löschen 171 per DAO ansteuern 407 Datensatz anlegen per ADO 450 per DAO 420
813
Datensatz bearbeiten per ADO 451 Datensatz in Array speichern 442 Datensatz löschen in mehrschichtigen Anwendungen 667 per ADO 452 per DAO 421 Datensatzanzahl ermitteln per ADO 440 Datensatzgruppe laden 453 Datensatzgruppe speichern 453 Datensatzgruppen Ereigniseigenschaften 455 Datensatzherkunft 128, 192 Datensatzmarkierer 176 Datensatzzeiger aktuelle Position ermitteln per DAO 408 Datenschicht 655 Datentypen Performance 531, 550 unter ADO 432 Datenzugriff Performance 560 Datenzugriffobjekt Zugriff auf Datenschicht 657 Datenzugriffsobjekte automatisch erstellen 669 Datenzugriffsschicht 651, 654 DATETIME 368 Datumsangabe als Kriterienausdruck 137 Datumsangaben als Vergleichswert 345 dbAppendOnly 401 dbBigInt 392 dbBinary 392 dbBoolean 393 dbByte 393 dbChar 393 dbConsistent 401 dbCurrency 393 dbDate 393 dbDecimal 393 dbDenyRead 401 dbDenyWrite 401
814
dbDouble 393 DBEngine 382, 386 DBEngine(0)(0) 382 dbFailOnError 422 dbFloat 393 dbForwardOnly 401 dbGUID 393 dbInconsistent 402 dbInteger 393 dbLong 393 dbLongBinary 393 dbMemo 393 dbNumeric 393 dbOpenDynase 401 dbOpenForwardOnly 401 dbOpenSnapshot 401 dbOpenTable 401 dbOptimistic 402 dbPessimistic 402 dbReadOnly 402 dbRelationDeleteCascade 397 dbRelationDontEnforce 397 dbRelationInherited 397 dbRelationLeft 397 dbRelationRight 397 dbRelationUnique 397 dbRelationUpdateCascade 397 dbSeeChanges 402 dbSingle 393 dbSQLPassThrough 402 dbText 393 dbTime 393, 432 dbTimeStamp 393 dbUnsignedTinyInt 432 dbVarBinary 393 DDL 335 Debug.Print 502 Debuggen Symbolleiste 501 Debugging in der VBAEntwicklungsumgebung 501 DefaultValue 197 Deklarationsbereich Zeilen zählen 687 DELETE 363 Delete 182, 421, 452 DeleteLines 704
Index
DeleteRule 435 DESC 347 Detailansicht einfacher Daten 175 Digitale Signaturen 787 Dim 310 Direktbereich 706 Direktfenster 502 Dirty 174 DISTINCT 358 DISTINCTROW 358 DML 335 Do While 317, 440 DoCmd acCmdDeleteRecord 182, 189 acDesign 249 acDialog 249 acFormAdd 181 acFormEdit 184 acNormal 249 acPreview 249 acWindowNormal 249 DataMode 181 FilterName 249 OpenArgs 249 OpenForm 166, 181, 219, 599 OpenReport 249 RunCommand 182 SetWarnings 183 View 249 WhereCondition 184, 249 WindowMode 249 DoCmd.DataMode 183 Doppelte Datensätze behandeln 357 Dritte Normalform 59 DROP 377 DROP INDEX 377 DROP TABLE 377 DropDownLines 482 DropDownWidth 482 E Early Binding 328, 329 Performance 551 Edit 420 Eigenschaft Zugriff kontrollieren 586 Eindeutigen Index anlegen 371
Index
Eingabeformate mögliche Zeichen 63 Eingabevalidierung 238 Endlosformular mit einfachen Daten 180 Entitätsintegrität 64 Entwicklungsumgebung anpassen 673 programmieren 678 Enumerate 606 EOF 406, 440, 441 Ereignis abfangen 593 anlegen 167 eigenes anlegen 596 Ereigniseigenschaft 168 Ereigniseigenschaften von Datensatzgruppen 455 Ereignisprozedur 168 Ereignisprozeduren hinzufügen per VBA 704 Ereignisse in Berichten 253 Reihenfolge 168 von Steuerelementen 173 von Textfeldern 174 von Unterformularen 173 Ergonomie 35 Erl 517, 677 Err 510 Errors 387 Erste Normalform 51 Erweiterung der Entwicklungsumgebung 676 Event 596 Execute 422 Exit 319, 593 Exklusiver Zugriff Performance 563 Extremwert von Gruppierung ermitteln 155 Extremwerte ermitteln per Abfrage 154 per Unterabfrage 156
815
F FaceID 475 Fahrtenbuch Datenmodell 104 Fehler auswerten 509 benutzerdefinierte 515 Fehlerarten 497 Fehlerbehandlung Ablauf 509 automatisch hinzufügen 520 funktionale Fehlerbehandlung 512 in Formularen 523 in Runtime-Versionen 757 in VBA 507 per Knopfdruck 676 Fehlerdokumentation 516 Fehlerinformationen 516 Fehlermeldungen benutzerdefiniert 507 Felder referenzieren per DAO 411 Feldinhalt ändern 170 Feldliste 176 Feldnamen 47 ersetzen mit AS 341 Sonderzeichen 341 Fields 411 FileCopy 778 Filter 250, 251, 448 Filter aktiv 250 Filtern in Berichten 250 per ADO 448 zur Laufzeit 251 FilterName 249 FilterOn 252 Find Suche in Modulen 693 unter ADO 447 FindFirst 414 FindLast 414 FindNext 414 FindPrevious 414 First 347
816
FLOAT 368 For Each 317 Auflistungen durchlaufen 385 For Next 316 Auslistungen durchlaufen 385 ForceNewPage 267 FOREIGN KEY 372 Form_Dirty 230 FormatCount 262 Formular Anzeige von reflexiven Beziehungen 214 Aufruf weiterer Formulare 218 aufrufen 219 auslesen und schließen 219 Bearbeitung von Daten verhindern 193 Bei Geändert 170 Bei Größenänderung 170 Bei Löschbestätigung 172 Bei Rückgängig 171 Beim Aktivieren 170 Beim Anzeigen 170, 171, 172 Beim Deaktivieren 170 Beim Entladen 170 Beim Laden 170 Beim Löschen 172 Beim Öffnen 170 Beim Schließen 170 Daten anlegen 196 Daten bearbeiten 196 Daten löschen 196 Datenblattansicht 184 Ereignis 166 IsLoaded 220 mit leerem Datensatz öffnen 181 modal öffnen 181 Nach Aktualisierung 171 Nach Eingabe 171 Nach Löschbestätigung 172 neue Instanz erzeugen 639 öffnen 165 Performance 543 Recordset 225 Standardfunktionen auslagern 623 Suchen in 243 Transaktion 230
Index
Undo 223 Visible 220 Vor Aktualisierung 171 Vor Eingabe 170 zum Anzeigen externer Bilddateien 108 Formulardaten Performance 543 Formularfehler behandeln 524 dokumentieren 525 Formularinstanzen alle schließen 645 in einer Collection 642 mehrere anzeigen 638 Fremdschlüsselfeld 67 benennen 48, 67 Fremdschlüsselfelder festlegen 372 Funktionen als Vergleichswert in SQL 346 G Geplante Tasks 780 Gespeicherte Abfrage als Datenherkunft 130 Gespeicherte Abfragen Vergleich mit Ad-hoc-Abfragen 540 gespeicherter Abfrage Performance 561 GetRows 442 GetSelection 691, 692 GetString 442 Globale Variablen 312 GlobalMultiUse-Klassen 577 GoTo 320 Goto 511 GROUP BY 349 Groups 388 Gruppenfuß 252, 266 Gruppenkopf 252, 266 Gruppieren nach 266 Gruppieren von Daten 348 Gruppierungen einschränken 350 Gültigkeitsregel festlegen 370 GUID 85, 368 GUI-Schicht 651, 653
Index
H Haltepunkt 503 HAVING 350 Height 263 Hierarchie anzeigen per Treeview 214 Hinzufügen eines Feldes per SQL 376 I IDTExtensibility2 708 If Then 313 Performance 559 IIf Performance 559 IMAGE 368 IN 344, 357 Index 417 Index erstellen per ADOX 433 per DAO 394 Index Intersection 534 Index löschen per ADOX 434 per DAO 395 per SQL 377 Index Range 533 Index Union 534 Indizes Performance 529 Initialize 593, 656 INNER JOIN 353 INSERT INTO 364 mit UNION-Abfrage 145 InsertLines 703 INTEGER 368 Integrität referentielle 64 semantisch 62 von Werten 61 Integritätsregeln 61 Intervall 266 IsLoaded 220 IsMissing 323
817
J Jet Compact Utility 795 Jet Expression Service 784 Jet-Engine 533 Jetshowplan 536 Joker unter ADO und SQL 444 K Kapselung 576 KeepTogether 267 Keine Duplikate 358 Kennwortschutz 733 Klasse Eigenschaften 584 Methoden 591 Standardereignis 593 Klassen 576 Klassenmodul Anlegen 583 benennen 583 Klassenmodule 583 Kombinationsfeld Bei Änderung 175 Bei Fokuserhalt 175 Bei Fokusverlust 175 Bei Geändert 175 Bei nicht in Liste 175 Beim Hingehen 175 Beim Klicken 175 Beim Verlassen 175 Datensatzherkunft 192 Ereignisse 174 in Menüs 481 mit UNION-Abfrage 142 Nach Aktualisierung 175 Performance 546 Schnellauswahl 633 Spaltenanzahl 192 Spaltenbreiten 192 Vor Aktualisierung Bei Änderung 175 zur Schnellauswahl 243 Kommentare 304 kompilierten Abfragen Performance 539
818
Komprimieren Performance 563 Konstanten 305 Kontextmenü an ein Steuerelement binden 488 Hinzufügen per Benutzungsoberfläche 486 Hinzufügen per VBA 489 löschen 489 Kontextmenüs 458 Kontrollstrukturen 312 Kopieren mit FileCopy 778 per API-Funktion 778 Kopieren und komprimieren 779 Kopieren und zippen 779 Kriterienausdruck Zahlenwert 136 Zeichenkette 136 Kriterienausdrücke Datumsangabe 137 mit SQL unter VBA 136 Kundenverwaltung Datenmodell 86, 102 L Last 347 Late Binding 328 Performance 551 Laufvariablen Benennung 308 Laufzeitfehler 500 Layout von Code 298 LBound 310 Lebensdauer von Objekten 583 Leere Berichte vermeiden 260 LEFT OUTER JOIN 355 Lesezeichen 416, 450 LIKE 344 Line 261 Lines 690 Listenfeld als Datenübersicht 188 anlegen 188 Datensatzherkunft 208 Detailansicht per Doppelklick 199
Index
Performance 546 Requery 189 Listenfelder filtern 246 Literaturverwaltung Datenmodell 95 LockType 439 Löschen von Datensätzen 182 Löschen einer Tabelle per DAO 393 Löschen eines Feldes per SQL 376 Löschweitergabe 65 per SQL festlegen 373 Logische Ausdrücke Performance 557 Logische Fehler 500 Lokal-Fenster 506 Lokalfenster 706 LONGTEXT 368 Lookup-Beziehung 72 Loop 318 M m:n-Beziehung 75 Daten entfernen per Listenfeld 212 Daten hinzufügen per Listenfeld 210 Hauptformular 201 in Berichten 280 in Formularen 199 per Listenfeld 205 suchen in 145 Unterformular 202 Makro-Sicherheitsstufe 792 Max 348 Me Performance 554 Mehrbenutzerbetrieb 762 Mehrschichtige Anwendungen 650 MenuBar 467 Menü Einträge ausgeben 463 Hinzufügen per Benutzungsoberfläche 465 Hinzufügen per VBA 467 importieren 459 Kombinationsfeld 481
Index
positionieren 492 Steuerelement referenzieren 490 Trennlinien einfügen 475 Menüleiste 457 beim Anwendungsstart ersetzen 491 Menüs 457 Menüschaltfläche aktivieren 478 deaktivieren 478 Menüschaltflächen Eigenschaften einstellen 477 Microsoft Visual Studio 6.0 709 Min 348 mit DAO 388 Mitarbeiterverwaltung Datenmodell 95 Mitgliederverwaltung Datenmodell 97 Modaler Dialog 181 Model View Controller Pattern 668 Modul anlegen 684 entfernen 685 per VBA ausgeben 690 Modul entnummerieren 520 Modul nummerieren 520 Module auflisten 683 eines Projekts anzeigen 695 MONEY 368 MoveFirst 407 MoveLast 407 MoveNext 406, 440 MovePrevious 407 msoBarBottom 493 msoBarFloating 493 msoBarLeft 493 msoBarMenuBar 493 msoBarPopup 489, 493 msoBarRight 493 msoBarTop 493 msoControlButton 462 msoControlDropdown 462 msoControlPopup 462, 468 MSysObjects 540 MultiUse-Klassen 577
819
N n:1-Beziehung 72 im Formular 191 Nach Aktualisierung 132, 171, 174 Nach Eingabe 171 Nach Löschbestätigung 172 Nachschlagefeld von Hand anlegen 191 Namenskonvention 44 für Felder 44 für Tabellen 44 Namenskonventionen in VBA 297 NavigateComplete2 596 Navigationsschaltflächen 176 Neue Seite 267, 278 Neue Tabelle erstellen 366 Neue Zeile oder Spalte 268 Neuer Datensatz in mehrschichtigen Anwendungen 663 NewRowOrCol 268 NoMatch 413, 447 Normalform dritte 59 erste 51 zweite 56 Normalisieren 50 halbautomatisch 50 Normalisierung 49 NOT NULL 371 Nothing 312 Null 135 als Vergleichswert in SQL 346 NUMERIC 368 Nummerieren per Unterabfrage 161 Nummerieren von Codezeilen 705 Nummerierung von Datensätzen in Abfragen 160 Nummerierungen entfernen 520 Nummerierungen hinzufügen 520 O Objekt erzeugen 581 instanzieren 578
820
Lebensdauer 583 Zugriff auf Eigenschaften 581 Zugriff auf Methoden 582 Objekte 577 Beispiele 578 eingebaute 577 Objektinstanzen Vorhandensein per Fehlerbehandlung prüfen 513 Objektkatalog 328 Objektklassen automatisch erstellen 669 Objektnamen-Autokorrektur Performance 563 Objektorientierte Programmierung 573 Objektorientierung in der Praxis 623 Objektvariable 587 lesen 590 Objektvariablen Performance 553 Öffentliche Eigenschaften 585 Office-Anwendungen per VBA steuern 331 OK-Schaltfläche 179 OldValue Textfeldeigenschaft 174 OLE-Feld Datei extrahieren aus 119 mit Datei füllen 114 zum Speichern von Bildern 106 ON DELETE CASCADE 374 On Error Goto 508 On Error Goto 0 508 On Error Resume Next 508 ON UPDATE CASCADE 374 Open 438 OpenArgs 249 OpenDatabase 388 OpenForm 166 OpenRecordset 134, 400 OpenReport 249 Operatoren in SQL 344 Option Base 310 Option Explicit 499 Optional 323
Index
ORDER BY 339, 346 OrderBy 251 OrderByOn 251 OUTER JOIN 355 Outlook Zugriff auf E-Mails 331 P ParamArray 325 Parameter in Abfragen 131 in SQL-Abfragen 360 PARAMETERS 360 Parameters 133, 443 per SQL 370 per Unterabfrage zum Ermitteln von Extremwerten 156 PercentPosition 408 Performance 527 messen 565 Performance-Test 565 Pflichtenheft 23 Platzhalter unter ADO und SQL 444 Präsenteverwaltung Datenmodell 102 Preserve 310 Primärschlüssel in Formularen 178 per SQL 370 Primärschlüsselfeld 67 benennen 47 Primärschlüssel anlegen 370 ProcBodyLine 688 ProcCountLines 688 ProcStartLine 687, 688 Projektbrowser 706 Projektverwaltung Datenmodell 94 Projektzeitverwaltung Datenmodell 100 Property Get 586, 589 Property Let 586, 588 Property Set 586, 589 Prozedur Code auslesen 701 Zeilenanzahl 688
Index
Prozedurbrowser 674 Prozedurliste eines Moduls anzeigen 697 Punkt zwischen Objektbezeichnern 386 Q Quellcode lesen per VBA 685 manipulieren 702 QueryDef 133, 404, 422 R Raise 515 RaiseEvent 596 REAL 368 Rechnungserstellung 284 Rechtschreibprüfung Performance 565 RecordCount 409 RecordsAffected 422 Recordset 225, 384, 438 als Datenherkunft 227 auf Basis einer Tabelle öffnen 402 Delete 182 Eigenschaft von Steuerelementen 133 Öffnen auf Basis eines QuerydefObjekts 404 Öffnen auf Basis eines Recordsets 403 Recordset ausgeben per ADO 442 RecordsetTyp 188 RecordSource 131 ReDim 310 Referentielle Integrität 64 festlegen 69 Reflexive 1:n-Beziehung 83 in Abfragen 162 Reflexive Beziehung 83 in Formularen 214 Reflexive m:n-Beziehung 84 Rekursive Beziehung siehe Reflexive Beziehung Remove 685 Reparaturversuch 795 ReplaceLine 703
821
Replikation 768 Erzeugen weiterer Replikate 770 Funktionsweise 769 ReportName 249 Requery 189, 410 Response 524 Resume 511 Resume Next 511 Rezepteverwaltung Datenmodell 89 RIGHT OUTER JOIN 355 Rollback 232, 423 Routinen 320, 322 Lose Kopplung 322 Parameter 323 Rückgabewerte 323, 326 Starker Zusammenhalt 322 Routinenarten 321 Routinennamen 321 RowSource 131 RunCommand 182 Runtime-Simulation 757 Rushmore 534 Rushmore Restriction 533 S Sandbox 784 Sandbox-Modus 784 Sandbox-Modus einstellen 792 Saved 684 Schaltfläche zu Menü hinzufügen per VBA 473 Schaltflächen zu Menü hinzufügen per Benutzungsoberfläche 471 Schichten Zusammenhänge 655 Schleifen Performance 559 Schließen einer Datenbank Aktion ausführen 760 Schnellauswahl per Kombinationsfeld 243 Schnittstelle erstellen 618 implementieren 619 Zugriff auf 619
822
Schnittstellen 26, 615 Anwendungsmöglichkeiten 621 Schnittstellenvererbung 615 Anwendungsmöglichkeiten 621 Realisierung 618 zur Vereinheitlichung 617 Schutz vor bösartigem Code 783 vor bösartigen SQL-Statements 784 SearchDirection 447 Section 255 Seek 412 unter ADO 445 SeekOption 445 Seitenfuß nur auf bestimmten Seiten 292 Seitenkopf nur auf bestimmten Seiten 292 SELECT 339 Select Case 315 SELECT INTO 366 SelectedVBComponent 682 semantische Integrität 62 SetWarnings 183 showplan.out 536 Sicheres Ausführen von AccessAnwendungen 783 Sicherheit 731 Sicherheitseinstellungen per Registry vornehmen 792 Sicherheitssystem 735 aktivieren 739 Leistungen 735 Sicherheitswarnungen deaktivieren 785 Sichern eines Datenbank-Backend 778 Sichern von Access-Datenbanken 775 Sicherungsstrategie 779 Skalare Variable 587 lesen 589 SkipRecords 447 SMALLINT 368 Snapshot 188 Snapshot-Format 750 Sonderzeichen in Feldnamen 341 Sort 416
Index
Sortieren in Berichten 250 per ADO 449 zur Laufzeit 251 Sortieren per SQL 346 Sortiert nach 250 Sortierung aktiv 250 Spaltenanzahl 192 Spaltenbreiten 192 Speichern in mehrschichtigen Anwendungen 664 Sprungmarken 320 SQL 335 Aggregatfunktionen 347 ALL 357 ALTER TABLE 375 AS 341 ASC 347 Avg 347 Bedingungen 343 BETWEEN 137, 344 CHECK 370 CONSTRAINT 367, 370 Count 347 CREATE TABLE 367 Daten aktualisieren 363 Daten auswählen 339 Daten löschen 363 Daten manipulieren 363 Datentypen 368 Datumsangaben 345 DELETE 363 DESC 347 DISTINCT 358 DISTINCTROW 358 DROP 377 DROP INDEX 377 DROP TABLE 377 Eindeutigen Index anlegen 371 Einsatzmöglichkeiten 338 First 347 FOREIGN KEY 372 Fremdschlüsselfelder festlegen 372 GROUP BY 349 Gruppierung 349 Gültigkeitsregel festlegen 370
Index
HAVING 350 IN 344, 357 INNER JOIN 353 INSERT INTO 364 Last 347 LEFT OUTER JOIN 355 LIKE 344 Max 348 Min 348 Neue Tabelle erstellen 366 NOT NULL 371 ON DELETE CASCADE 374 ON UPDATE CASCADE 374 Operatoren 344 ORDER BY 339, 346 OUTER JOIN 355 Parameter 360 PARAMETERS 360 Primärschlüssel anlegen 370 RIGHT OUTER JOIN 355 SELECT 339 SELECT INTO 366 Sortieren 346 Standardwert 369 StDev 348 StDevP 348 Sum 348 Tabellen erstellen 367 Tabellen festlegen 341 Tabellennamen ersetzen per Alias 342 TOP 360 TOP, Beispiel 155 UNION 361 UNIQUE 371 Unterabfragen 357 UPDATE 363 VALUES 365 Var 348 VarP 348 Vergleichsausdrücke 344 Verknüpfen von Tabellen 352 Versionen 335 WHERE 339 Zeichenketten 345 Zugriff auf externe Datenquellen 362 SQL-Ansicht 337 SQL-Ausdruck als Datenherkunft 129
823
Standardansicht 203 im Unterformular 185 Standardereignis 593 Standardwert 369 Starten einer Datenbank Code ausführen 758 Formular anzeigen 759 Statusvariablen Benennung 308 StDev 348 StDevP 348 Steuerelement Ereignisse 173 Verweis aus Abfrage 139 Steuerelemente Performance 544 Referenzieren im Unterformular 186 Vergrößerbar 268 Verkleinerbar 268 String 310 Structured Query Language 335 Style 482 Suche in Formularen 243 Suchen in m n-Beziehung 145 Sum 348 Summenbildung in Berichten 289 Symbole eigene Symbole in Menüs 479 Symbole für Menüs anzeigen 475 Symbolleiste eingebaute Symbolleiste deaktivieren 492 hinzufügen 485 Symbolleisten 458 Symptome bei beschädigter Datenbank 794 Synchronisation 770 auf Feldebene 771 Syntaxfehler 497 Syntaxprüfung 499 SYSTEM.MDW 736 Systemaufbau 26
824
T Tabelle als Datenherkunft 129 Prüfen auf Vorhandensein 399 prüfen auf Vorhandensein 437 Tabelle ändern per SQL 375 Tabelle anlegen per ADO 430 Tabelle erstellen per DAO 391 Tabelle löschen per ADOX 433 per SQL 377 Tabellen 43 alle ausgeben per DAO 398 als Verknüpfung einbinden 763 importieren in neue Datenbank 763 Performance 527 Tabellen auflisten 437 Tabellen erstellen 367 Tabellennamen 45 Table 431 Table Scan 533 Tables 431 Temporäre Tabellen 47 Temporäre Variablen Benennung 308 Temporary 467 TEXT 368 Text Textfeldeigenschaft 174 Textfeld Bei Änderung 174 Bei Fokuserhalt 174 Bei Fokusverlust 174 Bei Geändert 174 Beim Hingehen 174 Beim Verlassen 174 Dirty 174 Nach Aktualisierung 174 OldValue 174 Text 174 Value 174 Vor Aktualisierung 174 Textfelder Ereignisse 174
Index
TooltipText 474 Toolwindow anlegen 709 Anzeigen 719 benutzerdefiniert 707 testen 719 Toolwindows 706 TOP 155, 360 Transaktion BeginTrans 230 CommitTrans 233 inFormularen 230 Rollback 232 Transaktionen unter ADO 453 unter DAO 422 Transaktionsstatus 226 Treeview-Steuerelement 214 Auswahl von Daten 217 mit Daten füllen 215 Trennlinien 176 Trennlinien einfügen 475 Type 682 Type Libraries 327 U UBound 310 Übertrag in Berichten 290 Überwachungsausdrücke 505, 706 UML 29 Undo in Haupt- und Unterformular 223 Ungarische Notation 44 Ungebundene Recordsets 454 UNION 361 UNION-Abfrage als Datensatzherkunft 142 eindeutige Schlüssel 143 mit INSERT INTO 145 UNION-Abfragen 142 UNIQUE 371 UniqueValues 358 Unterabfragen 357 Unterbericht 280 in Hauptbericht einbinden 281 über mehrere Seiten 284
Index
Unterdatenblätter Performance 564 Unterformular Datenblattansicht 185, 203 Eingabe von Daten ohne Detaildatensatz 222 Endlosansicht 203 Performance 546 Steuerelemente referenzieren 186 Undo 223 Verknüpfen nach 195 Verknüpfen von 195 Unterformulare 222 Ereignisse 173 Untermenü Hinzufügen per Benutzungsoberfläche 468 Hinzufügen per VBA 470 Unterstrich in Objektnamen 48 UPDATE 363 Update 420, 450 UpdateRule 435 Urlaubsverwaltung Datenmodell 99 Use-Case-Diagramm 29 Userdocument 712 Users 388 V Validieren bei der Eingabe 238 Sonderfälle 241 vor dem Speichern 239 Validierung 238 Value Textfeldeigenschaft 174 VALUES 365 Var 348 Variablen 307 Performance 550 Variablendeklaration erzwingen 499 Variablennamen 307 VarP 348 VB_UserMemId 605
825
VBA 297 Arrays 310 Aufzählungstypen 309 Benutzerdefinierte Typen 311 Code einrücken 299 Code-Layout 298 Do While 317 Exit 319 For Each 317 For Next 316 Globale Variablen 312 GoTo 320 If Then 313 Kommentare 304 Konstanten 305 Kontrollstrukturen 312 Namenskonventionen 297 Objektkatalog 328 Performance 550 Routinen 320 Routinenarten 321 Routinennamen 321 Select Case 315 Sprungmarken 320 Variablen 307 Variablennamen 307 Zugriff auf Bibliotheken 327 Zugriff auf Menüs 462 Zugriff auf Office-Anwendungen 331 VBA in Formularen Performance 547 VBA-Entwicklungsumgebung 673 Objektmodell 680 VBA-Projekt signieren 787 VBComponent 682 vbext_ct_Document 684 vbext_pk_Get 687 vbext_pk_Let 687 vbext_pk_Proc 687 vbext_pk_Set 687 vbObjectError 515 VBProjects 682 Vererbung 615 Vergleichsausdrücke in SQL 344 Vergrößerbar 268 Verkleinerbar 268
826
Verknüpfen nach 195 Verknüpfen von 195 Verknüpfung wiederherstellen 764 Verknüpfungstabelle 77 Verschlüsseln einer Datenbank 734 Verweise 327, 796 arbeiten ohne Verweise 798 View 249 Visible 220 Visual Basic for Applications Extensibility 5.3 680 Visual Studio Tools für Microsoft Office System 756 Vor Aktualisierung 171, 174 Vor Eingabe 170 W Webbrowser-Control 594 Weitergabe Verweise 799 Weitergabe ohne Runtime 757 Weitergabe von AccessDatenbanken 755 mit Runtime 755 ohne Runtime 757
Index
Wertbereichsintegrität 61 WHERE 339 WhereCondition 184, 249 Wiedereinbinden von Tabellen 764 WillContinue 279 WindowMode 166, 219, 249 WithEvents 594 Workspace 382, 387 Workspaces 387 Z Zählervariablen Benennung 308 Zahlen als Vergleichswert 344 Zeichenketten als Vergleichswert 345 Zeilen nummerieren 517 Zugriff auf Bibliotheken 327 Zusammengesetzte Primärschlüssel per SQL 375 Zusammenhalten 267 Zweite Normalform 56 Zwischensumme in Berichten 290
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