Zur Hauptseite ... Zum Onlinearchiv ... Zum Abonnement ... Zum Newsletter ... Zu den Tools ... Zum Impressum ... Zum Login ...

Gedrucktes Heft

Diesen Beitrag finden Sie in Ausgabe 2/2007.

Unser Angebot für Sie!

Lesen Sie diesen Beitrag und 500 andere sofort im Onlinearchiv, und erhalten Sie alle zwei Monate brandheißes Access-Know-how auf 72 gedruckten Seiten! Plus attraktive Präsente, zum Beispiel das bald erscheinende Buch 'Access 2010 - Das Grundlagenbuch für Entwickler'!

Diesen Beitrag twittern

Zusammenfassung

Lernen Sie die objektorientierte Programmierung mehrschichtiger Anwendungen am Beispiel eines einfachen Formulars kennen.

Techniken

VBA, Formulare, Objektorientierte Programmierung

Voraussetzungen

Access 2000 und höher

Beispieldatei

MehrschichtigeAnwendung.mdb

Shortlink

453

Mehrschichtige Anwendungen

André Minhorst, Duisburg

Access ist - rein architektonisch betrachtet - auf das Anlegen monolithischer Anwendungen mit Objekten mit starker Abhängigkeit ausgelegt. Formulare und Berichte sind starr mit den zu Grunde liegenden Datenherkünften verbunden und der Code liegt jeweils im passenden Formular- oder Berichtsmodul oder, falls nötig, in einem Standard- oder Klassenmodul. Wie man auch mit Access mehrschichtige Anwendungen baut, zeigt dieser Beitrag.

Im Gegensatz zu objektorientierten Programmiersprachen gibt es in Access-Anwendungen keine einzelnen Dateien, die unterschiedliche Klassen repräsentieren, sondern nur eine einzige Datei mit der Endung .mdb, die alle Objekte enthält.

Wenn Sie hier eine mehrschichtige Anwendung bauen möchten und dies von richtigen objektorientierten Programmiersprachen gewohnt sind, müssen Sie von dem Gedanken Abschied nehmen, jede Klasse fein säuberlich in einer eigenen Datei zu speichern.

Davon abgesehen funktioniert das Programmieren mehrschichtiger Anwendungen aber genauso wie in anderen objektorientierten Programmiersprachen - nur dass Sie auf Eigenschaften wie Vererbung und Polymorphie verzichten müssen und alle Klassen in der .mdb-Datei enthalten sind.

Ein Grund, eine mehrschichtige Anwendung zu erstellen, ist die starke Verzahnung von Benutzeroberfläche und Anwendungslogik. Das gilt vor allem für den in Formularen und Berichten enthaltenen Code, der nicht nur die Benutzeroberfläche steuert, sondern auch die Businesslogik und die Funktionen für den Datenzugriff enthält. Letztere sind dabei noch tiefer verborgen, nämlich in den passenden Formular-, Berichts- und Steuerelementeigenschaften, die für die Bindung an die entsprechenden Tabellen und Abfragen sorgen. Teilweise befindet sich die Businesslogik sogar direkt in den Tabellen - etwa in Form der Gültigkeitsregeln und der übrigen Integritätsmechanismen wie der referentiellen Integrität und der darin enthaltenen Lösch- und Aktualisierungsweitergabe.

Wegen dieser Aufteilung kann die Pflege solcher Anwendungen sehr zeitintensiv werden: Wenn Sie etwa 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.

Ganz klar: Wer eine konsistente Vorgehensweise bei der Programmierung von Anwendungen an den Tag legt, der wird auch genau wissen, wo in seiner Anwendung sich welche Funktionen befinden. Aber erstens kann es ja auch sein, dass sich mal jemand anders um die Wartung oder Weiterentwicklung der Anwendung kümmern muss, und zweitens entwickeln Sie sich selbst ja auch weiter und erkennen nach einer gewissen Zeit die funktionale Struktur früherer Anwendungen nicht mehr wieder. Und Access ist natürlich zunächst 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 man für kleine Änderungen beinahe genauso lange braucht, als wenn man die halbe Anwendung neu programmieren würde, sollte man ü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 Benutzeroberflä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 Beitrags werden Datenzugriffsschicht und Daten als zwei Schichten betrachtet.

