Lade...
 

Grundlagen InstantView®

Grundlagen InstantView®

In diesem Tutorial wird ein einfaches Beispielmodul implementiert und dann schrittweise erweitert. Es soll eine Personenverwaltung entworfen werden, die folgende Aufgaben erfüllt:

  • Anlegen von Personendaten
  • Anzeige und Bearbeitung aller gespeicherten Personen
  • Errechnen des Alters einer Person
  • Definition von Verwandschaftsbeziehungen
  • Anzeige des Stammbaumes

Die Anwendung ist bewusst sehr einfach gehalten, um die einfache Umsetzung der grundlegenden Konzepte zu zeigen. Natürlich ist sie in keiner Weise mit der Komplexität der Module des AppsWarehouse® zu vergleichen.

Die Lösungen jedes Abschnittes finden Sie im Menü unter Tutorials → Lösungen → Grundlagen InstantView.

Oberfläche mit Datenbindung

Ziel

In diesem Abschnitt werden wir das Modul für das Tutorial und ein Bearbeitungsfenster für Personendaten erstellen. In diesem Fenster können Vor- und Nachname, Geburtstag und das Geschlecht einer Person bearbeitet werden. Außerdem wird das Alter der Person angezeigt.

Erstes Fenster

Die Anwendung wird in einem neuen Modul entwickelt. Erzeugen Sie dazu die Datei tutorial1.app mit dem folgenden Inhalt:

Module(tutorial1)
[
    EXEC_TUTORIAL1: OpenWindow(TutorialPersonEdit, 1)
]

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
{
}

Dieses Modul tut bisher nichts weiter, als ein leeres Fenster zu öffnen. Sehen Sie sich dazu auch die Referenz für Fenster und den Befehl OpenWindow an.

Der Titel des Fensters ist multilingual, genauso wie alle konstanten Zeichenketten multilingual sein können. Beim Start des CyberEnterprise® wird ein Index für die aktuelle Sprache bestimmt (0-te, 1-te, …, n-te Sprache). Es ist jederzeit möglich, die Sprache zu wechseln. Dabei stellen alle Oberflächenelemente ihre Texte automatisch in der neu ausgewählten Sprache dar. Die Übersetzungen der Texte können in einer Datei festgelegt werden. Dann gelten die Texte im Quelltext nur als Schlüssel. Andernfalls werden die Texte aus dem Quelltext direkt verwendet.

Das Fenster soll als Reaktion auf die Message EXEC_TUTORIAL1 geöffnet werden. Diese Message muss natürlich noch gesendet werden können. Dafür muss die Message als erstes deklariert werden. Außerdem muss angegeben werden, dass bei dieser Message das Modul tutorial1 geladen werden soll. Das geschieht in der Datei myTutorialMenu.ext über folgende Zeilen:

Msg(EXEC_TUTORIAL1)
Extern(tutorial1, File(tutorial1.app), triggeredBy(EXEC_TUTORIAL1))

Um die Message senden und somit das Fenster öffnen zu können, soll in das Menü ein Menüpunkt eingefügt werden, der die Message sendet. Dafür werden die folgenden Zeilen in die Datei myTutorialMenu.mod eingefügt:

Item(Tutorial1Item, T("Tutorial 1", "Tutorial 1"))
[ SELECT: SendMsg(EXEC_TUTORIAL1) ]

Das Fenster lässt sich jetzt über das Menü öffnen.

Datenbindung

Das Fenster soll eine Bearbeitung von Personen, also Objekten der Klasse CX_PERSON, ermöglichen. Zur Bearbeitung der Daten bietet InstantView® über 30 Oberflächenelemente an. Zudem bietet InstantView® eine einfache Kopplung der Oberfläche an das Modell, wie es z.B. auch die Bibliotheken Angular, React oder Vue.js anbieten. Um ein Oberflächenelement an ein Datenelement zu koppeln, muss das Oberflächenelement statt eines beliebigen Namens lediglich den Zugriffsausdruck auf das Datenelement erhalten.

Wir wollen nun das Fenster um Felder zur Bearbeitung des Vor- und Nachnamens und des Geburtstages einer Person erweitern:

Module(tutorial1Step1)
[
    EXEC_TUTORIAL1_STEP1: OpenWindow(TutorialPersonEdit, 1)
]

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
{
  Group(PersonGrp, 5, 2, 860, 66, T("Person", "Person"))
  {
    Prompt(NamePmt, 15, 11, T("Name:", "Name:"))
    String(CX_PERSON::name, 250, 11, 595)

    Prompt(FirstNamePmt, 15, 22, T("Vorname:", "First Name:"))
    String(CX_PERSON::firstName, 250, 22, 595)

    Prompt(DateOfBirthPmt, 15, 33, T("Geburtstag:", "Birthday:"))
    Date(CX_PERSON::dateOfBirth, 250, 33, 595)
  }

  Group(ActionGrp, 5, 72, 860, 20, T("Aktion", "Action"))
  {
    Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
    Button(SaveBtn, 645, 11, 200, 7, T("&Speichern", "&Save"))
  }
}

Hierarchien innerhalb der Oberfläche werden durch die geschweiften Klammern { und } ausgedrückt. Alle Oberflächenelemente in einem solchen Block sind dem Element zugeordnet, zu dem der Block gehört. In diesem Beispiel enthält das Fenster zwei Oberflächenelemente vom Typ Group. Diese wiederum enthalten die eigentlichen Bearbeitungselemente. Eine Group gruppiert die ihr zugeordneten Elemente visuell. Es gibt auch das Element Composite, das die Elemente ohne eine visuelle Darstellung gruppiert. Für die Ausrichtung bei der Anzeige im Web ist eine Group oder ein Composite notwendig. Daher sollte niemals ein anderes Oberflächenelement als eine Group oder ein Composite als direktes Kindelement eines Fensters verwendet werden.

Die Oberflächenelemente können durch Flags in ihrem Verhalten angepasst werden. Zum Beispiel kann der Geburtstag folgendermaßen mit Wochentag und Monatsnamen angezeigt werden:

Prompt(DateOfBirthPmt, 15, 33, T("Geburtstag:", "Birthday:"))
Date(CX_PERSON::dateOfBirth, DF_DAY_OF_WEEK, DF_ALPHA_MONTH, 250, 33, 595)

Aktionslisten

Die Zuordnung von Datenfeldern zu Oberflächenelementen war nur ein vorbereitender Schritt. Jetzt wollen wir wirkliche Aktionen mit der Oberfläche verbinden. Zuerst müssen wir angeben, wann etwas Bestimmtes geschehen soll, z.B. nach einem Mausklick auf einem Button oder als Reaktion auf das Drücken der Eingabetaste in einem Eingabefeld. Solche vom Benutzer ausgelösten Ereignisse nennen wir System Events. In unserem Beispiel sollen die eingegebenen Daten einer Person in einem transienten, also temporären, Objekt gespeichert werden, wenn der Speichern-Button gedrückt wird. Dafür ändern wir die Definition des Speichern-Buttons folgendermaßen ab:

