Home > Artikel > Ausgabe 11/2015 > Navigation in und zwischen Formularen

Navigation in und zwischen Formularen

  PDF ansehen

  Download PDF und Beispieldatenbank

Wie gelangt man in einem Formular intuitiv zu einem bestimmten Datensatz? Wie kann man zum zuletzt angezeigten Datensatz zurückspringen? Auf welche Weise lässt sich zu einem Detaildatensatz ohne Umwege ein Hauptformular öffnen? Antworten auf diese Fragen versucht dieser Beitrag mit einigen trickreichen Routinen zu geben, die teilweise die Navigation auf Webseiten nachbilden.

Beispieldatenbank

Die Beispiele dieses Artikels finden Sie in der Datenbank 1511_FormNavigation.accdb

Datenmodell

Die Beispielanwendung verwendet ein Datenmodell (Bild 1), welches sich um Adressen, Kunden, Bestellungen und Artikel rankt. Dabei handelt es sich nicht um eine ausgefeilte Vertriebslösung, sondern um ein Demonstrationsobjekt, das auf die wesentlichen Teile reduziert ist. Zwei Hauptformulare speisen sich dann aus den Tabellen des Modells – so viel sei schon an dieser Stelle verraten. Das eine verwendet nur die Tabelle tblAdressen und deren Nachschlagetabellen tblAnreden, tblOrte und tblLaender.

Datenmodell der Beispieldatenbank mit den Beziehungen zwischen Adressen, Bestellungen und Artikeln

Bild 1: Datenmodell der Beispieldatenbank mit den Beziehungen zwischen Adressen, Bestellungen und Artikeln

Das andere simuliert eine Kunden- und Bestellverwaltung, wobei hier die Adresstabelle als Kundenbasis zweckentfremdet wird. An ihr hängen die Bestellungen, Bestelldetails und Bestellartikel.

Die Verknüpfung zwischen Kundenadresse und Bestellung geschieht über die ID der Primärtabelle und KundeID der Fremdschlüsseltabelle. Ähnlich sieht die Beziehung zwischen Bestellung und Bestelldetail aus. Die Tabelle tblArtikel ist hingegen eher wieder eine Nachschlagetabelle. Zu einer Kundenadresse kann es also mehrere Bestellungen geben und zu jeder Bestellung wiederum mehrere Bestellartikel in den Bestelldetails. Damit die Artikeltabelle etwas aufgepeppt wird, verweist ein Feld Herkunftsland zusätzlich auf die Tabelle tblLaender.

Adressen verwalten

Über das Formular frmAdressen in Bild 2 werden nun die Adressen angezeigt und bearbeitet. Es enthält Steuerelemente für alle Felder der Tabelle, wobei die drei Nachschlagefelder IDAnrede, IDOrt und IDLand als Kombinationsfelder daher kommen und sich in ihrer Datenherkunft direkt aus den jeweiligen Tabellen speisen. Gebunden sind diese Felder an die erste unsichtbare Spalte ID der Detailtabellen. Die Datenquelle des Formulars selbst ist ebenfalls direkt die Tabelle tblAdressen. Soweit weist das Formular keinerlei Besonderheiten auf.

Das Adressformular frmAdressen der Beispieldatenbank

Bild 2: Das Adressformular frmAdressen der Beispieldatenbank

Zusätzlich sind ist aber ein Unterformular sfrmKinder integriert, welches ebenfalls auf der Adresstabelle basiert, jedoch deren Felder über eine Abfrage auf Nachname und Vorname beschränkt. Das sollen Kinder des angezeigten Hauptdatensatzes sein. Die Verbindung kommt über das unscheinbare Feld ParentID der Tabelle zustande. Ein Datensatz, der einen Wert in diesem Feld enthält, verweist damit auf den Elterndatensatz mit der verwiesenen ID. In der letzten Ausgabe von Access Basics lernten Sie diese Methode bereits im Beitrag zu Rekursiven Tabellen kennen. Beim Navigieren durch die Hauptdatensätze des Formulars werden also im Unterformular die zugehörigen Kinder angezeigt.