Beispiel für den mehrschichtigen Datenzugriff

Als Beispiel für den mehrschichtigen Datenzugriff dienen eine Tabelle namens tblPersonal (s. Abb. 1) und ein Formular namens frmPersonal (s. Abb. 2). Dabei verwenden wir aus Gründen der Übersicht nur einige der in der Tabelle enthaltenen Felder.

pic001.tif

Abb. 1: Diese Tabelle dient als Datenherkunft des Formulars.

pic002.tif

Abb. 2: Das Formular zum Anzeigen der Daten ist ungebunden.

Das Formular ist komplett ungebunden und enthält die folgenden Steuerelemente:

  • cboSchnellsuche: dient der Auswahl von Mitarbeitern
  • txtPersonalID: schreibgeschützt
  • txtVorname, txtNachname, txtPosition, txtAnrede, txtEinstellung: Textfelder der Tabelle
  • cmdOK: Schaltfläche zum Schließen des Formulars und zum Speichern des aktuellen Inhalts
  • cmdLoeschen: Schaltfläche zum Löschen des aktuellen Datensatzes
  • cmdNeu: Schaltfläche zum Anlegen eines neuen Datensatzes

Die GUI-Schicht

Die GUI-Schicht - also die Benutzeroberfläche - bildet für das nachfolgende Beispiel das Formular frmPersonal aus Abb. 2. Die GUI-Schicht enthält nur Methoden für den Zugriff auf die Business-Schicht, auf keinen Fall kann sie direkt auf eine der darunter liegenden Schichten 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.

Beim hier verwendeten Schichtenmodell greift die GUI-Schicht ausschließlich auf die Objekte der Business-Schicht zu: auf die Datenobjekte mit den anzuzeigenden Informationen und auf das Controller-Objekt, das die Funktionen zum Füllen der Datenobjekte, zum Speichern der per GUI-Schicht geänderten Inhalte der Datenobjekte, zum Speichern neuer Datenobjekte und zum Löschen bestehender Datenobjekte bereitstellt.

Die Business-Schicht

Die Business-Schicht enthält zwei Typen von Objekten: Der erste Typ repräsentiert die in den Datensätzen der Tabellen enthaltenen Daten (Daten-Objekte), der 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.

Wie viele und welche Objekte sich in der Business-Schicht befinden, hängt von der Art der enthaltenen Daten und der Benutzeroberflä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 Benutzeroberfläche oder vielleicht sogar ein großes Controller-Objekt verwenden. Übersichtlicher dürfte ein Objekt pro Formular sein.

Die Datenzugriffsschicht

Die Datenzugriffsschicht enthält Datenzugriffsobjekte. Zu jedem Datenobjekt gibt es ein Datenzugriffsobjekt, das verschiedene Operationen ausführen kann:

  • Erzeugen eines Datenobjekts auf Basis eines Datensatzes der zu Grunde 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 von einem Wechsel der Datenquelle unberührt.

Im Beispiel erfahren Sie, wie die fünf Operationen eines Datenzugriffsobjekts 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 zu verwenden. 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.

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. Die Benutzeroberfläche und die Business-Schicht bleiben unangetastet.

Beispielanwendung

Nach der Theorie zurück zum Beispiel: Das Formular aus Abb. 2 soll nun also über die einzelnen Schichten auf die in der Datenbank gespeicherten Daten zugreifen, diese anzeigen, neue Datensätze anlegen oder bestehende Datensätze ändern oder löschen. Beginnen wir also einfach mal am Anfang: den Zustand beim Öffnen des Formulars.

Person-Objekt erstellen