Button(SaveBtn, 645, 11, 200, 7, T("&Speichern", "&Save"))
[ SELECT: CreateTransObject(CX_PERSON) DrainWindow ]

Der Button hat eine Aktionsliste bekommen. Sämtlicher InstantView®-Code, der die dynamischen Abläufe beschreibt, befindet sich innerhalb von Aktionslisten. Dadurch ist er von der statischen Definition der Fensteroberfläche außerhalb der eckigen Klammern getrennt. Die Anweisung CreateTransObject erzeugt ein transientes Objekt und legt es auf den Stack. Mit der Anweisung DrainWindow werden die Daten aller Oberflächenelemente, die sich auf die Klasse CX_PERSON beziehen, in das erzeugte Objekt geschrieben. In unserem Fall sind das die Datenfelder name, firstName und dateOfBirth. Das Objekt, in das DrainWindow die Daten schreibt, wird über den Stack übergeben. CreateTransObject hinterlegt das erzeugte Objekt oben auf dem Stack, daher werden die Daten in diesem Objekt gespeichert. Die entgegensetzte Anweisung zu DrainWindow ist FillWindow, die alle Oberflächenelemente mit den Daten aus einem Objekt füllt.

Beim Drücken des Zurück-Buttons soll das Fenster geschlossen werden. Das geschieht über die Anweisung CloseWindow. Dafür ändern wir den Zurück-Button folgendermaßen ab:

Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
[ SELECT: CloseWindow ]

Ein Objekt besitzt neben Datenfeldern (Attributen) üblicherweise auch Funktionen (Methoden). Die Klasse CX_PERSON besitzt, neben vielen anderen, die Funktion Age. Diese berechnet aus dem Geburtsdatum und dem aktuellen Tagesdatum das Alter einer Person. Wir wollen in unserer Anwendung das Alter der gerade dargestellten Person anzeigen. Dafür benötigen wir ein reines Anzeigefeld, denn das Alter kann nicht über die Oberfläche eingegeben oder verändert werden. Eine Funktion kann, wie auch ein Datenfeld, direkt an ein Oberflächenelement gebunden werden. Ein Anzeigefeld des Alters kann folgendermaßen dem Fenster hinzugefügt werden:

Prompt(AgePmt, 15, 44, T("Alter:", "Age:"))
String(CX_PERSON::Age(), VIEW_ONLY, NO_DRAIN, 250, 44, 595)

Das Flag NO_DRAIN ist wichtig, denn es sorgt dafür, dass DrainWindow dieses Datenfeld, trotz der Bindung an CX_PERSON, ignoriert. Im Gegensatz zu normal benannten oder per Datenfeld referenzierten Oberflächenelementen, kann dieses Oberflächenelement, mit einem Funktionsausdruck als Namen, nicht referenziert werden. Dafür kann dem Oberflächenelement ein Alias vergeben werden. Ändern Sie die Definition folgendermaßen ab, um das Oberflächenelement über den Namen age ansprechen zu können:

String(CX_PERSON::Age()~age, VIEW_ONLY, NO_DRAIN, 250, 44, 595)

Bisher wird das Alter noch nicht angezeigt. Das kommt daher, dass es nur bei einem FillWindow automatisch aktualisiert wird. Daher ändern wir jetzt den Speichern-Button so ab, dass bei einem Klick ein transientes Objekt erzeugt, mit den Daten aus dem Fenster gefüllt und gleich danach das Fenster wieder mit den Daten aus dem Objekt gefüllt wird:

Button(SaveBtn, 645, 11, 200, 7, T("&Speichern", "&Save"))
[ SELECT: CreateTransObject(CX_PERSON) Dup DrainWindow FillWindow ]

Die Anweisung Dup dupliziert das oberste Element auf dem Stack und bewirkt somit, dass das erzeugte transiente Objekt zweimal auf dem Stack liegt. Das ist daher wichtig, dass DrainWindow das oberste Objekt auf dem Stack nutzt und entfernt und dann das Objekt aber nochmal für FillWindow auf dem Stack liegen muss.

Variablen

Im vorherigen Beispiel hat sich gezeigt, dass ein Arbeiten ohne Variablen möglich, aber ab einer gewissen Komplexität nicht mehr verständlich ist. Variablen müssen vor ihrer Verwendung über die Anweisungen Var, GlobalVar, StaticVar und LocalVar deklariert werden. Mit dem Befehl -> wird der oberste Wert vom Stack entfernt und einer Variablen zugewiesen. Die Nennung einer Variablen sorgt dafür, dass ihr Wert oben auf den Stack abgelegt wird. Wir wollen das obige Beispiel einmal mit Variablen schreiben. Da wir eine lokale Variable verwenden, muss der Anweisungsblock in geschweiften Klammern stehen:

Button(SaveBtn, 645, 11, 200, 7, T("&Speichern", "&Save"))
[ SELECT: {
             LocalVar(person)
             CreateTransObject(CX_PERSON) -> person
             person DrainWindow
             person FillWindow
           }
]

Der Quelltext ist zwar länger geworden, doch dafür lässt er sich jetzt deutlich leichter lesen. Die Variable person enthält jetzt ein Objekt der Klasse CX_PERSON. Wenn Sie lieber sagen wollen, dass die Variable person auf das Objekt verweist oder eine Referenz auf das Objekt hält, so ist auch das richtig. Allerdings macht es auf der Ebene von InstantView® keinen Unterschied. Immer wenn wir unser Objekt wieder benötigen, können wir es über die Variable person wieder auf den Stack legen. Das CyberEnterprise® kümmert sich dabei darum, ob das Objekt aus der Datenbank geholt werden muss und wann dafür eine Datenbanktransaktion gestartet und beendet werden muss. Wir können uns ganz auf die Anwendung konzentrieren. Variablen sind in InstantView® außerdem nicht typgebunden, im Gegensatz zu vielen anderen Programmiersprachen. Man kann also jeder Variablen alle Werte zuweisen, die in InstantView® vorkommen.

Natürlich ist es nicht Sinn der Sache, ein Objekt erst aus dem Fenster zu lesen und direkt danach wieder ins Fenster zu schreiben, nur um das Alter zu aktualisieren. Dieser Schritt sollte nur demonstrieren, dass das Alter bei FillWindow tatsächlich berechnet wird. Sinnvoller ist es, beim Drücken der Eingabetaste im oder beim Verlassen des Oberflächenelementes mit dem Geburtstag das Alter neu zu berechnen. Dafür setzen wir die Aktionsliste des Speichern-Buttons wieder zurück und ergänzen eine Aktionsliste für das Geburtsdatum:

Prompt(DateOfBirthPmt, 15, 33, T("Geburtstag:", "Birthday:"))
Date(CX_PERSON::dateOfBirth, 250, 33, 595)
[ NON_CURRENT:
  SELECT: {
            LocalVar(tmp)
            CreateTransObject(CX_PERSON) -> tmp
            tmp DrainWindow
            tmp Call(Age) PutValue(, age)
          }
]