Es gibt nun drei Spezialelemente im Formular. Zum einen listet eine Combobox rechts oben im Kopfbereich alle Adressen namentlich auf. Durch Auswahl eines Eintrags gelangen Sie zum entsprechenden Adressdatensatz, wie Bild 3 demonstriert. Deshalb ist die Navigationsleiste des Formulars auch ausgeblendet. Denn diese ist in den meisten Fällen ohnehin überflüssig. Wer sucht Datensätze schon über Vor- und Zurücksteppen oder über die Eingabe eines Zahlenwerts heraus?

Sprung zu anderen Datensätzen im Adressformular über Kombinationsfeld

Bild 3: Sprung zu anderen Datensätzen im Adressformular über Kombinationsfeld

Ebenfalls im Kopfbereich untergebracht ist die Schaltfläche Zurück. Sie ist zunächst deaktiviert. Sobald Sie aber über die Navigations-Combo zu einem anderen Datensatz springen, wird sie aktiv und zeigt die eben verlassene Adresse an, etwa den Karol Wagner (Bild 4). Mit Klick auf diesen Button gelangen Sie wieder zurück zum aufrufenden Datensatz.

Die Zurück-Schaltfläche oben zeigt die zuletzt verlassene Adresse an

Bild 4: Die Zurück-Schaltfläche oben zeigt die zuletzt verlassene Adresse an

Wie der Text unter dem Unterformular bereits erläutert, reagieren die Datenzeilen der Kinder auf einen Doppelklick. Der löst nämlich aus, dass zum Adressdatensatzes dieses Kindes gesprungen wird. Auch in diesem Fall wird die Zurück-Schaltfläche aktiv, damit Sie schnell zum Elterndatensatz zurückgelangen können.

Eingebaut ist hier also eine History ähnlich der des Webbrowsers. Dass all diese Navigationsfunktionen mit nur sehr wenig Aufwand zu realisieren sind, zeigt ein Blick in das Code-Modul des Formulars. Listing 1 gibt den Kopf und die Initialisierung wieder.

Private arrUndo() As String

Private lUndo As Long

Private Sub Form_Load()

     ReDim arrUndo(1, 30)

     lUndo = -1

     Me.Recordset.FindFirst "ID=19"

End Sub

Private Sub Form_Current()

     Me!cbFind = Null

End Sub

Listing 1: Erster Teil des Formularmoduls von frmAdressen

Dort ist ein Array arrUndo deklariert, welches der History-Funktion dient. In diesem Array werden zuletzt verlassene Datensätze anhand ihrer ID und des Adressnamens gespeichert. In der Ereignisprozedur Beim Laden (Form_Load) wird das Array auf zwei Ebenen mit 30 Elementen dimensioniert. 30 ist ein willkürlich gewählter Wert, der die maximale Zahl der Rücksprünge darstellt. Er sollte ausreichen. Die erste Ebene des Arrays enthält die IDs der Datensätze, die zweite die Adressnamen. Wir kommen noch darauf zu sprechen, wie und wo es befüllt wird. Jedenfalls könnte ein Array-Datensatz so aussehen:

arrUndo(0,12) = 78

arrUndo(1,12) ="Karol, Wagner"

Die Variable lUndo ist ein Positionszeiger für das Array. Wäre die aufgeführten Elemente eben erst gespeichert worden, so enthielte lUndo den Wert 12. Im Load-Ereignis jedoch wird lUndo zunächst auf -1 gesetzt, weil es ja noch keinen Array-Eintrag gibt. Nur zur Demonstration wird in diesem Ereignis auch noch der Datensatz mit der ID 19 angesprungen. Diese Zeile ist elementar für das Navigieren zu bestimmten Datensätzen. Statt sich etwa der DoCmd-Methode GotoRecord zu bedienen, ist die Navigation über das Formular-Recordset erheblich einfacher. Dessen Methode FindFirst findet automatisch den Datensatz mit der ID 19 und, das ist der Clou, zeigt ihn auch gleich an.

In der Ereignisprozedur Beim Anzeigen (Form_Current) wird außerdem das Kombinationsfeld cbFind zur Auswahl der Adressdatensätze auf den Wert Null gesetzt. Bei jedem Wechsel zu einem anderen Datensatz leert sich damit das Steuerelement, um Verwirrung vorzubeugen.