Während der Arbeit mit dem Formular werden die Daten der Mitarbeiter, wie bereits erwähnt, nicht direkt aus der Tabelle ins Formular transportiert, sondern über ein spezielles Objekt, das im Prinzip mit einem Datensatz einer Tabelle vergleichbar ist, mit dem Unterschied, dass es statt Felder Eigenschaften aufweist und dass Sie seine Methoden selbst definieren können beziehungsweise müssen. Da Sie dieses Objekt auf Basis einer Klasse erzeugen und die Beschreibung dieser Klasse die folgenden Erläuterungen unnötig behindern würde, legen Sie diese Klasse schnell vorab an. Die Klasse heißt clsPerson und enthält den Code aus Listing 1. Dazu gehören lediglich die Membervariablen, die die Eigenschaften speichern, sowie zu jeder Membervariablen eine Property Get- und eine Property Let-Methode. Wenn Sie sich noch nicht mit der Programmierung von Klassen auskennen, finden Sie in [1] weitere Informationen.

Listing 1: Code der Klasse clsPerson

Option Compare Database

Option Explicit

Dim mPersonalID As Long

Dim mVorname As String

Dim mNachname As String

Dim mPosition As String

Dim mAnrede As String

Dim mEinstellung As Date

Public Property Get PersonalID() As Long

    PersonalID = mPersonalID

End Property

Public Property Let PersonalID(lngPersonalID As Long)

    mPersonalID = lngPersonalID

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 Position() As String

    Position = mPosition

End Property

Public Property Let Position(strPosition As String)

    mPosition = strPosition

End Property

Public Property Get Anrede() As String

    Anrede = mAnrede

End Property

Public Property Let Anrede(strAnrede As String)

    mAnrede = strAnrede

End Property

Public Property Get Einstellung() As Date

    Einstellung = mEinstellung

End Property

Public Property Let Einstellung(strEinstellung As Date)

    mEinstellung = strEinstellung

End Property

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. Das Controller-Objekt befindet sich im Übrigen in einer eigenen Klasse, die Sie weiter unten erstellen. Den Code aus Listing 2 speichern Sie im Klassenmodul des Formulars frmPersonal. Außerdem müssen Sie noch die Eigenschaft Beim Öffnen auf den Wert [Ereignisprozedur] einstellen.

Listing 2: Initialisieren des Formulars

Dim objController As clsController

Private Sub Form_Open(Cancel As Integer)

    Set objController = New clsController

    cboSchnellsucheAktualisieren

End Sub

Die Prozedur cboSchnellsucheAktualisieren soll das Kombinationsfeld mit den in der Tabelle tblPersonal enthaltenen Daten füllen. Unter Access brauchen Sie dafür keine einzige Codezeile, sondern weisen einfach der Eigenschaft Datensatzherkunft eine passende Abfrage zu. Bei der Programmierung einer mehrschichtigen Anwendung wird das Ganze hingegen schon mächtig interessant - Sie füllen dazu zunächst in der Datenzugriffsschicht ein Collection-Objekt, das dann nach oben gereicht und von der GUI-Schicht in das Kombinationsfeld gefüllt wird.

Fangen Sie also ganz unten an und erstellen Sie eine Klasse namens clsPersonal_Datenzugriffsobjekt. Dieser fügen Sie eine Methode namens Find hinzu, die nichts anderes erledigen soll, als eine Collection mit allen in der Tabelle tblPersonal enthaltenen Mitarbeitern zurückzuliefern. Um späteren Anwendungszwecken vorzubeugen, fügen Sie dieser direkt einen optionalen Parameter namens varSearch hinzu, dem Sie eine Zeichenkette mit einer WHERE-Klausel übergeben können - dazu jedoch später mehr. Die komplette Routine finden Sie in Listing 3. Sie durchläuft in einer Do While-Schleife alle in der Tabelle gespeicherten Datensätze und schreibt diese in jeweils ein eigenes Objekt des Typs clsPerson. Jedes dieser Objekte landet dann in der Collection objPersonen. Wichtig ist hierbei, dass beim Anfügen der Objekte an die Collection der Primärschlüsselwert des Datensatzes mit dem zweiten Parameter der Add-Methode der Collection als Schlüssel des aktuellen Elements angehängt wird. Anderenfalls können Sie die Elemente der Collection später nicht mehr identifizieren. Die Collection dient im Übrigen als Rückgabewert der Funktion Find an die aufrufende Instanz.

