Loading...
 

Tutorials | Simple example

Simple example

In this tutorial a simple example module is implemented and then extended step by step. A person management system is to be designed, which fulfils the following tasks:

The application has deliberately been kept very simple to show the easy implementation of the basic concepts. Of course, it cannot be compared in any way with the complexity of the AppsWarehouse®modules.

To complete the tutorial, you must first complete the following steps:

  1. Create a free cloud account or log in with your cloud account.
    Note: Temporarily, Pandora can no longer be accessed via the Cloud.
  2. Book Pandora for free if you did not create the account using the link from the first point.
  3. Start Pandora. All necessary settings will be done automatically.
  4. The IDE is automatically started together with Pandora and is available shortly after Pandora. You can find the URL to the IDE in the account management. Click on CloudAccount in the menu and then go to the Infrastructure tab. Here you will find your username, password and URL to the IDE. If you see the message Bad Gateway after accessing the URL, the IDE is not yet fully started. In this case just wait a short time and then refresh the page.

The paths in the tutorial refer to your customer folder. If you have connected the folder as a network drive, the paths are always the absolute path on this drive. In the online IDE your customer folder is linked with the name of your account number. If nnnnnn appears in a path specification, you must replace it with your account number. You can find this by clicking on Cloud Account in the Cloud Administration in the tab Company Data.

By the way, you can also use the direct link https://classix.cloud/direct/nnnnnn/Pandora so that you don't always have to start Pandora via the cloud administration.

The solutions of each section can be found in the menu under Tutorials → Solutions → Basic InstantView.

Creation of personal data (interface with data binding)

Objective

In this section we will create the module for the tutorial and an editing window for personal data. In this window you can edit a person's first and last name, birthday and gender. The age of the person is also displayed.

First window

The application is developed in a new module . To do this, create the file appswh/nnnnnn/tutorial1.app with the following content:

Module(tutorial1) [ EXEC_TUTORIAL1: OpenWindow(TutorialPersonEdit, 1) ] Window(TutorialPersonEdit, 0, 0, 870, 94, T("Personenverwaltung", "Personal File")) { }

This module does nothing more than open an empty window. See also the reference for windows and the command OpenWindow.

The title of the window is multilingual, just as all constant strings can be multilingual. When the CyberEnterprise® is started, an index for the current language is determined (0th, 1st, ..., nth language). It is possible to change the language at any time. All interface elements automatically display their texts in the newly selected language. The translations of the texts can be defined in a file. Then the texts in the source text are only valid as keys. Otherwise, the texts from the source text are used directly.

The window is to be opened in response to the message EXEC_TUTORIAL1. Of course, it must still be possible to send this message. For this, the message must be declared first. In addition, it must be specified that the tutorial1 module is to be loaded with this message. This is done in the file system/nnnnnn/nnnnnn.ext with the help of the external statement, to which you should add the following lines:

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

After changing the file system/nnnnnn/nnnnnn.ext, the parser running in the background must re-parse all files. This takes a moment and is shown in the status bar. Afterwards you can close the file tutorial1.app once and open it again to have it parsed as well.

In order to be able to send the message and thus open the window, a menu item is to be inserted in the menu that sends the message. To do this, please add the following section to the file projects/nnnnnn/pandora_nnnnnn.cxp:

Window(ControlWin, 10, 10, 1550, 200, T("Pandora - IDE", "Pandora - IDE")) { Menu { Item(TutorialsItem, T("Tutorials", "Tutorials")) { Item(MyTutorialMenuLN, "") Item(Tutorial1Item, T("Tutorial 1", "Tutorial 1")) [ SELECT: SendMsg(EXEC_TUTORIAL1) ] } } }

After restarting Pandora, the window can be opened via the menu.

If Pandora doesn't start, you can check the log files in the system/SystemOut folder, sorted by date, and check the latest ones to see if a typo has crept into the source code.

Once loaded, modules are kept in the memory until further notice. To avoid having to restart Pandora after every change, you can add the following lines to the projects/nnnnnn/Pandora_nnnnnn.cxp file under the menu item Tutorial1Item

