Klassenmodule und benutzerdefinierte Typen

Die allermeisten Prozeduren und Funktionen des VBA-Projekts Ihrer Datenbank werden Sie in ganz normalen Modulen unterbringen. Dabei wird leicht übersehen, dass es sich etwa bei Formularen um eine andere Art Module handelt, nämlich um Klassenmodule. Diese können Sie aber auch selbst anlegen. Erfahren Sie hier, wie Sie dabei vorgehen. Wir leiten das Thema zunächst jedoch mit den benutzerdefinierten Typen ein, welche Klassenmodule verständlicher machen.

Beispieldatenbank

Die Beispiele dieses Artikels finden Sie in der Datenbank 1601_Klassenmodule.accdb

Benutzerdefinierte Typen

Ein benutzerdefinierter Typ ist nichts weiter, als eine zusammengefasste Struktur von Variablen.

Interessanterweise findet man Beispiele dafür im VBA-Objektkatalog weder in der Access-Bibliothek, noch unter den VBA- oder Office-Bibliotheken. Also stellen wir gleich eine eigene Kreation vor:

Public Type TAdresse
     ID As Long
     Nachname As String
     Vorname As String
     Strasse As String
     PLZ As String
     Ort As String
End Type

Der reservierte Begriff Type definiert, dass unter dem folgenden Namen TAdresse eine Variablenstruktur folgt. Solche Typen können im Gültigkeitsbereich auch mit Private gekennzeichnet sein, was sie nur für das Modul zugreifbar macht, in dem sich die Deklaration befindet. Public bedeutet hier, dass nun das gesamte VBA-Projekt den Typen kennt und auch aus anderen Modulen heraus verwendet werden kann.

Die einzelnen Variablen des Typs werden in den folgenden Zeilen durch Namen und Variablentypen definiert. Jeglicher Datentyp ist hier möglich – etwa auch As Object.

Wozu ist das gut Kann man die Elemente der Struktur nicht auch als einzelne Variablen deklarieren Es gibt sicher viele Fälle, für die die Antwort hier Ja lautet. Doch schauen wir uns an, welche Vorteile der benutzerdefinierte Typ bietet. Denn er verhält sich genau so, wie jeder andere Datentyp. Also kann er einer Variablen zugeordnet werden:

Dim A As TAdresse

Der Zugriff auf die Elemente des Typs geschieht über Punkt und Namen:

A.Nachname = "Minhorst"
A.Vorname = "André"
A.Ort = "Duisburg"
Debug.Print A.Nachname

Der Code lässt sich noch über die With-Anweisung vereinfachen:

With A
     .Nachname = "Minhorst"
     .Vorname = "André"
     .Ort = "Duisburg"
     Debug.Print .Nachname
End With

Das wäre schon einmal Vorteil Nummer eins. Nach Eingabe von A und Punkt stellt das IntelliSense von VBA gleich alle Elemente des Typs in der Auswahlliste vor. Und das übrigens auch innerhalb des With-Blocks, wo allein die Punkt-Eingabe für das Auslösen von IntelliSense ausreicht.

Besonders nützlich sind benutzerdefinierte Typen jedoch dann, wenn Sie in Arrays Verwendung finden:

Dim Adressen() As TAdresse
ReDim Adressen(99)
Adressen(0).Nachname = "Minhorst"
Adressen(0).Vorname = "André"
Adressen(1).Nachname = "Trowitzsch"
Adressen(1).Vorname = "Sascha"
...

Das Array Adressen ist vom Typ TAdresse. Es wird im ersten Schritt auf Hundert Elemente dimensioniert. Danach folgt die Zuweisung der Werte einerseits über die Indizes des Arrays, andererseits über den Namen der Elemente. Wollte man das über separate Variablen erreichen, so benötigte man für jedes Element des Typs ein eigenes gesondertes Array. Das machte die Angelegenheit deutlich unüberschaubarer, als es das Handling mit einem Array des benutzerdefinierten Typs erlaubt.