Listing 3: Die Find-Methode des Datenzugriffsobjekts clsPerson_Datenzugriffsobjekt

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 tblPersonal"

    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

            .PersonalID = rst!PersonalID

            .Vorname = Nz(rst!Vorname, "")

            .Nachname = Nz(rst!Nachname, "")

            .Position = Nz(rst!Position, "")

            .Anrede = Nz(rst!Anrede, "")

            .Einstellung = Nz(rst!Einstellung, "")

        End With

        objPersonen.Add objPerson, CStr(objPerson.PersonalID)

        rst.MoveNext

    Loop

    Set Find = objPersonen

Find_Exit:

    On Error Resume Next

    Set db = Nothing

    Exit Function

Find_Err:

    GoTo Find_Exit

End Function

Wer ruft nun diese Funktion auf und wohin geht die Collection? Nun, dafür ist das Controller-Objekt zuständig. Dieses legen Sie ebenfalls zunächst in Form einer eigenen Klasse namens clsController an.

Für den Aufruf der Find-Methode ist in diesem Fall die kleine Methode GetPersons zuständig. Diese Methode liefert ein Collection-Objekt zurück, das in dem Objekt objPersonen gespeichert wird. Die Methode sieht wie in Listing 4 aus.

Listing 4: Die Methode GetPersons der Klasse clsController

Public Function GetPersons() As Collection

    Set objPersonen = objPerson_Datenzugriffsobjekt.Find

    Set GetPersons = objPersonen

End Function

Nun kennt diese Methode das Objekt objPerson_Datenzugriffsobjekt aber noch gar nicht. Das wird ihm indirekt wiederum von der darüberliegenden Schicht, nämlich der GUI-Schicht mitgeteilt. Indirekt deshalb, weil diese das Controller-Objekt instanziert und dadurch das im Controller-Objekt enthaltene Ereignis Class_Initialize ausgelöst wird. Damit dies alles geschieht, fügen Sie auch noch die Deklarationen und die Prozedur Class_Initialize aus Listing 5 zur Klasse clsController hinzu.

Listing 5: Initialisieren der Klasse clsController der Business-Schicht

Dim objPerson_Datenzugriffsobjekt As clsPerson_Datenzugriffsobjekt

Dim objPersonen As Collection

Private Sub Class_Initialize()

    Set objPerson_Datenzugriffsobjekt = New clsPerson_Datenzugriffsobjekt

End Sub

Nun endlich können Sie auch der GUI-Schicht die für das Füllen des Kombinationsfelds notwendige Funktionalität hinzufügen. Diese manifestiert sich in Form der Routine cboSchnellsucheAktualisieren und kann nicht nur beim Öffnen des Formulars, sondern auch durch andere Ereignisse aufgerufen werden. Die Routine aus Listing 6 deklariert zunächst ein Objekt des Typs clsPerson und eine Collection. Die oben vorgestellte Methode GetPersons des Controller-Objekts liefert (über die Find-Methode des Datenzugriffsobjekts) die Collection mit den einzelnen Personen-Objekten. Diese durchläuft die Routine in einer For Each-Schleife und trägt den Vor- und den Nachnamen in eine Semikola-separierte Zeichenkette ein. Diese muss die Routine nun nur noch der Datensatzherkunft des Kombinationsfelds zur Schnellauswahl zuweisen - fertig! Natürlich müssen Sie die Eigenschaft Herkunftstyp des Kombinationsfelds noch auf Wertliste sowie die beiden Eigenschaften Spaltenanzahl und Spaltenbreite auf 2 und 0cm einstellen.

