Lies diesen Artikel und viele weitere mit einem kostenlosen, einwöchigen Testzugang.
In den Beiträgen „Undo in Haupt- und Unterformular“ und „Undo in Haupt- und Unterformular mit Klasse“ haben wir gezeigt, wie Sie die Undo-Funktion etwa durch einen Abbrechen-Schaltfläche nicht nur auf das Hauptformular, sondern auch auf die änderungen im Unterformular erstrecken. Nun hat ein Leser gefragt, ob man dies auch für mehrere Unterformulare erledigen kann. Klar kann man – die angepasste Lösung stellt der vorliegende Beitrag vor.
Dabei setzen wir auf der Lösung aus dem Beitrag Undo in Haupt- und Unterformular mit Klasse (www.access-im-unternehmen.de/912) auf. Diese enthält im Vergleich zu dem vorherigen Beitrag eine Klasse mit der vollständigen Funktionalität für das Implementieren des Undo in Haupt- und Unterformular, die man nur noch im Ereignis Beim Laden des Hauptformulars initialisieren und mit einigen Eigenschaften versehen muss. Der dazu anzulegende Code war sehr überschaubar gemessen an der Funktion, die er hinzufügte.
Die Einschränkung dieser Klasse ist, dass man in dieser lediglich das Hauptformular und das Unterformular angeben konnte, auf die sich die Undo-Funktion auswirken sollte. Wenn man ein zweites Unterformular hinzufügen wollte, war das nicht möglich. Also schauen wir uns im vorliegenden Beitrag an, wie wir dieses Feature nachrüsten.
Aber wie viele Formular dürfen es denn sein Reichen zwei aus – oder benötigt man drei oder vier Unterformulare Und macht man sich denn mit so vielen Unterformularen nicht die übersicht im Formular kaputt Der Leser, der mit seiner Anfrage an uns herangetreten ist, lieferte gleich einen Screenshot seines Formulars mit, auf dem ersichtlich wurde, dass er ein Register-Steuerelement verwendet, um die verschiedenen Unterformulare im Formular unterzubringen. So gelingt das natürlich bei Erhaltung guter übersicht.
Damit war klar: Wir sollten uns nicht unbedingt einer Einschränkung hingeben und eine Lösung programmieren, die beliebig viele Unterformulare berücksichtigt.
Erster Ansatz: Zwei Unterformulare
Dennoch haben wir, um uns ein wenig in den VBA-Code der anzupassenden Lösung einzuarbeiten, zunächst die bestehende Lösung auf den Einsatz mit zwei Unterformularen erweitert. Dazu haben wir lediglich im Code überall dort, wo bisher das Unterformular erwähnt wurde, den Code für ein zweites Unterformular untergebracht. Das klappte recht leicht – die Hauptarbeit war Copy and Paste und ein wenig Achtsamkeit an den richtigen Stellen.
Zweiter Ansatz: So viele Unterformulare wie nötig
Klar: Die Anzahl der Unterformular je Formulare ist begrenzt. Dennoch wollen Sie den Code der Klasse ja nicht jedes Mal erweitern, wenn ein neues Unterformular zum Formular hinzukommt. Wir wollen dann maximal ein paar Zeilen Code im Beim Laden-Ereignis hinzufügen, die das neu hinzugekommene Formular und dessen Datenherkunft sowie das Fremdschlüsselfeld der zugrunde liegenden Tabelle an die Klasse übergeben.
Dafür ist dann zum Umwandeln des Codes ein wenig mehr Aufwand erforderlich. Aber es lohnt sich!
Aus eins mach zwei
Wenn Sie bereits andere Lösungen von uns gesehen haben, die Funktionen in Klassen auslagern und bei denen sich die Funktion auf ein oder mehrere Steuer-elemente bezieht, dann wissen Sie bereits: Es wird eine Collection geben, welche die Informationen für die in beliebiger Anzahl auftretenden Unterformulare aufnehmen soll. Dazu legen wir für jedes Unterformular eine neue Klasse an, die dann in der Collection gespeichert wird. Das heißt weiter: Wir benötigen nicht mehr nur eine Hauptklasse, sondern noch eine weitere Klasse für die untergeordneten Steuer-elemente.
Bild 1 zeigt schon einmal, wie das Endergebnis aussehen soll. Zu diesem Zweck haben wir drei Tabellen namens tblHaupt, tblUnter1 und tblUnter2 erstellt, von denen die letzten beiden per Fremdschlüssel die erste Tabelle referenzieren.
Bild 1: Formular mit zwei Unterformularen
Das Hauptformular ist an die Tabelle tblHaupt gebunden, die beiden Unterformulare an die Tabellen tblUnter1 und tblUnter2.
Undo-Funktion implementieren
Damit die Daten in diesen drei Formularen auch nach dem ändern von Daten im Hauptformular und in den beiden Unterformularen noch wieder hergestellt werden können, ist nur wenig Code notwendig, wenn die beiden Klassen clsUndoMultiMain und clsUndoMultiSub einmal zur Datenbank hinzugefügt wurden.
Diese sieht dann nämlich wie in Listing 1 aus. Hier deklarieren wir im allgemeinen Teil der Code behind-Klasse des Formulars die Variable objUndoMultiMain vom Typ clsUndoMultiMain. Diese erstellen wir dann in der Beim Laden-Ereignisprozedur, wo wir zuerst die Datenherkunft des Hauptformulars für RecordsourceForm angeben, dann das Primärschlüsselfeld der Datenherkunft des Hauptformulars und schließlich einen Verweis auf das Hauptformular selbst.
Dim objUndomultiMain As clsUndoMultiMain Private Sub Form_Load() Set objUndomultiMain = New clsUndoMultiMain With objUndomultiMain .RecordsourceForm = "tblHaupt" .PKForm = "HauptID" Set .Form = Me .AddSubform Me!sfmUnter1.Form, "tblUnter1", "HauptID" .AddSubform Me!sfmUnter2.Form, "tblUnter2", "HauptID" Set .OKButton = Me!cmdOK Set .CancelButton = Me!cmdAbbrechen End With End Sub
Listing 1: Code zum Einbinden der Funktion zum Undo in Haupt- und Unterformular
Dann folgen die neuen Elemente: Die Methode AddSubform kann nämlich so oft aufgerufen werden, wie es nötig ist, und damit ein oder mehrere Unterformulare in die Undo-Funktion einschließen.
Dazu übergeben Sie dieser Methode die folgenden drei Parameter:
- frm: Verweis auf das Unterformular (nicht auf das Unterformular-Steuerelement, sondern auf das darin enthaltene Element – zu referenzieren mit der Form-Eigenschaft)
- strRecordsource: Bezeichnung der Datenherkunft (Tabelle oder Abfrage)
- strFKSubform: Fremdschlüsselfeld der Datenherkunft des Unterformulars, über welches die Beziehung zum Datensatz im Hauptformular hergestellt wird
Grundlegende Technik
Die grundlegende Idee ist es, die gesamte Bearbeitung des aktuellen Datensatzes des Hauptformulars und der mit diesem Datensatz verknüpften Datensätze im Unterformular in eine Transaktion einzuarbeiten. Dazu gibt es einen wichtigen Unterschied zur herkömmlichen Arbeit mit an Formulare gebundene Datenherkünften: Diese dürfen nämlich nicht wie sonst einfach etwa über die Eigenschaft Recordsource beziehungsweise Datenherkunft an die Formulare und Unterformulare gebunden werden. Stattdessen erstellen wir diese als zunächst unabhängiges Recordset und weisen dieses dann der Recordset-Eigenschaft des Hauptformulars und der Unterformulare zu.
Die Recordsets erstellen wir im Kontext eines Database-Objekts, das direkt dem Workspace-Objekt der aktuellen Sitzung untergeordnet ist. Auf diese Weise können wir in den Recordsets änderungen vornehmen und diese dann über die Transaction-Methoden BeginTrans, CommitTrans und Rollback des Workspace-Objekts nach den Wünschen des Benutzers entweder zusammen speichern oder verwerfen.
Zusammenhang zwischen den beiden Klassen
Die Hauptklasse clsUndoMultiMain nimmt die wichtigsten Elemente wie das Database– und das Workspace-Objekt auf und enthält die Ereignisprozeduren, die für das Hauptformular angelegt werden. Die Klasse clsUndoMultiSub, die für jedes Unterformular einmal als Objekt angelegt wird, erhält den Verweis auf das jeweilige Unterformular und implementiert die Ereignisprozeduren für das Unterformular.
Wichtig: Hierbei ist zu beachten, dass das Unterformular ein VBA-Klassenmodul enthält! Sie können das am einfachsten erreichen, indem Sie die Eigenschaft Enthält Modul auf Ja einstellen. Anderenfalls werden die Ereignisprozeduren, die in der Klasse clsUndoMultiSub angelegt sind, nicht für das betroffene Unterformular ausgelöst. Beim Hauptformular sollte dies standardmäßig der Fall sein, da wir dort ja auch die Ereignisprozedur Form_Load unterbringen, in der wie die Undo-Klassen initialisieren und einstellen.
Die ersten beiden Einstellungen, die der Benutzer vornehmen muss, sind der Name des Primärschlüssels der Datenherkunft des Formulars sowie der Name der Datenherkunft. Diese landen in den beiden folgenden Variablen:
Private m_PKForm As String Private m_RecordsourceForm As String
Damit diese Variablen über die Eigenschaft PKForm gefüllt und ausgelesen werden kann, legen wir die beiden folgenden Property Let/Get-Prozeduren an:
Public Property Let PKForm(str As String) m_PKForm = str End Property Public Property Get PKForm() As String PKForm = m_PKForm End Property
Für m_RecordsourceForm benötigen wir nur eine öffentliche Eigenschaft zum Zuweisen, ausgelesen wird diese Eigenschaft von außen nicht:
Public Property Let RecordsourceForm(str As String) m_RecordsourceForm = str End Property
Die Klasse clsUndoMultiMain enthält außerdem die folgenden zwei Elemente, für welche Ereignisprozeduren implementiert werden:
Private WithEvents m_OKButton As CommandButton Private WithEvents m_CancelButton As CommandButton
über die folgenden beiden Property Set-Methoden können die Schaltflächen des Formulars den Variablen zugewiesen werden.
Dabei stellen wir auch gleich ein, dass das Ereignis Beim Klicken in dieser Klasse implementiert werden soll:
Public Property Set OKButton(cmd As CommandButton) Set m_OKButton = cmd With m_OKButton .OnClick = "[Event Procedure]" End With End Property Public Property Set CancelButton(cmd As CommandButton) Set m_CancelButton = cmd With m_CancelButton .OnClick = "[Event Procedure]" End With End Property
Angabe des Hauptformulars
Der Verweis auf das Hauptformular soll in der Variablen m_Form gespeichert werden, für das wir ebenfalls Ereignisprozeduren implementieren wollen können:
Private WithEvents m_Form As Form
Die beiden folgenden Variablen nehmen die Verweise auf das Database– und das Workspace-Objekt auf:
Private m_db As DAO.Database Private m_wrk As DAO.Workspace
Die Variable m_Form wird über die folgende Property Set-Methode gefüllt, die auch gleich einstellt, für welche Ereignisse im aktuellen Modul Ereignisprozeduren hinterlegt werden sollen. Außerdem werden hier die beiden Variablen m_db und m_wrk gefüllt.
Schließlich füllt sie noch das Recordset des Hauptformulars mit der in m_RecordsourceForm angegebenen Datenherkunft (zuerst als Recordset rst als Element des aktuelle Database-Objekts und dann durch Zuweisen dieser Variablen an die Recordset-Eigenschaft des Formulars):
Public Property Set Form(frm As Form) Dim rst As DAO.Recordset Set m_Form = frm With m_Form .AfterUpdate = "[Event Procedure]" .BeforeDelConfirm = "[Event Procedure]" .OnCurrent = "[Event Procedure]" .OnDelete = "[Event Procedure]" .OnDirty = "[Event Procedure]" .OnError = "[Event Procedure]" .OnOpen = "[Event Procedure]" .OnUnload = "[Event Procedure]" .OnUndo = "[Event Procedure]" End With Set m_db = DBEngine(0)(0) Set m_wrk = DBEngine.Workspaces(0) Set rst = m_db.OpenRecordset(m_RecordsourceForm, _ dbOpenDynaset) Set m_Form.Recordset = rst End Property
Wir müssen auch von der Klasse clsUndoMultiSub auf das in clsUndoMultiMain enthaltene Formular zugreifen können, also legen wir auch eine Property Get-Prozedur an:
Public Property Get Form() As Form Set Form = m_Form End Property
Auch die Variable m_wrk mit dem Workspace-Objekt wollen wir von den Unterklassen aus nutzen. Dazu legen wir die folgende Property Get-Methode an:
Public Property Get wrk() As DAO.Workspace Set wrk = m_wrk End Property
Es gibt noch drei Eigenschaften, mit denen wir den aktuellen Zustand der Daten im Formular speichern müssen. Diese sollen in der Hauptklasse clsUndoMultiMain gespeichert werden, aber auch von den Unterklassen zugreifbar sein. Daher legen wir auch diese als private Variablen an:
Private m_DirtyForm As Boolean Private m_SavedForm As Boolean Private m_DeletedForm As Boolean
m_DirtyForm müssen wir lesen und schreiben können:
Public Property Get DirtyForm() As Boolean DirtyForm = m_DirtyForm End Property Public Property Let DirtyForm(bol As Boolean) m_DirtyForm = bol End Property
Gleiches gilt für m_SavedForm:
Public Property Get SavedForm() As Boolean SavedForm = m_SavedForm End Property Public Property Let SavedForm(bol As Boolean) m_SavedForm = bol End Property
m_DeletedForm verwenden wir nur innerhalb der Klasse clsUndoMultiMain, also brauchen wir keine Property-Eigenschaften. Schließlich fehlt noch eine Collection-Variable, in der wir die durch die weiter unten beschriebene AddSubform-Methode hinzugefügten Instanzen der Klasse clsUndoMultiSub speichern:
Private col As Collection
Diese instanzieren wir in dem folgenden Ereignis, das gleich beim Erstellen der Klasse clsUndoMultiMain ausgelöst wird:
Private Sub Class_Initialize() Set col = New Collection End Sub
Unterklassen für Unterformular hinzufügen
Damit kommen wir auch gleich zur Methode AddSubform. Diese sieht wie folgt aus und erwartet die weiter oben beschriebenen Parameter:
Public Sub AddSubform(frm As Form, _ strRecordsourceSubform As String, _ strFKSubform As String) Dim objUndoMultiSub As clsUndoMultiSub Set objUndoMultiSub = New clsUndoMultiSub With objUndoMultiSub Set .Main = Me Set .Subform = frm .RecordsourceSubform = strRecordsourceSubform .FKSubform = strFKSubform End With col.Add objUndoMultiSub End Sub
Sie erstellt mit jedem Aufruf ein neues Objekt des Typs clsUndoMultiSub und speichert dieses in objUndoMultiSub. Dann stellt sie die mit den Parametern übergebenen Werte ein (die Innereien dieser Klasse schauen wir uns ebenfalls weiter unten an). Die in dieser Methode deklarierte Objektvariable objUndoMultiSub würde mit Beenden der Methode ihre Gültigkeit verlieren, also fügen wir diese mit der Add-Methode zur die Auflistung col hinzu.
Daten im Unterformular anzeigen
Damit die Unterformulare anzeigen, muss das Ereignis Beim Anzeigen des Hauptformulars ausgelöst werden. Warum Weil immer, wenn der Benutzer den Datensatz im Hauptformular wechselt, auch die Datensätze in den Unterformularen aktualisiert werden müssen. Sie erinnern sich Wir können nicht mit einer herkömmlichen Bindung zwischen Haupt- und Unterformular arbeiten, weil wir die Daten der Formulare jeweils erst in Recordsets auf Basis des aktuellen Workspaces schreiben und dann an die Eigenschaft Recordset der Formulare übergeben müssen. Anders können wir die änderungen an allen Recordsets nicht innerhalb einer Transaktion erfassen und gegebenenfalls rückgängig machen.
Deshalb richten wir für das Ereignis Beim Anzeigen des Recordsets im Hauptformular die Ereignisprozedur aus Listing 2 ein. Entscheidend für den Moment ist der untere Teil, wo eine If…Then-Bedingung prüft, ob das Hauptformular gerade einen neuen oder einen bereits vorhandenen Datensatz anzeigt. Im Falle eines vorhandenen Datensatzes stellt die Prozedur eine Abfrage zusammen, welche alle Datensätze der Datenherkunft des Unterformulars ermittelt, deren Fremdschlüsselfeld dem aktuellen Primärschlüssel im Hauptformular entspricht. Das auf dieser Abfrage basierende Recordset wird dann der Eigenschaft Recordset des jeweiligen Unterformulars zugewiesen.
Private Sub m_Form_Current() Dim objUndoMultiSub As clsUndoMultiSub If m_DeletedForm = True Then m_wrk.CommitTrans m_SavedForm = False m_DirtyForm = False m_DeletedForm = False Else If m_DirtyForm = True Then DoCmd.RunCommand acCmdSaveRecord m_wrk.CommitTrans m_SavedForm = False m_DirtyForm = False End If End If If Not m_Form.NewRecord Then For Each objUndoMultiSub In col Set objUndoMultiSub.rst = m_db.OpenRecordset("SELECT * FROM " & objUndoMultiSub.RecordsourceSubform _ & " WHERE " & objUndoMultiSub.FKSubform & " = " & Nz(m_Form.Recordset.Fields(m_PKForm), 0), dbOpenDynaset) Next objUndoMultiSub Else For Each objUndoMultiSub In col Set objUndoMultiSub.rst = m_db.OpenRecordset("SELECT * FROM " & objUndoMultiSub.RecordsourceSubform _ & " WHERE 1=2", dbOpenDynaset) Next objUndoMultiSub End If End Sub
Listing 2: Code, der beim Wechseln des Datensatzes im Hauptformular ausgeführt wird.
Neu gegenüber der vorherigen Version, die nur ein einziges Unterformular erlaubte, eine For Each-Schleife, welche alle Unterformulare der in der Collection col gespeicherten Klassen durchläuft und diese mit den passenden Daten füllt.
Handelt es sich hingegen um einen neuen Datensatz im Hauptformular, durchläuft die Prozedur in einer alternativen For Each-Schleife alle Unterformular-Klassen und fügt allen einen Recordset auf Basis der angegebenen Datenherkunft zu, allerdings mit einem Kriterium (1=2), das keine Ergebnisse liefert.
Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...
Testzugang
eine Woche kostenlosen Zugriff auf diesen und mehr als 1.000 weitere Artikel
diesen und alle anderen Artikel mit dem Jahresabo