Item(Tutorial1Reload, T("Tutorial 1 aktualisieren", "Refresh Tutorial 1")) [ SELECT: Reload(tutorial1) ]

After a restart of Pandora, you can then see changes to the module tutorial1 without restarting Pandora.

Data binding

The window should allow editing of persons, i.e. objects of the class CX_PERSON. InstantView® offers over 30 interface elements for editing the data. In addition, InstantView® offers a simple coupling of the user interface to the model, as is also offered, for example, by the libraries Angular, React or Vue.js. In order to link a UI element to a data element, the UI element must simply receive the access expression to the data element instead of any name.

We now want to extend the window with fields for editing a person's first and last name and birthday:

Module(tutorial1) [     EXEC_TUTORIAL1: 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"))   } }

Hierarchies within the surface are expressed by the curly brackets { and }. All surface elements in such a block are assigned to the element to which the block belongs. In this example, the window contains two UI elements of type Group. These in turn contain the actual editing elements. A Group visually groups the elements assigned to it. There is also the element Composite, which groups the elements without a visual representation. A Group or Composite is necessary for the alignment when displayed on the web. Therefore, never use a UI element other than a Group or a Composite as the direct child element of a window.

The interface elements can be adapted in their behaviour by means of flags . For example, the birthday can be displayed with weekday and month name as follows:

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

Action lists

The assignment of data fields to UI elements was only a preparatory step. Now we want to associate real actions with the interface. First we need to specify when something specific should happen, e.g. after a mouse click on a button or in response to pressing the Enter key in an input field. We call such user-triggered events system events. In our example, the data entered by a person is to be saved in a transient, i.e. temporary, object when the Save button is pressed. To do this, we change the definition of the save button as follows:

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

The button has been given an action list. All InstantView® code that describes the dynamic processes is located within action lists. This separates it from the static definition of the window surface outside the square brackets. The CreateTransObject statement creates a transient object and places it on the stack. The DrainWindow statement writes the data of all interface elements that refer to the CX_PERSON class into the created object. In our case, these are the data fields name, firstName and dateOfBirth. The object into which DrainWindow writes the data is passed via the stack. CreateTransObject deposits the created object on top of the stack, therefore the data is stored in this object. The opposite instruction to DrainWindow is FillWindow, which fills all UI elements with the data from an object.

When the Back button is pressed, the window should be closed. This is done with the instruction CloseWindow. Therefore we change the back-button as follows:

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

In addition to data fields (attributes), an object usually also has functions (methods). The class CX_PERSON has, among many others, the function Age. This function calculates the age of a person from the date of birth and the current date. In our application we want to display the age of the person currently displayed. Therefore we need a pure display field, because the age cannot be entered or changed via the user interface. A function, like a data field, can be linked directly to a user interface element. A display field of the age can be added to the window as follows

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

The NO_DRAIN flag is important because it ensures that DrainWindow ignores this data field, despite the binding to CX_PERSON. Unlike UI elements with normal names or referenced by data field, this UI element cannot be referenced with a function expression as name. Instead, you can assign an alias to the UI element. Change the definition as follows to be able to address the UI element using the name age:

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

The age is not yet displayed. This is because it is only updated automatically when a FillWindow is installed. Therefore we now change the save button so that a click creates a transient object, fills it with the data from the window and immediately afterwards the window is filled again with the data from the object:

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

The Dup instruction duplicates the topmost element on the stack and thus causes the generated transient object to lie twice on the stack. It is therefore important that DrainWindow uses and removes the topmost object on the stack and then the object must be on the stack again for FillWindow.

Variables

The previous example showed that it is possible to work without variables, but from a certain level of complexity it is no longer understandable. Variables must be declared before use using the Var, GlobalVar, StaticVar and LocalVar statements. With the command -> the topmost value is removed from the stack and assigned to a variable. Naming a variable ensures that its value is placed on top of the stack. We want to write the above example once with variables. Since we use a local variable, the instruction block must be enclosed in curly brackets:

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

The source text has become longer, but it is now much easier to read. The variable person now contains an object of class CX_PERSON. If you prefer to say that the variable person refers to the object or holds a reference to the object, this is also correct. However, it makes no difference at the level of InstantView®. Whenever we need our object again, we can put it back on the stack using the variable person. The CyberEnterprise® takes care of whether the object has to be retrieved from the database and when a database transaction has to be started and ended for this. We can concentrate fully on the application. Variables in InstantView® are also not type-bound, unlike many other programming languages. This means that you can assign any value to any variable that occurs in InstantView®.

Of course, it is not the point of reading an object out of the window first and writing it back in the window immediately afterwards, just to update the age. This step should only demonstrate that the age is actually calculated in FillWindow. It makes more sense to recalculate the age when pressing the enter key in or when leaving the UI element with the birthday. For this purpose we reset the action list of the save button and add an action list for the date of birth:

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)           } ]