Listing 6: Zuweisen einer Datensatzgruppe mit allen Personen an das Kombinationsfeld zur Schnellsuche

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

            str = str & objPerson.PersonalID & ";" & objPerson.Nachname _

                & ", " & objPerson.Vorname & ";"

        Next objPerson

        Me.cboSchnellsuche.RowSource = str

    Else

        MsgBox "Personenliste konnte nicht geladen werden."

    End If

End Sub

Wenn Sie nun das Formular in der Formularansicht öffnen und das Kombinationsfeld aufklappen, sollte sich etwa ein Bild wie in Abb. 3 präsentieren. Glückwunsch! Sie haben Ihr erstes Kombinationsfeld mittels objektorientierter, mehrschichtiger Datenbankentwicklung gefüllt. Zugegebenermaßen ist der Aufwand nicht gerade gering, aber die fehlende Datenbindung und die gewünschte Flexibilität erfordern diesen Weg. Nun geht es aber schnell weiter - immerhin soll der ausgewählte Datensatz ja auch noch komplett im Formular angezeigt werden.

pic003.tif

Abb. 3: Kombinationsfeld mit Daten aus einer Collection von Personen-Objekten

Auswählen und anzeigen eines Datensatzes

Beim Füllen des Formulars 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. Die Ereigniseigenschaft Nach Aktualisierung löst dabei über die Prozedur aus Listing 7 die Routine in Listing 8 aus.

Listing 7: Diese Prozedur ruft die eigentliche Routine zum Laden eines Mitarbeiters auf.

Private Sub cboSchnellsuche_AfterUpdate()

    MitarbeiterLaden

End Sub

Listing 8: Diese Routine wird nach der Auswahl eines Eintrags des Kombinationsfelds aufgerufen.

Private Sub MitarbeiterLaden()

    Dim objPerson As clsPerson

    Set objPerson = objController.LoadPerson(Me!cboSchnellsuche)

    objController.SavePerson Nz(Me!txtPersonalID, 0), Me!txtVorname, _

        Me!txtNachname, Me!txtPosition, Me!txtAnrede, Me!txtEinstellung

    With objPerson

        Me!txtPersonalID = .PersonalID

        Me!txtVorname = .Vorname

        Me!txtNachname = .Nachname

        Me!txtPosition = .Position

        Me!txtAnrede = .Anrede

        Me!txtEinstellung = .Einstellung

    End With

End Sub

Und das sieht so aus: Nach dem Deklarieren der Objektvariablen objPerson wird diese mit Hilfe der Methode LoadPerson des Controller-Objekts (siehe weiter unten) gefüllt. Als Parameter wird dabei das gebundene Feld des Kombinationsfeldes übergeben, das die PersonalID des anzuzeigenden Mitarbeiters enthält (s. Listing 9).

Listing 9: Weiterdelegieren des Ladens von Personendaten in das entsprechende Objekt

Public Function LoadPerson(lngPersonalID As Long) As clsPerson

    Dim objPerson As clsPerson

    Set objPerson = objPersonen(CStr(lngPersonalID))

    If objPerson Is Nothing Then

        Set LoadPerson = objPerson_Datenzugriffsobjekt.Read(lngPersonalID)

    Else

        Set LoadPerson = objPerson

    End If

End Function

Zum Füllen der Eigenschaften mit den Inhalten der Felder des Datensatzes mit der gesuchten PersonalID 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 - dazu später mehr.

Anderenfalls dient die in der Auflistung vorgefundene Instanz des gesuchten Personenobjekts der Rückgabewert der Methode (s. Listing 9).

Listing 17: Vorbereitung der Aktualisierung eines Datensatzes im Controller-Objekt

Private Function UpdatePerson(lngPersonalID As Long, strVorname As String, _

        strNachname As String, strPosition As String, strAnrede As String, _

        strEinstellung As String)

    Dim objPerson As clsPerson

    Set objPerson = New clsPerson

    With objPerson

        .PersonalID = lngPersonalID

        .Vorname = strVorname

        .Nachname = strNachname

        .Position = strPosition

        .Anrede = strAnrede

        .Einstellung = strEinstellung

    End With

    objPerson_Datenzugriffsobjekt.Update objPerson