Zuerst wird ein transientes Objekt der Klasse CX_PERSON erzeugt und mit den Daten aus dem Fenster gefüllt. Danach wird die Methode Age mittels Call auf dem Objekt aufgerufen und das Ergebnis mit PutValue in das Oberflächenelement geschrieben.

Enumerationen

Wir wollen das Fenster jetzt noch um die Möglichkeit der Definition des Geschlechts einer Person erweitern. Aufzählungen der Form {weiblich, männlich, divers} oder {keine Disposition, Materialentnahme, Bestellanforderung, Fertigungsauftrag} werden im CyberEnterprise® auf ganze Zahlen abgebildet, also aus Gründen der Effektivität in kodierter Form gespeichert. Eine Anwendung sollte den Anwender nicht dazu zwingen, sich mit solchen Kodierungen zu befassen. Deshalb übernimmt das CyberEnterprise® automatisch die Transformation von der kodierten in die lesbare Form und umgekehrt.

Die Klasse CX_PERSON besitzt das Datenfeld sexEnum, das das Geschlecht einer Person definiert. Wir erweitern das Bearbeitungsfenster um die Möglichkeit der Definition des Geschlechts:

Prompt(SexEnumPmt, 15, 55, T("Geschlecht:", "Sex:"))
Enumeration(CX_PERSON::sexEnum, 250, 55, 595, 30)

Das Oberflächenelement Enumeration benutzt eine mehrsprachige Übersetzungstabelle. Das Datenfeld sexEnum ist vom Datentyp ENUMCHAR. Für Datenfelder der Typen ENUMCHAR, ENUMSHORT und ENUMINT muss eine Verbindung mit einer Transformationstabelle existieren. Der Ort dafür ist das System Dictionary, welches sich in der Datei classix.dic befindet. Dort ist sexEnum definiert als:

Member(sexEnum, T("Geschlecht", "Sex", "Sexe"), ENUMCHAR, "classix.num~sexEnum")

Member steht für einen Eintrag der ein Datenfeld beschreibt. Die Transformationstabelle wird durch classix.num~sexEnum näher bestimmt.

Wenn das System diese Tabelle benötigt, wird

  1. mit classix.num~sexEnum in der Datenbank nach einem entsprechenden Objekt der Klasse CX_ENUM_TABLE gesucht,
  2. wenn in der Datenbank nichts gefunden wurde, wird die Datei classix.num im Verzeichnis system und in dieser Datei die Sektion sexEnum gesucht,
  3. wenn auch das fehlschlägt, erscheint eine Fehlermeldung.

In classix.num sieht die Transformationstabelle für sexEnum so aus:

sexEnum
{
  0, T("männlich", "male"), 0xff
  1, T("weiblich", "female"), 0xff
  2, T("divers", "miscellaneous"), 0xff
}

Der Sektionsname ist beliebig. Die Verbindung zum Datenfeld erfolgt allein über den Eintrag im System Dictionary, auch wenn Sektion und Datenfeld sinnvollerweise gleich benannt sind. Den möglichen Werten sind die Bezeichnungen als multilinguale Konstanten gegenübergestellt. Das Flag, hier immer 0xff, kann zur Auswahl einer Teiltabelle dienen.

Persistente Speicherung

Bisher wurde beim Speichern nur ein transientes Objekt erzeugt. Jetzt wollen wir dafür sorgen, dass das Objekt ein persistentes Objekt wird. Es soll also dauerhaft in der Datenbank gespeichert werden. Dazu ändern wir den Speichern-Button folgendermaßen ab:

Button(SaveBtn, 645, 11, 200, 7, T("&Speichern", "&Save"))
[ SELECT: CreatePersObject(CX_PERSON) DrainWindow ]

Durch die Änderung der Anweisung CreateTransObject zu CreatePersObject wird ab sofort ein persistentes Objekt erzeugt. DrainWindow speichert dann direkt und ohne unser Zutun alle Daten in der Datenbank.

Zusammenfassung

In diesem Abschnitt haben Sie gelernt, wie

  • ein neues Fenster erzeugt wird,
  • Oberflächenelemente zur Bearbeitung erzeugt und mit den Daten des CyberEnterprise® verbunden werden,
  • auf Benutzeraktionen reagiert werden kann,
  • Enumerationen definiert und verwendet werden,
  • transiente und persistente Objekte erzeugt werden und
  • Variablen deklariert und verwendet werden.

Auflistung von Objekten

Ziel

Bisher können wir neue Personen anlegen, jedoch noch nicht die in der Datenbank vorhandenen Personen anzeigen. In diesem Schritt soll deshalb ein neues Fenster mit der Auflistung aller Personen erstellt werden. Dabei wollen wir die angezeigten Spalten festlegen und die Objekte automatisch sortieren lassen. Außerdem werden wir die beiden Fenster lose koppeln, so dass die Fenster unabhängig voneinander sind.

Listenansichten

Wir erzeugen in unserem Modul ein neues Fenster, das alle Personen in der Datenbank auflistet:

Window(TutorialPersonList, FLOAT, 0, 0, 870, 110, T("Alle Personen", "All Persons"))
{
  Group(PersonsGrp, 5, 2, 860, 106, T("Personen", "Persons"))
  {
    ObjectListView(ListBox, AUTO_POSITION, 15, 11, 830, 93)
    [ INITIALIZE: Path(CX_PERSON::name)        SetFormat
                  Path(CX_PERSON::firstName)   SetFormat
                  Path(CX_PERSON::dateOfBirth) SetFormat
                  FindAll(CX_PERSON) FillObox
    ]
  }
}

Das Flag FLOAT des Fensters besagt, dass dieses Fenster über dem Bearbeitungsfenster liegt, sich jedoch beide Fenster trotzdem gleichzeitig bedienen lassen.

In der Aktionsliste der ObjectListView bekommen wir mit FindAll(CX_PERSON) die Collection aller Personen-Objekte und reichen sie über den Stack weiter an die Anweisung FillObox. Diese Anweisung zeigt die Objekte in der Liste an. Die Zeilen mit der Anweisung SetFormat definieren, welche Daten des Objektes als Spalten dargestellt werden sollen.

Das System Event INITIALIZE wird beim Öffnen eines Fensters für jedes Oberflächenelement im Fenster, einschließlich des Fensters selbst, genau einmal ausgelöst. In unserer Anwendung nutzen wir dieses Ereignis, um alle Objekte der Klasse CX_PERSON in die ObjectListView zu laden. Das System Event INITIALIZE wird übrigens auch beim Laden an ein Modul gesendet.

