Lade...
 

Aufrufstruktur in InstantView

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)

 

Beispiel einer einfachen Abarbeitung
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.

Beispiel für den Kontrollfluss
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.

Schematische Darstellung des Call-Stacks in diesem Beispiel
Schematische Darstellung des Call-Stacks in diesem Beispiel

 

Das oben beschriebene Beispiel sieht im VSCode-Debugger wie folgt aus:

Darstellung des Call-Stacks im VSCode-Debugger
Darstellung des Call-Stacks im VSCode-Debugger

 

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.

Schematische Darstellung des Zusammenhangs von Call-Stack und Event-Queue
Schematische Darstellung des Zusammenhangs von Call-Stack und Event-Queue

 

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.