Session D-FEHL

Wie man sich sauber aus der Affäre zieht

Jürgen Wondzinski
ProLib Software GmbH


Einführung

Wie die meisten anderen Dinge ist auch die Fehlerbehandlung in Visual FoxPro flexibler, aber auch erheblich komplexer, als wir es von FoxPro 2.x gewöhnt waren. Die Objekte verfügen mittels ihrer Error-Methoden über eine lokale Fehlerbehandlung. Wie können Sie nun Ihrer Applikation eine gemeinsame, globale Fehlerbehandlung zur Verfügung stellen? Wie reagieren Sie, wenn ein Fehler auftritt? In dieser Session suchen wir nach einer Möglichkeit, eine Fehlerbehandlung zu implementieren. Ausgehend von individuellen Kontrollelementen hangeln wir uns hoch bis zu einem globalen Fehlerobjekt.

Grundlagen des Error Handling

Die Fehlerbehandlung enthält eine ganze Reihe unterschiedlicher Aspekte: Einrichten der Fehlerbehandlung; entscheiden, was den Fehler ausgelöst hat; informieren des Anwenders,  daß ein Fehler aufgetreten ist (und, wenn möglich, Festhalten des Fehlers in einer Logging-Datei, um den Fehler später analysieren zu können); und schließlich auch noch den Versuch, das Problem zu lösen (z.B. durch den erneuten Versuch, das Kommando auszuführen; einen Sprung zu dem Kommando, das auf das Kommando folgt, das den Fehler ausgelöst hat; beenden der Anwendung etc.)

Einrichten des Error Handler

Das Einrichten einer globalen Fehlerbehandlung hat sich seit FoxPro 2.x nicht geändert: Sie benutzen einfach das Kommando on error. Hier ein Beispiel:

on error do ERR_PROC with error(), sys(16), lineno()

Die Parameter teilen dem Error Handler die Fehlernummer, den Namen der Routine und die Zeile, in der der Fehler aufgetreten ist, mit. Sie können dem Error Handler jeden Parameter, den Sie wünschen, übergeben.

VFP hat die globale Fehlerbehandlung mit der Möglichkeit, individuelle Fehlerbehandlungsroutinen durch die Implementierung des Error-Ereignisses bereitzustellen, kombiniert. Jedes Objekt hat in VFP diesen Error-Event. Aber natürlich hat nicht jedes Objekt eine Error-Methode. Wenn Ihnen der Unterschied nicht klar ist, bedenken Sie, das ein Ereignis eine Aktion ist, das durch irgend eine Handlung des Anwenders oder des Systems ausgelöst wird (z.B. einen Tastendruck, ein Mausklick oder irgend etwas anderes, von dem FoxPro annimmt, daß es sich hierbei um einen Fehler handelt), während eine Methode der Code ist, der ausgeführt wird, wenn das Ereignis eingetreten ist. Der Code einer Methode wird also ausgeführt, wenn ein Objekt eine Nachricht erhält, daß die Methode ausgeführt werden soll. Bei vielen Ereignissen, z.B. einem Mausklick, wird entweder das Ereignis ignoriert oder es wird ein Standardverhalten ausgeführt, wenn das Objekt einen Code für die entsprechende Methode hat. Auf jeden Fall geschieht eine ganze Menge, wenn ein Fehler auftritt.

Die Error-Methode eines Objekts wird aufgerufen, wenn sie existiert. Ein Fehler tritt entweder in einer Methode eines Objekts auf oder in einem Programm, das nicht zu dem Objekt gehört, das aber von diesem Objekt aufgerufen wird (z.B. ein PRG). Was passiert aber, wenn das Objekt keine Fehlerbehandlung besitzt? Als ich meine Arbeit mit VFP begann, nahm ich an, daß die Error-Routine des Containers, in dem das Objekt steckt (z.B. ein Formular), aufgerufen wird. Das ist allerdings nicht der Fall; statt dessen wird die on error- Routine (wenn sie existiert) aufgerufen. Existiert diese on error-Routine nicht, führt Visual FoxPro seine eigene Fehlerbehandlung (also diesen begeisternden Abbruch/Ignorieren-Dialog) aus; und wir können feststellen, daß die Applikation abgestürzt ist.

Die Fehlerusrsache beseitigen

Bei dieser Aufgabe helfen uns verschiedene Funktionen in FoxPro 2.x und VFP, einschließlich error(), message(), lineno(), sys(16), und sys(2018). VFP enthält zusätzlich die Funktion aerror(), die den letzten aufgetretenen Fehler in ein Array schreibt. Obwohl Sie einige dieser Informationen auch durch andere Funktionen erhalten können, erhalten Sie Informationen über viele Fehlerarten (z.B. OLE, OBDC und Trigger) ausschließlich mit Hilfe von aerror(). Diese Informationen sind nirgends anders verfügbar (z.B. welcher Trigger den Fehler hervorgerufen hat).

Informieren des Anwenders

Ehrlicherweise muß man auch den Anwender informieren, daß ein Fehler aufgetreten ist. Häufig wird diese Mitteilung mit der Möglichkeit verbunden, den Anwender entscheiden zu lassen, wie das Problem behoben werden soll. Hier müssen Sie entscheiden, welche Meldung Sie dem Anwender anzeigen lassen und welche Entscheidungsmöglichkeiten Sie ihm anbieten. Die angezeigte Meldung ergibt sich aus dem Fehlertyp und sollte so formuliert sein, daß der Anwender nicht in Panik gerät und Ctrl-Alt-Del drückt. Sie sollten ihn auch informieren, wie das Problem behoben werden kann. Ein Beispiel: Etwas Einfaches wie ein nicht betriebsbereiter Drucker kann in der Form behandelt werden, daß man den Anwender fragt, ob Papier im Drucker vorhanden ist, ob er angeschaltet und mit dem Rechner verbunden ist etc. Der Anwender kann nun entscheiden, ob er erneut versuchen will, sein Dokument zu drucken oder ob er lieber abbrechen will. Versucht ein Anwender, einen Datensatz zu ändern, der bereits durch einen anderen Anwender gesperrt wurde, könnten Sie ihm mitteilen, daß dieser Satz bereits editiert wird und ihm die Möglichkeit geben, es erneut zu versuchen oder den Vorgang abzubrechen.

Wenn Sie bedenken, daß es mehr als 600 mögliche Fehler in VFP gibt, könnten Sie bei dem Gedanken erschrecken, auf all diese Fehler mit sinnvollen Meldungen reagieren zu müssen. Wenn Sie sich allerdings die FoxPro-Hilfe ansehen, werden Sie feststellen, daß die meisten Fehlermeldungen in wenige Kategorien fallen:

Bei Fehlern, die in die erste Kategorie fallen, hat Ihre Anwendung keine andere Wahl, als sich zu beenden. Fehler der zweiten Kategorie sollten während des Tests der Software auftreten. Wenn nicht, sollten sie den Fehler melden und in eine Datei schreiben und die Anwendung anschließend beenden. Auch die Fehler der dritten Kategorie sollten bereits im Test auftreten; wenn nicht, genügt eine einfache Mitteilung „Sie können diese Aktion im Moment nicht ausführen„. Es bleiben lediglich die Fehler der vierten Kategorie, die Sie speziell behandeln müssen. Einige Beispiele dieses Fehlertyps sind die Eingabe ungültiger Werte in Felder, Fehler des Primärindexes (diese Fehler lassen sich minimieren, indem Sie den Primärschlüssel automatisch vergeben), das Fehlschlagen eines Triggers, sowie „Datei nicht gefunden„ (was Sie allerdings eher behandeln sollten, indem Sie sicherstellen, daß die Datei vorhanden ist, statt sie durch die Fehlerbehandlung suchen zu lassen).