Die Auswahl eines Eintrags aus der Combobox cbFind löst nun die Ereignisprozedur Nach Aktualisierung (AfterUpdate) aus (siehe Listing 2). Hier wird ihr gebundener Wert, also die ID des gewünschten Datensatzes, an die Hilfsprozedur NavigateTo weitergeleitet. Die wird nämlich noch von anderer Stelle benötigt, dem Anspringen eines Kind-Datensatzes aus dem Unterformular heraus, weshalb sie auch mit Public öffentlich gemacht wurde. Nur so kann sie auch von außerhalb des Formular-Codes aufgerufen werden.

Private Sub cbFind_AfterUpdate()

     NavigateTo Me!cbFind.Value

End Sub

Public Sub NavigateTo(ByVal lID As Long)

     lUndo = lUndo + 1

     arrUndo(0, lUndo) = Me!ID.Value

     arrUndo(1, lUndo) = Me!Nachname.Value & ", " & Me!Vorname.Value

     

     Me!cmdBack.Caption = "Zurück zu " & arrUndo(1, lUndo)

     Me!cmdBack.Enabled = True

     

     Me.Recordset.FindFirst "ID=" & lID

End Sub

Private Sub cmdBack_Click()

     Me.Recordset.FindFirst "ID=" & arrUndo(0, lUndo)

     lUndo = lUndo - 1

     If lUndo < 0 Then

         Me!cmdBack.Enabled = False

         Me!cmdBack.Caption = "Zurück"

     Else

         Me!cmdBack.Caption = "Zurück zu " & arrUndo(1, lUndo)

     End If

End Sub

Listing 2: Die eigentlichen Navigationsprozeduren des Formularmoduls

In NavigateTo, das als Parameter die ID des anzuspringenden Datensatzes erwartet, wird zunächst der History-Zeiger lUndo um eins erhöht. Beim ersten Aufruf der Prozedur wäre sein Wert demnach 0. Anschließend wird das Array mit einem neuen Element versehen. Das ist die ID des momentanen Datensatzes und dessen aus Nachname und Vorname zusammengesetzte Adressname. Dieser Eintrag wird sogleich auch benötigt, um die Aufschrift für die Zurück-Schaltfläche cmdBack zu bestimmen. Sie wird nun zudem mit Enabled aktiviert. Erst nach diesen Vorgängen geschieht der Sprung zum neuen Datensatz über FindFirst des Formular-Recordsets.

Bei Klick auf die Zurück-Schaltfläche (cmdBack_Click) wird im Prinzip genau der entgegengesetzte Ablauf absolviert. Erst tätigt FindFirst den Sprung zum früheren Datensatz, der ja im Array arrUndo zwischengespeichert ist. Der Positionszeigers lUndo wird nun verringert. Je nach dessen Wert verzweigt die Routine jetzt.

Ist der Wert des Positionszeiger kleiner, als 0, so gibt es kein gültiges Element mehr im Array und somit der der History. Folglich wird die Zurück-Schaltfläche nun wieder deaktiviert und ihre Aufschrift mit einem einfachen Zurück versehen. Andernfalls ändert sich die Beschriftung mit dem Inhalt des neuen Array-Elements.

Bliebe noch die Funktion zum Anspringen von Kind-Datensätzen aus dem Unterformular sfrmKinder heraus. Bei Doppelklick auf entweder dessen Vorname- oder Nachname-Feld wird nur eine Zeile aufgerufen:

Me.Parent.NavigateTo Me!ID.Value

Me.Parent verweist auf das Hauptformularobjekt und NavigateTo ist dessen bereits erläuterte öffentliche Prozedur zum Navigieren zu einer bestimmten Datensatz-ID.

Soweit zu den Sprungtechniken innerhalb eines Formulars. Kommen mehrere datentechnisch verbandelte Formulare ins Spiel, so sind diese Techniken etwas abzuwandeln.

Kunden, Bestellungen, Artikel

In zweiten Teil der Beispieldatenbank kommt mit dem Formular frmKunden eine Anwendung zur rudimentären Verwaltung von Kunden und deren Bestellungen zum Einsatz. Bild 5 zeigt das Formular im Betrieb.

Das Kundenformular mit einer Übersicht zu den Bestellungen zur Laufzeit

Bild 5: Das Kundenformular mit einer Übersicht zu den Bestellungen zur Laufzeit