End Function

Einlesen von Personen, die nicht in der Collection enthalten sind

Für den in diesem Beispiel eigentlich nicht vorgesehenen Fall, dass ein Mitarbeiter-Objekt 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.

Diese öffnet zunächst eine Datensatzgruppe aller Datensätze mit der übergebenen PersonalID, 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 (s. Listing 10).

Listing 10: Einlesen eines Datensatzes in ein Objekt der Business-Schicht

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

        .PersonalID = rst![PersonalID]

        .Vorname = rst![Vorname]

        .Nachname = rst![Nachname]

        .Position = rst![Position]

        .Anrede = rst![Anrede]

        .Einstellung = rst![Einstellung]

    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

Von hier aus geht es dann über die Business-Schicht direkt in die GUI-Schicht. Dort wartet die Routine aus Listing 9 bereits und trägt die Eigenschaften der gewünschten Person in die entsprechenden Textfelder des Formulars ein.

Neuen Datensatz anlegen

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 bevor man nicht die OK-Schaltfläche anklickt oder zu einem anderen beziehungsweise neuen Datensatz wechselt, geschieht gar nichts.

Falls noch ein Datensatz im Formular angezeigt wird, soll dieser allerdings erst einmal gespeichert und das Formular geleert werden.

Dazu verwenden Sie die Schaltfläche mit der Beschriftung Neu. Die durch das Klicken ausgelöste Routine (s. Listing 11) ruft eine weitere Funktion namens FormularLeeren auf, die alle Textfelder des Formulars leert (s. Listing 12).

Listing 11: Diese Routine ruft eine weitere Routine zum Leeren des Formulars auf.

Private Sub cmdNeu_Click()

    FormularLeeren

End Sub

Listing 12: Leeren der Textfelder des Formulars

Private Sub FormularLeeren()

    With Me

        !txtPersonalID = Null

        !txtVorname = ""

        !txtNachname = ""

        !txtPosition = ""

        !txtAnrede = ""

        !txtEinstellung = ""

    End With

End Sub

Speichern eines Datensatzes

Nach der Eingabe der Daten ist es sinnvoll, diese zu speichern. Das soll entweder beim Anzeigen eines anderen Datensatzes, beim Anlegen eines weiteren Datensatzes oder beim Schließen des Formulars geschehen. Letzteres erfolgt mit der Schaltfläche cmdOK. Diese löst die Prozedur aus Listing 13 aus, die - Sie ahnen es bereits - eine weitere Methode des Controllers aufruft, die wiederum eine Methode der Klasse clsPerson_Datenzugriffsobjekt einsetzt.

Listing 13: Auslösen des Speicher-Vorgangs im Formular

Private Sub cmdOK_Click()

    Me!txtPersonalID = objController.SavePerson( _

        Nz(Me!txtPersonalID, 0), Me!txtVorname, Me!txtNachname, _

        Me!txtPosition, Me!txtAnrede, Me!txtEinstellung)

    cboSchnellsucheAktualisieren

    DoCmd.Close acForm, Me.Name

End Sub

Die Routine prüft, ob der Datensatz bereits einen Wert im Feld PersonalID 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 OK-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 erledigt.

Die Methode SavePerson (s. Listing 14) prüft dies und reicht die zu speichernden Daten entweder an die Routine CreatePerson oder UpdatePerson weiter.

Listing 14: Diese Methode entscheidet, ob ein Objekt in der Datenbank gespeichert oder nur aktualisiert werden soll.

Public Function SavePerson(lngPersonalID As Long, strVorname As String, _

        strNachname As String, strPosition As String, strAnrede As String, _

        strEinstellung As String) As Long

    If lngPersonalID = 0 Then

        lngPersonalID = CreatePerson(strVorname, strNachname, _

            strPosition, strAnrede, strEinstellung)

    Else

        UpdatePerson lngPersonalID, strVorname, strNachname, _

            strPosition, _strAnrede, strEinstellung

    End If

    SavePerson = lngPersonalID