Bisher können wir das Fenster noch nicht öffnen. Um das Fenster zu öffnen, definieren wir einen Menüeintrag im Bearbeitungsfenster:

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
{
  Menu
  {
    Item(T("Alle Personen", "Show All Persons"))
    [ SELECT: OpenWindow(TutorialPersonList, 1) ]
  }

Die Daten der Objekte werden jetzt als Liste dargestellt, allerdings haben die Spalten keine Überschriften. Dafür ändern wie die Definition des Oberflächenelements folgendermaßen ab:

ObjectListView(ListBox, AUTO_POSITION, 15, 11, 830, 93)
[ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
              [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
              [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
                FindAll(CX_PERSON) FillObox
]

Außerdem sollen die Objekte sortiert angezeigt werden. Die Objekte sollen erst nach Nachnamen und zusätzlich absteigend nach dem Geburtstag sortiert werden. So stehen die Familien immer zusammen und innerhalb der Familien erscheint die jüngste Person zuerst. Dafür ändern wir das Oberflächenelement noch einmal ab:

ObjectListView(ListBox, AUTO_POSITION, 15, 11, 830, 93)
[ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
              [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
              [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
              [ Path(CX_PERSON::name) Path(CX_PERSON::dateOfBirth) DESCENDING ]  SetSort
                FindAll(CX_PERSON) FillObox
]

Wie Sie sehen können, ist es mit SetSort ganz einfach, nach mehreren Kriterien zu sortieren.

Bisher ist die Liste bei der Anzeige noch recht klein und nutzt nicht den verfügbaren Platz aus. Mittels der Anweisung Attach kann Oberflächenelementen mitgeteilt werden, wie sie mit freiem Platz umgehen sollen. Wir ändern die Gruppe so ab, dass die Liste sich immer nach rechts und unten so weit wie möglich ausdehnt:

Group(PersonsGrp, 5, 2, 860, 106, T("Personen", "Persons"))
{
  ObjectListView(ListBox, AUTO_POSITION, 15, 11, 830, 93)
  [ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
                [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
                [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
                [ Path(CX_PERSON::name) Path(CX_PERSON::dateOfBirth) DESCENDING ]  SetSort
                  FindAll(CX_PERSON) FillObox
  ]

  Attach(ListBox, STRETCH, RIGHT, 15)
  Attach(ListBox, STRETCH, BOTTOM, 2)
}

Lose Kopplung durch Messages

Wir wollen die Anwendung jetzt so erweitern, dass bei einem Doppelklick auf eine Zeile in der Liste das entsprechende Objekt in das Bearbeitungsfenster geladen wird. Ein erster Ansatz wäre, das Objekt direkt mit FillWindow zu laden:

ObjectListView(ListBox, AUTO_POSITION, 15, 11, 830, 93)
[ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
              [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
              [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
              [ Path(CX_PERSON::name) Path(CX_PERSON::dateOfBirth) DESCENDING ]  SetSort
                FindAll(CX_PERSON) FillObox
  DOUBLE_CLICK: GetObject FillWindow(TutorialPersonEdit)
]

Dieser Ansatz würde zwar funktionieren, hat aber den Nachteil, dass hier eine feste Kopplung zwischen den beiden Fenstern entsteht. Wenn das erste Fenster umbenannt wird, zwischenzeitlich geschlossen wurde oder wenn vielleicht sogar mehrere Fenster an der Objektauswahl interessiert sind, funktioniert dieser Ansatz nicht mehr.

Die Zusammenarbeit der Fenster regeln wir mit einer besonderen Form der Kommunikation, nämlich durch Verschicken von Messages. InstantView® erweitert das Konzept der System Events um vom Programmierer definierte Messages. Diese werden explizit mit der Anweisung SendMsg versendet. Beim Empfänger werden sie in den Aktionslisten wie System Events verwendet. Ein Oberflächenelement, in dessen Aktionsliste die Message vorkommt, ist ein Empfänger der Message. Es kann beliebig viele Empfänger geben, oder auch gar keinen. SendMsg schickt die gerade auf dem Stack liegenden Werte mit zu den Empfängern. Die erste auf eine Message reagierende Anweisung findet den Stack also mit der gleichen Wertebelegung vor wie SendMsg. Nach SendMsg ist der Stack leer, es sei denn der Empfänger schickt sein Ergebnis explizit mittels ReturnStack zurück. Messages sind ähnlich zum Observer-Pattern der Design Patterns, Listenern in Java oder Signals und Slots in Qt.

Messages müssen deklariert werden, was normalerweise in der ext-Datei geschieht. In diesem Fall brauchen wir das nicht zu tun, da die Message TUTORIAL1_PERSON_SELECTED bereits deklariert wurde. Wir ändern die Liste so ab, dass statt des direkten Füllens des Bearbeitungsfensters die Message gesendet wird:

ObjectListView(ListBox, AUTO_POSITION, 15, 11, 830, 93)
[ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
              [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
              [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
              [ Path(CX_PERSON::name) Path(CX_PERSON::dateOfBirth) DESCENDING ]  SetSort
                FindAll(CX_PERSON) FillObox
  DOUBLE_CLICK: GetObject SendMsg(TUTORIAL1_PERSON_SELECTED)
]

Zusätzlich müssen wir das Bearbeitungsfenster um eine Aktionsliste erweitern, damit die Message empfangen und das mit der Message gesendete Objekt ins Fenster geladen werden kann:

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
[
    TUTORIAL1_PERSON_SELECTED: ClearWindow Dup if FillWindow else Drop
]

Auf den ersten Blick erscheinen einige Anweisungen in dieser Anweisungsfolge als unnötig. Die Anweisungsfolge ist aber auf Fehlersicherheit ausgelegt. ClearWindow stellt erstmal sicher, dass das Fenster vor dem Laden des neuen Objektes wirklich geleert ist. Dup if prüft, ob wirklich ein Objekt gesendet wurde. Nur in dem Fall wird FillWindow aufgerufen, ansonsten das duplizierte Element wieder vom Stack entfernt. Auch wenn zum Beispiel der Stack am Ende der Behandlung der Message vom CyberEnterprise® automatisch geleert würde, so empfiehlt es sich trotzdem, aus Gründen der Zuverlässigkeit und Fehlerfreiheit, selbst Ordnung im Stack zu halten. Eine explizite Programmierweise, die sich nicht implizit auf das CyberEnterprise® verlässt, erleichtert ein späteres Verstehen und Ändern und des Programmes.

Zusammenfassung

In diesem Abschnitt haben Sie gelernt, wie

  • Daten in einer Liste angezeigt werden können,
  • Listenansichten formatiert und sortiert werden können und
  • Fenster durch Messages lose gekoppelt werden können.

Benutzerführung und Procedures

Ziel

Bisher können wir eine Person aus der Liste per Doppelklick in das Bearbeitungsfenster laden. Allerdings wird bei jedem Druck auf den Speichern-Button ein neues Objekt erzeugt. Daher ist es noch nicht möglich, bereits vorhandene Objekte zu bearbeiten. In diesem Schritt wollen wir die Möglichkeit der Bearbeitung vorhandener Elemente schaffen und die Benutzerführung verbessern, indem der Speichern-Button nur dann angeklickt werden kann, wenn das Objekt geändert wurde.

Bearbeitung vorhandener Objekte

Wir fügen einen neuen Button zur Erstellung neuer Objekte ein:

Group(ActionGrp, 5, 72, 860, 20, T("Aktion", "Action"))
{
  Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
  [ SELECT: CloseWindow ]

  Button(NewBtn, 435, 11, 200, 7, T("&Neu", "&New"))

  Button(SaveBtn, LOCKED, 645, 11, 200, 7, T("&Speichern", "&Save"))
  [ SELECT: CreatePersObject(CX_PERSON) DrainWindow ]
}

Damit wir das gerade angezeigte Objekt auch wirklich bearbeiten können, müssen wir es uns merken. Dafür erzeugen wir eine neue modulglobale Variable person:

Module(tutorial1)
[
    Var(person)

    EXEC_TUTORIAL1: OpenWindow(TutorialPersonEdit, 1)
]

Wenn ein Objekt zur Bearbeitung aus der Datenbank geladen wurde, soll die Variable person das Objekt beinhalten. Andernfalls soll sie den Wert NULL enthalten. So kann beim Speichern entschieden werden, ob ein neues Objekt erzeugt oder nur das bestehende verändert werden muss. Zuerst implementieren wir den Neu-Button:

Button(NewBtn, 435, 11, 200, 7, T("&Neu", "&New"))
[ SELECT: ClearWindow, NULL -> person ]

Hier wird erst das Fenster geleert und danach der Wert NULL in der Variablen person gespeichert. Als nächstes ändern wir die Reaktion auf die Message TUTORIAL1_PERSON_SELECTED:

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
[
    TUTORIAL1_PERSON_SELECTED: ClearWindow
                               Dup if
                               {
                                 -> person, person FillWindow
                               }
                               else
                               {
                                 NULL -> person, Drop
                               }
]

Die Message erhält das ausgewählte Objekt auf dem Stack, speichert es in der Variablen person und füllt dann das Fenster mit den Daten. Sollte kein Objekt übergeben worden sein, wird die Variable person auf den Wert NULL gesetzt.

Schlussendlich muss auch noch der Speichern-Button geändert werden, damit nur dann ein neues Objekt erzeugt wird, wenn die angezeigten Daten nicht aus einem bestehenden Objekt kommen:

Button(SaveBtn, 645, 11, 200, 7, T("&Speichern", "&Save"))
[ SELECT: person ifnot { CreatePersObject(CX_PERSON) -> person }
          person DrainWindow
]

Jetzt können wir den Speichern-Button so oft drücken, wie wir wollen. Es entstehen keine weiteren Objekte. Wir können jetzt endlich ein Objekt aus der Liste holen und bereits gespeicherte Daten ändern.

Benutzerführung

Wir wollen jetzt dafür sorgen, dass der Speichern-Button überhaupt nur dann aktiv ist, wenn vorher Daten neu eingegeben oder geändert wurden. Zu diesem Zweck gibt es das System Event ALTERED. ALTERED wird genau dann ausgelöst, wenn Daten in einem Oberflächenelement nach der Aktivierung durch Alert verändert werden. Mit den Cursor-Tasten im Eingabefeld hin- und herfahren ändert nichts, ALTERED wird nicht ausgelöst. Beim Bearbeiten des Inhaltes eines Oberflächenelementes hingegen wird ALTERED ausgelöst. Nach der Aktivierung mit der Anweisung Alert kann ALTERED höchstens einmal ausgelöst werden.

Die Änderungen ziehen sich quer durch das Bearbeitungsfenster:

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
[
    TUTORIAL1_PERSON_SELECTED: ClearWindow
                               Dup if
                               {
                                 -> person, person FillWindow
                               }
                               else
                               {
                                 NULL -> person, Drop
                               }
]
{
  Menu
  {
    Item(T("Alle Personen", "Show All Persons"))
    [ SELECT: OpenWindow(TutorialPersonList, 1) ]
  }

  Group(PersonGrp, 5, 2, 860, 66, T("Person", "Person"))
  {
    Prompt(NamePmt, 15, 11, T("Name:", "Name:"))
    String(CX_PERSON::name, 250, 11, 595)
    [ ALTERED: Unlock(, SaveBtn) ]

    Prompt(FirstNamePmt, 15, 22, T("Vorname:", "First Name:"))
    String(CX_PERSON::firstName, 250, 22, 595)
    [ ALTERED: Unlock(, SaveBtn) ]

    Prompt(DateOfBirthPmt, 15, 33, T("Geburtstag:", "Birthday:"))
    Date(CX_PERSON::dateOfBirth, DF_DAY_OF_WEEK, DF_ALPHA_MONTH, 250, 33, 595)
    [ ALTERED: Unlock(, SaveBtn)
      NON_CURRENT:
      SELECT: {
                LocalVar(tmp)
                CreateTransObject(CX_PERSON) -> tmp
                tmp DrainWindow
                tmp Call(Age) PutValue(, age)
              }
    ]

    Prompt(AgePmt, 15, 44, T("Alter:", "Age:"))
    String(CX_PERSON::Age()~age, VIEW_ONLY, NO_DRAIN, 250, 44, 595)

    Prompt(SexEnumPmt, 15, 55, T("Geschlecht:", "Sex:"))
    Enumeration(CX_PERSON::sexEnum, 250, 55, 595, 30)
    [ ALTERED: Unlock(, SaveBtn) ]
  }

  Group(ActionGrp, 5, 72, 860, 20, T("Aktion", "Action"))
  {
    Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
    [ SELECT: CloseWindow ]

    Button(NewBtn, 435, 11, 200, 7, T("&Neu", "&New"))
    [ SELECT: ClearWindow, NULL -> person, Lock(, SaveBtn) Alert(TutorialPersonEdit) ]

    Button(SaveBtn, LOCKED, 645, 11, 200, 7, T("&Speichern", "&Save"))
    [ SELECT: person ifnot { CreatePersObject(CX_PERSON) -> person
              person DrainWindow, Lock Alert(TutorialPersonEdit)
     ]
  }
}

Das Flag LOCKED versetzt den Speichern-Button zunächst in einen inaktiven Zustand. Ein verändertes Aussehen zeigt, dass er auf Maus- oder Tastatureingaben nicht mehr reagiert. Die Anweisung Lock tut das gleiche zur Laufzeit. Mit Unlock wird ein Oberflächenelement in den aktiven Normalzustand versetzt.

Procedures

InstantView® erlaubt die Definition von eigenen Procedures zur besseren Strukturierung des Quelltextes. Wir wollen die ganzen Aktionen in der Oberfläche in Procedures zusammenfassen:

Module(tutorial1)
[
    Var(person)

    EXEC_TUTORIAL1: OpenWindow(TutorialPersonEdit, 1)

    Define(LockButton)
      Lock(, SaveBtn)
      Alert(TutorialPersonEdit)
    ;

    Define(NewObject)
      ClearWindow
      NULL -> person
      LockButton
    ;

    Define(EditObject)
      ClearWindow
      Dup if
      {
        -> person, person FillWindow
      }
      else
      {
        NULL -> person, Drop
      }
      LockButton
    ;

    Define(SaveObject)
      person ifnot { CreatePersObject(CX_PERSON) -> person }
      person DrainWindow
      LockButton
    ;

    Define(ObjectChanged)
      Unlock(, SaveBtn)
    ;
]

Procedures werden, wie Anweisungen, mit ihrem Namen aufgerufen. Wie alle anderen Anweisungen nehmen sie Eingabewerte vom Stack und hinterlassen dort ihr Ergebnis. Es gibt keine formalen Parameter. Eine Procedure wird mittels Define definiert. Einziger Parameter der Anweisung Define ist der Name der Procedure, darauf folgen beliebige Anweisungen. Die Procedure endet mit einem Semikolon. Procedures dürfen nur innerhalb einer Aktionsliste definiert werden. Außerdem müssen sie vor ihrem Aufruf bekannt sein.

Das Bearbeitungsfenster vereinfacht sich jetzt etwas:

Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File"))
[
    TUTORIAL1_PERSON_SELECTED: EditObject
]
{
  Menu
  {
    Item(T("Alle Personen", "Show All Persons"))
    [ SELECT: OpenWindow(TutorialPersonList, 1) ]
  }

  Group(PersonGrp, 5, 2, 860, 66, T("Person", "Person"))
  {
    Prompt(NamePmt, 15, 11, T("Name:", "Name:"))
    String(CX_PERSON::name, 250, 11, 595)
    [ ALTERED: ObjectChanged ]

    Prompt(FirstNamePmt, 15, 22, T("Vorname:", "First Name:"))
    String(CX_PERSON::firstName, 250, 22, 595)
    [ ALTERED: ObjectChanged ]

    Prompt(DateOfBirthPmt, 15, 33, T("Geburtstag:", "Birthday:"))
    Date(CX_PERSON::dateOfBirth, DF_DAY_OF_WEEK, DF_ALPHA_MONTH, 250, 33, 595)
    [ ALTERED: ObjectChanged
      NON_CURRENT:
      SELECT: {
                LocalVar(tmp)
                CreateTransObject(CX_PERSON) -> tmp
                tmp DrainWindow
                tmp Call(Age) PutValue(, age)
              }
    ]

    Prompt(AgePmt, 15, 44, T("Alter:", "Age:"))
    String(CX_PERSON::Age()~age, VIEW_ONLY, NO_DRAIN, 250, 44, 595)

    Prompt(SexEnumPmt, 15, 55, T("Geschlecht:", "Sex:"))
    Enumeration(CX_PERSON::sexEnum, 250, 55, 595, 30)
    [ ALTERED: ObjectChanged ]
  }

  Group(ActionGrp, 5, 72, 860, 20, T("Aktion", "Action"))
  {
    Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
    [ SELECT: CloseWindow ]

    Button(NewBtn, 435, 11, 200, 7, T("&Neu", "&New"))
    [ SELECT: NewObject ]

    Button(SaveBtn, LOCKED, 645, 11, 200, 7, T("&Speichern", "&Save"))
    [ SELECT: SaveObject ]
  }
}

Aktualisierung von Listenelementen

Die Anwendung besitzt noch einen kleinen Schönheitsfehler: Wenn die Auflistung der Personen geöffnet ist und eine Person im Bearbeitungsfenster gespeichert wird, zeigt die Liste noch die alten Daten an. Wir wollen jetzt die Anwendung erweitern, so dass die Liste beim Speichern automatisch aktualisiert wird. Dafür senden wir beim Speichern eine Message, dass eine Person geändert wurde. Dieser Message geben wir das geänderte Objekt mit:

Define(SaveObject)
  person ifnot { CreatePersObject(CX_PERSON) -> person }
  person DrainWindow
  LockButton
  person SendMsg(TUTORIAL1_PERSON_CHANGED)
;

Diese Message wird jetzt in der Liste mit den Personen empfangen und daraufhin die Liste mit der Anweisung UpdateObox akutalisiert:

ObjectListView(ListBox, AUTO_POSITION, 15, 11, 0, 0)
[ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
              [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
              [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
              [ Path(CX_PERSON::name) Path(CX_PERSON::dateOfBirth) DESCENDING ]  SetSort
                FindAll(CX_PERSON) FillObox
  DOUBLE_CLICK: GetObject SendMsg(TUTORIAL1_PERSON_SELECTED)
  TUTORIAL1_PERSON_CHANGED: Dup if UpdateObox else Drop
]

Durch die lose Kopplung wird die Liste aktualisiert, wenn das Fenster geöffnet ist. Es gibt keinen Fehler, falls es geschlossen ist.

Zusammenfassung

In diesem Abschnitt haben Sie gelernt, wie

  • eine Benutzerführung über Alert und ALTERED realisiert werden kann,
  • Procedures definiert und verwendet werden und
  • einzelne Elemente einer Liste aktualisiert werden können.

Relationen und Slots

Ziel

Bisher können wir eine Menge von Personen anlegen, bearbeiten und anzeigen. In diesem Schritt wollen wir die Möglichkeit der Definition von Beziehungen zwischen diesen Objekten hinzufügen. Außerdem werden Sie eine Einführung in das Konzept der dynamischen Datenfelder erhalten.

Relationen

Relationen, also Beziehungen zwischen Objekten, können im CyberEnterprise® auf verschiedene Arten abgebildet werden. Ein Objekt A kann auf ein anderes Objekt B verweisen. Außerdem kann das Objekt A auf mehrere Objekte B1, B2, …, Bn verweisen. Die Relation kann auch leer sein. Dann verweist im ersten Fall A auf kein Objekt, also auf NULL. Im zweiten Fall ist n = 0. Eine weitere Unterscheidung liegt darin, ob es von den Objekten B bzw. Bk eine Rückrelation zu A gibt oder nicht.

Diesen Varianten entsprechen im CyberEnterprise® die folgenden Datentypen:

Relation ohne Rückrelation mit Rückrelation
B → A B → (A1, A2, …, Am)
A → B POINTER REL_11 REL_1N
A → (B1, B2, …, Bn) COLLECTION REL_N1 REL_MN

Zur vollständigen Beschreibung einer Relation gehört die Einschränkung, dass sie nur auf Objekte einer bestimmten Klasse verweist. Dazu gehören dann auch Objekte abgeleiteter Klassen.

Slots

Wir wollen in unserer Anwendung Eltern/Kind-Beziehungen zwischen Personen abbilden. Diese Relationen möchten wir über die Datenfelder mother, father und children modellieren. Wenn wir in die Dokumentation der Klasse CX_PERSON gucken, sehen wir, dass diese Felder dort nicht existieren. Damit kommen wir zu einer Besonderheit des CyberEnterprise®. Die Klassen des CyberEnterprise® enthalten nur die Datenfelder, die für ihre Funktion als allgemein einsetzbarer Baustein unbedingt notwendig sind. Die Klassen enthalten keine Datenfelder, die eventuell irgendwo nützlich sein könnten. Allerdings können die meisten Klassen des CyberEnterprise® (alle von CX_EXPANDABLE abgeleiteten) durch dynamische Datenfelder, Slots genannt, erweitert werden.  Das bedeutet, dass man zur Laufzeit in ein Datenfeld schreibt, das in der Klassendefinition gar nicht vorgesehen ist. Mit Slots werden Merkmalsleisten nach DIN4000 aufgebaut. Der Vorrat an möglichen Slots kann als eine Menge von unternehmensweiten Variablen gesehen werden. Diese Variablen sind in jedem Unternehmen anders und können individuell definiert und erweitert werden.

Slots haben folgende Eigenschaften:

  • Slots verhalten sich weitgehend wie echte Datenfelder. Dies gilt in vollem Umfang für InstantView®, z.B. für Queries in der Datenbank.
  • Ein Slot wird niemals explizit erzeugt. Er entsteht, wenn sich eine schreibende Anweisung zum ersten Mal auf ihn bezieht.
  • Objekte können unterschiedliche Slots besitzen, obwohl sie Instanzen der gleichen Klasse sind.
  • Der Vorrat möglicher Slots wird vom CyberEnterprise® verwaltet. Die Einführung weiterer potenzieller Slots liegt in der Hand des Anwenders.
  • In einem gewissen Sinne besitzt jedes erweiterbare Objekt alle potenziell möglichen Slots. Die nur virtuell existierenden Datenfelder belegen jedoch keinen Speicherplatz und haben den speziellen Null-Wert INVALID.

 Die Slots mother, father und children wurden über folgende Zeilen bereits in der Datei classix.dic definiert:

Slot(father,   T("Vater", "Father"),    63, REL_1M, CX_PERSON)
Slot(mother,   T("Mutter", "Mother"),   64, REL_1M, CX_PERSON)
Slot(children, T("Kinder", "Children"), 65, REL_M1, CX_PERSON)

Die Vereinbarung von Slots in der Initialisierungs-Datei ist nur eine Möglichkeit. Es existieren Anweisungen, mit deren Hilfe zur Laufzeit eine solche Definition aufgebaut werden kann. Neue Slots können vereinbart werden, ohne dass die laufende Anwendung gestoppt werden muss. Die Vereinbarung mit der Slot-Direktive legt den Namen und Typ des Slots fest und vergibt eine eindeutige ganze Zahl für die interne Kodierung. Der beschreibende multilinguale Text ist optional.

Alle bisher in der Datenbank erzeugten Objekte haben natürlich keinen dieser Slots. Beim Lesen eines nichtexistenten Slots bekommt man den Wert INVALID. Dieser wird von FillWindow als leere Menge oder Zeichenkette dargestellt.

Definition von Relationen an der Oberfläche

Zur Definition der Verwandschaftsbeziehungen erzeugen wir ein neues Fenster:

Window(TutorialPersonRelations, 0, 0, 870, 110, T("Verwandschaftsbeziehungen", "Family Relationships"))
{
  Menu
  {
    Item(T("Alle Personen", "Show All Persons"))
    [ SELECT: OpenWindow(TutorialPersonList, 1) ]
  }

  Group(RelationsGrp, 5, 2, 860, 82, T("Verwandschaftsbeziehungen", "Family Relationships"))
  {
    Prompt(ParentPmt, 15, 11, T("Person", "Person"))
    ObjectCombobox(Parent, NULL_ELEMENT, NO_CLEAR, 250, 10, 595, 60)
    [ INITIALIZE: FindAll(CX_PERSON) FillObox ]

    Prompt(ChildrenPmt, 15, 22, T("Kinder", "Children"))
    ObjectListView(CX_PERSON::children, AUTO_POSITION, 15, 33, 0, 0)
    [ INITIALIZE: [ Path(CX_PERSON::name) HEADER T("Name", "Name") ]               SetFormat
                  [ Path(CX_PERSON::firstName) HEADER T("Vorname", "First Name") ] SetFormat
    ]

    Attach(Parent, STRETCH, RIGHT, 15)
    Attach(children, STRETCH, RIGHT, 15)
    Attach(children, STRETCH, BOTTOM, 2)
  }

  Group(ActionGrp, 5, 88, 860, 20, T("Aktion", "Action"))
  {
    Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
    [ SELECT: CloseWindow ]

    Button(AssignBtn, 645, 11, 200, 7, T("Kinder zuordnen", "Assign Children"))
  }
}

Die ObjectCombobox ist ein Oberflächenelement, das zur Bearbeitung von Relationen verwendet werden kann. Das Flag NULL_ELEMENT fügt ein leeres Element an den Anfang der Liste ein, so dass es nicht nur möglich ist, ein Objekt auszuwählen, sondern auch kein Objekt auszuwählen.

Erwähnenswert ist hierbei auch das Flag ENTIRE. Wenn dieses Flag gesetzt ist, wird das referenzierte Objekt automatisch in die Liste geladen, aber auch nur dieses Objekt. Das Oberflächenelement agiert dann als reine Anzeige welches Objekt referenziert ist. Dieses Flag ist vor allem bei sehr großen Datenmengen wichtig, da ansonsten alle potentiellen Elemente in die Liste geladen würden. Die beiden Flags NULL_ELEMENT und ENTIRE sind in Kombination sehr sinnvoll, da die Liste dann nur das leere und das referenzierte Objekt enthält und somit zur Anzeige und Löschung der Referenz verwendet werden kann.

Das Flag NO_CLEAR sorgt dafür, dass das Oberflächenelement bei der Anweisung ClearWindow nicht geleert wird.

Das Hinzufügen von Kindern zu der ausgewählten Person soll über das Übersichtsfenster geschehen. Wenn dort auf eine Person doppelt geklickt wird, soll sie der aktuellen Person als Kind zugewiesen werden. Wenn wiederum in der Liste mit den Kindern ein Element ausgewählt ist und die Entfernen-Taste gedrückt wird, soll die Person aus der Liste der Kinder entfernt werden. Dafür ändern wir die Liste folgendermaßen ab:

Prompt(ChildrenPmt, 15, 22, T("Kinder", "Children"))
ObjectListView(CX_PERSON::children, AUTO_POSITION, 15, 33, 0, 0)
[ INITIALIZE: [ Path(CX_PERSON::name) HEADER T("Name", "Name") ]               SetFormat
              [ Path(CX_PERSON::firstName) HEADER T("Vorname", "First Name") ] SetFormat
  DELETE: GetObject Dup if RemoveObox else Drop
  TUTORIAL1_PERSON_SELECTED: Dup if UpdateObox else Drop
]

Jetzt müssen wir uns noch darum kümmern, dass die richtigen Kinder geladen werden, wenn eine andere Person in der ObjectCombobox ausgewählt wird:

Prompt(ParentPmt, 15, 11, T("Person", "Person"))
ObjectCombobox(Parent, NULL_ELEMENT, NO_CLEAR, 250, 10, 595, 60)
[ INITIALIZE: FindAll(CX_PERSON) FillObox
  SELECT:     ClearWindow GetObject Dup if FillWindow else Drop
]

Damit ist es uns jetzt möglich, Kinder einer Person zuzuordnen und die Zuordnung zu laden und anzuzeigen. Allerdings können wir die festgelegten Zuordnungen noch nicht speichern. Die Slots sind so definiert, dass es immer eine Rückrelation gibt. Die Relation in die eine Richtung wird über den Slot children definiert. Die Relation in die andere Richtung wird allerdings, abhängig vom Geschlecht der Person, über father oder mother definiert. Es muss also beim Speichern mittels der Anweisung BackRefName manuell festgelegt werden, welcher Slot verwendet werden soll:

Button(AssignBtn, 645, 11, 200, 7, T("Kinder zuordnen", "Assign Children"))
[ SELECT: GetObject(, Parent) Copy(sexEnum)
          if "mother" else "father" BackRefName(, children)
          GetObject(, Parent) DrainWindow
          GetObject(, Parent) SendMsg(TUTORIAL1_PERSON_CHANGED)
]

Die letzte Zeile sendet wieder die Message TUTORIAL1_PERSON_CHANGED, um zu signalisieren, dass die aktuelle Person geändert wurde.

Anonyme Prozeduren

Wir wollen jetzt in der Liste aller Personen noch eine Spalte mit der Anzahl der Kinder einführen. Dafür ändern wir die Liste im Übersichtsfenster folgendermaßen ab:

ObjectListView(ListBox, AUTO_POSITION, 15, 11, 0, 0)
[ INITIALIZE: [ Path(CX_PERSON::name)        HEADER T("Name", "Name") ]          SetFormat
              [ Path(CX_PERSON::firstName)   HEADER T("Vorname", "First Name") ] SetFormat
              [ Path(CX_PERSON::dateOfBirth) HEADER T("Geburstag", "Birthday") ] SetFormat
              [ Path(CX_PERSON::call({Cardinality(children) Dup ifnot {Drop 0}}))
                HEADER T("Anzahl Kinder", "Number of Children") ]                SetFormat
              [ Path(CX_PERSON::name) Path(CX_PERSON::dateOfBirth) DESCENDING ]  SetSort
                FindAll(CX_PERSON) FillObox
  DOUBLE_CLICK: GetObject SendMsg(TUTORIAL1_PERSON_SELECTED)
  TUTORIAL1_PERSON_CHANGED: Dup if UpdateObox else Drop
]

Die Liste bekommt ein weiteres Formatelement: CX_PERSON::call(…). Der Zugriffsausdruck ist etwas ungewöhnlich. Es sieht so aus, als wäre call eine Funktion der Klasse CX_PERSON. Das ist aber nicht so. Die Pseudofunktion call ist für Objekte aller Klassen zuständig. Sie ruft eine Procedure auf, deren Ergebnis sie dann zurückgibt. Für kurze und nur einmalig zu benutzende Procedures lohnt sich eine Deklaration inklusive Namen nicht. Dann können, wie hier geschehen, auch anonyme Prozeduren verwendet werden. Anonyme Prozeduren sind ähnlich zu Lambdas in anderen Programmiersprachen, wie z.B. Java, C++ oder Python.

Zusammenfassung

In diesem Abschnitt haben Sie gelernt,

  • wie Relationen im CyberEnterprise® definiert werden,
  • was Slots sind und wie sie verwendet werden,
  • wie sich Relationen an der Oberfläche darstellen lassen und
  • wie anonyme Prozeduren in InstantView® verwendet werden können.

Baumansicht

Ziel

In diesem Abschnitt wollen wir den Stammbaum einer Person anzeigen lassen. Dafür werden wir sehen, wie sich baumartige Strukturen darstellen lassen.

Stammbaum

Der Stammbaum unserer Personen soll als Baum angezeigt werden. Vor jedem Namen soll ein Symbol des Geschlechts der Person angezeigt werden. Für diese Symbole definieren wir erstmal zwei neue Variablen und laden die Bilder beim Laden des Moduls:

Module(tutorial1)
[
    Var(person, maleBitmap, femaleBitmap)

    INITIALIZE: CreateTransObject(CX_BITMAP) -> maleBitmap
                "CX_ROOTDIR\\Bmp\\CX_PERSON_male.bmp" maleBitmap Put
                CreateTransObject(CX_BITMAP) -> femaleBitmap
                "CX_ROOTDIR\\Bmp\\CX_PERSON_female.bmp" femaleBitmap Put
    EXEC_TUTORIAL1: OpenWindow(TutorialPersonEdit, 1)

Die Bilder sind transiente Objekte der Klasse CX_BITMAP. Mit der Anweisung Put wird der Pfad zu einem Bild in das Objekt geschrieben und das Objekt importiert das Bild aus der Datei.

Jetzt können wir ein neues Fenster mit dem Stammbaum erstellen:

Window(TutorialPersonsRelationsTree, 0, 0, 870, 110, T("Stammbaum", "Family Tree"))
{
  Group(TreeGrp, 5, 2, 860, 82, T("Stammbaum", "Family Tree"))
  {
    Prompt(ParentPmt, 15, 11, T("Person", "Person"))
    ObjectCombobox(Parent, NULL_ELEMENT, NO_CLEAR, 250, 10, 595, 60)
    [ INITIALIZE: FindAll(CX_PERSON) FillObox
      SELECT:     ClearWindow GetObject Dup if FillWindow else Drop
    ]

    ObjectTree(CX_PERSON::this~tree, NO_DRAIN, 15, 22, 830, 58)
    [ INITIALIZE: Path(CX_PERSON::call({
                    Copy(sexEnum) if femaleBitmap else maleBitmap
                  }))                                SetFormat
                  Path(CX_PERSON::firstName)         SetFormat
                  Path(CX_PERSON::name)              SetFormat
                  [ Path(CX_PERSON::children) NODE ] SetFormat
    ]

    Attach(Parent, STRETCH, RIGHT, 15)
    Attach(tree, STRETCH, RIGHT, 15)
    Attach(tree, STRETCH, BOTTOM, 2)
  }

  Group(ActionGrp, 5, 88, 860, 20, T("Aktion", "Action"))
  {
    Button(CloseBtn, 15, 11, 200, 7, T("Zurü&ck", "Ba&ck"))
    [ SELECT: CloseWindow ]
  }
}

Aus Sicht der Programmierung mit InstantView® ist ein ObjectTree so etwas wie eine ObjectListView mit hierarchischer Unterordnung. Deshalb ist auch das einzig wirklich Neue die Formatangabe mit dem Flag NODE. Mit diesem Formatelement wird nicht die Darstellung beschrieben, sondern ein Zugriffsausdruck angegeben, der zur nächsten Hierarchiestufe führt.

Zusammenfassung

In diesem Abschnitt haben Sie gelernt, wie einfach sich Baumstrukturen mit InstantView® darstellen lassen. Hiermit endet auch das Tutorial Grundlagen InstantView®. Sie können ab hier, je nach Interessenlage, mit jedem beliebigen der anderen Tutorials fortfahren.

InstantView Scriptsprache