Aufrufstruktur in InstantView
InstantView ist ereignisgesteuert. Das heißt, die Abarbeitung wird immer durch ein Event ausgelöst. Im einfachsten Fall steht dann hinter einem Event-Bezeichner eine Reihe an Anweisungen, die nacheinander abgearbeitet werden. (Siehe Empfang von Events)
Button(ClickBtn, 0, 0, 80, T("Klick mich", "Click me")) [ SELECT: T("Der Knopf wurde gedrückt", "The button was clicked") Attention(, INFO) ]
Im Normalfall muss auf ein Event jedoch komplexer reagiert werden, so dass eine Strukturierung der Anweisungen notwendig wird. Hierzu stehen in InstantView zwei Mittel zur Auswahl: Prozeduren und Messages.
Prozeduren
Prozeduren sind primär dafür gedacht, Quelltext zu strukturieren, sinnvolle Blöcke zusammenzufassen und Duplizität zu vermeiden. Sie werden mit dem Schlüsselwort Define definiert und können aufgerufen werden, wenn sie im gleichen Modul oder aber in einem Provider liegen. Provider sind Module, die in ihrer Extern-Anweisung entsprechend als Provider markiert wurden sind.
Messages
Messages werden für die lose Kopplung von Modulen benutzt. Ein Modul informiert, dass etwas passiert ist. Wer diese Message empfängt und bearbeitet (und ob überhaupt), ist für den Aufrufer nicht bekannt und kann flexibel über Extern-Anweisungen gesteuert werden. Ausnahmen hierzu sind SendMsg(..., SUPER) und SendMsg(..., DIRECT). Bei SendMsg(, SUPER) wird die Implementation dieser Message im beerbten Modul aufgerufen. Bei SendMsg(, DIRECT) wird die Message nur an das auf dem Stack liegende Objekt geschickt.
Der Call-Stack
Beim Aufruf einer Prozedur oder einer Message per SendMsg wird die Abarbeitung des Aufrufers angehalten und nach Abschluss des aufgerufenen fortgeführt.
Beispiel
Im nachfolgenden Beispiel wird beim Auslösen des Events "SELECT" zunächst der Befehl GetValue ausgeführt, der direkt hinter "SELECT:" steht. Es folgt die Prozedur GetObjectToProcess. Somit wird als nächstes die 1 auf den Stack gelegt, dann die Vergleichsoperationen ausgeführt etc. Die Zuweisung zur Variablen person erfolgt erst, nachdem die Prozedur GetObjectToProcess beendet ist.
Define(GetObjectToProcess) 1 = if CreateTransObject(CX_PERSON) else CreatePersObject(CX_PERSON) ; [...] Integer(objectType, 0, 0, 20) [ Var(person) SELECT: GetValue GetObjectToProcess -> person ]
Die Position in der Abarbeitung der SELECT-Message wird dabei gemerkt und virtuell im sogenannten Frame auf den Call-Stack gelegt. Oben auf diesen Stapel folgt der Frame der Prozedur GetObjectToProcess. Ist diese abgeschlossen, wird ihr Frame entfernt und es wird die Ausführung des nun zuoberst liegenden Frame der SELECT-Message fortgesetzt.
Das oben beschriebene Beispiel sieht im VSCode-Debugger wie folgt aus:
Da diese Befehle somit in einer chronologischen Reihenfolge stehen, spricht man von einem synchronen Aufruf. Da der Aufrufer nicht weiterrechnet, spricht man auch von einem "blockierenden Aufruf".
Die Event-Queue
In manchen Fällen ist es jedoch nötig, eine Befehlsfolge auszuführen, nachdem die aktuelle Abarbeitung beendet ist. Hierfür kann PostMsg genutzt werden.
Bei wird der aktuelle Stack-Inhalt zusammen mit dem Empfänger und dem auszulösenden Event zusammengestellt und wird bearbeitet, sobald der aktuelle Call-Stack abgearbeitet ist.
Der Call-Stack hat dabei immer ein sogenanntes Root-Event (im obigen Fall das SELECT-Event) und diese Root-Events werden hintereinander aufgereiht abgearbeitet. Diese Struktur nennen wir die Event-Queue, da das erste Event, welches in die Queue gestellt wird auch als erstes bearbeitet wird. Das Event von PostMsg wird nun also – genau wie alle anderen während der Abarbeitung des ersten Root-Events auflaufenden Events – ans Ende der Event-Queue gestellt und nach Abarbeitung aller vohreriger Root-Events bearbeitet.
Transaktionen
Das implizite Transaktions-Handling in ClassiX öffnet eine Transaktion beim ersten Zugriff auf persistente Daten und beendet diese, wenn das letzte Root-Event aus der Event-Queue abgearbeitet ist. Hierdurch werden alle zu einem Auslöser gehörenden Daten-Manipulationen in einer Transaktion zusammengefasst.
Unabhängig davon können Transaktionen mit BeginTXN begonnen, mit EndTXN abgeschlossen und mit AbortTXN abgebrochen werden.
return, cancel und Exceptions
Mit return lässt sich die Abarbeitung des aktuellen Frames beenden. Der Kontrollfluss wird dann im nächsten Frame auf dem Call-Stack fortgeführt.
Mit cancel hingegen lässt sich die Abarbeitung des gesamten Call-Stacks und der Event-Queue beenden. Das heißt insbesondere, dass PostMsg-Aufrufe nicht mehr abgearbeitet werden.
Exceptions verhalten sich generell wie ein cancel: Der Callstack wird abgeräumt und falls die Exception nicht innerhalb eines ExecuteWeak/CallWeak aufgetreten ist, dann wird anschließend die Transaktion abgebrochen, die Event-Queue geleert und dem Benutzer eine Fehlermeldung angezeigt, die darauf hinweist, was passiert ist.
Treten Exceptions innerhalb eines Aufrufs einer Klassenmethode auf, können diese per CallWeak abgefangen und anschließend per SystemObject::GetLastError ausgelesen werden.
Sonderfälle
Die bisher beschriebene Struktur wird in ein paar Fällen unterbrochen.
WaitOnInput
WaitOnInput hält die Abarbeitung im aktuellen Zustand an und setzt die Abarbeitung auf einer leeren1 Kopie von Event-Queue und Call-Stack fort. Auf dieser Kopie wird so lange gearbeitet bis FinishInput diesen Zustand beendet. Hierdurch kann die Abarbeitung nach WaitOnInput im gleichen Frame an der gleichen Stelle fortgesetzt werden.
1= Die Event-Queue ist eine leere Kopie, jedoch werden Root-Events wie UI-Events und PostMsg in eine gesonderte, zentrale Queue für Root-Events gestellt, aus welcher diese Events rauskopiert werden, falls die aktuelle Abarbeitung abgeschlossen ist. Dies führt dazu, dass ein PostMsg, vor WaitOnInput innerhalb des WaitOnInputs abgearbeitet wird.
Hinweis: Ein Aufruf von cancel und Exceptions innerhalb von WaitOnInput beenden sowohl den inneren Callstack, als auch den des aufrufenden WaitOnInputs. Die Ausführung wird nach einem cancel also nicht nach dem WaitOnInput fortgesetzt.
Ausnahme: Aufgrund der besonderen Verarbeitungsweise von Code in der ClassiX-Shell führt eine Exception innerhalb der ClassiX-Shell innerhalb von WaitOnInput aktuell dazu, dass die Exception gemeldet und der Code nach dem WaitOnInput fortgesetzt wird.
Achtung: Per PostMsg in die Event-Queue gestellte Messages werden nicht in der Event-Queue außerhalb von WaitOnInput "eingefroren", sondern werden in der Event-Queue innerhalb von WaitOnInput abgearbeitet. Da die Event-Queue in diesem Moment frei ist, werden die Messages sogar sofort ausgeführt.
Achtung: Eine Ausnahme von der obigen Regel stellt aktuell folgende Konstellation dar:
PostMsg(WAIT) PostMsg(A) PostMsg(B)
Falls die Message WAIT ein WaitOnInput ausführt, dann wurden die PostMsg-Requests für WAIT, A & B bereits in die lokale Event-Queue kopiert und bleiben dort eingefrohren, bis der WaitOnInput-Befehl abgeschlossen wurde.
Wird FinishInput aufgerufen, so werden die verbleibenden Befehle des "inneren" Call-Stacks abgearbeitet. Dann kehrt die Ausführung zum Befehl hinter WaitOnInput zurück.
Vor Dll-Version 225137:
PostMsg-Aufrufe innerhalb des Call-Stacks von FinishInput gehen bis auf maximal einen verloren. Der nicht verlorene Aufruf von PostMsg wird nach Abarbeitung des inneren Call-Stack und vor dem Befehl nach WaitOnInput ausgeführt.
Runtime-Befehle
Generell wird bei einem cancel oder einer Exception die Abarbeitung abgebrochen und versucht, auf einen sicheren Zustand zurückzukehren. Ist der Call-Stack jedoch nicht so einfach aufgebaut, dass lediglich Prozeduren-Aufrufe und SendMsg, sondern auch Runtime-Befehle involviert sind, sind die Regeln differenzierter.
OpenWindow
Während der Bearbeitung des Befehls OpenWindow wird unter anderem das Event INITIALIZE an das entsprechende Window verschickt. Tritt hier eine Exception auf oder wird ein cancel ausgeführt, wird das Öffnen des Fenster wieder rückgängig gemacht, die Transaktion abgebrochen und die Event-Queue geleert.
CloseWindow
Während der Bearbeitung des Befehls CloseWindow wird zur Information die Events CLOSE und NON_CURRENT an das entsprechende Window verschickt. Tritt hier eine Exception auf oder wird ein cancel ausgeführt, kann das Schließen des Fensters nicht wieder zu einem konsistenten Zustand rückgängig gemacht werden.
Exceptions in diesem kritischen Zustand führen daher zu einem Abbruch des Systems.
Aufrufe von cancel beenden lediglich die Abarbeitung des CLOSE-Events (als ob es ein Root-Event wäre), leeren aber nicht die Event-Queue, sodass ein PostMsg anschließend trotzdem ausgeführt wird. Das Fenster wird trotz cancel geschlossen, da das Schließen im CLOSE nicht mehr abgebrochen werden kann.
Schließt der Benutzer ein Fenster, wird vor dem eigentlichen Schließen des Fensters hingegen das System-Event CLOSING geschickt. Exceptions und cancel-Aufrufe innerhalb dieser Message führen in diesem Fall dazu, dass das Fenster nicht geschlossen wird.
Zugriffsaudrücke in ObjectBox
In ObjectBoxen können per SetFormat Zugriffsausdrücke angegeben werden, die zum Befüllen der Zellen herangezogen werden. Tritt hier eine Exception auf, so wird nicht der Prozess des Befüllens abgebrochen. Stattdessen wird in der entsprechenden Zelle in rot der Fehler angezeigt.