End Function

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 dieses dann an das Datenzugriffsobjekt übergeben wird (s. Listing 15).

Listing 15: Die Funktion CreatePerson des Controller-Objekts erwartet die zu speichernden Eigenschaften des Person-Objekts als Parameter.

Public Function CreatePerson(strVorname As String, strNachname As _

        String, strPosition As String, strAnrede As String, strEinstellung _

        As String) As Long

    Dim objPerson As clsPerson

    Set objPerson = New clsPerson

    With objPerson

        .Vorname = strVorname

        .Nachname = strNachname

        .Position = strPosition

        .Anrede = strAnrede

        .Einstellung = strEinstellung

    End With

    If objPerson_Datenzugriffsobjekt.Create(objPerson) = True Then

        CreatePerson = objPerson.PersonalID

    End If

    Set objPerson = Nothing

End Function

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 16 kann dann aus dem per Referenz übergebenen Objekt den neuen Wert der Eigenschaft PersonID auslesen.

Listing 16: Die Create-Methode des Datenzugriffsobjekts legt einen neuen Datensatz auf Basis des übergebenen Objekts an.

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 tblPersonal WHERE [PersonalID] = 0", dbOpenDynaset)

    With objPerson

        rst.AddNew

        rst![Vorname] = .Vorname

        rst![Nachname] = .Nachname

        rst![Position] = .Position

        rst![Anrede] = .Anrede

        rst![Einstellung] = .Einstellung

        .PersonalID = rst![PersonalID]

        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

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 (s. Listing 17). Die Methode Update des Datenzugriffsobjekts öffnet eine Datensatzgruppe, die lediglich einen Datensatz enthält - den mit der übergebenen PersonalID (s. Listing 18).

Listing 18: Aktualisieren eines Datensatzes auf Basis des passenden Objekts

Public Function Update(objPerson As clsPerson)

    On Error GoTo Update_Err

    Dim db As DAO.Database

    Dim rst As DAO.Recordset

    Set db = CurrentDb

    Set rst = db.OpenRecordset("SELECT * FROM tblPersonal WHERE [PersonalID] = " _

        & objPerson.PersonalID, dbOpenDynaset)

    With objPerson

        rst.Edit

        rst![Vorname] = .Vorname

        rst![Nachname] = .Nachname

        rst![Position] = .Position

        rst![Anrede] = .Anrede

        rst![Einstellung] = .Einstellung

        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

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 PersonalID an die dortige Methode DeletePerson. Nach erfolgreichem Löschvorgang leert die Routine das Formular, aktualisiert das Kombinationsfeld zur Schnellsuche und leert auch dieses (s. Listing 19).

Listing 19: Starten des Löschvorgangs

Private Sub cmdLoeschen_Click()

    If objController.DeletePerson(Me!txtPersonalID) = True Then

        FormularLeeren

        cboSchnellsucheAktualisieren

        Me!cboSchnellsuche = Null

    End If

End Sub

Die Methode DeletePerson des Controller-Objekts (s. Listing 20) reicht die ID des zu löschenden Datensatzes direkt an die Methode Delete der Datenzugriffsklasse weiter.

Listing 20: Die Methode DeletePerson des Controller-Objekts

Public Function DeletePerson(lngPersonalID As Long) As Boolean

    DeletePerson = objPerson_Datenzugriffsobjekt.Delete(lngPersonalID)

End Function

Diese löscht den Datensatz mit einem DELETE-Statement und gibt bei Gelingen den Wert True zurück (s. Listing 21).

Listing 21: Entfernen eines Datensatzes aus der Tabelle tblPersonen

Public Function Delete(PersonalID As Long)

    On Error GoTo Delete_Err

    Dim db As DAO.Database

    Set db = CurrentDb

    db.Execute "DELETE FROM tblPersonal WHERE [PersonalID] = " & PersonalID, dbFailOnError

    Delete = True

Delete_Exit:

    On Error Resume Next

    Set db = Nothing

    Exit Function