First a transient object of the class CX_PERSON is created and filled with the data from the window. Then the method Age is called on the object by means of Call and the result is written to the UI element with PutValue.

Enumerations

We now want to extend the window to include the possibility of defining the gender of a person. Enumerations of the form {female, male, diverse} or {no disposition, material withdrawal, purchase requisition, production order} are mapped to whole numbers in CyberEnterprise®, i.e. stored in coded form for reasons of efficiency. An application should not force the user to deal with such codings. Therefore the CyberEnterprise® automatically takes over the transformation from coded to readable form and vice versa.

The class CX_PERSON has the data field sexEnum, which defines the sex of a person. We extend the editing window with the possibility to define the gender:

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

The UI element Enumeration uses a multilingual translation table. The data field sexEnum is of data type ENUMCHAR. For data fields of the types ENUMCHAR, ENUMSHORT, and ENUMINT, a connection with a transformation table must exist. The place for this is the System Dictionary, which is located in the file classix.dic There sexEnum is defined as:

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

Member stands for an entry that describes a data field. The transformation table is further specified by classix.num~genderEnum.

If the machine requires this table, the

  1. searched for a corresponding object of the class CX_ENUM_TABLE in the database with classix .num~genderEnum,
  2. if nothing is found in the database, the file classix.num is searched in the directory system and in this file the section sexEnum is searched,
  3. if this also fails, an error message appears.

In classix.num the transformation table for sexEnum looks like this

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

The section name is arbitrary. The connection to the data field is made solely via the entry in the System Dictionary, even if the section and data field are named the same. The possible values are compared to the names as multilingual constants. The flag, here always 0xff, can be used to select a partial table.

Persistent storage

Previously, only a transient object was created when saving. Now we want to make sure that the object becomes a persistent object. It should therefore be saved permanently in the database. Therefore we change the save button as follows:

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

By changing the statement CreateTransObject to CreatePersObject a persistent object is created from now on. DrainWindow will then directly save all data in the database without our intervention.

Summary

In this section you have learned how to

  • a new window is created,
  • interface elements can be created for editing and linked to the data of the CyberEnterprise®,
  • can react to user actions,
  • enumerations can be defined and used,
  • transient and persistent objects are created and
  • variables are declared and used.

Display and edit all stored persons (listing of objects).

Objective

So far we can create new people, but we cannot yet display the people in the database. In this step, therefore, we will create a new window with a list of all persons. Thereby we want to define the displayed columns and have the objects sorted automatically. We will also loosely couple the two windows so that the windows are independent of each other.

List views

We will create a new window in our module that lists all persons in the database:

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     ]   } }

The FLOAT flag of the window indicates that this window is above the editing window, but both windows can still be operated simultaneously.

In the action list of the ObjectListView we get with FindAll(CX_PERSON) the collection of all person objects and pass it over the stack to the statement FillObox. This statement displays the objects in the list. The rows with the SetFormat statement define which data of the object should be displayed as columns.

The Event INITIALIZE system is triggered exactly once when a window is opened for each UI element in the window, including the window itself. In our application we use this event to load all objects of the class CX_PERSON into the ObjectListView. The system event INITIALIZE is also sent to a module when loading.

So far we cannot open the window yet. To open the window, we define a menu item in the editing window:

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

The data of the objects are now displayed as a list, but the columns have no headings. To do this, change the definition of the interface element as follows:

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 ]

In addition, the objects should be displayed sorted. The objects should first be sorted by surname and then by birthday in descending order. So the families are always together and within the families the youngest person appears first. To do this, we change the interface element once again:

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 ]

As you can see, with SetSort it is very easy to sort by several criteria.

So far, the list is still quite small when displayed and does not use the available space. You can use the Attach statement to tell interface elements how they should handle free space. We change the group so that the list always expands to the right and below as much as possible:

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) }

Loose coupling through messages

We now want to extend the application so that when you double-click on a line in the list, the corresponding object is loaded into the editing window. A first approach would be to load the object directly with FillWindow:

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) ]

This approach would work, but has the disadvantage of creating a fixed coupling between the two windows. If the first window is renamed, has been closed in the meantime or if perhaps several windows are interested in the object selection, this approach no longer works.

We regulate the cooperation of the windows with a special form of communication, namely by sending messages. InstantView® extends the concept of system events by messages defined by the programmer. These are sent explicitly with the SendMsg instruction. At the receiver they are used in the action lists like system events. A UI element in whose action list the message appears is a receiver of the message. There can be any number of receivers, or none at all. SendMsg sends the values currently on the stack to the receivers. The first statement reacting to a message therefore finds the stack with the same value assignment as SendMsg. After SendMsg the stack is empty, unless the receiver sends back the result explicitly via ReturnStack. Messages are similar to the Observer pattern of Design Patterns, Listeners in Java or Signals and Slots in Qt.

Messages must be declared, which is normally done in the ext-file. In this case we do not need to do this, because the message TUTORIAL1_PERSON_SELECTED has already been declared. We modify the list so that instead of filling the editing window directly, the message is sent:

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) ]

Additionally, we need to add an action list to the editing window so that the message can be received and the object sent with the message can be loaded into the window:

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

At first glance, some of the instructions in this sequence appear unnecessary. However, the instruction sequence is designed to be error-proof. ClearWindow first makes sure that the window is really empty before loading the new object. Dup if checks whether an object was really sent. Only in this case FillWindow is called, otherwise the duplicated element is removed from the stack. Even if, for example, the stack would be emptied automatically by the CyberEnterprise® at the end of message handling, it is still advisable to keep order in the stack for reasons of reliability and freedom from errors. An explicit programming method that does not implicitly rely on the CyberEnterprise® makes it easier to understand and change the program later.

Summary

In this section you have learned how to

  • data can be displayed in a list,
  • list views can be formatted and sorted and
  • windows can be loosely coupled by messages.

User guidance and procedures

Objective

So far we can load a person from the list into the editing window by double-clicking. However, every time we press the save button, a new object is created. Therefore it is not yet possible to edit already existing objects. In this step we want to create the possibility to edit existing elements and improve the user guidance by making it possible to click the save button only if the object has been changed.

Editing existing objects

We add a new button for creating new objects:

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 ] }

In order to be able to edit the object that is currently displayed, we have to remember it. For this purpose we create a new module-global variable person:

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

If an object was loaded from the database for editing, the variable person should contain the object. Otherwise, it should contain the value NULL. So when saving it can be decided whether a new object has to be created or only the existing one has to be changed. First we implement the New button:

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

Here the window is first cleared and then the value NULL is saved in the variable person. Next we change the response to the 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 } ]

The message receives the selected object on the stack, saves it in the person variable and then fills the window with the data. If no object has been transferred, the variable person is set to the value NULL.

Finally, the save button must also be changed so that a new object is only created if the data displayed does not come from an existing object:

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

Now we can press the save button as often as we want. No further objects will be created. Now we can finally get an object from the list and change already saved data.

