Fehler in der Runtime von Access finden

Wenn wir eine Datenbank, die unter der Vollversion von Access fehlerfrei läuft, in der Runtime öffnen, kann es zu unerklärlichen Fehlern kommen. Die Runtime verabschiedet sich dann in der Regel mit einer Meldung wie “Die Ausführung dieser Anwendung wurde wegen eines Laufzeitfehlers angehalten.” Damit können wir natürlich erst einmal nicht viel anfangen. Anlass zu diesem Beitrag ist ein konkretes Problem einer Kundin, deren Anwendung auf der Runtime-Version von Access nicht wie gewünscht funktioniert. Beim Testen der Anwendung in der Runtime kam ich jedoch gar nicht erst soweit wie die Kundin. Es tauchte bereits vorher die besagte Meldung auf. Wie können wir nun herausfinden, was genau den Fehler verursacht und welche Zeile ihn auslöst? Vorgehensweisen dazu stellen wir in diesem Beitrag vor.

Optimale Lösung: Die Anwendung ist vollständig mit einer Fehlerbehandlung ausgestattet.

Probleme wie das geschilderte sollten eigentlich gar nicht in dieser Form auftreten. “In dieser Form” bedeutet, dass es durchaus einmal vorkommen kann, dass eine Anwendung unter der Runtime-Version von Access anders arbeitet als unter der Vollversion.

Aber wenn dabei Probleme auftauchen, werden diese in der Regel durch Fehler im VBA-Code ausgelöst. Und wenn die Meldung aus Bild 1 erscheint, ist die Wahrscheinlichkeit hoch, dass es in der Runtime einen Fehler gibt, der durch eine VBA-Anweisung ausgelöst wurde.

Typische Fehlermeldung der Runtime

Bild 1: Typische Fehlermeldung der Runtime

Eine professionell programmierte Anwendung würde in jeder Prozedur eine Fehlerbehandlung enthalten, die beim Auftreten eines Fehlers dafür sorgt, dass diese nicht die eingebaute Access-Fehlermeldung auslöst. Genau diese wird nämlich durch die hier gezeigte Fehlermeldung der Runtime-Version kaschiert.

Fehler im Griff mit umfassender Fehlerbehandlung

Wenn wir flächendeckend unsere eigene Fehlerbehandlung in den Routinen der Anwendung untergebracht hätten, wäre die eingebaute Fehlermeldung der Runtime gar nicht nötig. Eine solche Fehlerbehandlung ist relativ einfach zu realisieren, wenn man dies gleich von Beginn an berücksichtigt. Wenn man jedoch erst einmal eine umfangreiche Anwendung programmiert hat und anschließend die Fehlerbehandlung hinzufügen will, kommt eine Menge Arbeit auf einen zu.

Fehlerbehandlung per MZ-Tools

Auch diese kann man weitgehend vereinfachen, indem man ein Werkzeug wie MZ-Tools zum halbautomatischen Anlegen der Fehlerbehandlung nutzt. Mit MZ-Tools können wir sogar automatisch Zeilennummern hinzufügen, was in einem Runtime-Setting essenziell ist, denn sonst können wir kaum herausfinden, wo in einer längeren Prozedur genau der Fehler aufgetreten ist.

Fehlerbehandlung per vbWatchDog

Die nächste Möglichkeit ist der vbWatchDog, ein Add-In von Access-Spezialist Wayne Philips. Das ist ein Tool, mit dem man nur durch Hinzufügen einiger weniger Objekte und ein wenig Code die komplette Anwendung mit einer allgemeinen Fehlerbehandlung ausstattet, ohne den einzelnen Prozeduren auch nur eine einzige Zeile Code hinzuzufügen (es sei denn, es ist eine spezielle Fehlerbehandlung erwünscht).

Problem bei der Runtime: Kein Debug.Print möglich

Wenn wir eine Anwendung in der Vollversion testen, um ein eventuell fehlerhaftes Verhalten zu prüfen, hilft uns oft die Debug.Print-Anweisung weiter. Damit können wir praktisch vor oder nach jeder Zeile Informationen im Direktbereich des VBA-Editors ausgeben.