Delete_Err:

    Delete = False

    GoTo Delete_Exit

End Function

Bei Datensatzwechsel speichern?

Üblicherweise geht man heutzutage davon aus, dass Änderungen an einem Datensatz beim Wechsel zu einem anderen Datensatz gespeichert werden. Dies soll auch hier der Fall sein: Dazu fügen Sie an zwei Stellen der GUI-Schicht noch eine Anweisung zum Speichern des aktuellen Datensatzes ein.

Die passende Anweisung lautet folgendermaßen:

objController.SavePerson Nz(Me!txtPersonalID, 0), Me!txtVorname, Me!txtNachname, Me!txtPosition, Me!txtAnrede, Me!txtEinstellung

Einfügen müssen Sie diese in die Routine, die nach dem Auswählen eines neuen Mitarbeiters über das Schnellsuche-Kombinationsfeld ausgelöst wird, und zwar vor dem Füllen der Textfelder mit den neuen Werten, sowie in die Routine, die das Formular zum Anlegen eines neuen Mitarbeiters leert. In der Beispieldatenbank finden Sie die notwendige Zeile bereits in den entsprechenden Routinen.

Fraglich ist nur, wie man einmal vorgenommene Änderungen wieder verwerfen kann. Dazu braucht man noch eine Abbrechen-Schaltfläche. Diese soll einfach den Zustand des bearbeiteten Mitarbeiters vor der Bearbeitung wiederherstellen.

Das ist allerdings nicht ganz so einfach, denn immerhin gibt es bei ungebundenen Textfeldern, die ja hier nun einmal vorliegen, keine Eigenschaft wie OldValue, mit der man die ursprünglichen Werte ermitteln könnte, oder gar eine Undo-Funktion.

Also lädt man in dieser Situation einfach wieder den letzten gespeicherten Stand in das Formular. Die Abbrechen-Schaltfläche statten Sie dazu mit der Routine aus Listing 22 aus.

Listing 22: Die Abbrechen-Schaltfläche macht Änderungen am aktuellen Mitarbeiter rückgängig.

Private Sub cmdAbbrechen_Click()

    If IsNull(Me!txtPersonalID) Then

        FormularLeeren

    Else

        MitarbeiterLaden

    End If

End Sub

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 auch Data Access Objects bedeutet, aber nichts mit der DAO-Klasse von Access zu tun hat) - diese Stichwörter nur für diejenigen, die sich ein wenig 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 eine Menge mehr Flexibilität und Übersicht.

Sie können die Businessregeln komplett in der Business-Schicht versenken, was sich natürlich erst dann auszahlt, wenn Sie nicht nur mit einem, sondern mit mehreren Objekten, einer umfangreicheren Benutzeroberfläche und einem dementsprechenden Datenmodell arbeiten.

Die Validierung der Daten kann man 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.

Zusammenfassung und Ausblick

Der Artikel zeigt, dass mehrschichtige Architekturen eine code-intensive Sache sind. Wenn Sie das Prinzip des hier verwendeten Model-View-Controller-Modells einmal verinnerlicht haben, ist die Programmierung aber reine Fleißarbeit.

Es bleiben jedoch noch einige Funktionen offen: Wie werden verknüpfte Daten angezeigt, wie erfolgt die Validierung und welche Vorteile bietet die objektorientierte Entwicklung noch? Dieses Thema wird sicher in einer der folgenden Ausgaben von Access im Unternehmen weiter behandelt.

Quellen

[1] Ausgabe 5/2004, Objektorientiertes Programmieren mit Klassen, Shortlink 232

Kompletten Artikel lesen?

Einfach für den Newsletter anmelden, dann lesen Sie schon in einer Minute den kompletten Artikel und erhalten die Beispieldatenbanken.

E-Mail:

Download

Download

Die .zip-Datei enthält folgende Dateien:

MehrschichtigeAnwendung.mdb

Beispieldateien downloaden

© 2003-2015 André Minhorst Alle Rechte vorbehalten.