Bevor die Fehlermeldung ausgegeben wird, lassen viele Programmierer den Fehler in eine Logdatei schreiben. Dieses Vorgehen zahlt sich von Zeit zu Zeit aus, da die Anwender häufig nur ungenau die Situation beschreiben können, wenn sie Ihnen einen Fehler melden. Die Logdatei kann eine Textdatei sein, aber ich bevorzuge eine Tabelle mit Feldern für das Datum und die Zeit (oder einem DateTime-Feld in VFP), an der der Fehler aufgetreten ist, für den Namen des Anwenders, die Fehlernummer und –meldung, die Nummer der Zeile,  in der der Fehler aufgetreten ist, den Code, der den Fehler hervorgerufen hat und ein Memofeld, das die Inhalte der Speichervariablen enthält.

Den Fehler auflösen

Einen Fehler aufzulösen ist manchmal recht schwierig. Das Kommando retry versucht erneut, das Kommando auszuführen, das den Fehler ausgelöst hat. Bei den meisten Fehlern hilft das allerdings nicht, da die Möglichkeiten des Anwenders, den Konflikt aufzulösen, der der Grund für den Fehler ist, beschränkt ist.

Das Kommando return führt die Anweisung aus, die der fehlerhaften Anweisung folgt. Da aber das Kommando, das den Fehler ausgelöst hat, nicht ausgeführt wird, folgt meist ein weiterer Fehler, da irgend etwas nicht ausgeführt wurde. Zum Beispiel könnte eine Variable nicht erstellt worden sein. (Vor allem: Wenn die Anweisung, die den Fehler ausgelöst hat, problemlos ausgelassen werden kann: Warum steht sie überhaupt im Programm? <g>

cancel ist keine realistische Option, da mit dieser Anweisung die Anwendung beendet wird, ohne daß sie die Möglichkeit hatte, ihre Daten sauber wegzuräumen. Die Applikation kontrolliert herunterzufahren ist eine Möglichkeit, da viele Fehler aus Fehlern eines Programmierers oder des Systems resultieren; und dann gibt es nicht viele Möglichkeiten weiterzumachen, bis das Problem beseitigt ist. Auch als Entwickler wissen Sie sicher in der Entwicklungsumgebung die Möglichkeit zu schätzen, die Anwendung abzubrechen und zum Befehlsfenster zurückzukehren. Der Code, der diese Aktion ausführt, sollte allerdings die Umgebung so weit wie möglich aufräumen.

Eine andere Möglichkeit ist der Gebrauch von return to master oder return to <program>, um aus dem Programm, das den Fehler hervorgerufen hat, herauszuspringen und ins Hauptprogramm oder ein anderes spezielles Programm zu kommen. Seien Sie aber vorsichtig mit dieser Möglichkeit, da sie das System in einen unordentlichen Zustand versetzen kann: Formulare und Fenster bleiben bestehen, Tabellen oder Cursor bleiben geöffnet etc. Es ist vorteilhaft, diese Elemente so weit wie möglich aufzuräumen, bevor Sie return to benutzen. Damit werden wir uns später noch näher beschäftigen.

Entwurf eines Schemas für die Fehlerbehandlung

Ein globaler Error Handler und die Error-Methode eines Objekts sind die zwei Seiten der gleichen Medaille:

Der globale Error Handler ist weit entfernt von dem Punkt, an dem der Fehler entsteht, während die Error-Methode Teil des Objektes ist, das den Fehler hervorgerufen hat. Folglich hat die Error-Methode mehr Wissen über die Umgebung, in der sie sich befindet, die potentiellen Fehler, die auftreten können und wie diese zu behandeln sind. Zum Beispiel kann das ActiveX Control CommonDialogs (das die Datei- Drucker, drucken und Farbendialoge anzeigt) einen Fehler hervorrufen, wenn der Anwender „Abbruch„ wählt. Es macht wenig Sinn, den globalen Error Handler versuchen zu lassen, diesen Fehler zu behandeln, da er ihn lediglich als irgend einen OLE-Fehler sieht und daher nicht wissen kann, wie dieser Fehler zu behandeln ist. Es ist sinnvoller, Code in die Error-Methode von CommonDialogs zu schreiben, der die Abbruch-Situation behandeln kann.

Der globale Error Handler wird von außerhalb der Ereignisverwaltung von VFP aufgerufen. Der Aufruf erfolgt über FoxPro's altes „ON„-Ereignisschema, das auch weiterhin von den Menüs und von „ON KEY„ benutzt wird. Daher können Sie die Objektsyntax wie Thisform nicht benutzen, und durch Features wie die private Datensitzung kompliziert sich die Fehlerbehandlung noch weiter.

Der globale Error Handler kann die normalen Fehlerbehandlungen (z.B. Error Logging und Display) effizient von einer zentralen Stelle aus unterstützen. Im Gegensatz dazu ist es nicht sehr effizient zu versuchen, jeden denkbaren Fehler (z.B. wenn die Netzwerkverbindung zusammenbricht) in der Error-Methode jedes einzelnen Objekts in Ihrer Applikation zu behandeln.

Lassen Sie uns den Aufbau einer Fehlerbehandlung betrachten, die das Beste beider Welten enthält. Wir wollen Fehler so effizient wie möglich behandeln und dabei den individuellen Objekten die Möglichkeit geben, ihre spezifischen Fehler selbst zu behandeln. Folgende Strategie wollen wir anwenden:

Wie die meisten Kinder weiß auch ein Objekt alles besser als seine Eltern; es weiß, was vor sich geht, daher behandelt auch die Error-Methode eines Objekts so viele Fehler wie möglich. Fehler, die nicht behandelt werden können, werden mit Hilfe von dodefault() in der Klassenhierarchie hochgereicht. Auf diese Weise arbeitet jede Klasse innerhalb der Hierarchie. Braucht eine Klasse keine speziellen Fehler zu behandeln, enthält die Error-Methode keinen Code, da automatisch der Code der Elternklasse ausgeführt wird. Folgerichtig ruft die GanzTiefUntenTextBox.Error den  TiefUntenTextBox.Error auf, der seinerseits den TextBox.Error aufruft.

Die Error-Methode der höchsten Elternklasse des Objekts behandelt alle Fehler, die sie behandeln kann. Fehler, die sie nicht selbst behandeln kann, gibt sie weiter an ihren Container weiter, indem sie This.Parent.Error() aufruft.

Da die Containerklassen wie die Klassen der Kontrollelemente arbeiten (sie reichen Fehler, die sie nicht behandeln können, an ihre Elternklasse weiter, und die höchste Elternklasse leitet die Fehler an ihre Containerklasse weiter), arbeiten wir uns erst die Klassen- und dann die Containerhierarchie hoch.

Die Error-Methode der höchsten Elternklasse des äußersten Containers behandelt alle Fehler, bei denen es ihr möglich ist. Fehler, die sie nicht behandeln kann, gibt sie an den globalen Error Handler weiter.

Dies ist eine Kette von Verantwortlichkeiten: jedes Objekt in der Kette behandelt den Fehler entweder selber oder gibt ihn an das nächste Objekt der Kette weiter. In diesem mehrschichtigen Schema wird die Fehlerbehandlung weniger spezifisch, wenn der Fehler vom Objekt in Richtung globaler Error Handler weitergegeben werden. Dadurch ist es möglich, Fehler auf der zu ihnen passenden Stufe zu behandeln. Abbildung 1 zeigt diese Strategie.

Abb. 1: Strategie des Error Handling.

Ich habe mich entschieden, ein globales Error Handling-Objekt zu erstellen, der in einer globalen Variablen oError der Klasse SFErrorMgr beim Starten der Applikation instantiiert wird. Eine seiner Methoden (ErrorHandler) kann direkt von den Objekten (wie weiter oben bereits beschrieben) aufgerufen werden. Die Methode kann auch indirekt aufgerufen werden, da sie auch der on error-Handler ist. Das Error Handling-Objekt sollte ein einfaches Interface (gemeint ist das programmatische, nicht das User-Interface) besitzen; daher akzeptiert SFErrorMgr nur die gleichen Parameter wie die Error-Methoden der Objekte (Fehlernummer, Methode, Zeilennummer) und gibt einen String zurück, der anzeigt, welche Wahl der Anwender (oder das Objekt) getroffen hat, um den Fehler zu behandeln. Das Error-Objekt steht am Ende der Kette, in der die Fehlermeldung weitergegeben wird und weiß daher nicht viel über die Umgebung, von der es aufgerufen wurde (es können Objekte gelöscht sein, der Fehler kann aus einer anderen Datenumgebung kommen usw.) Daraus resultiert, daß es nicht viel „behandeln„ (im Sinne von beseitigen) kann. Sein Zweck ist es, dem Anwender eine Fehlermeldung anzuzeigen, den Fehler für eine spätere Analyse in die Logdatei zu schreiben, und zu entscheiden, welche Aktion ausgeführt werden soll, bzw. noch besser, dem Anwender diese Frage zu stellen. Das Error-Objekt sollte lediglich vorhersehbare Fehler behandeln, die Sie bislang aber noch nicht vorhergesehen haben (wenn solche Fehler auftreten, sollten Sie in das Objekt, die Klasse oder Routine wechseln, in der der Fehler aufgetreten ist, um diesen Fall zu behandeln) und unvorhersehbare Fehler (echte Bugs oder unvorhersehbare Umgebungsbedingungen).

Der globale Error Handler darf sowohl selbständig globale Entscheidungen treffen (Aufruf des VFP-Debuggers oder Herunterfahren der Applikation) als auch dem Objekt, in dem der Fehler aufgetreten ist, erlauben, die endgültige Entscheidung zu treffen. Um letzteres zu ermöglichen, gibt jedes Glied der Kette die Entscheidung an den vorigen Level weiter. Zur Vereinfachung wird ein String zurückgegeben, der anzeigt, welche Entscheidung gefällt wurde: „retry„, um zu versuchen, das Kommando, bei dem der Fehler aufgetreten ist, erneut auszuführen; „continue„, um mit der Zeile fortzufahren, die hinter der Zeile liegt, in der der Fehler aufgetreten ist; oder „closeform„, um das Formular zu schließen, auf dem sich das Kontrollelement befindet. Jedes Objekt führt anschließend die zur Rückgabemessage passende Aktion aus. Da die Error-Methode eines Containers von einem eingebetteten Objekt oder auch von einer seiner eigenen Methoden aufgerufen sein kann, muß der Container entscheiden, welche Message er weitergeben und welche er selbst ausführen soll. Den Code dafür sehen wir später.

Dieses Schema hat ein Problem: Kontrollelemente, die sich auf den VFP-Basisklassen Page, Column, oder anderen Containern ohne Code in der Error-Methode befinden, haben keine Fehlerverfolgung, da sie eine leere Methode aufrufen. Die Lösung des Problems ist folgende: Wir „reisen„ die Containerhierarchie hinauf, bis wir eine Elternklasse finden, die in ihrer Error-Methode Code stehen hat. Wenn wir keine solchen Eltern finden können, müssen wir eine generische Fehlermeldung anzeigen (das ist allerdings unwahrscheinlich, da ich alle Formulare von der Formularklasse SFForm ableite, welche in der Error-Methode Code enthält).

Behalten Sie dabei eins im Kopf: der Code des Error Handlers sollte bugfrei sein, da die einzige Rückmeldung, die erscheint, wenn ein Fehler auftritt, während eine Fehlerbehandlung ausgeführt wird, der Abbruch/Ignorieren-Dialog von VFP ist. Glücklicherweise können Sie den meisten Code des Error-Handlers in Ihr Framework übernehmen; wenn es einmal arbeitet, sollten Sie in Zukunft keine Probleme damit bekommen (allerdings können merkwürdige Umgebungsbedingungen dazu führen, daß der Error Handler selbst abstürzt). Versuchen Sie nicht, innerhalb der Fehlerbehandlungsroutine eine Error-Methode zu erstellen: Sie wird nicht aufgerufen, wenn ein Fehler innerhalb der Fehlerbehandlung auftritt.

Die Error Methode

Sehen wir uns nun die Strategie detaillierter an. Der Startpunkt, wenn ein Fehler auftritt, ist die Error Methode des Objekts, in dem der Fehler auftritt. Lassen Sie uns also dort starten.

Den Code für die Error-Methode der meisten Klassen meiner Applikations-Basisklassen, die in SFCTRLS.VCX enthalten sind, sehen Sie unten (die Konstanten wie ccMSG_RETRY sind in LIBRARY.H definiert, die jede Klasse in ihrer Include-Datei hat). Ich sprach von „den meisten Klassen„, da Klassen wie PageFrames, OptionGroups und Container wie Formulare und Toolbars ein wenig anders arbeiten müssen. Dies ist einer der seltenen Momente, in denen ich mir wünsche, VFP würde die Mehrfachvererbung unterstützen; so wie es jetzt aussieht, müssen wir die VB-Vererbung benutzen und den Code der Error-Methode in die Error-Methode jeder Klasse kopieren (markieren Sie den Code, von dem Sie eine Subklasse ableiten wollen, drücken Sie Ctrl-C, plazieren Sie den Cursor in die neue Methode und drücken dann Ctrl-V, um der Klasse mitzuteilen, das sie den Code der gewünschten Vaterklasse ausführen soll <g>

lparameters nError, cMethod, nLine

local oParent, ;
  lcReturn
 
* Hocharbeiten in der Containerhierarchie, bis eine
* Elternklasse Code in ihrer Error-Methode enthält.
 
if type('Thisform') = 'O'
  oParent = iif(pemstatus(Thisform, 'FindErrorHandler', ;
    5), Thisform.FindErrorHandler(This), .NULL.)
else
  oParent = .NULL.
endif type('Thisform') = 'O'
do case
 
* Wir haben eine Elternklasse, die den Fehler handeln kann
 
  case not isnull(oParent)
    lcReturn = oParent.Error(nError, This.Name + '.' + ;
      cMethod, nLine)
 
* Wir haben ein Error Handling-Objekt, also rufen wir seine
* ErrorHandler() Methode auf.
 
  case type('oError') = 'O' and not isnull(oError)
    lcReturn = oError.ErrorHandler(nError, ;
      This.Name + '.' + cMethod, nLine)
 
* Anzeige einer generischen Dialogbox
 
  otherwise
    messagebox('Error #' + ltrim(str(nError)) + ;
      ' occurred in line ' + ltrim(str(nLine)) + ;
      ' of ' + cMethod + ' in object ' + This.Name, ;
      0, _VFP.Caption)
endcase
lcReturn = iif(type('lcReturn') <> 'C' or ;
  empty(lcReturn), ccMSG_CONTINUE, lcReturn)
 
* Behandeln des Rückgabewertes.
 
do case
  case lcReturn = ccMSG_RETRY
    retry
  case lcReturn = ccMSG_CANCEL
    cancel
  otherwise
    return
endcase

Dieser Code prüft, ob das Formular, auf dem sich das Kontrollelement befindet, über eine FindErrorHandler Methode verfügt. Ist die Methode vorhanden, wird sie aufgerufen, um die erste Elternklasse zu finden, die in ihrer Error-Methode Code enthält (wir wollen uns den Code jetzt nicht ansehen; Sie finden ihn auf Ihrer Begleitdiskette). Dadurch wird verhindert, daß die Fehlerbehandlung bei den Basisklassen Page, Column oder anderen Containern aufhört, wenn diese keinen Code in der Error-Methode enthalten. Wenn eine Elternklasse vorbereitet ist, diesen Fehler zu behandeln, wird ihre Error-Methode mit ihren Parametern aufgerufen, außer der Name des Objekts steht in cMethod, so daß unser Errorhandler-Dienst das Objekt kennt, in dem der Fehler entstanden ist. Wenn keine Elternklasse gefunden werden kann, aber ein globaler Error Handler existiert (wir werden ihn uns später ansehen) wird dessen Methode ErrorHandler aufgerufen. Wenn wir den Fehler nicht mehr weitergeben können, benutzen wir messagebox(), um eine Fehlermeldung auszugeben. Der Rückgabewert der Fehlerbehandlung wird dann eingesetzt, um zu entscheiden, wie im Programm weiter verfahren wird: erneuter Versuch, Abbruch oder fortsetzen.

„Dazwischenliegende„ Containerklassen (SFContainer, SFControl, SFGrid, SFOptionGroup, und SFPageFrame) besitzen in etwa den gleichen Code wie den obenstehenden; sie müssen lediglich die Message mit der Entscheidung, was zu tun ist, zurückgeben statt die Entscheidung auszuführen, wenn es nicht ihr eigener Fehler ist. Wir prüfen das mit Hilfe des Namens der Methode, in der der Fehler aufgetreten ist. VFP übergibt nur den Namen der Methode, wenn der Fehler innerhalb einer Methode aufgetreten ist, eingebettete Objekte übergeben den Namen es Objekts und die Methode. Dadurch steht uns ein schneller Weg zur Verfügung, um zu entscheiden, ob ein Fehler im Objekt selber oder in einem eingebetteten Objekt aufgetreten ist. Hier ein Auszug aus der Error-Methode dieser Klassen, der die return-Anweisung zeigt:

* Behandlung des Rückgabewertes.
 
do case
  case '.' $ cMethod
    return lcReturn
  case lcReturn = ccMSG_RETRY
    retry

Die Error-Methoden der Klassen SFForm und SFToolbar unterscheiden sich von denen anderer Klassen, da es sich bei ihnen um „Top-Level„-Container handelt. Diese Methode benutzt die benutzerdefinierte Methode SETERROR(), um einige benutzerdefinierte Eigenschaften mit Informationen über den Fehler zu initialisieren und die Methode HandleError, um den Fehler zu behandeln. Anschließend wird der Inhalt des Rückgabewertes ausgeführt, indem die Klasse entweder die Aktion (zum Beispiel das Schließen des Formulars) selbst ausführt oder der Rückgabewert an das Objekt zurückgegeben wird, das die Methode aufgerufen hat. Beachten Sie, daß kein Wert zurückgegeben wird, wenn das Objekt die Datenumgebung ist, sie behandelt sie ihre Fehler selbst. Dieses Verhalten wollen wir jetzt ändern.

lparameters tnError, ;
  tcMethod, ;
  tnLine
local lcReturn
 
* Aufruf von SetError() und HandleError(), um die
* Fehlerinformation zu sichern und den Fehler zu behandeln.
 
with This
  .SetError(tnError, tcMethod, tnLine)
  lcReturn = .HandleError()
endwith
 
* Behandlung des Rückgabewertes, abhängig davon, ober der
* Fehler „unserer„ ist oder von einer eingebetteten Klasse kommt..
 
do case
  case lcReturn = ccMSG_CLOSEFORM
    This.Release()
    return to master
  case '.' $ tcMethod and ;
    not 'DATAENVIRONMENT' $ upper(tcMethod)
    return lcReturn
  case lcReturn = ccMSG_RETRY
    retry
  case lcReturn = ccMSG_CANCEL
    cancel
  otherwise
    return
endcase
 

Wir wollen uns jetzt nicht den Code der Methode SetError ansehen (Sie finden ihn auf der Begleitdiskette), aber hier ist der Code der Methode HandleError:

local lnError, ;
  lcMethod, ;
  lnLine, ;
  loError, ;
  lcMessage, ;
  lcReturn

with This

  lnError  = .aErrorInfo[.nLastError, cnAERR_NUMBER]
  lcMethod = .Name + '.' + ;
    .aErrorInfo[.nLastError, cnAERR_METHOD]
  lnLine   = .aErrorInfo[.nLastError, cnAERR_LINE]
 
* Erhalten einer Referenz auf unser Error Handling-Objekt, falls eines
* existiert. Es kann sowohl ein eingebettetes als auch ein globales
* Objekt sein.
 
  do case
    case type('.oError') = 'O' and not isnull(.oError)
      loError = .oError
    case type('oError') = 'O' and not isnull(oError)
      loError = oError
    otherwise
      loError = .NULL.
  endcase
 
* Wir haben kein Error Handling-Objekt,
* daher zeigen wir eine Dialogbox an.
 
  if isnull(loError)
    lcMessage = ccMSG_ERROR_NUM + ltrim(str(lnError)) + ;
      ccCR + ccMSG_MESSAGE + ;
      .aErrorInfo[.nLastError, cnAERR_MESSAGE] + ccCR + ;
      iif(empty(.aErrorInfo[.nLastError, cnAERR_SOURCE]), ;
      '', ccMSG_CODE + ;
      .aErrorInfo[.nLastError, cnAERR_SOURCE] + ccCR) + ;
      iif(lnLine = 0, '', ccMSG_LINE_NUM + ;
      ltrim(str(lnLine)) + ccCR) + ccMSG_METHOD + lcMethod
    lcReturn = iif(messagebox(lcMessage, ;
      MB_ICONEXCLAMATION + MB_OKCANCEL, ;
      _screen.Caption) = IDCANCEL, ccMSG_CANCEL, ;
      ccMSG_CONTINUE)
 
* Haben wir ein Error Handling-Objekt, so rufen
* wir seine Methode ErrorHandler() auf.
 
  else
    lcReturn = loError.ErrorHandler(lnError, ;
      lcMethod, lnLine)
  endif isnull(loError)
endwith
lcReturn = iif(type('lcReturn') <>'C' or ;
  empty(lcReturn), ccMSG_CONTINUE, lcReturn)
return lcReturn

HandleError versucht, den Fehler an ein globales Error Handling-Objekt weiterzugeben, das entweder in der globalen Variablen oError oder in der oError-Eigenschaft des Formulars referenziert wird. Dieses Schema ermöglicht es Ihnen, eine individualisierte Version der globalen Fehlerbehandlung einzusetzen, die mit dem speziellen Formular verbunden ist. Wird kein globaler Error Handler gefunden, wird eine Fehlermeldung mit Hilfe von messagebox() ausgegeben. Der Rückgabewert des Error Handlers wird dann an die Error-Methode zurückgegeben.

Der globale Error Handler

SFErrorMgr ist eine nichtvisuelle Klasse, die auf SFCustom basiert. Sie ist in SFMGRS.VCX enthalten und benutzt die #INCLUDE-Datei ERRORMGR.H für die Definitionen der unterschiedlichen Konstanten. Sie wird beim Start der Applikation instantiiert (vgl. SYSMAIN.PRG). Wir brauchen uns jetzt nicht den gesamten Code der Klasse anzusehen, lediglich den Code der Methoden, die helfen, das grundsätzliche Schema des Error Handling zu verstehen. Die anderen Methoden finden Sie auf der Begleitdiskette.

Die Init-Methode empfängt drei Parameter: den Titel für den Dialog, der angezeigt wird, wenn ein Fehler auftritt (gespeichert in der Eigenschaft cTitle), ein Flag, das anzeigt, ob Init den aktuellen on error-Handler speichern und durch seine Methode ErrorHandler ersetzen soll, und den Namen des Objektes, in dem die Klasse instantiiert wurde (das benötigen wir für das Kommando on error, da wir This hier nicht benutzen können).

Im Allgemeinen räumt die Destroy-Methode alles auf, das die Klasse geändert hat; in diesem Fall setzt sie VFP’s Error Handler auf den Stand zurück, in dem er sich befunden hat, bevor das Objekt instantiiert wurde.

Die Methode ErrorHandler wird sowohl von den Objekten direkt als das letzte Glied in der Kette der Verantwortung für die Fehlerbehandlung als auch indirekt aufgerufen, da es auch der on error-Handler ist. Hier ist der Code für diese Methode:

lparameters tnError, ;
  tcMethod, ;
  tnLine
local lcCurrTalk, ;
  lcChoice, ;
  lcProgram
with This
 
* Sicherstellen, daß TALK auf off geschaltet ist.
 
  if set('TALK') = 'ON'
    set talk off
    lcCurrTalk = 'ON'
  else
    lcCurrTalk = 'OFF'
  endif set('TALK') = 'ON'
 
* Schreiben des Fehlers in das Array aErrorInfo
* und Setzen des Schalters lErrorOccurred.
 
  .GetErrorInfo(tcMethod, tnLine)
 
* Wenn Fehler nicht unterdrückt werden, Anzeige des Fehlers,
* und Einlesen der Auswahl des Anwenders.
 
  lcChoice = ccMSG_CONTINUE
  if not .lSuppressErrors
 
* Wenn notwendig, Fehler in Datei speichern.
 
    if .lLogErrors
      .LogError()
    endif .lLogErrors
 
* Anzeige des Fehlers und Einlesen der Auswahl des Anwenders.
 
    if .lDisplayErrors
      lcChoice = .DisplayError()
      do case
 
* Abbruch oder Quit in der Entwicklungsumgebung: Löschen aller
* WAIT WINDOWS, schließen aller offenen Cursor und Ausführen eines CLEAR
* EVENTS (im Fall von Quit); anschließen Rückkehr zum
* Hauptprogramm.
 
        case lcChoice = ccMSG_CANCEL or ;
          (lcChoice = ccMSG_QUIT and version(2) <> 0)
          wait clear
          if lcChoice = ccMSG_QUIT
            .lQuit = .T.
            .RevertAllTables()
            clear events
          endif lcChoice = ccMSG_QUIT
          lcProgram = .cReturnToOnCancel
          return to &lcProgram
 
* Aufruf des Debuggers (in der Entwicklungsumgebung): aktivieren
* des Trace- and Debugfensters.
 
        case lcChoice = ccMSG_DEBUG and version(2) <> 0
          activate window debug
          set step on
 
* Versuch, den Code erneut auszuführen: Wir müssen das RETRY hier
* durchführen, da nichts die RETRY-Message empfängt (wie es mit
* einem Ebjekt der Fall ist).
 
        case lcChoice = ccMSG_RETRY
          lcMethod = upper(tcMethod)
          if at('.', lcMethod) = 0 or ;
            inlist(right(lcMethod, 4), '.FXP', '.PRG', ;
            '.MPR', '.MPX')
            if lcCurrTalk = 'ON'
              set talk on
            endif lcCurrTalk = 'ON'
            retry
          endif at('.', lcMethod) = 0 ...
 
* Quit: Schließen aller offenen Cursor, anschließend Quit.
 
        case lcChoice = ccMSG_QUIT
          .lQuit = .T.
          .RevertAllTables()
          on shutdown
          quit
      endcase
    endif .lDisplayErrors
  endif not .lSuppressErrors
 
* Wiederherstellen des TALK-Status.
 
  if lcCurrTalk = 'ON'
    set talk on
  endif lcCurrTalk = 'ON'
endwith
return lcChoice

Wenn ein Fehler auftritt, werden dem ErrorHandler drei Parameter übergeben: die Fehlernummer, der Name der Routine, in der der Fehler auftrat, und die Zeilennummer, die den Fehler hervorgerufen hat. Der ErrorHandler benutzt die Methode GetErrorInfo, um die Eigenschaft lErrorOccurred auf .T. zu setzen und die Informationen über den Fehler in die Eigenschaft aErrorInfo zu schreiben. Wenn die Eigenschaft lSuppressErrors .T. ist, wird der Fehler nicht in der Fehlerdatei gespeichert und es wird auch keine Fehlermeldung angezeigt (dieses Vorgehen wird gewählt, wenn Sie einen Fehler zwar behandeln, aber weder speichern noch dem Anwender anzeigen wollen). Anderenfalls wird die Methode LogError aufgerufen, um den Fehler zu speichern und die Methode DisplayError, um dem Anwender eine Nachricht über den Fehler anzuzeigen und ihn entscheiden zu lassen, was zu tun ist. Er hat folgende Möglichkeiten:

Debug:

Diese Option, die nur verfügbar ist, wenn die Eigenschaft lShowDebug .T. ist und wir in der Entwicklungsumgebung arbeiten und öffnet das Trace- und das Debug-Fenster. lShowDebug sollte nur für Entwickler auf  .T. gesetzt sein (das kann in einer Anwender-Tabelle oder der Windows-Registry geprüft werden).

Fortsetzen:

Setzt die Programmausführung mit dem Kommando fort, das auf das Kommando, das den Fehler ausgelöst hat, folgt.

Neuer Versuch:

versucht erneut, das Kommando, das den Fehler ausgelöst hat, auszuführen. Hier müssen wir mit einem kleinen Trick arbeiten: Sie können nicht einfach das Kommando retry aufrufen, da dadurch einfach die Kontrolle an die Methode übergeben wird, die das retry aufgerufen hat statt an die Methode, in der der Fehler auftrat. In diesem Fall geben wir nur die Message „Retry„ zurück. Wenn der Fehler aber in einem programmatischen Code (einem PRG oder MPR) aufgetreten ist, wurde der ErrorHandler als on error-Routine aufgerufen, so daß die einfache Rückgabe der Message nicht ausgeführt wird. In diesem Fall muß der ErrorHandler das retry selbst aufrufen.

Abbruch:

eine häufig gestellte Frage auf CompuServe ist: Wie verhindere ich die Ausführung des restlichen Code in der Methode oder dem Programm, das den Fehler hervorgerufen hat? Ich kann cancel nicht einsetzen, da es das gesamte Programm abbricht. return kehrt zu der gleichen Methode zurück, so daß beide Befehlen nicht helfen. Die Lösung ist, zum Hauptprogramm zurückzukehren, das Sie wieder auf die Anweisung read events zurücksetzt (wo in der Regel kein Code irgendeiner Methode ausgeführt wird). Da das „Main„-Programm nicht unbedingt das erste Programm der Applikation ist, kehren wir zu dem Programm zurück, das in der Eigenschaft cReturnToOnCancel eingetragen ist, statt ein return to master auszuführen.

Ende:

Beendet die Applikation. Hier haben wir unterschiedliche Anforderungen, je nachdem, ob wir uns in der Entwicklungsumgebung von VFP befinden oder nicht. Da es nicht sehr sinnig wäre, bei jedem Fehler in der Entwicklungsumgebung VFP erneut zu starten, sollte diese Option ein clear events ausführen und zum Hauptprogramm zurückkehren. So kann die Applikation sauber heruntergefahren werden und Sie kehren zum Befehlsfenster zurück. In der Laufzeitumgebung von VFP räumen wir kurz auf und Quit. In beiden Fällen benutzen wir eine benutzerdefinierte Methode RevertTables, um ein tablerevert(.T.) auf alle Cursor in allen Datensessions auszuführen. Dadurch vermeiden wir zusätzliche Fehler beim Herunterfahren der Applikation.

Behandlung spezifischer Fehler

Das Schema der Fehlerbehandlung, das wir bis jetzt betrachtet haben, ist generisch: jeder Fehler ruft den Dialog auf. Dieses Schema ist nicht für vorhersehbare Fehler geeignet und sollte nur bei unvorhersehbaren Fehlern eingesetzt werden. Vorhersehbare Fehler sollten in der Error-Methode des Objekts, in dem sie auftreten, behandelt werden.

Als ein Beispiel für die Behandlung spezifischer Fehler sehen wir uns die von SFForm abgeleitete Klasse SFMaintForm (in SFFORMS.VCX) an, die für die Dateneingabe erstellt wurde. Dies ist ein gutes Beispiel, wie ein Objekt spezielles Wissen über seine Umgebung und vorhersehbare Fehler hat und wie diese behandelt werden sollten.

Da die ErrorMethode von SFForm die Methode HandleError aufruft, die lediglich den Fehler an die Methode ErrorHandler von SFErrorMgr weitergibt, überschreiben wir dieses Verhalten von HandleError in SFMaintForm, um spezifische datenbasierende Fehler zu behandeln. Hier ist der Code des Error Handlers:

local lnError, ;
  llDisplay, ;
  lcReturn, ;
  loObject
with This
  lnError = .aErrorInfo[.nLastError, cnAERR_NUMBER]
  do case
 

* Tabelle oder Datenbank nicht in der Datenumgebung gefunden

 
    case (lnError = cnERR_TABLE_MOVED or ;
      lnError = cnERR_FILE_NOT_FOUND) and ;
      'DATAENVIRONMENT' $ upper(.aErrorInfo[.nLastError, ;
      cnAERR_METHOD])
      llDisplay = oError.lDisplayErrors
      oError.lDisplayErrors = .F.
      oError.ErrorHandler(lnError, ;
      .aErrorInfo[.nLastError, cnAERR_METHOD], ;
      .aErrorInfo[.nLastError, cnAERR_LINE])
      oError.lDisplayErrors = llDisplay
      messagebox(ccERR_TABLE_MOVED, MB_ICONSTOP, ;
        _screen.Caption)
      lcReturn = ccMSG_CLOSEFORM
 
* Kein Zugriff auf die Tabelle in der Datenumgebung.
 
    case lnError = cnERR_ACCESS_DENIED and ;
      'DATAENVIRONMENT' $ upper(.aErrorInfo[.nLastError, ;
      cnAERR_METHOD])
      llDisplay = oError.lDisplayErrors
      oError.lDisplayErrors = .F.
      oError.ErrorHandler(lnError, ;
        .aErrorInfo[.nLastError, cnAERR_METHOD], ;
        .aErrorInfo[.nLastError, cnAERR_LINE])
      oError.lDisplayErrors = llDisplay
      messagebox(ccERR_ACCESS_DENIED, MB_ICONSTOP, ;
        _screen.Caption)
      lcReturn = ccMSG_CLOSEFORM
 
* „Datei wird bereits benutzt„ in der Datenumgebung.
 
    case lnError = cnERR_TABLE_IN_USE and ;
      'DATAENVIRONMENT' $ upper(.aErrorInfo[.nLastError, ;
      cnAERR_METHOD])
      llDisplay = oError.lDisplayErrors
      oError.lDisplayErrors = .F.
      oError.ErrorHandler(lnError, ;
        .aErrorInfo[.nLastError, cnAERR_METHOD], ;
        .aErrorInfo[.nLastError, cnAERR_LINE])
      oError.lDisplayErrors = llDisplay
      messagebox(ccERR_TABLE_IN_USE, MB_ICONSTOP, ;
        _screen.Caption)
      lcReturn = ccMSG_CLOSEFORM
 
* Fehler "DatenUmgebung schon entladen" wird durch
* Nichtbeachten bestraft
 
    case lnError = cnERR_DE_UNLOADED
      lcReturn = ccMSG_CONTINUE
 
* Trigger fehlgeschlagen.
 
    case lnError = cnERR_TRIGGER_FAILED
      lcReturn = .ErrTriggerFailed()
 
* Feldgültigkeit verletzt.
 
    case lnError = cnERR_FIELD_RULE_FAILED
      loObject = .ErrFieldRuleFailed()
      if not isnull(loObject)
        .ActivateObjectPage(loObject)
        loObject.SetFocus()
      endif not isnull(loObject)
      lcReturn = ccMSG_CONTINUE
 

* Tabellengültigkeit verletzt.

 
    case lnError = cnERR_TABLE_RULE_FAILED
      lcReturn = .ErrTableRuleFailed()
 
* Konfikt Primär-Pontentiell-Index.
 
    case lnError = cnERR_DUPLKEY
      lcReturn = .ErrDuplicatekey()
 
* Datensatz gesperrt.
 
    case lnError = cnERR_RECINUSE
      lcReturn = .ErrRecordInUse()
 
* Datensatz wurde während des Löschens
* von einem anderen Anwender geändert.
 
    case lnError = cnERR_RECMODIFIED and ;
      lcMethod = 'DeleteRecord'
      messagebox(ccERR_REC_MODIFIED, MB_ICONSTOP, ;
        _screen.Caption)
      .Refresh()
      lcReturn = ccMSG_CONTINUE
 
* Datensatz wurde von zwei Anwendern gleichzeitig geändert.
 
    case lnError = cnERR_RECMODIFIED
      lcReturn = .ErrRecChangedByAnother()
 
* Ansonsten Benutzung des normalen Error Handlers.
 
    otherwise
      lcReturn = dodefault()
  endcase
endwith
return lcReturn

Wie Sie wahrscheinlich erwartet haben, besteht eine Routine, die spezielle Fehler behandelt, einfach aus einer case-Anweisung mit einem otherwise, mit dem die nicht behandelten Fehler an SFErrorMgr weitergegeben werden. Im Fall von SFMaintForm behandeln wir die folgenden Fehler:

        Fehler der Datenumgebung, Fehler beim Ausführen eines Triggers oder Fehler in der Feldgültigkeit: Diese Fehler sehen wir uns gleich näher an.

        Tabellenregeln verletzt, Bruch beim Primär-/Potentiellindex oder „jemand anderes hat den Datensatz gesperrt„: Anzeige einer Fehlermeldung.

        Der Datensatz wurde geändert, während wir versucht haben, ihn zu löschen: Anzeige einer Fehlermeldung und Anzeige der Änderungen, die der andere Anwender vorgenommen hat.

        Der Datensatz wurde durch einen anderen Anwender geändert, während wir versucht haben, ihn auch zu ändern: Aufruf von Code, der den Konflikt zwischen den Änderungen behandelt (Wir sehen uns diesen Code jetzt nicht an; Sie finden ihn auf der Begleitdiskette).

Wir fangen hier natürlich nicht alle möglichen Fehler ab, aber es ist schon ein Beispiel, das die häufigsten Fehler behandelt.

Datenumgebung

Grundsätzlich besitzt die Datenumgebung ihre eigene Error-Methode. Auf der anderen Seite besitzen Klassen keine eigene Datenumgebung. Daher müßten wir manuell der Methode DataEnvironment.Error für jedes von uns erstellte Formular Code hinzufügen. Statt dessen können wir Fehler der Datenumgebung auch in der Methode HandleError der Formulare behandeln, einschließlich „Tabelle oder Datenbank nicht gefunden„, „Tabellenzugriff verweigert„ oder „Tabelle bereits geöffnet„. Sicherlich kennen Sie auch weitere Fehler, die Sie hier abfangen können. Grundsätzlich können wir hier nicht viel mehr tun als eine Fehlermeldung anzeigen und das Formular zu schließen. Nun wollen wir aber einen Error Handling-Service aufrufen, also rufen wir oError.ErrorHandler auf, nachdem wir die Eigenschaft lDisplayErrors auf .F. gesetzt haben. Dadurch bewirken wir, daß Fehler zwar protokolliert, aber nicht angezeigt werden. Wir können dann unsere eigene Meldung anzeigen, bevor wir der Error-Methode „closeform„ übergeben.

Ein Fehler, den wir beachten müssen, ist „Datenumgebung wurde bereits freigegeben„. Dieser Fehler tritt auf, wenn ein vorhergehender Fehler unser Formular bereits geschlossen hat. Wir brauchen also nichts zu tun, wenn ein solcher Fehler auftritt.

Trigger

Wenn ein Trigger fehlschlägt, wird die Error-Methode des Objekts, das den Trigger ausgelöst hat,  aufgerufen, nicht die on error-Routine des Triggers (Die Prozedur RIError). Das ist ein großes Problem: RIError setzt die globale Variable pnError auf einen Wert ungleich null, die anschließend von anderen Routinen benötigt wird, um zu wissen, daß der Trigger fehlgeschlagen ist. Das Resultat kann sein, daß der Trigger vielleicht nur teilweise fehlschlägt. Hier ein Beispiel:

CUSTOMER gibt Löschaktionen an ORDERS weiter, während ORDERS das Löschen untersagt, wenn in ORDITEMS untergeordnete Datensätze vorhanden sind. Der aktuelle Datensatz in CUSTOMER hat zwei verbundene Datensätze in ORDERS, von denen der erste keine Detaildatensätze, der zweite einen Detaildatensatz in ORDITEMS besitzt. Was glauben Sie wohl, was geschieht, wenn Sie den Datensatz in CUSTOMER löschen? Sie erhalten einen Fehler, daß der Trigger fehlgeschlagen ist, stellen aber bei der Überprüfung fest, daß der Datensatz in CUSTOMER und der erste Datensatz in ORDERS gelöscht wurden. Lediglich der zweite Datensatz in ORDERS existiert als Zombie weiter.

(Vielleicht denken Sie, daß dies ein unwahrscheinliches Beispiel ist. Aber dieser Fall ist bei mir aktuell vorgekommen; deshalb habe ich dieses Problem hier an den Anfang gestellt.)

Die Lösung ist, daß der Error Handler pnError auf einen Wert ungleich null setzen muß. Ein Bug in den Prozeduren RIDelete und RIUpdate (die einen abhängigen Datensatz löschen oder updaten) muß ebenfalls behoben werden: sie setzen llRetVal auf .F., wenn pnError ungleich null ist; dadurch schlägt der Trigger fehl. Wie auch immer, da die Routine RIOpen die Tabellen im ungepufferten Modus (erneut) öffnet, wird kein nächster Level des Triggers (der die Tabelle über zwei Relationen [also einen Enkeldatensatz] überprüft) vor der Anweisung unlock ausgeführt, das wiederum erst ausgeführt wird, wenn llRetVal gesetzt ist. Daher setzt ein Fehler beim Versuch, einen Enkeldatensatz zu löschen oder zu verändern (was bewirkt, daß die Fehlermeldung des Triggers erscheint) llRetVal nicht auf .F., so daß dieser Level des Triggers nicht fehlschlägt. Daher müssen wir die llRetVal-Anweisung hinter das Kommando unlock verschieben:

procedure RIDELETE
local llRetVal
llRetVal=.t.
 IF (ISRLOCKED() and !deleted()) OR !RLOCK()
    llRetVal=.F.
  ELSE
    IF !deleted()
      DELETE
      IF CURSORGETPROP('BUFFERING') > 1
         =TABLEUPDATE()
      ENDIF
*** Verschieben Sie die folgende Zeile...
*      llRetVal=pnerror=0
    ENDIF not already deleted
  ENDIF
  UNLOCK RECORD (RECNO())
*** ... nach hier
  llRetVal=pnerror=0
RETURN llRetVal

Bruch der Regeln auf Feldebene

Ein Fehler in den Funktionen sys(2018) und aerror() von VFP 5 (einschließlich 5.0a) verhindert, daß der aktuelle Feldname verfügbar ist, wenn eine Regel auf Feldebene verletzt wurde. Statt dessen erhalten Sie eine von zwei Meldungen: Entweder den Meldungstext, den Sie bei der Feldgültigkeit eingetragen haben (wenn sie ihn eingetragen haben) oder die generische Meldung „Die Gültigkeitsregel für Feld <field> wurde verletzt.„, wenn Sie nichts eingetragen haben.

Na und? Nun, was ist, wenn Sie vorhaben, eine Meldung wie „Geben Sie einen gültigen Wert für <Feldüberschrift> ein„ anzuzeigen? Das Problem ist, daß Sie nicht wissen, welche Regel auf Feldebene gebrochen wurde. Woher nehmen Sie nun die passende Feldüberschrift? Was ist, wenn Sie vorhaben, den Fokus auf das Kontrollelement, das mit dem Feld verbunden ist, zu setzen, nachdem Sie die Fehlermeldung angezeigt haben? Schließlich würde dieses Verhalten es dem Anwender einfacher machen, den Wert zu ändern. Was ist, wenn Sie vorhaben, die Hintergrundfarbe des Kontrollelements zu ändern, um anzuzeigen, wo sich der Fehler (oder die Fehler) befinden?

Um diesen Bug (oder „undokumentierte Verhalten„, wie einige dieses Verhalten nennen <g>) zu umgehen, müssen wir zwei Dinge tun: Wenn die Meldung lautet: „Die Gültigkeitsregel für Feld <field> wurde verletzt.„, müssen wir das Feld aus diesem String herausziehen. Wenn es sich um eine andere Meldung (also den eingegebenen Meldungstext der Feldgültigkeit) handelt, müssen wir feststellen, welches Feld der Datenbank den gegebenen Text in der Eigenschaft RuleText enthält. Im Code von SFMaintForm.ErrFieldRuleFailed sehen Sie detailliert, wie Sie das erreichen können.

ErrFieldRuleFailed findet heraus, in welchem Feld das Problem auftritt und ruft die Methode FindControlSourceObject auf, um herauszufinden, welches Objekt des Formulars mit dem Feld verbunden ist. Wird ein solches Objekt gefunden, wird eine Referenz auf dieses Objekt zurückgegeben, so daß HandleError den Fokus darauf setzen kann.

Informationen zu verschiedenen Fehlern

Hier noch einige Tips, die Sie für die Fehlerbehandlung in VFP kennen sollten:

In VFP 5 wird die Error-Methode eines an ein Datenfeld gebundenen Kontrollelements aufgerufen, wenn die Gültigkeitsregel des Felds verletzt wurde. Dadurch haben Sie die Kontrolle über Art und Inhalt der anzuzeigenden Fehlermeldung. In VFP 3 waren diese Fehler nicht abfangbar; VFP zeigt den Inhalt der RuleText-Eigenschaft des Feldes (oder eine häßliche generische Meldung, wenn Sie wieder einmal RuleText keinen Wert mitgegeben haben) in einem messagebox()-Dialog, über den Sie keine Kontrolle erhielten.

Obwohl Variablen, die als local definiert wurden, außerhalb der Routine, in der sie definiert wurden, nicht sichtbar sind, kann das Kommando list memory alle Variablen, die in einer Routine im Aufruf-Stack definiert wurden, anzeigen. Das ist eine gute Sache, da dieses Vorgehen es Ihnen ermöglicht, einen Überblick über die Inhalte aller Variablen zum Zeitpunkt, an dem der Fehler auftritt, zu erhalten und diese in einer Datei festzuhalten. Die Methode LogError aus SFErrorMgr zeigt Ihnen wie.

Das Kommando list status ist nicht so hilfreich: es sieht nur die Vorgänge, die in der aktuellen Datensitzung auftreten (z.B. offene Tabellen und Einstellungen, die mit set durchgeführt wurden). Wenn sich der Error Handler in der aktuellen Sitzung befindet, kann er nicht feststellen, was in der privaten Datensitzung geschehen ist, in der der Fehler aufgetreten ist. Das ist für Sie wichtig. Bevor Sie dieses Kommando benutzen, sollten zu der Datensitzung des Formulars umschalten oder auch eine Instanz von SFErrorMgr in der Eigenschaft oError des Formulars instantiieren, damit das Kommando in der gleichen Datensitzung wie das Formular ausgeführt wird.

Aus Performancegründen könnten Sie sich überlegen, die Containerhierarchie nicht Schritt für Schritt durchlaufen, wenn ein Fehler auftritt. Sie könnten vorhaben, direkt von einem Objekt zur Error-Methode des Formulars oder, wenn gewünscht, zu SFErrorMgr.ErrorHandler zu springen. Meiner Meinung nach ist in dem Fall, daß ein Fehler aufgetreten ist, die Geschwindigkeit nicht das Problem. Ich halte lieber meine Klassenhierarchie sauber, indem ich den Mechanismus verwende, der in dieser Session beschrieben wurde.

Auch das Kommando error ist interessant: es generiert einen "künstlichen" Fehler. Es ist jederzeit verfügbar, wenn Sie unterschiedliche Arten „leichter„ Fehler behandeln wollen, vorausgesetzt, es handelt sich um VFP-Fehler. Wenn zum Beispiel file() .F. zurückgibt, zeigt das an, daß eine Datei nicht vorhanden ist. Nun könnten Sie das Kommando error einsetzen, um den Error Handler aufzurufen und auf diese Weise die passende Fehlerbehandlung (Loggen des Fehlers, Anzeige einer Fehlermeldung usw.) durchzuführen. Ich setze dieses Vorgehen nur selten ein; warum sollte ich auf einen „leichten„ Fehler prüfen, wenn ich ihn wie einen schweren Fehler behandle? Außerdem ist nicht sichergestellt, daß Sie sich in der richtigen Methode und auf der richtigen Anweisung befinden, da das Kommando error nicht aufgerufen wird, wenn der Fehler auftritt, sondern wenn Sie das Problem feststellen.

Wenn ein Fehler in einem PRG-Programm, das von einer Methode aufgerufen wird, auftritt, wird die Error-Methode des Objekts oder on error aufgerufen. Das heißt, daß zwei unterschiedliche Mechanismen genutzt werden können, um einen Fehler in einem PRG zu behandeln, je nachdem wie das Programm aufgerufen wurde. Also gestaltet sich die Fehlerbehandlung in einem PRG nicht so einfach wie in Objekten. Dies ist ein weiterer Grund, sich von PRG-Programmen zu verabschieden und den Code in Objektbibliotheken abzulegen.

on error fängt keine Fehler ab, die durch eine skip for-Klausel eines Menüs ausgelöst werden. Diese Fehler sind nicht abzufangen; stellen Sie also sicher, daß Sie Ihre Menüs mit allen Bedingungen von skip for testen.

Zusammenfassung

In dieser Session haben wir uns ein Schema für die Fehlerbehandlung angesehen, daß das Beste aus zwei Welten (lokale Fehlerbehandlung für die meisten Fehler, globale Behandlung für den Rest) angesehen. Dieses Schema wurde in verschiedenen Applikationen erfolgreich eingesetzt, trotzdem arbeiten wir daran, es weiter zu verbessern. Ich hoffe, daß Sie es auch in Ihren Applikationen einsetzen. Informieren Sie mich bitte, wenn Sie irgend welche Erweiterungen hinzufügen oder über weitere Punkte, von denen Sie meinen, daß sie verbessert werden müssen.

Quellenangabe:

Nachdem ich auf den letzten Entwicklerkonferenzen schon ähnliche Konzepte zur Fehlerbekämpfung vorgestellt hatte, besuchte ich wie jedes Jahr die diesjährige amerikanische Entwicklerkonferenz. Die dort vorgestellte Lösung von Doug Hennig war so perfekt, daß es mir sinnlos erscheint, das Rad nochmals neu zu erfinden, oder komplett andere Lösungen vorzustellen.. Aus diesem Grunde bat ich Doug um Freigabe des Artikels für unsere Konferenz, und möchte mich hiermit bei ihm auch dafür ausdrücklich bedanken. Doug Hennig ist auch Autor es Stonefield Database Toolkits, einer Erweiterung des DatenbankContainers zum echten dataDictionary. Wenn Sie es noch nicht kennen: tss, dann haben sie was verpasst!

Die Übersetzung wurde in hervoragender Weise von Mathias Gronau, Essen erledigt (Sie erreichen ihn übrigens via 106210.1042@compuserve.com).

Für mich blieb diesmal nur das Korrekturlesen und Ausbessern übrig... (Auch nich schlecht, gell? <g>

Copyright © 1997 Doug Hennig. All Rights Reserved

Doug Hennig
Stonefield Systems Group Inc.
2055 Albert Street, Suite 420
Regina, SK Canada S4P 2T8
Phone: (306) 586-3341
Fax: (306) 586-5080
CompuServe: 75156,2326
Internet: dhennig@stonefield.com
World Wide Web: www.stonefield.com