Die Sache lässt sich noch weiterspinnen. Durch den Fakt, dass ein Typelement Variablen beliebigen Datentyps aufweisen darf, kommt für dieses selbst ein benutzerdefinierter Typ infrage:

Public Type TAdressen
     Items() As TAdresse
End Type

Zwar enthält der Typ nur ein Element, welches jedoch als Array des benutzerdefinierten TAdresse ausgelegt ist. Damit ist nur eine Variable vom Typ TAdressen imstande mehrere Adressen zu speichern:

Dim A As TAdressen
ReDim A.Items(99)
With A
     .Items(0).Nachname = "Minhorst"
     .Items(0).Vorname = "André"
     .Items(1).Nachname = "Trowitzsch"
     .Items(1).Vorname = "Sascha"
    ...
End With
Debug.Print A.Items(1).Vorname

Auch hier führt der benutzerdefinierte Typ zu strukturierterer Programmierung. Treiben wir es noch einen Schritt weiter. Es lägen etwa Adressen für verschiedene Anwendungsbereiche vor, seien es Kundenadressen, Ansprechpartner, Behörden, Firmen.

Dann könnten alle Adressen zusammen wiederum in einen neuen Typ überführt werden:

Public Enum eAddressType
     eAddressUnspecific = 0 
     eAddressCustomer = 1
     eAddressPartner = 2
     eAddressCompany = 3
End Enum
Public Type TAdressenGesamt
     AddressType As sAddressType
     AddressBlock As TAdressen
End Type

Die Zuordnung der Werte wird nun zwar etwas länglich, ist aber dennoch gut zu überblicken:

Dim A() As TAdressenGesamt
ReDim A(3)
A(1).AddressType = eAddressCustomer
Redim A(1).AdressBlock.Items(9)
With A(1).AdressBlock
    .Items(0).Nachname = "Minhorst"
    .Items(0).Vorname = "Minhorst"
    .Items(1).Nachname = "Trowitzsch"
    .Items(1).Vorname = "Sascha"
     ...
End With
A(2).AddressType = eAddressUnspecific
Redim A(2).AdressBlock.Items(9)
With A(2).AdressBlock
    .Items(0).Nachname = "Häberle"
    .Items(0).Vorname = "Adam"
    .Items(1).Nachname = "Gutschmidt"
    .Items(1).Vorname = "Andrea"
     ...
End With
...

Immerhin haben wir nun mehrere Adressblöcke unterschiedlichen Umfangs und Typs in einer Variablen vereint! Das einzig hinderlich ist die Notwendigkeit, die Items-Arrays jeweils neu zu dimensionieren. Das ließe sich mit einer festen Anzahl von Elementen umgehen:

Public Type TAdressen
     Items(99) As TAdresse
End Type

Damit läge die Maximalzahl von Adressen für einen Block bei Hundert. Probleme gibt stellen sich also ein, wenn doch mehr benötigt würden. Und bei kleinerer Anzahl würde außerdem Speicherplatz vergeudet. Dieses Beispiel sollte eigentlich nur eines demonstrieren, nämlich wie sich benutzerdefinierte Typen im Interesse von Strukturierung verschachteln lassen. Das Ganze geht in Richtung Objektorientierter Programmierung, mit der wir es später bei den Klassenmodulen noch mehr zu tun bekommen.

Tabellendaten in benutzerdefinierten Typen

Damit es nicht bei trockener Theorie bleibt, folgt ein Beispiel, das in der einen oder anderen Datenbank durchaus vertreten sein kann. Es geht hier darum, die Datensätze einer Tabelle oder Abfrage in VBA-Strukturen zu überführen.

Listing 1 zeigt das Grundgerüst mit der Prozedur TestUDT. Nebenbei erwähnt ist UDT das Kürzel für einen benutzerdefinierten Typ (User Defined Type). Eine Datensatzgruppe rs wird auf alle Datensätze der Tabelle tblAdressen geöffnet. In einer Do-Loop-Schleife werden sie alle durchlaufen und die einzelnen Feldinhalte den Elementen der Variablen T des Typs TAdresse verabreicht. Da mache Felder der Datensätze leer sind und Leer nicht in einem String gespeichert werden kann – Strasse ist etwa vom Typ String – müssen die Werte noch einer Vorbehandlung mit der Funktion Nz unterzogen werden, die aus Nullwerten ordnungsgemäße Strings der Länge 0 macht.

