Implementing dynamic dialogs

Dialogs can be created at runtime with the ui.Dialog class.

Dynamic dialog basics

The ui.Dialog class can create dialog objects at runtime, to implement generic code controlling forms that are created at runtime, when the data structure is not known at compile time.
Important: Dynamic dialogs are provided to resolve specific needs, like implementing a generic zoom window to select a record in a list, and control forms generated at runtime. This feature is not a replacement for regular "static" dialog instructions, used to control the forms defined in form specification files.

The dynamic dialogs can be used in conjunction with base.SqlHandle objects, to get database table column information in order to build forms dynamically.

Unlike static dialog instructions, dynamic dialogs do not require a data model (i.e. program variables containing the values for fields): Dynamic dialogs hold the data model internally, and behave by default in unbuffered mode: When an action is fired and the corresponding trigger handler is executed, the field values are available.

Creating the form

Before you instanciate a new ui.Dialog object, you must load an existing compiled .42f form, or create a new form dynamically in your program.

Forms build at runtime must be created with the ui.Window.createForm() method, and must contain a valid definition with layout containers, form fields, and screen records.

Note: See Genero BDL demos for a complete example of form creation at runtime.

The createForm() method will be invoked by using the current window. For the main form of the program, use directly the (empty) SCREEN window. For child windows, create the windows without a form by using following syntax:

OPEN WINDOW w1 WITH 1 ROWS, 1 COLUMNS

Assuming that there is a current empty window, you can then create the ui.Form object, to finally get the om.DomNode object to build your form:

DEFINE w ui.Window,
       f ui.Form,
       n om.DomNode
LET w = ui.Window.getCurrent()
LET f = w.createForm()
LET n = f.getNode()
...

Use om classes, to build you form dynamically. A good practice to create dynamic forms is to write first a .per file, that implements a static version of one of the forms you want to build at runtime. Compile the .per to a .42f and inspect the generated XML file, to understand the structure of the form file.

For more details, see:

Creating the dialog object

To reference the dialog object, first declare a variable with the type ui.Dialog:
DEFINE d ui.Dialog

The dynamic dialog creation methods take the list of field definitions as parameter, as a dynamic array with a record structure using two members to define the field name and data type.

In order to defined the fields used by the dynamic dialog, define a dynamic array with the following structure:
DEFINE fields DYNAMIC ARRAY OF RECORD
           name STRING,
           type STRING
       END RECORD
The field definition array will identify form fields and the data types to be used to store the values. The data types are provided as strings, using the same syntax as a regular Genero type:
LET fields[1].name = "formonly.cust_id"
LET fields[1].type = "INTEGER"
LET fields[2].name = "formonly.cust_name"
LET fields[2].type = "VARCHAR(50)"
LET fields[3].name = "formonly.cust_modts"
LET fields[3].type = "DATETIME YEAR TO FRACTION(5)"
Note: The type names used by the dynamic dialog API is the same as the type names returned by the base.SqlHandle.getResultType() method.

When the list of field definition is complete, create the dynamic dialog object.

To create a dynamic dialog handling simple record input:
LET d = ui.Dialog.createInputByName(fields)
For more details, see ui.Dialog.createInputByName.
To create a display array dynamic dialog:
LET d = ui.Dialog.createDisplayArrayTo(fields, "sr_custlist")
Note: The list handling, the createDisplayArrayTo() method requires the name of the screen record used to group form fields, as defined in the INSTRUCTIONS section of the .per form file.

For more details, see ui.Dialog.createDisplayArrayTo.

To create a dynamic dialog handling query by example:
LET d = ui.Dialog.createConstructByName(fields)
For more details, see ui.Dialog.createConstructByName.

Add user-defined triggers

Dynamic dialogs can be configured with user-defined triggers, for example to execute code when a specific action is fired.

After creating the dialog object, add user-defined triggers with the ui.Dialog.addTrigger() method:
DEFINE d ui.Dialog
...
CALL d.addTrigger("ON ACTION print")
CALL d.addTrigger("ON DELETE")
...

Note that some triggers must be identified with the user-defined action name, as in "ON ACTION print".

User-defined triggers will then be handled in the dynamic dialog loop, when the event occurs.

For more details, see: ui.Dialog.addTrigger.

Handling dialog events

To implement the "body" of a dynamic dynamic, mix a WHILE loop with the ui.Dialog.nextEvent() method, to handle dialog events.

The WHILE loop will act as the main event handler of your dynamic dialog, and will loop, waiting for dialog events until you explicitely exist the loop with an EXIT WHILE instruction.

DEFINE d ui.Dialog
...
WHILE TRUE
    CASE d.nextEvent()
       WHEN "BEFORE DISPLAY"
          ...
       WHEN "ON ACTION print"
          ...
       WHEN "ON DELETE"
          ...
       WHEN "AFTER DISPLAY"
          ...
END WHILE

Several implicit trigger names are supported by dynamic dialogs, such as "BEFORE ROW", "AFTER FIELD field-name". These triggers are equivalent to the static dialog control blocks, to control the behavior of your dynamic dialog.

The event handlers for the user-defined triggers that have been added with the addTrigger() method must also be handled in the dynamic dialog loop.

