Durchsuchbare Dokumentation aufrufen | Zurück zur Dokumentationsübersicht
Navigation: Dokumentationen agorum core > agorum core aguila
aguila (agorum user interface layer) ist ein in agorum core integriertes UI-Framework. Es bildet die Grundlage für bei agorum core mitgelieferte Oberflächen und ermöglicht es, auf einfache Weise eigene Bedienoberflächen auf Basis von agorum core zu entwickeln.
Die Logik einer aguila-Oberfläche wird vollständig serverseitig abgebildet, typischerweise in Form einer oder mehrerer JavaScript-Dateien. Hierbei wird Event-basiert gearbeitet, Aktionen werden immer als Reaktion auf Ereignisse ausgelöst. Ein Ereignis kann etwa die Bestätigung einer Schaltfläche, eine Änderung in einem Textfeld oder ein abgelaufener Timer sein.
Durch diese Verlagerung auf den Server ist dort pro aktivem Client eine gewisse Menge an Hauptspeicher nötig, abhängig von der Zahl und Komplexität der eingesetzten Widgets und Datenstrukturen. Gleichzeitig werden die Clients entlastet und benötigen weniger Ressourcen.
Die rein serverseitige Entwicklung hat außerdem den Vorteil, dass Anpassungen mit zukünftigen Versionen von agorum core nutzbar bleiben. Eventuelle Änderungen am Client-Framework werden durch eine neue Implementierung des Client-Teils von aguila durch agorum core selbst abgedeckt.
Durch den vollen Zugriff auf die agorum core javascript library sind alle Funktionen von agorum core direkt nutzbar.
Durch die automatische Übertragung des Zustands der Bedienoberfläche von Server zu Client ergeben sich weitere Vorteile:
Ein Widget ist eine Komponente eines grafischen Fenstersystems.
Oberflächenelemente werden serverseitig durch Widgets repräsentiert. Hierbei kann ein Widget entweder ein Basis-Widget, etwa eine Checkbox, oder eine Textanzeige sein oder ein Container, der selbst weitere Widgets enthält.
Um eine Struktur aus Widgets zu definieren, wird das aon-Format (aguila object notation) verwendet, das für jedes zu erzeugende Widget Typ und Parameter definiert.
Beispiel
Dieses Beispiel verwendet eine einfache aon-Struktur, die eine Textausgabe mit einer darunter rechtsbündig angeordneten Schaltfläche kombiniert:
module.exports = { type: 'agorum.vbox', items: [ { type: 'agorum.textArea', text: 'Hallo, Welt!', name: 'textArea', width: 300, flexible: true }, { type: 'agorum.hbox', items: [ { type: 'agorum.spacer', flexible: true }, { type: 'agorum.button', text: 'Hallo, aguila!', name: 'button' } ] } ] };
Ergebnis
Der Lebenszyklus eines Widgets besteht aus folgenden Phasen und Übergängen:
Übergang | Phase | Beschreibung |
---|---|---|
Widget wird erzeugt | Danach ist das Widget im serverseitigen Kontext registriert und auch für andere Widgets zugänglich. | |
Erzeugt | – | |
Widget wird gerendert | Übermittlung an den Client zur Darstellung. | |
Gerendert | – | |
Widget wird geschlossen | 3 Möglichkeiten existieren:
|
|
Widget löst das Event „destroying“ aus |
|
|
Geschlossen | – | |
Widget löst das Event „destroyed“ aus |
|
|
Zerstört | – |
Wurde ein Widget noch nicht geschlossen, kann es in einen beliebigen Container im selben Kontext verschoben werden. Gerade in größeren Widget-Strukturen mit vielen dynamisch generierten Widgets empfiehlt es sich aus Performancegründen, nach Möglichkeit vorhandene Widgets wiederzuverwenden, statt sie zu zerstören und durch neue Widgets zu ersetzen, wenn die Darstellung geändert wird.
Widget-Typen werden nach folgendem Schema benannt:
<Paket>.<Typ>
Hierbei kann der Paketname noch weiter in Pakete und Unterpakete strukturiert sein, die ebenfalls mit . voneinander getrennt werden.
Der oberste / erste Paketname entspricht typischerweise der Firmenbezeichnung des Widget-Urhebers. So beginnen etwa alle mitgelieferten Widget-Typen mit agorum.
Verwenden Sie für eigene Widgets eindeutige Namen nach einem einheitlichen Schema, etwa:
<Firma>.<Projekt>[.<Teil>].<Typ>
aguila-Oberflächen bestehen aus mehr als einem Widget. Um die Widgets zu gruppieren und die Anordnung zu definieren, gibt es Container-Widgets oder nur Container.
Sie können den Inhalt von Containern je nach Typ auf zwei verschiedene Arten definieren:
Art | Beschreibung |
---|---|
Als Array von Elementen (items) für Container |
|
Unter direkter Verwendung von definierten Regionen (docked) des Containers |
|
Welcher Mechanismus jeweils verwendet wird, ist abhängig von dem konkret verwendeten Container-Typ.
Die vollständige Referenz der Container-Widgets finden SIe hier: Container-Widgets
Die einfachsten Container-Typen sind agorum.hbox und agorum.vbox. Sie ordnen die enthaltenen Elemente (items) horizontal nebeneinander (hbox) oder vertikal untereinander (vbox) an.
Das System stellt im Standard jedes Widget nur so breit (hbox) oder hoch (vbox) dar, wie es selbst festlegt. Das kann eine natürliche Größe sein, etwa die Höhe eines Textfelds, oder eine explizit über height oder width festgelegte Größe.
Wenn sich der zur Verfügung stehende Platz dynamisch ändern kann (etwa wenn ein Benutzer ein Fenster vergrößert / verkleinert), dann kann für ein oder mehrere Elemente stattdessen das Attribut flexible auf true gesetzt werden. Eine hbox / vbox verteilt den zusätzlich zur Verfügung stehenden Platz in seiner Hauptrichtung gleichmäßig auf alle Elemente, die als flexibel definiert wurden. Meistens enthält eine hbox / vbox höchstens ein flexibles Element, etwa ein mehrzeiliges Textfeld oder auch einen weiteren Container, der seinerseits wieder flexible Elemente enthält.
In der Nebenrichtung (vertikal für vbox, horizontal für hbox) streckt das System Elemente grundsätzlich auf die vorhandene Breite (vbox) oder Höhe (hbox).
Die äußere vbox verteilt den zur Verfügung stehenden Platz folgendermaßen:
Höhe
Breite
Sowohl textArea als auch hbox werden auf die verfügbare Breite erweitert
Die untere hbox verteilt den zur Verfügung stehenden Platz wie folgt:
Höhe
Da die hbox selbst nicht flexibel ist, wird sie nur so hoch dargestellt wie ihr höchstes Element. Da ein spacer-Element von sich aus keine Höhe hat, ist das die Schaltfläche, die seine natürliche Größe erhält. Damit wird das unsichtbare spacer-Element ebenfalls auf diese gemeinsame Höhe vergrößert.
Breite
Wenn das Fenster vergrößert / verkleinert wird, wächst / schrumpft die textArea und die Schaltfläche wird in fester Größe unten rechts verankert.
Ist eine hbox / vbox nicht groß genug, um alle nicht flexiblen Elemente gleichzeitig darzustellen, erscheinen Scrollbalken.
Das agorum.border-Layout stellt ein zentrales Element (immer vorhanden, center) und bis zu vier optionale Rand-Elemente (north, south, west, east) dar, die Sie einzeln in ihrer Größe verändern und weglappen können.
Abgesehen davon, dass die Randelemente einzeln in ihrer Größe geändert werden können, sind sie in ihrer Höhe (north / south) und Breite (west / east) statisch. Eventuelle Größenänderungen von außen werden direkt an das center-Element weitergegeben.
Beispiel
Dieses Beispiel verdeutlicht die Aufteilung in die fünf Regionen:
module.exports = { type: 'agorum.border', width: 600, height: 400, docked: { center: { type: 'agorum.textArea', text: 'center' }, north: { type: 'agorum.textArea', text: 'north', height: 100 }, south: { type: 'agorum.textArea', text: 'south', height: 100 }, west: { type: 'agorum.textArea', text: 'west', width: 100 }, east: { type: 'agorum.textArea', text: 'east', width: 100 } } };
Ergebnis
Das border-Layout wird typischerweise an oberster Ebene einer Widget-Struktur verwendet, um die Darstellung in den Hauptinhalt (center) und zusätzliche Informationen / Bedienelemente zu gliedern.
Hinweis: Im Gegensatz zu den anderen Layout-Arten werden die Elemente des border-Layouts nicht per items, sondern ausschließlich per docked deklariert.
Das agorum.tabContainer-Layout stellt jedes seiner Elemente in einer eigenen Registerkarte dar. Das title-Attribut des Elements wird dabei als Überschrift des Reiters verwendet.
Beispiel
Dieses Beispiel stellt die beiden vorangegangen Beispiele in zwei getrennten Registerkarten dar:
let tab1 = require('./hello-world-aon'); let tab2 = require('./border-layout'); tab1.title = 'hbox und vbox'; tab2.title = 'border'; module.exports = { type: 'agorum.tabContainer', width: 800, height: 600, items: [ tab1, tab2 ] };
Ergebnis (Tab1)
Ergebnis (Tab2)
Das agorum.toolBar-Layout stellt seine Elemente (typischerweise Schaltflächen) in einer horizontalen oder vertikalen Toolbar dar.
Container verwenden das toolBar-Layout in der Regel nur für die vier zusätzlichen Regionen top, bottom, left und right, vergleichbar mit north, south, west und east, jedoch immer mit fester Größe ganz am Rand des Containers angedockt.
Beispiel
Dieses Beispiel verdeutlicht die Anordnung der Regionen um eine Erweiterung des border-Beispiels mit allen vier möglichen Toolbar-Positionen:
module.exports = { type: 'agorum.border', width: 600, height: 400, docked: { top: { type: 'agorum.toolBar', items: [ { type: 'agorum.button', text: 't' } ] }, bottom: { type: 'agorum.toolBar', items: [ { type: 'agorum.button', text: 'b' } ] }, left: { type: 'agorum.toolBar', items: [ { type: 'agorum.button', text: 'l' } ] }, right: { type: 'agorum.toolBar', items: [ { type: 'agorum.button', text: 'r' } ] }, center: { type: 'agorum.textArea', text: 'center' }, north: { type: 'agorum.textArea', text: 'north', height: 100 }, south: { type: 'agorum.textArea', text: 'south', height: 100 }, west: { type: 'agorum.textArea', text: 'west', width: 100 }, east: { type: 'agorum.textArea', text: 'east', width: 100 } } };
Ergebnis
Verwenden Sie bei left und right in der toolBar nur Schaltflächen mit Symbol und ohne Text, da das System diese sonst unnötig breit darstellt.
Das agorum.single-Layout kann nur ein einzelnes Element darstellen, das den gesamten verfügbaren Platz einnimmt. Dabei versucht es nicht (so wie alle anderen Layout-Arten) zwischen sichtbaren Elementen einen festen Abstand einzuhalten.
Das System setzt das single-Layout nur als Platzhalter ein, um die Einschränkung zu umgehen, dass eingedockte (docked) Elemente von Containern nicht nachträglich geändert werden können. Stattdessen verwendet das System als eingedocktes Element einen single-Container und tauscht dessen item.
Verwenden Sie aufgrund der Besonderheiten bei den Widget-Abständen das single-Layout nur für diesen Zweck.
Im Gegensatz zu den anderen Layouts kann das single-Layout nicht in einem statischen Beispiel demonstriert werden, da es für den dynamischen Austausch von Widgets zur Laufzeit gedacht ist. Daher wird hier nicht wie bisher nur eine Struktur per aon definiert, sondern es wird zusätzlich noch Event-Handling verwendet, das hier noch nicht näher besprochen wird.
Beispiel
let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.border', width: 600, height: 400, docked: { top: { type: 'agorum.toolbar', items: [ { type: 'agorum.button', text: 'Rotate', name: 'rotate' } ] }, west: { type: 'agorum.single', title: 'west', name: 'west', width: 100, items: [ { type: 'agorum.vbox', background: 'white' } ] }, center: { type: 'agorum.single', title: 'center', name: 'center', items: [ { type: 'agorum.vbox', background: 'light' } ] }, east: { type: 'agorum.single', title: 'east', name: 'east', width: 100, items: [ { type: 'agorum.vbox', background: 'dark' } ] } } }); let west = widget.down('west'); let center = widget.down('center'); let east = widget.down('east'); widget.down('rotate').on('clicked', () => { let tmp = east.items; east.items = center.items; center.items = west.items; west.items = tmp; }); widget;
Ergebnis
Ergebnis (Schaltfläche „Rotate“ 1x geklickt)
Ergebnis (Schaltfläche „Rotate“ noch einmal geklickt)
Folgende Widgets erwarten eine Eingabe des Benutzers und stellen einen Textinhalt dar:
Folgende Widgets stellen weder einen Input noch einen Container dar:
Nachdem die Widget-Struktur als aon (aguila object notation) definiert wurde, können aus ihr konkrete Widgets erzeugt werden, auf deren Ereignisse / Events reagiert werden kann und deren Parameter abgefragt und geändert werden können.
Beispiel
// aguila-Bibliothek einbinden let aguila = require('common/aguila'); // extern definierte aon-Struktur einbinden let aon = require('./hello-world-aon'); // Widget(s) aus aon-Struktur erzeugen let widget = aguila.create(aon); // später verwendete Unter-Widgets nach ihrem Namen (siehe aon) suchen let textArea = widget.down('textArea'); let button = widget.down('button'); // Event-Handler für das Event 'clicked' der Schaltflächen definieren button.on('clicked', () => { // aktuellen Text aus der Text Area auslesen und verdoppeln textArea.value = textArea.value + ' ' + textArea.value; }); // Event-Handler für das Event 'valueChanged' der Text Area definieren textArea.on('valueChanged', value => { // aktuellen Text aus der TextArea als Titel des Fensters verwenden, in dem das Widget aktuell angezeigt wird widget.form.title = value; }); /** // erzeugtes Widget in einem neuen Fenster öffnen widget.popup({ title: 'Test mit Events', width: 600, height: 400 }); /**/ // Alternativ, für den Test im Skript-Editor: Rückgabewert des Skripts == Das Widget, das angezeigt werden soll widget;
Ergebnis (Schaltfläche 1x geklickt)
Neben den von agorum core mitgelieferten Typen können Sie eigene Widget-Typen in den folgenden Fällen definieren.
Als Beispiel wird hier Fall 2 besprochen, die zugrundeliegenden Prinzipien sind aber für Fall 1 identisch:
Jedes Mal, wenn eine neue Instanz des neuen Widget-Typs erzeugt werden soll, wird ein definiertes Skript aufgerufen, das als Widget-Konstruktor bezeichnet wird. Dafür bekommt der Konstruktor zusätzlich noch eine Kopie der aon-Struktur, durch die es erzeugt werden soll, in der globalen Variable parameters zur Verfügung gestellt. Als Rückgabewert des Konstruktor-Skripts wird das fertige Widget erwartet.
/* global parameters */ let Calendar = java.util.Calendar; let LocalDate = java.time.LocalDate; let aguila = require('common/aguila'); /* Beschreibung: type: mein_konfigurationsprojekt.dateTime (Konstruktor für diese Registrierung) event properties increment value label */ // [A] // Widgets erzeugen let widget = aguila.create({ type: 'agorum.hbox', properties: [ 'value', 'label' ], items: [ { type: 'agorum.dateInput', name: 'date', flexible: true // FALSCHE Verwendung: // das wäre eine falsche Nutzung von "label", da es als properties definiert ist // würde aber gehen //label: parameters.label }, { type: 'agorum.numberInput', integer: true, name: 'hour', width: 50, // Dieser Parameter wird beim Aufruf des Widget übergeben - siehe // Beispiel später, wo wir das widget dann aufrufen werden // (es wird noch registriert und kann dann verwendet werden) // Hier dafür sorgen, dass auch ein nicht gesetzter Parameter richtig funktioniert // Also hier den Defaultwert setzen, wenn parameters.increment nicht belegt ist increment: parameters.increment || 1, }, { type: 'agorum.numberInput', integer: true, name: 'minute', width: 50 }, { type: 'agorum.button', name: 'clear', icon: 'aguila-icon clear' } ] }); console.log('parameters: ' + JSON.stringify(parameters, null, 4)); // Referenzen auf verwendete Widgets hinterlegen let date = widget.down('date'); let hour = widget.down('hour'); let minute = widget.down('minute'); let clear = widget.down('clear'); // [B] // Event-Handler für alle Properties widget // Wenn sich Property "value" ändert, neuen Wert auf die einzelnen Felder date, hour und minute verteilen .on('valueChanged', value => { if (value) { let cal = Calendar.instance; cal.time = new Date(value); date.value = LocalDate.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)); hour.value = cal.get(Calendar.HOUR_OF_DAY); minute.value = cal.get(Calendar.MINUTE); } else { date.value = null; hour.value = null; minute.value = null; } }) // Wenn sich Property "label" ändert, neuen Wert als Beschriftung des date-Felds übernehmen .on('labelChanged', label => date.label = label); // [C] // Alle Felder zurücksetzen clear.on('clicked', () => { //widget.value = null; date.value = null; hour.value = null; minute.value = null; }); // Interne handler - hier ein Beispiel mit mehreren Handlern, auf ein Event // hour und minute - hier registriueren 2 x "valueChanged" // die abarbeitung erfolgt hier in der Reihenfolge der Registrierung // Wertebereich des Felds "hour" einschränken hour.on('valueChanged', v => { if (v < 0) v += 24; if (v > 23) v -= 24; hour.value = v; }); // Wertebereich des Felds "minute" einschränken minute.on('valueChanged', v => { if (v < 0) v += 60; if (v > 59) v -= 60; minute.value = v; }); // Bei Änderung eines Felds die Werte aller Felder lesen und als Wert von Property "value" zusammenfassen let calcDate = () => { if (date.value) { let cal = Calendar.instance; cal.time = new Date(date.value); cal.set(Calendar.HOUR_OF_DAY, hour.value || 0); cal.set(Calendar.MINUTE, minute.value || 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); widget.value = cal.time; } else { widget.value = null; } }; date.on('valueChanged', calcDate); hour.on('valueChanged', calcDate); minute.on('valueChanged', calcDate); /** setImmediate(() => { widget.label = 'Hallo'; widget.value = new Date(); }); /***/ // [D] widget;
Das durch diesen Konstruktor erzeugte Widget stellt eine kombinierte Datums- und Uhrzeit-Eingabe (Stunden + Minuten) dar, grob zerlegt in folgende Schritte.
Benutzerdefinierte Widgets sind ihrerseits immer aus anderen Widgets zusammengesetzt. Meistens ist das ein äußerer Container (hier die umgebende hbox) zusammen mit seinen Elementen. Dieser Container stellt das äußere Widget dar, dem eine besondere Bedeutung zukommt: Es ist das Widget, das dem Aufrufer zurückgegeben wird, und ist daher für beide Seiten direkt sichtbar. An diesem Widget findet daher der Datenaustausch in beide Richtungen statt.
In unserem Fall werden dafür auf diesem Widget zwei benutzerdefinierte Parameter value und label definiert, die eine hbox normalerweise nicht hat und mit der uns sowohl der Aufrufer mitteilen kann, was Beschriftung und Inhalt unseres Widgets sein soll als auch wir den Aufrufer über den aktuellen Inhalt in Kenntnis setzen können.
Für Widgets dieser Art, die einen einzelnen Wert darstellen, hat es sich eingebürgert, diesen im Parameter value zu hinterlegen. Das hat für den Aufrufer den Vorteil, dass er etwa über mehrere Felder iterieren und jeweils .value abrufen kann, ohne den Feldtyp vorher zu prüfen. Außerdem ist es so einfacher, später einzelne Widgets auszutauschen, ohne die Logik zu ändern.
Zwei weitere Möglichkeiten des Datenaustauschs zwischen Aufrufer und Widget sind Parameter (in dem Beispiel increment) und explizite Events:
Bei jeder Änderung eines als Property xyz definierten Werts am äußeren Widget wird automatisch das Event xyzChanged ausgelöst mit dem neuen Wert als Parameter. Dieses Event muss von einem Widget für jedes von ihm definierte Property verarbeitet werden. In diesem Fall sind das valueChanged und labelChanged, die jeweils an die zugehörigen Unter-Widgets verteilt / weitergegeben werden.
Diese Events werden auch ausgelöst, wenn diese Werte von innen durch das Widget selbst geändert werden, sodass auch der Aufrufer benachrichtigt wird, wenn sich etwa der Wert ändert.
Danach wird das Zusammenspiel der Unter-Widgets durch entsprechende Event-Handler festgelegt. Hierbei wird bei einer Änderung des nach außen sichtbaren Werts stets widget.value angepasst.
In diesem Beispiel ist außerdem zu sehen, dass aus Gründen der Übersichtlichkeit die Beschränkung des Wertebereichs von hour und minute und die Neuberechnung des Datum-/Uhrzeit-Werts in getrennte Event-Handler ausgelagert wurden. Werden für dasselbe Event mehrere Event-Handler definiert, so werden diese in der Reihenfolge ihrer Definition aufgerufen (hier also zuerst die Beschränkung und danach die Berechnung).
Der Rückgabewert des Skripts muss das oben erzeugte äußere Widget sein. In JavaScript ist der Rückgabewert eines Skripts der Rückgabewert der letzten ausgeführten Anweisung, daher genügt hier die Angabe der Variablen widget.
Um das Widget verwenden zu können, muss es unter einem geeigneten Namen registriert werden. Für diesen Namen gelten folgende Einschränkungen:
Beispiele für gültige Namen
Da Widgets im Allgemeinen wiederverwendbar sein sollten, sollte hier ein möglichst eindeutiger Name gewählt werden, um spätere Konflikte zu vermeiden.
MAIN_MODULE_MANAGEMENT/aguila/control/widgets/<Widget-Name>
<Pfad zum Konstruktor-Skript>
Zur einfachen Registrierung von neuen Widgets kann auch der agorum core template manager verwendet werden.
Dieser erwartet Widget-Konstruktoren in der folgenden normierten Ablagestruktur:
/agorum/roi/customers/<Projektname>/.../aguila/<Segment1>/../<Widget-Konstruktor>.js
Der Name des Widgets wird automatisch aus dem Namen des Projekts, der Ordner unterhalb des Ordners aguila und dem Dateinamen des Widget-Konstruktors gebildet. Für das Widget aus unserem Beispiel war das etwa:
/agorum/roi/customers/mein_konfigurationsprojekt/doc/Einführung/aguila/date-time.js
Dies führt zu folgendem Namen:
mein_konfigurationsprojekt.dateTime
Zum Abschluss hier noch ein Beispiel für die Verwendung des gerade erstellten Widget-Typs:
let aguila = require('common/aguila'); let templates = require('common/templates'); // Widgets erzeugen let widget = aguila.create({ type: 'agorum.vbox', items: [ { type: 'agorum.hbox', items: [ { label: 'Datum/Uhrzeit', // Belegung properties "label" type: 'mein_konfigurationsprojekt.dateTime', name: 'input', flexible: true, increment: 2, // Belegung parameters "increment" // value: new Date() // Vorbelegung des properties "value" }, { type: 'agorum.button', name: 'now', text: 'Now' } ] }, { type: 'agorum.textDisplay', label: 'Eingabe', name: 'display' } ] }); // Referenzen auf verwendete Widgets hinterlegen let input = widget.down('input'); let display = widget.down('display'); let now = widget.down('now'); // Bei einem Klick auf "now" den Wert des Eingabefelds auf den aktuellen Zeitpunkt setzen now.on('clicked', () => input.value = new Date()); // Wenn sich der Wert des Eingabefelds ändert, diesen als String im Anzeigefeld darstellen let format = value => templates.fill('${value:dd.MM.yyyy HH:mm}', { value: value }); input.on('valueChanged', value => display.value = value ? format(value) : ''); widget;
Zur Illustration wurde hier noch eine weitere Schaltfläche namens now ergänzt, mit dem der aktuelle Zeitpunkt eingesetzt wird, außerdem ein Anzeigefeld display, das bei valueChanged aktualisiert wird.
Hier noch ein weiteres Beispiel für ein eigenes Widget. Im Gegensatz zu dem eingebauten Widget agorum.basicTree übernimmt dieser Baum zusätzlich die Verwaltung des übergebenen Baums. Das bedeutet, dass im Gegensatz zu basicTree bei Events und so weiter nicht IDs verwendet werden, sondern die tatsächlichen Knoten des Baums. Gerade bei Konfigurationsbäumen ist dieses Verhalten typischerweise praktischer:
/* global parameters */ let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.border', width: 800, height: 600, docked: { west: { type: 'mein_konfigurationsprojekt.dataTree', name: 'tree', width: 200, border: true, // hideRoot: false }, center: { type: 'agorum.textArea', name: 'display', border: true, monospace: true } } }); let tree = widget.down('tree'); let display = widget.down('display'); tree.data = { text: 'root', expanded: true, items: [ { text: 'root/item1', expanded: true, items: [ { text: 'root/item1/item2' } ] }, { text: 'root/item2' } ] }; tree.on('selectionChanged', nodes => { display.value = JSON.stringify(nodes, null, 2); }); // Hier zum zeigen, die Events des agorum.basicTree werden durchgeschleust und kommen auch hier an. [ 'nodeClicked', 'nodeDblClicked', 'nodeRightClicked', 'nodeExpanded', 'nodeCollapsed' ] //.forEach(event => tree.on(event, id => widget.fire(event, nodes[id]))); .forEach(event => tree.on(event, node => { console.log('Aussen: + ' + event + ': "' + node.text + '"'); })); /**/ // Timeout führt alles nach 1 Sec. aus tree.setTimeout(() => { // Hier machen wir einen weiteren Knoten in den Baum // let item = { text: 'root/item3' }; // Knoten wird in den Baum gesetzt tree.data.items.push(item); tree.fire('refresh'); tree.selection = [ item ]; }, 2000); /**/ widget;
Wie Sie sehen, bekommt das selectionChanged-Event als Parameter nun den vollständigen Knoten zurück und kann direkt damit arbeiten.
/* global parameters */ let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.single', width: 300, height: 500, properties: [ 'data', 'selection' ], items: [ { type: 'agorum.basicTree', name: 'tree', hideRoot: parameters.hideRoot } ] }); let tree = widget.down('tree'); // internal node management let id, nodes; // Diese Funktion macht aus einem Baum eine Array, auf das // Direkt mit der id zugegriffen werden kann // Der Baum wird neu durchnummeriert let decorate = node => { // Hier wird eine Kopie gemacht, von node in myNode let myNode = Object.assign({}, node); // Hier wird die id hochgezählt nodes[myNode.id = (id++).toFixed(0)] = node; if (node.items) { myNode.items = node.items.map(item => decorate(item) ); // myNode.items = node.items.map(decorate); } return myNode; }; // two-way property value replication let replicate = (property, transformIn, transformOut) => { // echo suppression let replicating; let single = callback => { if (!replicating) { replicating = true; try { callback(); } finally { replicating = false; } } }; // in widget.on(property + 'Changed', value => single(() => tree[property] = transformIn(value))); // out tree.on(property + 'Changed', value => single(() => widget[property] = transformOut(value))); }; // replicate "data" (inward only) let update = data => { data = data || widget.data; id = 0; nodes = {}; tree.data = decorate(data); // Nach diesem Aufruf ist der Baum neu durchnummeriert (id) // und in einem Array (nodes) enthalten. console.log('nodes:' + JSON.stringify(nodes,null, 4) ); }; widget.on('dataChanged', update); widget.on('refresh', update); // replicate "selection" (inward and outward) let transformIn = selection => selection.map(node => { console.log('transformIn: ' + JSON.stringify(selection, null, 4)); for (let id in nodes) { if (nodes[id] === node) { return id; } } }) // ignore missing nodes .filter(id => id !== undefined); let transformOut = selection => { console.log('transformOut: ' + JSON.stringify(selection, null, 4)); return selection.map(id => nodes[id]); }; replicate('selection', transformIn, transformOut); // forward events while translating IDs to nodes (outward only) [ 'nodeClicked', 'nodeDblClicked', 'nodeRightClicked', 'nodeExpanded', 'nodeCollapsed' ] //.forEach(event => tree.on(event, id => widget.fire(event, nodes[id]))); .forEach(event => tree.on(event, id => { widget.fire(event, nodes[id]); console.log(event + ': "' + id + '"'); })); widget;
Zur Isolation der Events des inneren basicTree-Widgets und des äußeren dataTree-Widgets wurde hier ein zusätzlicher agorum.single-Container verwendet, der den eigentlichen Baum enthält.
Der Rest des Codes ist prinzipiell die Übersetzung der Parameter und Events von ID-basiert (basicTree) zu Daten-basiert (dataTree) zusammen mit der Verwaltung der Baumknoten.
Die Funktion replicate ist ein Beispiel dafür, wie eventuelle Echos bei der Replikation von Parametern verhindert werden können. Wird etwa der innere Wert von selection geändert (ein Array von IDs) und dadurch der äußere Wert von selection neu berechnet (ein Array von Knoten-Objekten), dann wird dadurch nicht auch eine unnötige Neuberechnung des inneren Werts angestoßen.
aguila arbeitet – wie praktisch alle UI-Frameworks – zur Garantie der Ereignisreihenfolge Single-Threaded: Pro aktivem Client (Browserfenster) wird maximal ein zugehöriger (serverseitiger) UI-Thread erzeugt. Um effektiv mit aguila arbeiten zu können, ist es hilfreich, die grundsätzliche Arbeitsweise eines UI-Threads zu verstehen.
Der UI-Thread arbeitet in Zyklen:
Benötigt nun die Verarbeitung eines einzelnen Events (Tick) besonders lange, müssen alle darauffolgenden Events warten und auch die Aktualisierung der Oberfläche auf dem Client ist blockiert, da Signale erst am Ende zugestellt werden können. Daher gibt es die Möglichkeit, für zeitintensive Aktionen einen separaten Thread zu starten (fork) und das Ergebnis später wieder im UI-Thread zu verwenden. Der Zugriff auf vorhandene Widgets und die Erzeugung neuer Widgets darf dabei ausschließlich innerhalb des UI-Threads stattfinden.
Das untere Schaubild basiert auf folgendem Code. Zum Ausführen muss das Widget aguila-flow.js registriert und der Name des Widgets in die Datei aguila-flow-start.js im aon Teil (mein_konfigurationsprojekt.aguilaFlow) ausgetauscht werden.
Die im Code vorhandenen Kommentare sind im unteren Schaubild zu finden und zeigen den kompletten Ablauf der Widget-Erzeugung, der Events, Parameter und von setImmediate.
aguila-flow-start.js
// Vor dem Starten mit F12 die Browser-Console öffnen und leeren
// Widget Erzeugung startet
let aguila = require('common/aguila');
console.log('01: aguila-flow-start start');
// Es wird ein Widget erzeugt und ein weiteres Widget eingebunden: aguilaFlow
let widget = aguila.create({
type: 'agorum.vbox',
width: 200,
height: 100,
items: [
{
type: 'mein_konfigurationsprojekt.aguilaFlow',
// propertyEins wird durch aon gesetzt auf aguilaFlow (Reihenfolge nicht definiert)
propertyEins: 'Wert von aussen 1',
// propertyZwei wird durch aon gesetzt auf aguilaFlow (Reihenfolge nicht definiert)
propertyZwei: 'Wert von aussen 2',
}
]
});
// setImmediate(4) wird definiert
setImmediate(() => {
console.log('16: setImmediate aguila-flow-start');
});
// Widget Erzeugung beendet
console.log('09: aguila-flow-start ende');
widget;
aguila-flow.js
// Widget Erzeugung startet let aguila = require('common/aguila'); console.log('02: aguila-flow start'); let widget = aguila.create({ type: 'agorum.vbox', properties: [ 'propertyEins', 'propertyZwei' ], items: [ { type: 'agorum.textDisplay', name: 'display' } ] }); let display = widget.down('display'); widget.on('propertyEinsChanged', value => { // Event propertyEinsChanged wird aufgerufen console.log('03/07: propertyEinsChanged: ' + value); // propertyZwei wird gesetzt widget.propertyZwei = 'Wert 2'; display.value = value; // setImmediate(1) wird definiert setImmediate(() => { console.log('11/15: setImmediate(1) aus propertyEinsChanged mit Wert: ' + value); }); }); widget.on('propertyZweiChanged', value => { // Event propertyZweiChanged wird aufgerufen console.log('04/06/08: propertyZweiChanged: ' + value); display.value = value; // setImmediate(2) wird definiert setImmediate(() => { console.log('10/13/14: setImmediate(2) aus propertyZweiChanged mit Wert: ' + value); }); }); // propertyEins wird gesetzt widget.propertyEins = 'Wert 1'; display.value = 'Wert start'; // setImmediate(3) wird definiert setImmediate(() => { console.log('12: setImmediate(3) aguila-flow'); }); console.log('05: aguila-flow ende'); // Widget-Erzeugung Ende widget;
Ablaufdiagramm zu den obigen Programmen
In fast allen Fällen wird aguila.fork() dafür verwendet, um I/O-Operationen (Datenbank, Index, externe Datenquellen) in einem separaten Thread auszuführen. Grundsätzlich gilt: Im UI-Thread sollte ausschließlich mit In-Memory-Ressourcen gearbeitet werden.
let aguila = require('common/aguila'); let objects = require('common/objects'); let widget = aguila.create({ type: 'agorum.vbox', items: [ { type: 'agorum.textInput', name: 'query' }, { type: 'agorum.basicGrid', name: 'results', flexible: true, border: true, columns: [ { text: 'ID', name: 'id', width: 75 }, { text: 'Name', name: 'name' } ] } ] }); let query = widget.down('query'); let results = widget.down('results'); // auf die Eingabetaste im Suchfeld reagieren query.on('enter', () => { // [A] // aktuellen Wert des Widgets zuvor im UI-Thread abrufen let q = query.value + ' inpath:${ID:/agorum/roi/customers/mein_konfigurationsprojekt} '; aguila .fork(() => { // [B] // Suche in separatem Thread durchführen und Ergebnis zurückliefern return objects.query(q).sort('name').limit(100).search('id', 'name'); }) .then(result => { // [C] // Suchergebnis im UI-Thread verwenden, um Fenstertitel und Inhalt der Tabelle zu aktualisieren widget.form.title = q + ': ' + result.total + ' Treffer'; // Anmerkung: Da die Feldnamen mit den Spaltennamen übereinstimmen, kann das Suchergebnis ohne Transformation verwendet werden results.rows = result.rows; }) .finally(() => { // [D] // Eingabefeld in jedem Fall wieder freigeben, nachdem die Suche beendet ist, auch nach einem Fehler query.locked = null; }); // [E] // Eingabefeld sperren (ohne Text) query.locked = ''; }); // UI-Thread A > E ..... C > D // \ / // fork-Thread B ------- // Widget in neuem Fenster öffnen widget.popup({ title: 'I/O mit aguila', width: 400, height: 600 }); // Tastaturfokus initial auf das Eingabefeld setzen query.focus();
Zur einfacheren Referenz wurden die relevanten Code-Regionen im Event-Handler für enter mit den Buchstaben A-E bezeichnet.
Für die Reihenfolge, in der diese Regionen in welchem Thread ausgeführt werden, gilt:
Daraus ergibt sich für die Regionen folgende Aufteilung:
Oft genügen A, B und C oder sogar nur B und C.
Wie schon erwähnt, gelten einige Einschränkungen für Code, der nicht auf dem UI-Thread ausgeführt wird:
Die Syntax für .then() und .finally() ist übrigens nicht aguila-spezifisch, sondern rührt daher, dass aguila.fork() ein Promise-Objekt zurückliefert (siehe https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Dadurch ist es auch einfach möglich, mehrere asynchron ausgeführte Prozesse miteinander zu verketten/verknüpfen, etwa das Warten auf die Ergebnisse mehrerer parallel per aguila.fork() gestarteter Prozesse per Promise.all() (siehe https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all).
let aguila = require('common/aguila'); // ACHTUNG: // Wenn das Script gestartet wird, sollte mit F12 die Console eingeblendet werden. // Dort sieht man dann an der Ausgabe, wie das Sript abläuft. // console.log('vor fork'); aguila.fork(() => { console.log('fork 1'); java.lang.Thread.sleep(1000); console.log('fork 1 end'); }) .then(() => { console.log('then 1'); // Wenn der return ein fork ist, dann bezieht sich // das nächste then auf diesen fork. // Wenn in diesem fork auch ein Wert returnt wird, // wird dieser in das nächste then übergeben return aguila.fork(() => { console.log('fork 2'); java.lang.Thread.sleep(1000); console.log('fork 2 end'); // dieser Fehler wird durchgeschleust in den catch(err) //let i; i.hallo(); return 'zurück'; }) .finally(() => { console.log('then 1 finally'); }); }) .then(p => { console.log('then 2: ' + p); // p = return fork 2 }) .catch(err => { // egal wo ein error passiert, // das kommt dann hier an console.log('error: ' + err); }) .finally(() => { // das finally wird erst aufgerufen wenn auch alle unter-forks erledigt sind // also tatsächlich erst am ende, wenn alle forks abgeschlossen sind console.log('finally'); }); console.log('nach fork');
Wenn dieses Beispiel gestartet wird und die Konsole mit F12 aktiv ist, erhalten Sie folgende Ausgabe:
vor fork nach fork fork 1 fork 1 end then 1 fork 2 fork 2 end then 1 finally then 2: zurück finally
Anhand dieser Ausgabe sehen Sie, wie der fork genau abläuft und wann was gemacht wird.
In manchen Situationen ist es wünschenswert, dass von mehreren Stellen im Code aus auf denselben asynchronen Prozess zugegriffen werden kann. Die Funktion synchronizer(), die auf jedem Widget aufgerufen werden kann, liefert zu diesem Zweck wiederverwendbares Promise-Objekt zurück. Synchronizer-Instanzen sind jeweils an das initiale Widget gebunden: Wird dieses zerstört, dann werden keine weiteren Callbacks darauf mehr ausgeführt, auch wenn noch asynchrone Operationen laufen. Im Folgenden zwei Beispiele für typische Einsatzszenarien:
Warten auf einen asynchronen Prozess, der an einer anderen Stelle ausgelöst wurde:
let initializer = widget.synchronizer(); initializer.fork(() => { // längere Initialisierungs-Aufgabe }); function a() { // ... initializer.then(() => { // ausführen, wenn Initialisierung erfolgreich abgeschlossen ist }); // ... } function b() { // ... initializer.finally(() => { // ausführen, wenn Initialisierung abgeschlossen ist }); // ... }
Sicherstellen, dass nur das Ergebnis des jeweils zuletzt gestarteten asynchronen Prozesses beachtet wird:
let loader = widget.synchronizer(); widget.on('idChanged', id => { loader .fork(() => { // Benötigte Daten vom Objekt laden und zurückgeben }) .then(result => { // Geladene Daten verwenden }); });
Die per then() übergebene Funktion wird hier nur aufgerufen, wenn inzwischen kein neuer fork() auf dem Objekt loader gestartet wurde und das Widget noch existiert. Somit wird sichergestellt, dass nur die jeweils letzte Änderung der Property-id beachtet wird, auch wenn sich der Wert schneller ändert als der Ladevorgang dauert. Zudem wird die Bearbeitung automatisch abgebrochen, wenn das Widget geschlossen wird.
Unter Umständen ist es erforderlich, gezielt Code auf dem UI-Thread auszuführen, diesen also von außen zu betreten. Typische Anwendungsbeispiele hierfür sind:
then()
-Teil übergibt.Hier ein Beispiel für die Einbindung im agorum core smart assistant:
Es soll ein Widget als Pop-up-Fenster ins Kontextmenü eingebunden werden, dazu werden zwei Komponenten benötigt:
Das Widget wird über den agorum core template manager registriert, in diesem Beispiel testvt.test3. Um aguila-Widgets im agorum core smart assistant zu verwenden, muss vorher per aguila.enter() auf den aguila-Kontext-Thread gewechselt werden. Dann erst kann das Widget mit widget.popup in einem Pop-up-Fenster angezeigt werden.
/* global sc, sca, folder, objects, data */ let beans = require('common/beans'); let aguila = require('common/aguila'); aguila.enter(() => { let widget = aguila.create({ type: 'testvt.test3' }); widget.popup({ title: 'Test Tree', width: 600, height: 400 }); });
Nachfolgend ein Beispiel für die Anzeige von Zwischenergebnissen:
let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.vbox', background: 'light', items: [ { type: 'agorum.textInput', name: 'text', width: 200 } ] }); let text = widget.down('text'); // Funktion definieren, die im UI-Thread die "locked"-Property des Widgets aktualisiert let lock = str => aguila.enter(() => widget.locked = str); aguila .fork(() => { // Eingabe sperren lock('Bitte warten...'); // längeren Prozess simulieren for (let i = 0; i < 100; ++i) { java.lang.Thread.sleep(100); // periodisch den Fortschritt anzeigen lock('Bitte warten... (' + i.toFixed(0) + '%)'); } // Eingabe freigeben lock(); // "Ergebnis" return 100; }) .then(result => { // Ergebnis anzeigen text.value = 'Fertig, Ergebnis: ' + result; }); widget;
Manchmal kann es nötig sein, eine Funktion nicht sofort auszuführen, sondern erst später oder sogar periodisch.
Aufgrund der asynchronen Natur dieser Operationen kann es sein, dass aufgrund von Änderungen (Fenster geschlossen und so weiter) die Ergebnisse des Funktionsaufrufs nicht mehr relevant sind und die Funktion nicht aufgerufen werden müsste. Insbesondere bei periodisch auszuführenden Operationen ist die Wahrscheinlichkeit hoch, dass diese wieder gestoppt werden müssen. Um diese Verwaltung zu vereinfachen, werden die betroffenen Operationen (mit einer Ausnahme) stets an die Lebensdauer eines existierenden Widgets gebunden. Wird dieses Widget zerstört, wird gleichzeitig auch ein eventuell noch laufender Timer deaktiviert.
Die hier beschriebenen Operationen führen die jeweils übergebene Funktion immer im UI-Thread aus. Zeitaufwändige Aufgaben müssen also trotzdem über aguila.fork() ausgelagert werden.
Durch setTimeout() kann eine übergebene Funktion später (definiert in ms) einmalig ausgeführt werden.
Ein typisches Anwendungsbeispiel hierfür ist automatisches Zwischenspeichern von Eingaben nach einer definierten Zeit nach der letzten Eingabe des Benutzers:
let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.border', width: 800, height: 600, docked: { center: { type: 'agorum.textArea', name: 'input' }, bottom: { type: 'agorum.toolbar', items: [ { type: 'agorum.spacer' }, { name: 'status', type: 'agorum.textDisplay' } ] } } }); let input = widget.down('input'); let status = widget.down('status'); // Erzeugt aus "callback" eine gepufferte Funktion, die erst nach der angegebenen Verzögerung gestartet wird. // Wird die gepufferte Funktion dazwischen erneut aufgerufen, startet die Verzögerung erneut. let buffered = (widget, callback, timeout) => { let waiting; return function() { /* jshint -W120 */ let me = waiting = {}; /* jshint +W120 */ widget.setTimeout(() => { if (waiting === me) { return callback.apply(this, arguments); } }, timeout); }; }; input.on('valueChanged', buffered(input, value => status.value = 'Gespeichert: ' + value.length + ' Zeichen', 2000)); widget;
Die Syntax von setInterval() ist dieselbe wie die von setTimeout(), die übergebene Funktion wird aber stattdessen periodisch ausgeführt statt nur einmal.
Eine typische Anwendung von setInterval() ist die regelmäßige Prüfung (polling) eines Zustands, um ggf. bei Änderungen die Bedienoberfläche zu aktualisieren:
let aguila = require('common/aguila'); let objects = require('common/objects'); let widget = aguila.create({ type: 'agorum.border', width: 800, height: 600, docked: { center: { type: 'agorum.textArea', name: 'input', monospace: true }, bottom: { type: 'agorum.toolbar', items: [ { type: 'agorum.spacer' }, { name: 'status', type: 'agorum.textDisplay' } ] } } }); let input = widget.down('input'); let status = widget.down('status'); let names; let refresh = () => aguila // alle Objekte im Home-Verzeichnis auflisten und deren Namen mit \n getrennt zurückgeben .fork(() => objects.find('home:MyFiles').items().map(item => item.name).join('\n')) .then(newNames => { if (newNames !== names) { // bei Änderung: Eingabefeld aktualisieren names = newNames; input.value = 'Objekte im Home-Verzeichnis:\n\n' + names; status.value = 'Änderung'; } else { status.value = 'Keine Änderung'; } }); // direkt zu Anfang aktualisieren refresh(); // periodisch aktualisieren (bis widget geschlossen wird) widget.setInterval(refresh, 1000); widget;
Im Gegensatz zu setTimeout() und setInterval() wird die hier übergebene Funktion so bald wie möglich, aber dennoch später ausgeführt. Genauer gesagt ist das direkt nach Ende des aktuellen Ticks (und nach Ende eventuell zuvor eingereihter setImmediate()-Ausführungen), aber garantiert vor der Übertragung des neuen Zustands an den Client.
Die häufigste Verwendung findet setImmediate() in den folgenden beschriebenen Szenarien:
Während der Ausführung eines Widget-Konstruktors (siehe auch Beispiel für ein eigenes Widget) wurde die komplette Struktur noch nicht erzeugt, deren Teil dieses Widget ist. Auch eventuelle Event-Handler von außen sind noch nicht gebunden. Manchmal ist es aber nötig, nach Abschluss des gesamten Konstruktionsprozesses noch weitere Operationen durchzuführen, die etwa auf Widgets außerhalb zugreifen.
Ein typisches Beispiel hierfür ist das Setzen des Fenstertitels durch das Widget, das darin angezeigt wird:
let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.vbox', width: 300, height: 200 }); // nach Konstruktion des vollständigen Widget-Baums inklusive umgebender Form deren Titel anpassen setImmediate(() => widget.form.title = 'Geänderter Titel'); widget;
Hinweis: Da der Client erst aktualisiert wird, nachdem alle setImmediate()-Blöcke ausgeführt wurden, gibt es keine unschöne, für den Benutzer sichtbare Änderung des Fenstertitels – erst der finale Zustand wird übertragen und angezeigt.
Bei Widgets, die mehrere Parameter nach außen veröffentlichen, kann es hilfreich sein, Änderungen nicht einzeln, sondern gesammelt zu betrachten, wenn der Aufrufer zwei oder mehr Parameter nacheinander ändert. Über setImmediate() kann bei Änderungen abgewartet werden, ob im selben Tick noch weitere Änderungen durchgeführt und diese dann danach einmalig abgearbeitet werden.
Nachfolgend ein Beispiel für ein Widget mit drei Parametern, nach deren Änderung der Inhalt eines Textfelds neu berechnet werden soll:
let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.vbox', properties: [ 'a', 'b', 'c' ], background: 'light', items: [ { type: 'agorum.textInput', name: 'text', width: 200 } ] }); let text = widget.down('text'); // Erzeugt aus "callback" eine verzögerte Funktion, die im selben Tick nur einmal gestartet wird. // Erneute Aufrufe der verzögerten Funktion werden ignoriert. let later = callback => { let wasCalled; return () => { if (!wasCalled) { wasCalled = true; setImmediate(() => { wasCalled = false; callback(); }); } }; }; // Neuberechnung von text.value aus den Properies a, b und c let recalc = () => { console.log('recalc() called', widget.a, widget.b, widget.c); text.value = widget.a + widget.b + widget.c; }; // Neuberechnung nur einmal pro Tick ausführen statt jedes Mal // Hier demo - anklemmen //recalc = later(recalc); widget.on('aChanged', recalc); widget.on('bChanged', recalc); widget.on('cChanged', recalc); widget.a = 100; widget.b = 200; widget.c = 300; widget.setTimeout(() => { widget.a = 600; widget.c = 900; }, 1000); widget.setTimeout(() => { widget.b = 1600; }, 500); widget;
Für die Kommunikation von Widgets mit anderen Widgets, die außerhalb ihres Kontexts liegen (etwa in einer anderen Registerkarte, einem anderen Browser oder sogar bei einem anderen Benutzer) oder mit Hintergrundprozessen wie Workern wird von agorum core ein einfaches Mittel bereitgestellt, das asynchronen Austausch von Nachrichten auf frei definierbaren Kanälen zulässt.
Die Funktion aguila.pub() (die auch außerhalb von aguila-Kontexten, etwa in einem Worker, verwendet werden kann) sendet die angegebene Nachricht auf dem angegebenen Kanal, sobald die aktuelle Transaktion erfolgreich abgeschlossen wurde. Damit ist sichergestellt, dass Nachrichten, die etwa auf Änderungen hinweisen, erst zugestellt werden, nachdem diese Änderungen sichtbar werden.
Der Kanal wird durch einen beliebigen String identifiziert, muss nicht gesondert angelegt oder wieder gelöscht werden und gilt immer auf dem gesamten Server. Soll ein Kanal stattdessen spezifisch für einen Benutzer sein, so kann etwa dessen UUID an den Namen angehängt werden.
Die Nachricht kann prinzipiell ein beliebiges Objekt sein – es ist jedoch empfohlen, hier ausschließlich statische Daten zu übertragen (etwa die UUID eines Ordners statt das Ordnerobjekt selbst), um Sender und Empfänger nicht unnötig zu koppeln. Ein Ordnerobjekt enthält etwa einen Verweis auf die Sitzung, aus der dieses Objekt stammt und die meistens nicht mit übertragen werden sollte.
Die Registrierung eines Empfängers auf einem Kanal läuft analog zur Registrierung eines Event-Handlers ab: Es wird der Name des Kanals sowie eine Funktion erwartet, die für jede empfangene Nachricht auf diesem Kanal aufgerufen werden soll.
Der Empfänger ist stets an ein Widget gebunden, aus demselben Grund wie setInterval(), um Leaks zu verhindern.
Beispiel
Als Beispiel für den pub/sub-Mechanismus soll hier ein einfacher Chat-Client vorgestellt werden:
module.exports = { type: 'agorum.border', width: 600, height: 800, docked: { center: { type: 'agorum.textArea', name: 'chat', readOnly: true, monospace: true, value: '' }, east: { width: 200, type: 'agorum.basicGrid', name: 'users', border: true, columns: [ { name: 'name', text: 'Name' }, { name: 'typing', text: 'Typing', width: 50 } ] }, south: { type: 'agorum.textInput', name: 'input' } } };
/* global sc */ let aguila = require('common/aguila'); let widget = aguila.create(require('./simple-chat-aon')); let chat = widget.down('chat'); let users = widget.down('users'); let input = widget.down('input'); let channel = 'simple-chat-broadcast'; let maxChatLength = 5000; let id = sc.loginUserId + '_' + Date.now(); let name = sc.loginUser.fullName; // message handling let allUsers = {}; let handlers = { // do nothing beacon: () => {}, // remove user from our list leave: user => delete allUsers[user.id], // update typing notification typing: (user, typing) => user.typing = typing ? '...' : '', // append text to chat text: (user, text) => chat.value = (chat.value + user.name + ': ' + text + '\n').slice(-maxChatLength) }; widget.sub(channel, msg => { // ensure user is known let user = allUsers[msg.id] = allUsers[msg.id] || { id: msg.id, name: msg.name }; // handle incoming message handlers[msg.type](user, msg.data); // update user list users.rows = Object.keys(allUsers).map(id => allUsers[id]); }); let send = (type, data) => aguila.pub(channel, { type: type, id: id, name: name, data: data }); // beacon widget.setInterval(() => send('beacon'), 1000); send('beacon'); // leave widget.on('destroyed', () => send('leave')); // typing notification input.on('valueChanged', value => send('typing', !!value)); // sending input.on('enter', () => { send('text', input.value); input.value = ''; }); widget;
Mit sehr einfachen Mitteln werden hier eine Liste der aktiven Benutzer, deren Status (tippt / tippt nicht) und gesendete Nachrichten ohne eine zentrale Kontrollinstanz (peer-to-peer) ausgetauscht.
Dieses Beispiel ließe sich leicht etwa um weitere Räume (Channel-Name) oder die Wahl des eigenen Anzeigenamens erweitern.
Wenn Änderungen sofort angewendet und der Index aktualisiert werden muss, können Sie dazu die Funktion aguila.flushIndex() verwenden. aguila.flushIndex() löst bei Bedarf einen commit ("flush") aus und wartet, um die UI nicht zu blockieren.
Die Funktion aguila.flushIndex() dient dazu, Änderungen, die am Index vorgenommen wurden, permanent zu machen und sicherzustellen, dass diese Änderungen im Index sichtbar sind. Dies kann nützlich sein, wenn diese Änderungen sofort im Suchindex verfügbar sein sollen, etwa um die Anzeige einer Liste zu aktualisieren.
Verwendung
let aguila = require('common/aguila'); // changes are made aguila.flushIndex().then(() => { // change is visible in the index });
Die Funktion flushIndex ist asynchron und gibt ein Promise zurück. Nachdem das Promise aufgelöst wurde, wird die im then-Block angegebene Callback-Funktion ausgeführt.
Sollten die Gestaltungsmöglichkeiten nicht ausreichen, die aguila von Haus aus mitbringt, können eigene CSS-Klassen definiert und für ausgewählte Widgets verwendet werden.
MAIN_MODULE_MANAGEMENT/home/control/Styles/[ mein_konfigurationsprojekt ]/mein_konfigurationsprojekt.css .test-highlight-red .x-form-display-field { color: red; font-weight: bold; }
In der MetaDb wird die neue CSS-Klasse test-highlight-red definiert, die den Text rot und fett gestaltet. CSS können Sie über den agorum core template manager registrieren.
let aguila = require('common/aguila'); let widget = aguila.create({ type: 'agorum.vbox', items: [ { type: 'agorum.textDisplay', value: 'Normaler Text' }, { type: 'agorum.textDisplay', cls: 'test-highlight-red', value: 'Hervorgehobener Text' } ] }); widget;
Mit der Kennzeichnung cls wird nun eine der beiden Textanzeigen mit der neuen Klasse versehen und dadurch farblich hervorgehoben. Diese Klasse kann auch nachträglich geändert werden, wenn das Widget schon existiert.
Bei der Benutzung sind zwei Dinge zu beachten: