Undo in Haupt- und Unterformular mit Klasse

Das Problem beim Einsatz von Haupt- und Unterformularen mit Daten aus verknüpften Tabellen ist, dass der Benutzer diese als Einheit ansieht. Enthält das Hauptformular eine Abbrechen-Schaltfläche, geht er davon aus, dass er die änderungen an Daten im Haupt- oder Unterformular damit komplett rückgängig machen kann. Leider ist das nicht so – die änderungen im Unterformular bleiben gespeichert, und auch die Werte im Hauptformular lassen sich nach dem Speichern etwa durch einen Mausklick auf den Datensatzmarkierer nicht mehr rückgängig machen. Grund genug, unsere bereits einmal beschriebene Technik nochmal unter die Lupe zu nehmen und in eine flexibel einsetzbare Klasse zu exportieren.

Beispieltabellen und -formulare

Im Rahmen dieses Beitrag verwenden wir die Tabellen der Südsturm-Datenbank, genau genommen die Tabellen tblBestellungen, tblKunden, tblBestelldetails und tblArtikel. Außerdem erstellen wir zunächst ein Hauptformular namens frmBestellungen und ein Unterformular namens sfmBestellungen, um die herkömmliche Situation zu betrachten.

Später statten wir ähnliche Formulare mit einem Klassenmodul und dessen Methoden und Eigenschaften aus, um das Undo sowohl im Haupt- als auch im Unterformular zu ermöglichen.

Die an der Beispiellösung beteiligten Tabellen finden Sie in der übersicht aus Bild 1. Direkt in die Formulare eingebunden werden dabei nur die beiden Tabellen tblBestellungen und tblBestelldetails, die Daten der Tabelle tblKunden stehen im Hauptformular als Daten eines Kombinationsfeldes zur Verfügung, die Daten der Tabelle tblArtikel in einem Kombinationsfeld im Unterformular.

Tabellen der Beispieldatenbank

Bild 1: Tabellen der Beispieldatenbank

Ausgangssituation

Wir wollen ein Element der Benutzeroberfläche einer Anwendung optimieren, das im Hauptformular jeweils einen Datensatz der Tabelle tblBestellungen anzeigt und im Unterformular die damit verknüpften Datensätze der Tabelle tblBestelldetails. Dazu weisen Sie einfach der Eigenschaft Datenherkunft von Haupt- und Unterformular die beiden Tabellen tblBestellungen und tblBestelldetails zu.

Das Hauptformular soll in diesem Fall die Felder BestellungID, KundeID, Bestelldatum, Lieferdatum und Versanddatum der Tabelle tblBestellungen enthalten, das Unterformular die Felder ArtikelID, Einzelpreis, Anzahl, Rabatt und BestellungID der Tabelle tblBestelldetails.

In der Entwurfsansicht sieht der Aufbau der beiden Formulare nun wie in Bild 2 aus. Das Unterformularsteuerelement haben wir sfm genannt, damit wir es später im Code vom eingebetteten Unterformular sfmBestellungen unterscheiden können.

Haupt- und Unterformular in der Entwurfsansicht

Bild 2: Haupt- und Unterformular in der Entwurfsansicht

Die beiden Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformularsteuerelements sfm enthalten beide den Namen des Feldes BestellungID. Auf diese Weise zeigt das Unterformular jeweils die zum Datensatz des Hauptformulars passenden Daten an. Das Hauptformular enthält noch zwei Schaltflächen. cmdOK löst diesen Code aus:

Private Sub cmdOK_Click()
     DoCmd.Close acForm, Me.Name
End Sub

Damit schließt sie das Formular und speichert den aktuellen Zustand der Daten. Die zweite Schaltfläche enthält die Beschriftung Abbrechen oder, in diesem Fall, Verwerfen. Sie führt lediglich die Undo-Methode des Hauptformular aus und verwirft somit die änderungen seit dem letzten Speichern im Hauptformular:

Private Sub cmdVerwerfen_Click()
     Me.Undo
End Sub

Dies ist für den Benutzer mitunter irreführend, da er den Eindruck erhalten kann, dass sich mit einer solchen Schaltfläche alle seit dem letzten öffnen getätigten änderungen verwerfen lassen. Dies ist mitnichten so: änderungen im Unterformular werden auf keinen Fall verworfen, denn diese werden spätestens dann in der zugrunde liegenden Tabelle gespeichert, wenn das Unterformular den Fokus verliert. Und das ist zwangsläufig der Fall, wenn der Benutzer ein Steuerelement des Hauptformulars betätigt, in diesem Fall die Verwerfen-Schaltfläche.