In Bild 6 finden Sie den Entwurf. Es ist ähnlich aufgebaut, wie das Adressformular und teilt mit diesem auch die Datenbasis in Gestalt der Tabelle tblAdressen. Im Unterformular werden hier aber keine Kinder dargestellt, sondern, in Kurzform, die Bestellungen der Person. Im Detail können diese Bestellungen in einem separaten Formular frmBestellungen eingesehen und bearbeitet werden, denn das Unterformular ist schreibgeschützt und dient nur zur Übersicht. Gleiches gilt für die Artikel, die das separate Formular frmArtikel verwaltet.

Das Kundenformular hier in der Entwurfsansicht

Bild 6: Das Kundenformular hier in der Entwurfsansicht

Auch das Kundenformular enthält die Sprung-Combobox rechts oben. Die Zurück-Schaltfläche des Adressformulars fehlt ihm und stattdessen findet sich im Kopf eine Checkbox, die die Datensätze filtert. In aktiviertem Zustand tauchen im Formular dann nur jene Personen auf, zu denen bereits Bestellungen vorliegen. Die Bestellzeilen des Unterformulars reagieren hier ebenfalls auf einen Klick, nur wird dabei nicht innerhalb des Formulars navigiert, sondern zu einem anderen Formular, das sich gegebenfalls öffnet. Dazu klicken Sie auf die erste Spalte mit den Zeichen «•». Ähnlich verhält sich die Spalte Artikelname, die gleichfalls ein gesondertes Formular mit dem Artikel des Interesses anspringt und dies dadurch deutlich macht, indem der Text im Hyperlink-Stil ausgeführt ist. Das Anspringen von Artikeln zeigt Bild 7.

Der Klick auf einen Artikel-Link führt zum Öffnen des Artikelformulars

Bild 7: Der Klick auf einen Artikel-Link führt zum Öffnen des Artikelformulars

Die Anzeige einer Bestellung hingegen finden Sie in Bild 8.

Der Klick auf einen Unterformulareintrag zu Bestellungen öffnet den Datensatz im Formular Bestellungen

Bild 8: Der Klick auf einen Unterformulareintrag zu Bestellungen öffnet den Datensatz im Formular Bestellungen

Schauen wir uns nun die Datenherkünfte des Hauptformulars an. Davon existieren zwei, denn je nach Zustand der Checkbox zur Filterung ändert sich das zugrundeliegende Recordset. Normalerweise ist das Formular direkt an die Tabelle tblAdressen gebunden, sortiert diese jedoch aufsteigend nach Namen, was diesem SQL-String entspricht:

SELECT * FROM tblAdressen

ORDER BY Nachname, Vorname

Das Ereignis Nach Aktualisierung der Checkbox chkBestell aber löst eine Prozedur aus, die die Datenquelle ändert:

If chkBestell.Value = False Then

Me.RecordSource = _

     "SELECT tblAdressen.* " & _

     "FROM tblAdressen " & _

     "INNER JOIN tblBestellungen " & _

     "ON tblAdressen.ID " & _

     "= tblBestellungen.KundeID " & _

     "ORDER BY Nachname, Vorname"

End If

Hier wird also ein neuer SQL-String als Quelle verabreicht, der neben der Adresstabelle auch die Bestelltabelle tblBestellungen berücksichtigt, indem beide miteinander über KundeID und ID verknüpft sind. Damit werden alle Adressen ausgeschlossen, zu denen es keine Kunden-ID in tblBestellungen gibt. Das Deaktivieren der Checkbox setzt den früheren SQL-Ausdruck wieder ein. Das allein reicht allerdings noch nicht aus. Denn nach Filterung der Adressen listet das Sprungkombinationsfeld des Formulars rechts oben noch immer alle Adressen auf. Also auch jene, zu denen infolge der Filterung gar nicht mehr gesprungen werden kann. Demzufolge ist auch dessen Datenquelle je nach Zustand der Checkbox zu ändern.

Die SQL-Strings dafür gleichen denen des Hauptformulars, nur dass nicht alle Felder in das Abfrageergebnis einbezogen werden, sondern lediglich Vor- und Nachname. Der Code des Kundenformulars weist darüber keine anderen Elemente auf, als das zuvor besprochene Adressformular. Auch hier gibt es die Prozedur NavigateTo zum Springen zu einem anderen Datensatz. Die Undo-Liste ist aber nicht implementiert.