Inside the WHILE loop, control the behavior of the dialog with the methods provided in the ui.Dialog class. For example, to jump to a different field when the "jump" action is fired:
WHILE TRUE
    CASE d.nextEvent()
       WHEN "ON ACTION jump"
          CALL d.nextField("customer.cust_name")
    ...
BEFORE/AFTER FIELD handlers must be identified with the field name (without the table/formonly prefix):
WHILE TRUE
    CASE d.nextEvent()
       WHEN "AFTER FIELD cust_name"
          IF LENGTH(d.getFieldValue("customer.cust_name")) < 3 THEN
             ERROR "Customer name is too short"
             CALL d.nextField("customer.cust_name")
          END IF
    ...

For more details, see the ui.Dialog.nextEvent() method reference.

Handling field values

A dynamic dialog stores field values in internal buffers created according to the field definitions provided in the creation method. Access to these values is required, to implement the dynamic dialog. For example, to set default values before entering the dialog loop, modifying and/or querying values during the dialog loop, and to get the entered values after dialog termination when accepted by the user.

To set or get values of fields controlled by a dynamic dialog, use respectively the ui.Dialog.setFieldValue() and ui.Dialog.getFieldValue() methods.
Note: These methods take a form field name as parameter, that can be provided in different notations. See Identifying fields in dialog methods for more details.

When implementing a display array dynamic dialog handling a record list, the set/get field value methods apply to the current row: If you want to set or get field values of a particular row, first move to the row with the ui.Dialog.setCurrentRow() method.

The next example copies the values from the fields in the current row of a display array dynamic dialog (d_list), to the field buffers of a record input dynamic dialog (d_rec):
CALL d_list.setCurrentRow("sr_custlist", index)
FOR i=1 TO fields.getLength()
    CALL d_rec.setFieldValue( fields[i].name,
               d_list.getFieldValue(fields[i].name)
         )
END FOR

Get query conditions for a field

A dynamic dialog created with ui.Dialog.createConstructByName handles query by example input.

To generate the SQL condition from the search value entered in a construct field, use the ui.Dialog.getQueryFromField method, by passing the field name as parameter:
LET field_condition = DIALOG.getQueryFromField("customer.cust_name")
To build the complete WHERE part for the SELECT statement, iter through all form fields and concatenate the form field condition by separating with the AND or with the OR operator:
FOR i=1 TO fields.getLength()
    LET field_condition = d.getQueryFromField(fields[i].name)
    IF field_condition IS NOT NULL THEN
       IF where_clause IS NOT NULL THEN
          LET where_clause = where_clause, " AND "
       END IF
       LET where_clause = where_clause, field_condition
    END IF
END FOR

Implementing the accept and cancel actions

Regular static dialog instructions implement the accept and cancel actions, to respectively validate or abort the dialog.

These actions are created automatically for static dialogs, but must be created by hand for dynamic dialogs.

In the case of cancel, you can mimic the behavior of static dialogs by setting the INT_FLAG register to TRUE and then leave the WHILE loop with an EXIT WHILE.

For the accept action, call the ui.Dialog.accept() method to validate field input and leave the dialog, and execute an EXIT WHILE in the "AFTER INPUT" event to leave the dialog loop.

For example, to implement the accept and cancel actions for a simple record input:
DEFINE d ui.Dialog
...
LET d = ui.Dialog.createInputByName(fields)
CALL d.addTrigger("ON ACTION cancel")
CALL d.addTrigger("ON ACTION accept")
...
WHILE TRUE
  CASE d.nextEvent()
    WHEN "ON ACTION cancel"
      LET int_flag = TRUE
      EXIT WHILE
    WHEN "ON ACTION accept"
      CALL d.accept()
    WHEN "AFTER INPUT"
      EXIT WHILE
  END CASE
END WHILE

Terminating the dialog

Some synchronization code needs to be implemented to properly destroy the dynamic dialog. A dialog needs to be destroyed by closing its corresponding window/form.

In order to terminate a dialog, assign NULL to the ui.Dialog variable referencing the dialog object. This will destroy the object, if no other variables references it, and the corresponding window can then be closed:
LET d = NULL
CLOSE WINDOW w1

Combining dynamic dialogs with dynamic cursors

To write generic code accessing a database, implement the dynamic dialog with field names and types coming from the base.SqlHandle cursor.

The next code example builds a list of fields according to the database table passed as first parameter. The function scans the result set column names and types of the base.SqlHandle cursor, to build the list of field definitions, that can then be used for the dynamic dialog creation:
FUNCTION build_field_list(dbtable, fields)
    DEFINE dbtable STRING,
           fields DYNAMIC ARRAY OF RECORD
               name STRING,
               type STRING
           END RECORD
    DEFINE h base.SqlHandle,
           i INT

    LET h = base.SqlHandle.create()
    CALL h.prepare("SELECT * FROM " || dbtable)
    CALL h.open()
    CALL h.fetch()
    CALL fields.clear()
    FOR i=1 TO h.getResultCount()
        LET fields[i].name = h.getResultName(i)
        LET fields[i].type = h.getResultType(i)
    END FOR

END FUNCTION

For more details, see The SqlHandle class.