Home > Artikel > Ausgabe 1/2016 > Klassenmodule und benutzerdefinierte Typen

Klassenmodule und benutzerdefinierte Typen

Achtung: Sie sind nicht angemeldet. Wenn Sie Abonnent sind und sich anmelden, lesen Sie den kompletten Artikel, laden das PDF herunter oder probieren die Beispieldatenbank aus (sofern vorhanden).

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

Der benutzerdefinierte Typ TAdresse lässt sich problemlos in eine Klasse verwandeln und anschließend auch ähnlich handhaben. Tatsächlich haben Klassen und benutzerdefinierte Typen einige Gemeinsamkeiten. Eine Klasse kann Imgrunde als benutzerdefinierter Typ angesehen werden, der neben den statischen Variablen auch noch Methoden, also Prozeduren, aufweisen kann.

Eine Klasse clsAdresse sollte also die Möglichkeit bieten, den Nachnamen, Vornamen und Ort einer Adresse speichern und wiedergeben zu können. Das ist auf einfache Weise möglich:

Public ID As Long

Public Nachname As String

Public Vorname As String

Public Strasse As String

Public PLZ As String

Public Ort As String

Das wäre dann auch schon der komplette Code des Klassenmoduls clsAdresse! So sprechen Sie dann die Eigenschaften an:

Dim C As clsAdresse

Set C = New clsAdresse

C.Nachname = "Minhorst"

C.Vorname = "André"

Debug.Print C.Nachname

Bis auf die Instanziierung entspricht das Verhalten hier also völlig dem benutzerdefinierten Typ.

Allerdings entspricht es nicht dem Prinzip der Kapselung, die Eigenschaften als öffentliche Variablen auszuführen. In der Regel geht man stattdessen den Weg über die sogenannten Properties und speichert indirekt in private Variablen. Properties verhalten sich nach außen wie Variablen, sind tatsächlich aber Funktionen. Hier ein Beispiel, das Sinn und Zweck verdeutlicht:

Private m_Nachname As String

Property Get Nachname() As String

   Nachname = m_Nachname

End Property

Property Let Nachname(Value As String)

   If Value = "Minhorst" Then

       Err.Raise 1,, _

          "Unerlaubter Nachname"

   End If

   m_Nachname = Value

End Property

m_Nachname ist die Variable, die tatsächlich den String speichert. Auf sie lässt sich aber nicht direkt im Adressenobjekt zugreifen, da sie als Private deklariert ist. Stattdessen kommt hier die Eigenschaft, das Property, Nachname zum Zug. Das Property teilt sich in zwei Eigenschaftenprozeduren auf, einmal die zum Zuweisen eines Werts (Let) und dann die zum Auslesen des Werts (Get). Nach außen sehen die beiden Prozeduren aus, wie eine. Voraussetzung ist allerdings, dass beide den gleichen Namen tragen und den gleichen Datentyp bearbeiten. Das Get-Property ist Imgrunde eine Funktion, die lediglich den Inhalt der Variablen m_Nachname zurückgibt. Das Let-Property ist eine Sub-Prozedur, der als Parameter der Eigenschaftswert übergeben wird. Dieser wird wiederum in der privaten Variablen m_Nachname abgespeichert.

Sie sprechen die Eigenschaft genau so an, wie im Beispiel oben angegeben. C.Nachname verhält sich wie eine Variable, die gelesen und beschrieben werden kann. Indessen wird es bei Ausführung des Codes zu einer Fehlermeldung kommen. Das jedoch nicht infolge falscher Programmierung, sondern deshalb, weil dieses Feature eingebaut wurde. Wird als Eigenschaftswert nämlich Minhorst übergeben, so zweigt die Let-Prozedur in den entsprechenden Zweig ab und ruft per Err.Raise ausdrücklich eine Fehlermeldung hervor. Minhorst lässt sich also im Klassenobjekt nicht speichern.

Hier wird deutlich, dass der Umweg über die Eigenschaftenprozeduren die Möglichkeit der Weiterverarbeitung, Überprüfung und Modifikation von Werten bietet. Der eigentliche Wert in m_Nachname ist eingekapselt. Das alles ist mit Variablen auf benutzerdefinierten Typen nicht möglich. Die Container-Klasse clsAdresse finden Sie auszugsweise in Listing 3.

Private m_ID As Long

Private m_Nachname As String

Private m_Vorname As String

Private m_Strasse As String

Private m_PLZ As String

Private m_Ort As String

Property Get Nachname() As String

     Nachname = m_Nachname

End Property

Property Let Nachname(Value As String)

     m_Nachname = Value

End Property

Property Get Vorname() As String

     Vorname = m_Vorname

End Property

Property Let Vorname(Value As String)

     m_Vorname = Value

End Property

Sie haben das Ende des frei verfügbaren Teil dieses Artikels erreicht!

Wenn Sie mehr lesen und auf viele weitere Artikel zugreifen möchten, melden Sie sich als Abonnent unter Login an. Falls nicht, bestellen Sie doch einfach ein Jahresabonnement!