User guidance

We now want to make sure that the save button is only active if data has been entered or changed before. For this purpose there is the system Event ALTERED. ALTERED is triggered exactly when data in a UI element is changed after it has been activated by Alert. Moving back and forth with the cursor keys in the input field does not change anything, ALTERED is not triggered. However, ALTERED is triggered when the content of a UI element is edited. After activation with the Alert statement, ALTERED can be triggered at most once.

The changes will drag across the editing window:

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) ] } }

The LOCKED flag initially sets the Save button to an inactive state. A changed appearance shows that it no longer reacts to mouse or keyboard inputs. The instruction Lock does the same at runtime. Unlock sets a UI element to the active normal state.

Procedures

InstantView® allows you to define your own procedures to better structure the source code. We want to combine all actions in the user interface into procedures:

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, like instructions, are called by their name. Like all other statements, they take input values from the stack and leave their result there. There are no formal parameters. A procedure is defined by a define. The only parameter of the Define statement is the name of the procedure, followed by any other statement. The procedure ends with a semicolon. Procedures may only be defined within an action list. Furthermore they must be known before they are called.

The editing window is now somewhat simplified:

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 ] } }

Updating of list elements

The application still has a small flaw: When the listing of persons is open and a person is saved in the editing window, the list still shows the old data. We now want to extend the application so that the list is automatically updated when it is saved. To do this, we send a message when saving that a person has been changed. TUTORIAL1_PERSON_CHANGED is already known to the SYSTEM and therefore does not need to be additionally declared in the ext file. We give this message the changed object:

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

This message is now received in the list of people and the list is then updated with the UpdateObox instruction:

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 ]

Due to the loose coupling, the list is updated when the window is open. There is no error if it is closed.

Summary

In this section you have learned how to

  • user guidance can be realised via Alert and ALTERED,
  • procedures are defined and used and
  • individual elements of a list can be updated.

Definition of relationships (relations and slots)

Objective

So far we can create, edit and display a lot of people. In this step we want to add the possibility to define relations between these objects. You will also get an introduction to the concept of dynamic data fields.

Relations

Relationships, i.e. relationships between objects, can be mapped in the CyberEnterprise® in various ways. An object A can refer to another object B. Furthermore, object A can refer to several objectsB1,B2, ..., Bn. The relation can also be empty. Then in the first case A does not refer to any object, i.e. to NULL. In the second case n = 0. A further distinction is whether there is a back relation to A from the objects B or Bk or not.

The following data types correspond to these variants in CyberEnterprise®:

Relation without regression with backrelation
B → A B → (A1,A2, ...,Am)
A → B POINTER REL_11 REL_1N
A → (B1,B2, ..., Bn) COLLECTION REL_N1 REL_MN

The complete description of a relation includes the restriction that it only refers to objects of a certain class. This then also includes objects of derived classes.

Slots

In our application we want to depict parent-child relationships between people. We would like to model these relations via the data fields mother, father and children. If we look in the documentation of class CX_PERSON, we see that these fields do not exist there. This brings us to a special feature of the CyberEnterprise®. The classes of the CyberEnterprise® only contain the data fields that are absolutely necessary for its function as a generally applicable module. The classes do not contain any data fields that might be useful anywhere. However, most CyberEnterprise® classes (all derived from CX_EXPANDABLE ) can be extended by dynamic data fields, called slots. This means that at runtime you write to a data field that is not even provided for in the class definition. Slots are used to construct characteristic strings according to DIN4000. The stock of possible slots can be seen as a set of company-wide variables. These variables are different in every company and can be defined and extended individually.

Slots have the following characteristics:

  • Slots largely behave like real data fields. This applies in full to InstantView®, e.g. for queries in the database.
  • A slot is never explicitly created. It is created when a writing instruction refers to it for the first time.
  • Objects can have different slots even though they are instances of the same class.
  • The stock of possible slots is managed by CyberEnterprise®. The introduction of further potential slots is up to the user.
  • In a certain sense, every expandable object has all potentially possible slots. However, the only virtually existing data fields do not occupy any memory space and have the special null value INVALID.