Wenden wir uns deshalb dem Unterformular sfrmBestellungen zu, dessen Code in Listing 3 dargestellt ist. Beim Anklicken des Artikelfeldes wird erst ermittelt, welche ID der angeklickte Artikel im Datensatz hat. Mit diesem Wert (lID) in der WhereCondition öffnet DoCmd.OpenForm das Artikelformular im Dialogmodus. Damit ist es modal geöffnet und sperrt damit die Bearbeitung des nun im Hintergrund befindlichen Kundenformulars.

Private Sub Artikelname_Click()

     Dim lID As Long

     

     lID = Me!ArtikelID.Value

     DoCmd.OpenForm "frmArtikel", , , "ID=" & lID, , acDialog

     Me.Requery

End Sub

Private Sub txtX_Click()

     Dim lID As Long

     

     lID = Me!BestellID.Value

     DoCmd.OpenForm "frmBestellungen", , , _

         "BestellID=" & lID, acFormReadOnly

     Forms!frmBestellungen.Recordset.FindFirst _

         "BestellDetailID=" & Me!BestellDetailID.Value

End Sub

Listing 3: Code des Unterformulars sfrmBestellungen

Vor allem aber hält nun der Programmcode an dieser Stelle an. Erst nach dem Schließen des Artikelformulars fährt er fort und führt das Requery aus. Wofür benötigen wir dieses? Es könnte sein, dass die Bezeichnung eines Artikels oder dessen Preis im Artikelformular geändert wurde. Dann reflektiert sich dies nicht automatisch im Bestellunterformular. Erst das Requery führt dazu, dass die Felddaten aus der nunmehr bearbeiteten Tabelle neu berechnet werden.

Schenken Sie diesem Umstand Beachtung! Häufig hängen die Daten von verschiedenen Formularen voneinander ab oder teilen diese. Änderungen im einen Formular führen nicht automatisch zur Anzeige im anderen. Nicht selten führt genau dies zu Datenbearbeitungsfehlern. Streuen Sie an relevanten Stellen immer Requery- oder Refresh-Anweisungen in den Code ein.

Die zweite Prozedur des Listings wird durch Klick auf die erste Spalte des Unterformulars ausgelöst. Das zugehörige Textfeld nennt sich txtX. Der Vorgang ist hier ähnlich, wie beim Artikel. Es öffnet sich das Bestellformular mit der gewünschten Bestellung, die aus der BestellID hervorgeht. Diese kann indessen wieder mehrere Bestellartikel aufweisen, auf die in der BestellDetailID verwiesen wird. Damit das Formular gleich den passenden Bestellartikel markiert, wird die Methode FindFirst seines Recordsets bemüht. Damit diese Zeile aber ausgeführt werden kann – das ist der springende Punkt! –, darf das Formular nicht im Dialogmodus geöffnet werden.

Hiermit handeln wird uns jedoch den Missstand ein, dass Änderungen an den Bestelldaten sich nicht mehr über ein Requery ins Kundenunterformular geholt werden könnten, es sein denn, dieses Requery auf das Unterformular würde direkt vom Bestellformular abgesetzt. Solche Abhängigkeiten sind aber zu vermeiden.

Darum ist in diesem Fall der Modus des Bestellformulars beim Öffnen auf acFormReadOnly eingestellt, was es dessen Datenbearbeitung sperrt. Bestellungen müssen folglich auf andere Weise verwaltet werden, etwa durch einen Standalone-Aufruf des Bestellformulars.

Vielleicht fällt Ihnen auf, dass die Artikelnamen im Bestellformular ebenfalls im blauen Hyperlink-Stil gehalten sind. Das weist darauf hin, dass auch hier das Artikelformular durch Klick auf einen Eintrag geöffnet wird. Das ist tatsächlich der Fall.

Die Ereignisprozedur dazu sieht genauso aus, wie die analoge in Listing 3. Zusätzlich aber ist auch die Kundennummer rechts im Datensatz Hyperlink-aktiviert.

Damit öffnen Sie das Kundenformular mit der Kundenadresse, die dieser Nummer entspricht. Sollte das Bestellformular vom Kundenunterformular aufgerufen worden sein, so macht diese Funktion natürlich wenig Sinn, denn erstens geraten Sie so nur zur gleichen aufrufenden Kundenadresse, und zweitens ist diese, weil das Bestellformular nun modal im Vordergrund steht, ohnehin gesperrt.

Aber beim erwähnten Standalone-Betrieb der Bestellungen ändert sich das Bild. Hier ist der Aufruf der Kundenadresse dann nützlich.