Sub TestUDT()
     Dim rs As DAO.Recordset
     Dim T As TAdresse
     
     Set rs = CurrentDb.OpenRecordset("SELECT * FROM tblAdressen", dbOpenDynaset)
     Do While Not rs.EOF
         With T
             .ID = rs!ID.Value
             .Nachname = Nz(rs!Nachname.Value)
             .Vorname = Nz(rs!Vorname.Value)
             .Strasse = Nz(rs!Strasse.Value)
             .PLZ = Nz(rs!PLZ.Value)
             .Ort = Nz(rs!Ort.Value)
         End With
         rs.MoveNext
     Loop
     rs.Close
End Sub

Listing 1: Hier werden die Daten der Tabelle tblAdressen hintereinander in einer Variablen T des benutzerdefinierten Typs TAdressen abgespeichert

Viel Sinn macht das Beispiel nicht. Schließlich landen alle Daten in der einen Variablen T und überschreiben sich damit bei jedem Schleifendurchlauf. In Listing 2 ist die Routine aufgebohrt und um den Typ TAdressen erweitert, welcher, wie bereits weiter oben dargestellt, mehrere Adressen aufnehmen kann. Die Items des Typs T, also die einzelnen Adressen, müssen zunächst auf die Zahl der Datensätze des Recordsets dimensioniert werden. Sie erhalten diese über die Methode RecordCount, nachdem Sie den Daten-Cursor über MoveLast einmal auf das Ende der Datensätze einstellen. Da das Array Items nullbasiert ist, muss die Obergrenze auf RecordCount – 1 festgelegt werden. In der folgenden Do-Loop-Schleife wird auf eine Adresse nun über den Array-Index i Bezug genommen, wobei T.Items(i) als Spezifikation des With-Blocks dient. Die Zählervariable i wird nach erfolgtem MoveNext innerhalb der Schleife erhöht. Im Endergebnis haben Sie damit alle Datensätze der Tabelle in der einen Variablen T.

Sub TestUDTArray()
     Dim rs As DAO.Recordset
     Dim T As TAdressen
     Dim i As Long
     
     Set rs = CurrentDb.OpenRecordset("SELECT * FROM tblAdressen", dbOpenDynaset)
     rs.MoveLast: rs.MoveFirst
     ReDim T.Items(rs.RecordCount - 1)
     Do While Not rs.EOF
         With T.Items(i)
             .ID = i
             .Nachname = Nz(rs!Nachname.Value)
             .Vorname = Nz(rs!Vorname.Value)
             .Strasse = Nz(rs!Strasse.Value)
             .PLZ = Nz(rs!PLZ.Value)
             .Ort = Nz(rs!Ort.Value)
         End With
         rs.MoveNext: i = i + 1
     Loop
     rs.Close
End Sub

Listing 2: Hier kommt der benutzerdefinierte Typ TAdressen zum Tragen

Was damit dann anstellen, ist ein anderes Thema. Beispiel wäre etwa das Abspeichern in eine Textdatei. Tatsächlich lassen sich Variablen eines benutzerdefinierten Typs über nur eine einzige Zeile exportieren. Sie könnten die Routine von Listing 2 am Ende mit diesen Zeilen erweitern:

Dim F As Integer
F = FreeFile
Open "c:\daten\tabelle.txt" For Binary As F
Put F,, T
Close F

Mit der dergestalt exportierten Textdatei kann allerdings kein anderes Programm etwas anfangen. Sie lässt sich aber analog in einem VBA-Programm einlesen, das den Typ TAdressen kennt; sinngemäß:

Dim T As TAdressen
Get F,, T

Klassenmodule

