Undo in mehreren Unterformularen

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.

Formular mit zwei Unterformularen

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

Schreibe einen Kommentar