Leider hilft uns auch das in einer unter der Runtime-Version von Access ausgeführten Anwendung nicht weiter, denn hier gibt es gar keinen VBA-Editor, mit dem wir uns die Ausgabe ansehen könnten.

Herausfinden, wo ein Fehler stattfindet, ohne alles mit Fehlerbehandlungen auszustatten

Typischerweise funktionieren Dinge, die unter einer Access-Vollversion reibungslos laufen, gelegentlich in der Runtime-Version nicht. Wenn wir keine Fehlerbehandlung eingebaut haben und vielleicht auch gerade nicht die Ressourcen verfügbar sind, um das nachzuholen, müssen wir ein wenig kleinschrittiger an das Problem herangehen.

Wir nehmen nun zusätzlich noch an, dass wir es mit einer Anwendung eines Kunden zu tun haben, die wir nicht so genau kennen wie unsere selbst programmierten Anwendungen.

Dann sehen wir vielleicht vor dem Erscheinen der ominösen Fehlermeldung noch, welche Formulare angezeigt werden, aber welche Prozedur den Fehler ausgelöst hat, finden wir auf diese Weise auch nicht heraus.

Wenn wir nicht alle Prozeduren mit einer Fehlerbehandlung versehen wollen, müssen wir einen anderen Weg gehen, um herauszufinden, welche Stelle im Code den Fehler auslöst. Dazu gehen wir wie folgt vor:

  • Wir fügen jeder Prozedur eine Anweisung hinzu, die unmittelbar hinter der Kopfzeile der Prozedur angelegt wird. Diese soll eine Hilfsfunktion aufrufen, die den Namen des Moduls, den Namen der Prozedur und Datum und Uhrzeit des Fehlers notiert. Die Hilfsfunktion soll diese Daten in eine eigens dafür angelegte Tabelle schreiben.
  • Dann kopieren wir die Datenbank auf den Rechner mit der Runtime-Version von Access und führen diese dort aus. Wir sollten nun den Fehler wie gewohnt erhalten.
  • Der Unterschied ist nun jedoch, dass wir in der Tabelle zum Aufzeichnen der aufgerufenen Prozeduren einige Einträge vorfinden, von denen der letzte vermutlich die Prozedur enthält, die den Fehler ausgelöst hat. Um diese zu lesen, kopieren wir die Datenbank wieder auf den Rechner mit der Vollversion von Access zurück.
  • Für die gefundene Prozedur fügen wir nun eine Fehlerbehandlung hinzu sowie eine Zeilennummerierung. Auch die hierbei anfallenden Informationen schreiben wir wieder in eine Tabelle.
  • Wenn wir die Datenbank nun wieder auf den Runtime-Rechner verschieben und ausführen, wird Access den Fehler nicht erneut anzeigen, da dieser ja nun behandelt wird. Es ist jedoch wahrscheinlich, dass nun ein Folgefehler dazu führt, dass die von der Runtime gelieferte Fehlermeldung erscheint. Das ist jedoch erst einmal irrelevant – wir wollen nun erst einmal diesen Fehler untersuchen und beheben.
  • Auf diese Weise arbeiten wir uns durch die Fehler, die im Runtime-Betrieb auftauchen und beheben diese.

Optimalerweise fügt man einer Anwendung für Runtime jedoch dennoch bei nächster Gelegenheit eine Fehlerbehandlung zu.

Untersuchen, welche Prozedur vermutlich den Fehler auslöst

Damit kommen wir zum ersten Schritt. Wir fügen einer Prozedur den folgenden Aufruf hinzu:

Public Sub BeispielCall()
     Call WriteCall("BeispielCall", _
         "mdlErrors", True)
     ''... weitere Anweisungen
End Sub

Die dadurch aufgerufene Prozedur sieht wie in Listing 1 aus. Sie erwartet den Namen der Prozedur und des Moduls als Parameter sowie einen Parameter namens bolBeginning.

Public Sub WriteCall(strProcedure As String, strModule As String, bolBeginning As Boolean)
     Dim db As DAO.Database
     Set db = CurrentDb
     db.Execute "INSERT INTO tblCalls(CallProcedure, CallModule, CallTime, Beginning) VALUES(''" & strProcedure _
         & "'', ''" & strModule & "'', " & Format(Now, "\#yyyy-mm-dd hh:nn:ss\#") & ", " & CInt(bolBeginning) & ")", _
         dbFailOnError