Andersherum wird der Datensatz im Hauptformular sofort gespeichert, wenn der Benutzer den Fokus in das Unterformular verschiebt. Beim klassischen Fall, also dem Anlegen der grundlegenden Bestelldaten wie Kunde, Bestelldatum et cetera und dem anschließenden Hinzufügen von Bestellpositionen bewirkt das Betätigen der Verwerfen-Schaltfläche schlicht und einfach nichts. Zu diesem Zeitpunkt sind alle Daten bereits gespeichert. Da die Undo-Methode genau die gleiche Wirkung hat wie das Betätigen der Escape-Taste, macht auch diese keine der getätigten und gespeicherten Eingaben mehr rückgängig.

Dies wollen wir nun ändern – und zwar mit einer Klasse, die alle dazu nötigen Funktionen enthält. Das Ganze ist nicht gerade trivial, denn wir müssen einige Fälle beachten. Bei der Programmierung einer solchen Klasse wird man kaum auf einen Schlag alle denkbaren Konstellationen erschlagen. Man beginnt also damit, einfache Vorfälle abzudecken, und programmiert deren Behandlung. Wenn dies funktioniert, schaut man sich den nächsten Vorfall an und programmiert weiter.

Beim Testen des Programmierfortschritts sollte man jedoch auch immer wieder die vorher behandelten Vorfälle testen. Damit stellen Sie sicher, dass Sie durch das Hinzufügen oder ändern des bestehenden Codes die bereits vorhandene Funktionalität beeinflussen.

Grundsätzlich soll die Klasse dafür sorgen, dass alle seit dem öffnen eines Datensatzes getätigten änderungen mit einem Klick auf die Verwerfen-Schaltfläche wieder rückgängig gemacht werden können.

Es gibt eine Ausnahme: Das Löschen einer kompletten Bestellung, also eines Datensatzes der Tabelle tblBestellungen und damit auch aller damit verknüpften Datensätze der Tabelle tblBestellpositionen. In dem Moment, in dem der Benutzer den Datensatz im Hauptformular löscht, spielt es keine Rolle mehr, ob die bisherigen änderungen der Transaktion durchgeführt werden oder nicht.

Tests festlegen

Um zu prüfen, ob alles jederzeit wie gewünscht funktioniert, legen wir einige Tests zurecht – diese sollten alle paar neuen Codezeilen geprüft werden, um vorzeitig ein Programmieren in die falsche Richtung zu verhindern:

  • Eine Bestellung (Bestelldatum 1.1.2013) ohne Bestellpositionen wird erstellt und gespeichert. Ist die Bestellung noch vorhanden
  • Eine Bestellung (Bestelldatum 2.1.2013) mit einer Bestellposition wird erstellt und gespeichert. Sind die Bestellung und die Bestellpositionen noch vorhanden
  • Eine Bestellung (Bestelldatum 3.1.2013) wird erstellt und gespeichert (zum Beispiel durch einen Klick auf den Datensatzmarkierer) und mit der Schaltfläche Verwerfen verworfen. Ist die Bestellung verschwunden
  • Eine Bestellung (Bestelldatum 4.1.2013) mit einer Bestellposition wird erstellt und endgültig gespeichert (durch Wechseln zu einem anderen Datensatz oder Schließen und erneutes öffnen des Formulars). Dann wird zu dieser Bestellung eine Bestellposition hinzugefügt und gespeichert. Entfernt Verwerfen diese Position wieder, während die Bestellung selbst erhalten bleibt
  • Eine Bestellung (5.1.2013) erstellen, eine Bestellposition hinzufügen und Formular schließen. Bestellung wieder anzeigen, Bestellposition löschen und mit Verwerfen wiederherstellen.
  • Eine Bestellung (6.1.2013) erstellen, eine Bestellposition hinzufügen und Formular schließen. Bestellung wieder anzeigen, Bestellposition löschen, mit Verwerfen wiederherstellen, wieder löschen und wieder herstellen.

Am schönsten wäre es natürlich, wenn man solche und ähnliche Tests der Benutzeroberfläche automatisch ablaufen lassen könnte. Dies ist jedoch sehr aufwendig zu realisieren. Gegebenenfalls kümmern wir uns zu einem späteren Zeitpunkt um ein entsprechendes Tool.

Transaktionen

Der Wechsel zum Unterformular speichert bereits die Daten im Hauptformular und der Wechsel von einem Datensatz zum nächsten im Unterformular das Gleiche mit den Datensätzen im Unterformular. Wie wollen wir dann dafür sorgen, dass die vollständigen änderungen am aktuellen Datensatz im Hauptformular samt den verknüpften Daten im Unterformular rückgängig gemacht werden können