Recht grundlos fristen Klassenmodule unter VBA ein Nischendasein. Entweder wird der Umgang mit ihnen immer wieder als kompliziert dargestellt, oder aber der Nutzen in Frage gestellt. Schließlich lässt sich mit den normalen Modulen dasselbe bewerkstelligen Dem ist aber nicht so. Grund genug, sich einmal deren Grundlagen zu widmen.

Rein mechanisch fügen Sie Ihrer Datenbank ein Klassenmodul hinzu, indem Sie den gleichnamigen Button im Ribbon unter der Registerseite Erstellen anklicken, oder, alternativ, im Projekt-Explorer der VBA-Umgebung und das Kontextmenü Einfügen | Klassenmodul betätigen. Bei Speichern vergeben Sie dem Modul einen Namen. Es hat sich eingebürgert, hier ein abweichendes Präfix zu verwenden. Versieht man normale Module meist mit dem Präfix mod oder mdl, so kommt hier eher ein cls oder schlicht C zum Einsatz. Ist das Thema des Klassenmoduls die Verwaltung von Adressen, so verwenden Sie etwa clsAdressen.

Vom programmtechnischen Standpunkt aus unterscheidet sich ein Klassenmodul nicht von normalen Modulen. Es existiert lediglich eine kleine Erweiterung, das Event, auf welches wir noch zu sprechen kommen. Der Unterschied besteht vielmehr darin, dass ein Klassenmodul eben eine Klasse zur Verfügung stellt, während normale Module eine Sammlung von allgemein verwendbaren Funktionen verzeichnet. Die Methoden der Klasse können aber nicht direkt aufgerufen werden. Eine Klasse ist nämlich lediglich der Bauplan für ein Objekt! Ein Objekt stellt eine Instanz einer Klasse dar, welche erst per New-Anweisung erzeugt werden muss, um an die Funktionen der Klasse zu kommen:

Dim C As clsAdressen
Set C = New clsAdressen

oder einfach

Dim C As New clsAdressen

über C gelangen Sie dann an die Methoden der Klasse. ähnlich, wie beim benutzerdefinierten Typ, zeigt IntelliSense die Methoden der Klasse, wenn Sie einen Punkt hinter die Variable setzen. Enthielte die Adressenklasse eine Prozedur FindeNachnamen, so riefen Sie jene auf diese Weise auf:

C.FindeNachname

Der Umstand, dass Klassenmodule erst auf dem Umweg über deren Instanziierung ansprechbar werden, macht sie wahrscheinlich unbeliebt, ist doch der Aufwand höher, als bei normalen Modulfunktionen.

Es gibt an anderer Stelle erschöpfende Darlegungen über Klassen, Objekte, Instanziierung und objektorientierte Programmierung. Da viele der Charakteristiken von Objektstrukturen unter VBA jedoch nicht implementiert sind, lassen wir es für den Anfang bei einigen grundlegenden Erläuterungen.

Ein Schlagwort bei Klassen ist Kapselung. Dadurch, dass eine Klasse erst dezidiert über eine Variable instanziiert werden muss, kapselt sich deren Funktionalität und schottet den Zugriff im Vergleich zu normalen Modulen ab. Die Methoden sind nur Teil des Objekts und damit nicht generell verfügbar.

Deshalb eignet sich eine Klasse und deren Objekt auch für die Aufnahme von Daten. Nicht zufällig hatten wir es eingangs mit benutzerdefinierten Objekten zu tun. UDT-Variablen sind häufig öffentlich ansprechbar und damit unsicherer, als ein Objekt.

Container-Klassen

Möchten Sie weiterlesen? Dann lösen Sie Ihr Ticket!
Hier geht es zur Bestellung des Jahresabonnements des Magazins Access [basics]:
Zur Bestellung ...
Danach greifen Sie sofort auf alle rund 400 Artikel unseres Angebots zu - auch auf diesen hier!
Oder haben Sie bereits Zugangsdaten? Dann loggen Sie sich gleich hier ein:

Schreibe einen Kommentar