End Sub

Listing 1: Prozedur zum Schreiben der Informationen zu einem Prozeduraufruf

Diesen stellen wir auf True ein, wenn wir den Aufruf von WriteCall am Anfang der Prozedur platzieren und auf False, wenn wir das Prozedurende verwenden.

Die Tabelle, die wir mit der INSERT INTO-Anweisung füllen wollen, finden wir in der Entwurfsansicht in Bild 2.

Entwurf der Tabelle für die Aufrufe

Bild 2: Entwurf der Tabelle für die Aufrufe

Wenn wir den Aufruf der Prozedur WriteCall in einige Prozeduren geschrieben und die Datenbank ausgeführt haben, sieht ihr Inhalt beispielsweise wie in Bild 3 aus. Nun stellt sich noch die Frage: Wie bekommen wir den Aufruf schnell in alle Prozeduren?

Tabelle mit einigen aufgezeichneten Prozedur-Calls

Bild 3: Tabelle mit einigen aufgezeichneten Prozedur-Calls

Aufruf der Prozedur WriteCall in alle Routine schreiben

Wir haben bereits davon gesprochen, dass unsere Anwendung recht viele Prozeduren hat. Wie also sollen wir mit vertretbarem Aufwand den Aufruf der Prozedur WriteCall in alle Routinen der Anwendungen schreiben?

Logisch: Copy und Paste nimmt uns bereits eine Menge Arbeit ab, und wenn wir den Namen des Moduls bereits vorab eintragen, brauchen wir nach dem Kopieren und Einfügen nur noch den Namen der jeweiligen Routine hinzuzufügen:

Call WriteCall("BeispielCall", "mdlErrors", True)

Allerdings ist das bei ein paar hundert Routinen immer noch ein hoher Zeitaufwand und zweitens wäre diese Vorgehensweise recht fehleranfällig.

Also programmieren wir uns eine eigene Prozedur, mit der wir diese Zeile zu allen Prozeduren hinzufügen. Mit dieser greifen wir auf das VBA-Projekt der aktuellen Datenbank zu, also brauchen wir das entsprechende Objektmodell. Dieses holen wir uns durch Setzen des Verweises aus Bild 4 hinzu.

Verweis auf die Bibliothek Microsoft Visual Basic for Applications 5.3

Bild 4: Verweis auf die Bibliothek Microsoft Visual Basic for Applications 5.3

Danach programmieren wir die Prozedur aus Listing 2. Diese referenziert zunächst das aktuelle VBA-Projekt und speichert es in objVBProject. Das VBA-Projekt enthält für jedes Modul, egal ob Standard- oder Klassenmodul, ein VBComponent-Objekt. Diese durchlaufen wir in einer For Each-Schleife über alle Elemente der Auflistung VBComponents.