The slots mother, father and children have already been defined in the file classix.dic using the following lines

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)

The agreement of slots in the initialisation file is only one possibility. There are instructions that can be used to build such a definition at runtime. New slots can be agreed without having to stop the running application. The agreement with the slot directive defines the name and type of the slot and assigns a unique integer for internal coding. The descriptive multilingual text is optional.

All objects created in the database so far have of course none of these slots. When reading a non-existent slot, you get the value INVALID. This is displayed by FillWindow as an empty set or string.

Definition of relations on the surface

To define the relationship relations we create a new window:

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")) } }

Please implement the opening of this window for the exercise itself.

The ObjectCombobox is a user interface element that can be used to edit relations. The flag NULL_ELEMENT inserts an empty element at the beginning of the list, so that it is not only possible to select an object, but also not to select any object.

The ENTIRE flag is also worth mentioning. If this flag is set, the referenced object is automatically loaded into the list, but also only this object. The UI element then acts as a pure display of which object is referenced. This flag is particularly important for very large amounts of data, since otherwise all potential elements would be loaded into the list. The two flags NULL_ELEMENT and ENTIRE are very useful in combination because the list then only contains the empty and the referenced object and can therefore be used to display and delete the reference.

The flag NO_CLEAR ensures that the UI element is not cleared when the ClearWindow statement is executed.

Adding children to the selected person should be done via the overview window. If a person is double-clicked there, he/she should be assigned to the current person as a child. If again an item is selected in the list of children and the delete key is pressed, the person should be removed from the list of children. To do this we change the list as follows:

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 ]

Now we have to take care that the right children are loaded when another person is selected in the ObjectCombobox:

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 ]

This now enables us to assign children to a person and to load and display the assignment. However, we are not yet able to save the defined assignments. The slots are defined so that there is always a back relation. The relation in one direction is defined by the slot children. The relation in the other direction, however, is defined via father or mother, depending on the gender of the person. Therefore, when saving, the slot to be used must be defined manually using the BackRefName instruction:

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) ]

The last line sends again the message TUTORIAL1_PERSON_CHANGED to signal that the current person has been changed.

Anonymous procedures

We now want to add a column in the list of all persons with the number of children. For this purpose we change the list in the overview window as follows:

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 ]

The list gets another format element: CX_PERSON::call(...). The access expression is somewhat unusual. It looks like call is a function of the class CX_PERSON. But this is not so. The pseudo function call is responsible for objects of all classes. It calls a procedure, whose result it then returns. For short procedures that are only used once, a declaration including name is not worthwhile. In this case, anonymous procedures can be used as well, as is done here. Anonymous procedures are similar to Lambdas in other programming languages, such as Java, C++ or Python.

Summary

In this section you have learned,

  • how relations are defined in CyberEnterprise®,
  • what slots are and how they are used
  • how relations can be represented on the surface and
  • how anonymous procedures can be used in InstantView®.

Display of the family tree

Objective

In this section we want to display the family tree of a person. Therefore we will see how tree-like structures can be displayed.

family tree

The family tree of our persons should be displayed as a tree. In front of each name a symbol of the gender of the person should be displayed. For these symbols we first define two new variables and load the images when loading the module:

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)

The images are transient objects of class CX_BITMAP. With the statement Put the path to an image is written into the object and the object imports the image from the file.

Now we can create a new window with the family tree:

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 ] } }

Calling up this window is your exercise again.

From the point of view of programming with InstantView® an ObjectTree is something like an ObjectListView with hierarchical subordination. Therefore the only really new thing is the format specification with the flag NODE. This format element does not describe the representation, but specifies an access expression, which leads to the next hierarchical level.

Summary

In this section you have learned how easy it is to display tree structures with InstantView®. This is also the end of the tutorial Basic InstantView®. You can continue from here on with any of the other tutorials, depending on your interests.