Dafür gibt es verschiedene Ansätze. Der erste wäre, den aktuellen Datensatz im Hauptformular vor der Bearbeitung im Hauptformular in einer temporären Tabelle zu speichern – ebenso wie die Daten des Unterformulars. Beim Mausklick auf die Schaltfläche OK werden dann alle Daten in die Originaltabellen übertragen, beim Anklicken von Verwerfen löschen wir einfach die Daten der temporären Tabellen.

Der zweite Ansatz verwendet Transaktionen. Das bedeutet, dass wir bei der ersten änderung an dem im Hauptformular angezeigten Datensatz oder an einem der Datensätze im Unterformular eine Transaktion starten müssen. Das gilt natürlich auch dafür, wenn wir im Hauptformular einen neuen Datensatz anlegen oder Datensätze zum Unterformular hinzufügen.

Der Einsatz von Transaktionen ist recht einfach: Sie definieren eine Workspace-Variable, die den Workspace der aktuellen Datenbank referenziert (mit DBEngine.Workspaces(0). Dieses Workspace-Objekt stellt dann die folgenden drei Methoden zur Verfügung:

  • BeginTrans: Startet eine Transaktion.
  • CommitTrans: Speichert die seit dem Start der Transaktion durchgeführten änderungen.
  • Rollback: Verwirft alle änderungen seit Beginn der Transaktion.

Nun muss man allerdings wissen, welche änderungen vom Start der Transaktion an protokolliert werden und entsprechend rückgängig gemacht werden können. Dabei handelt es lediglich um solche Transaktionen, die über das Database-Objekt der aktuellen Datenbank durchgeführt wurden. Wenn Sie also etwa nach dem Aufruf von BeginTrans mit der db.Execute-Methode eine Aktionsabfrage durchführen oder die Daten eines DAO-Recordsets mit Edit/AddNew und Update ändern, können Sie diese änderungen durch CommitTrans speichern oder durch Rollback verwerfen.

Dummerweise laufen die änderungen, die Sie an den Daten eines über die Eigenschaft Datenherkunft an eine Datenquelle gebundenen Formulars durchführen, nicht im Kontext einer Transaktion. Und hier wird es interessant: Sie können ein Formular nämlich auch über die Recordset-Eigenschaft mit den Daten aus einer Tabelle oder Abfrage füllen. Und wenn Sie dieses Recordset im Kontext einer Transaktion mit OpenRecordset erstellen und der Datenherkunft des Formulars beziehungsweise des Unterformulars zuweisen, können Sie auch die änderungen am Formular durch ein Rollback verwerfen oder durch ein CommitTrans speichern.

Das hat allerdings auch kleinere Nachteile: Normalerweise weisen Sie dem Hauptformular die eine Tabelle der 1:n-Beziehung als Datenherkunft zu und dem Unterformular die andere Tabelle. Dabei stellen Sie die beiden Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformular-Steuerelements auf das Fremdschlüsselfeld im Unterformular und das Primärschlüsselfeld im Hauptformular ein, damit das Unterformular jeweils die passenden Daten zum Hauptformular anzeigt.

Wenn Sie Haupt- und Unterformular jedoch etwa im Ereignis Beim Laden des Hauptformulars mit den zuvor erstellten Recordset-Objekten versehen, sind die beiden Eigenschaften Verknüpfen von und Verknüpfen nach wirkungslos. Deshalb müssen Sie dem Unterformular beim Wechsel des Datensatzes im Hauptformular jeweils ein neues Recordset zuweisen, dass die zum Datensatz im Hauptformular passenden Datensätze enthält.

So schlimm ist das aber auch nicht – wir müssen diesen Vorgang ja auch nur einmal programmieren.

Formular anpassen

Nachdem wir wissen, dass wir das Formular und das Unterformular mit einer Klasse ausstatten wollen, die dafür sorgt, dass änderungen an den kompletten angezeigten Daten des aktuellen Datensatzes des Hauptformulars durch einen Mausklick auf eine Schaltfläche rückgängig gemacht werden, bereiten wir zunächst das Formular vor.

Formular und Unterformular erstellen

Grundsätzlich sollten Haupt- und Unterformular zunächst mit den Tabellen oder Abfragen als Datenherkunft ausgestattet werden, die sie im normalen Betrieb verwenden würden.

Dies geschieht aus reiner Bequemlichkeit: Wir können so nämlich die Steuerelemente der jeweiligen Datenherkunft aus der Feldliste in die gewünschten Bereiche des Formulars ziehen. Danach leeren wir einfach die Eigenschaft Datenherkunft des Formulars.

Auf die gleiche Weise gehen wir beim Unterformular vor. Hier ist darauf zu achten, dass Sie auch die beiden Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformular-Steuerelements leeren (s. Bild 3).

Die Verknüpfungseigenschaften zwischen Haupt- und Unterformular werden geleert.

Bild 3: Die Verknüpfungseigenschaften zwischen Haupt- und Unterformular werden geleert.

Klasse erstellen

Anschließend erstellen wir schon das Klassenmodul (VBA-Editor, Einfügen|Klassenmodul). Dieses nennen wir schlicht und einfach clsUndo.

Normalerweise hätten wir die Ereignisse, die dazu führen, dass eine Transaktion gestartet, beendet oder verworfen wird, direkt in den Klassenmodulen des Haupt- und des Unterformulars untergebracht. Dann müssten Sie diesen Code jedoch, wenn Sie diesen in einem anderen Formular weiterverwenden wollten, jeweils in das entsprechende Klassenmodul des Formulars/Unterformulars übertragen.

Wenn Sie eine solche Funktionalität einmal programmiert haben und diese dann auf einen anderen Anwendungsfall übertragen möchten, sind meist individuelle Anpassungen nötig – was zu Fehlern führen kann, insbesondere dadurch bedingt, dass man nicht mehr genau weiß, an welchen Schrauben man wie drehen muss. Wenn Sie hingegen die komplette Funktion in einem eigenen Klassenmodul kapseln und dieses von dem damit auszustattenden Formular einfach nur instanzieren und mit einigen Eigenschaften ausstatten müssen, haben Sie leichtes Spiel.

Referenzen an das Klassenmodul übergeben

Die Ereignisse, Variablen und Prozeduren, die Sie normalerweise im Klassenmodul des betroffenen Formulars angelegt hätten, deklarieren Sie nun komplett im Klassenmodul clsUndo. Dabei ist allerdings zu beachten, dass dieses ja nicht weiß, auf welches Formular es sich beziehen soll. Genauso weiß es nicht, welches Unterformular betroffen ist (wenn mehrere vorhanden sind) und welche Schaltflächen für das übernehmen oder Verwerfen der änderungen verantwortlich sind.

Damit wir der Klasse beim Laden des Formulars die benötigten Informationen übergeben können, legen wir in der Klasse zunächst einige Variablen fest, welche die Verweise auf diese Objekte speichern sollen. Dies sieht wie folgt aus:

Wir benötigen also zwei Variablen des Typs Form und zwei des Typs CommandButton. Für alle vier Objekte wollen wir innerhalb des Klassenmoduls clsUndo eigene Ereignisprozeduren implementieren. Das geschieht prinzipiell genauso wie im Klassenmodul eines Formulars, allerdings steht die einfache Möglichkeit zum Erstellen solcher Ereignisprozeduren durch Eintragen des Wertes [Ereignisprozedur] in die jeweilige Ereigniseigenschaft und anschließendes Klicken auf die Schaltfläche mit den drei Punkten (…) rechts neben der Eigenschaft nicht zur Verfügung.

Bliebe noch die Möglichkeit, die entsprechenden Objektvariablen im linken Kombinationsfeld des VBA-Editors auszuwählen und das passende Ereignis aus dem rechten Kombinationsfeld zu ergänzen. Das gelingt aber auch nur unter einer Bedingung: Wenn die passende Objektvariable mit dem WithEvents-Schlüsselwort deklariert wurde – und genau dies erledigen wir wie folgt:

Private WithEvents m_Form As Form
Private WithEvents m_Subform As Form
Private WithEvents m_OKButton As  CommandButton
Private WithEvents m_CancelButton As  CommandButton

Anschließend fügen Sie über die beiden Kombinationsfelder im Codefenster die benötigten Ereignisprozeduren hinzu (s. Bild 4).

Hinzufügen einer Ereignisprozedur für eine mit WithEvents deklarierte Objektvariable

Sie haben das Ende des frei verfügbaren Textes erreicht. Möchten Sie ...

TestzugangOder bist Du bereits Abonnent? Dann logge Dich gleich hier ein. Die Zugangsdaten findest Du entweder in der aktuellen Print-Ausgabe auf Seite U2 oder beim Online-Abo in der E-Mail, die Du als Abonnent regelmäßig erhältst:

Schreibe einen Kommentar