Public Sub AddCallWriter()
     Dim objVBProject As VBIDE.VBProject
     Dim objVBComponent As VBIDE.VBComponent
     Dim objCodeModule As VBIDE.CodeModule
     Dim lngLine As Long
     Dim strProcedure As String
     Dim intProcType As vbext_ProcKind
     Dim lngProcBodyLine As Long
     Dim lngCountOfLines As Long
     Dim strNewLine As String
     Dim strModule As String
     Dim bolGeaendert As Boolean
     Set objVBProject = VBE.ActiveVBProject
     For Each objVBComponent In objVBProject.VBComponents
         Set objCodeModule = objVBComponent.CodeModule
         strModule = objVBComponent.Name
         If Not strModule = "mdlErrors" Then
             strProcedure = ""
             lngCountOfLines = objCodeModule.CountOfLines
             bolGeaendert = False
             For lngLine = 1 To lngCountOfLines
                 If Not strProcedure = objCodeModule.ProcOfLine(lngLine, intProcType) Then
                     strProcedure = objCodeModule.ProcOfLine(lngLine, intProcType)
                     lngProcBodyLine = objCodeModule.ProcBodyLine(strProcedure, intProcType)
                     strNewLine = "    Call WriteCall(""" & strProcedure & """, """ & strModule & """, True)"
                     If Not objCodeModule.Lines(lngProcBodyLine + 1, 1) = strNewLine Then
                         objCodeModule.InsertLines lngProcBodyLine + 1, strNewLine
                         bolGeaendert = True
                     End If
                     lngCountOfLines = objCodeModule.CountOfLines
                 End If
             Next lngLine
             If bolGeaendert = True Then
                 On Error Resume Next
                 DoCmd.Close acForm, Replace(strModule, "Report_", ""), acSaveYes
                 DoCmd.Close acForm, Replace(strModule, "Form_", ""), acSaveYes
                 DoCmd.RunCommand acCmdSave
                 DoEvents
                 On Error GoTo 0
             End If
         End If
     Next objVBComponent
End Sub

Listing 2: Prozedur zum Hinzufügen des Aufrufs von AddCallWriter in alle Prozeduren

Uns interessiert das im VBComponent-Element enthaltene CodeModule-Objekt, das wir mit objCodeModule referenzieren. In strModul speichern wir den Namen dieses Moduls. Die folgenden Schritte sollen für alle Module mit Ausnahme des aktuellen Moduls namens mdlErrors ausgeführt werden.

Wir durchlaufen nun alle Zeilen und identifizieren jeweils die erste Zeile der nächsten Prozedur. Dazu stellen wir strProcedure zunächst auf eine leere Zeichenkette ein. Dann ermitteln wir die Anzahl der Zeilen des Moduls und speichern diese in lngCountOfLines. Die Boolean-Variable bolGeaendert, die angeben soll, ob wir in dem Modul mindestens eine Zeile geändert haben, stellen wir zuerst auf False ein.

Dann durchläuft die Prozedur alle Zeilen von 1 bis lngCountOfLines in einer For…Next-Schleife. Dabei prüft sie jeweils, ob der in strProcedure gespeicherte Prozedurname mit dem Namen der Prozedur aus der aktuellen Zeile übereinstimmt. Ist das der Fall, befinden wir uns noch in der aktuellen Prozedur (oder, wenn wir uns am Modulanfang befinden, im Deklarationsbereich – hier enthält strProcedure immer noch eine leere Zeichenkette).

Den Namen der Prozedur, zu der die aktuelle Zeile gehört, ermitteln wir mit der Funktion ProcOfLine. Dieser übergeben wir die Zeilennummer und eine Variable, die den Typ der Prozedur entgegennimmt. Der Typ interessiert uns hier nicht, aber wir müssen die Variable zwingend als Parameter setzen. Die Funktion liefert schließlich den Namen der aktuellen Routine zurück. Stimmt dieser nicht mit dem Wert aus strProcedure überein, ist dies die erste Zeile der neuen Routine.

Dann ermitteln wir die tatsächliche erste Zeile der aktuellen Prozedur. Es kann nämlich auch sein, dass sich in der mit lngLine ermittelten Zeile eine leere Zeile vor der eigentlichen Prozedur befindet. Um die Kopfzeile der Prozedur zu erhalten, verwenden wir die Funktion ProcBodyLine, der wir den Namen der Prozedur und wieder die Variable für den Typ der Prozedur übergeben. Das Ergebnis speichern wir in lngProcBodyLine.

Wozu benötigen wir diese Zeile? Weil wir genau hinter dieser Zeile, also als erste Anweisung der Prozedur, unsere Anweisung Call WriteCall… platzieren wollen. Diese schreiben wir zuerst in die Variable strNewLine. Dazu fügen wir eine Zeichenkette bestehend aus Call WriteCall und dem Namen der Prozedur und des Moduls aus den Variablen strProcedure und strModule zusammen.

Nun prüfen wir, ob die erste Anweisung nicht bereits der Aufruf der Prozedur WriteCall ist. Dazu vergleichen wir die mit der Lines-Funktion mit dem Parameter lngProcBodyLine + 1 ermittelte Zeile mit der Zeile aus strNewLine. Warum + 1? Weil wir nicht die Kopfzeile, sondern die erste Anweisung prüfen wollen.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar