Ask Reuben

Dynamic Dialogs

How is fgl_zoom coded when there is no CONSTRUCT or DISPLAY ARRAY visible in the code?

How can I avoid having to write a second INPUT statement when the only thing different is the field-list?

How can I avoid writing a second DISPLAY ARRAY statement when the only thing different is the number of columns?

What is a Dynamic Dialog?

The fgl_zoom repository can trace its history back to a function named query_win() that lived inside a module coxxq01.4gl .  If you google you can find traces of it in IIUG software repositories.

It included wonderful CASE statements like so …

CASE
    WHEN l_cnt = 1 CONSTRUCT l_text ON qdkey1 FROM keyqbe1 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 2 CONSTRUCT l_text ON qdkey1, qdkey2 FROM keyqbe1, keyqbe2 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 3 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3 FROM keyqbe1, keyqbe2, keyqbe3 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 4 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3, qdkey4 FROM keyqbe1, keyqbe2, keyqbe3, keyqbe4 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 5 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3, qdkey4, qdkey5 FROM keyqbe1, keyqbe2, keyqbe3, keyqbe4, keyqbe5 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 6 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3, qdkey4, qdkey5,qdkey6 FROM keyqbe1, keyqbe2, keyqbe3, keyqbe4, keyqbe5,keyqbe6 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 7 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3, qdkey4, qdkey5, qdkey6, qdkey7 FROM keyqbe1, keyqbe2, keyqbe3, keyqbe4, keyqbe5, keyqbe6, keyqbe7 ATTRIBUTE(NORMAL)
    WHEN l_cnt = 8 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3, qdkey4, qdkey5, qdkey6, qdkey7, qdkey8 FROM keyqbe1, keyqbe2, keyqbe3, keyqbe4, keyqbe5, keyqbe6, keyqbe7, keyqbe8 ATTRIBUTE(NORMAL) 
    WHEN l_cnt = 9 CONSTRUCT l_text ON qdkey1, qdkey2, qdkey3, qdkey4, qdkey5, qdkey6, qdkey7, qdkey8, qdkey9 FROM keyqbe1, keyqbe2, keyqbe3, keyqbe4, keyqbe5, keyqbe6, keyqbe7, keyqbe8, keyqbe9 ATTRIBUTE(NORMAL)
...

… that is a different CONSTRUCT statement depending if there were 0,1,2,3,… and so on fields.  In the earlier 90’s that was how things were done if you wanted to write a generic function instead of coding each one individually.

In a similar vein, you may have dialogs that you want to have the same behaviour for different records

DEFINE recA LIKE tableA.*
...
INPUT BY NAME recA.*
    ... some logic ...
END INPUT

DEFINE recB LIKE tableB.* 
...
INPUT BY NAME recB.*
    ... some logic ...
END INPUT

DEFINE recC LIKE tableC.* 
...
INPUT BY NAME recC.*
    ... some logic ...
END INPUT

… you may recognise the pattern where the code inside the dialogs is effectively the same in each case, the only thing different is the name of the record and columns, and number of columns and fields.  It has always seemed a waste to repeat that code with only subtle differences.

This is where the concept of Dynamic Dialogs comes in.  They allow you to implement generic code where the data structure is not known at compile time, and the generic code can be reused for many different data structures.

The repository fgl_zoom makes heavy use of Dynamic Dialogs.  Given a SELECT SQL statement used to populate a list, it uses this as the starting point to dynamically build a CONSTRUCT so that the user can perform a QBE to add a where clause to this sql statement, and to dynamically build a DISPLAY ARRAY to display the result-set of the SQL statement and allow the user to select a row.

To understand the code for Dynamic Dialogs I like to break it into two stages.

The first is to define the dialog.  This is where you instantiate it with the steps on this page https://4js.com/online_documentation/fjs-fgl-manual-html/#fgl-topics/c_fgl_dynamic_dialogs_create.html, and then also defining the triggers which is the first half of the next page https://4js.com/online_documentation/fjs-fgl-manual-html/#fgl-topics/c_fgl_dynamic_dialogs_triggers.html

So these types of lines of code setup the dialog …

DEFINE d ui.Dialog
LET d= ui.Dialog.createInputByName(fields-array)   # different method for CONSTRUCT, DISPLAY ARRAY etc
CALL d.addTrigger("ON ACTION action-name")

… in future versions, look out for additional DIALOG methods that may allow you to specify additional dialog attributes.

The fields-array is a DYNAMIC ARRAY of a RECORD where the first two members are the name of each field, and the datatype of each field e.g.

DEFINE fields DYNAMIC ARRAY OF RECORD
     name STRING,
     datatype STRING
END RECORD

So at this point you have the equivalent of

INPUT BY NAME record-name.* ATTRIBUTES(UNBUFFERED, WITHOUT DEFAULTS=TRUE)
    ON ACTION action-name
END INPUT

The second stage is to execute the dialog.  This is a WHILE loop around the nextEvent() method

WHILE TRUE
   LET trigger = d.nextEvent()
   CASE trigger
       ...
    END CASE
END WHILE

and respond to what the user does i.e the  triggers, that is explained in the second half of this page https://4js.com/online_documentation/fjs-fgl-manual-html/#fgl-topics/c_fgl_dynamic_dialogs_triggers.html

WHEN trigger = "BEFORE INPUT"
    ...
WHEN trigger = "ON ACTION action-name"
    ...
WHEN trigger = "AFTER INPUT"
 ...

For triggers that would normally exit the dialog then you need to add an EXIT WHILE to exit the while loop, and then after the WHILE loop you need to close and free-up the dialogs https://4js.com/online_documentation/fjs-fgl-manual-html/#fgl-topics/c_fgl_dynamic_dialogs_terminate.html

CALL d.close()
LET d= NULL

What I haven’t mentioned is how to read the values the user enters and how to set the value in the display.  This is covered here https://4js.com/online_documentation/fjs-fgl-manual-html/#fgl-topics/c_fgl_dynamic_dialogs_fields.html and involves the setFieldValue and getFieldValue methods.  There are two quirks to note.  With a CONSTRUCT there is a getQueryFromField which adds the QBE clause.  With an array you work with one row of the array at a time, and so use the setCurrentRow method to indicate what row you are working with.

There are a few other methods but what you have hopefully realised is that if there was some 4gl syntax, there must be a ui.Dialog method equivalent.  So for NEXT FIELD there is ui.Dialog.nextField etc

So how does fgl_zoom using Dynamic Dialogs.  In fgl_zoom.4gl there are two dynamic dialogs.  A CONSTRUCT so in that code look for a function createConstructByName, and a DISPLAY ARRAY so look for a function createDisplayArrayTo.

LET d_c = ui.Dialog.createConstructByName(this.fields)
...
LET d_da = ui.Dialog.createDisplayArrayTo(this.fields, "data")

They both take as input a variable this.fields.  This is the DYNAMIC ARRAY that has been populated with the field names and data-types for each column as found in the this.column and this.sql parameter. This array is populated as part of the execute() method.  (You will notice I only use 4 data-types.  This is more a hangover from previous iterations of this function before dynamic dialogs were available, but those 4 types were enough for sorting and display to be correct.  I should perhaps change it to force the developer to specify the exact datatype rather than a 1 character code)

CALL this.fields.clear()
FOR i = 1 TO this.column.getLength()
    LET this.fields[i].name = this.column[i].columnname
    CASE this.column[i].datatypec
        WHEN "i"
            LET this.fields[i].type = "INTEGER"
        WHEN "d"
            LET this.fields[i].type = "DATE"
        WHEN "f"
            LET this.fields[i].type = "FLOAT"
        OTHERWISE
            LET this.fields[i].type = "STRING"
    END CASE
END FOR

Immediately after the two create methods, you will see the respective addTrigger methods

CALL d_c.addTrigger("ON ACTION close")
CALL d_c.addTrigger("ON ACTION list")
CALL d_c.addTrigger("ON ACTION accept")
...
CALL d_da.addTrigger("ON ACTION copy")
CALL d_da.addTrigger("ON ACTION copyall")
CALL d_da.addTrigger("ON ACTION qbe")
CALL d_da.addTrigger("ON ACTION selectnone")
CALL d_da.addTrigger("ON ACTION selectall")
CALL d_da.addTrigger("ON ACTION print")

CALL d_da.addTrigger("ON ACTION accept")
CALL d_da.addTrigger("ON ACTION cancel")
CALL d_da.addTrigger("ON ACTION close")

With the CONSTRUCT and the DISPLAY ARRAY there is an interesting difference between the way the values are populated.  A DISPLAY ARRAY is populated before the array i.e. before the while loop

FOR l_row = 1 TO l_row_count
    CALL d_da.setCurrentRow("data", l_row)
    FOR l_column = 1 TO l_sqlh.getResultCount()
        CALL d_da.setFieldValue(this.fields[l_column].name, this.data[l_row, l_column])
    END FOR
END FOR
CALL d_da.setCurrentRow("data", 1)

WHILE TRUE

… note the use of setCurrentRow to set the row number, and also to set the row back to the first row when finished,  whilst the default values for the CONSTRUCT are displayed as part of the BEFORE CONSTRUCT trigger

WHILE TRUE
    ...
    WHEN "BEFORE CONSTRUCT"
        ...
        FOR i = 1 TO this.column.getLength()
            -- Display the default value
            IF this.column[i].qbedefault IS NOT NULL THEN
                CALL d_c.setFieldValue(this.column[i].columnname, this.column[i].qbedefault)
            END IF
        END FOR

In both dialogs, you should see a WHILE loop and a CASE statement to respond to the users action …

WHILE TRUE
    CASE d_c.nextEvent()

...

WHILE TRUE
    LET l_event = d_da.nextEvent()
    CASE l_event

In the CONSTRUCT there is some code to make sure the user has entered some QBE criteria, note the use of getQueryFromField() to retrieve the value entered, and the use of nextField() method and CONTINUE WHILE to do the equivalent of NEXT FIELD field-name.

WHEN "ON ACTION accept"
    ...
    FOR i = 1 TO this.column.getLength()
        IF d_c.getQueryFromField(this.column[i].columnname) IS NOT NULL THEN
        ...
            IF this.column[i].qbeforce THEN
                ERROR % "fgl_zoom.error.column.qbeforce"
                CALL d_c.nextField(this.column[i].columnname)
                CONTINUE WHILE

When the CONSTRUCT is finished and there is no error, use getQueryFromField() to get the values entered and build up the where clause.

 -- Create SQL clause
LET this.where = NULL
FOR i = 1 TO this.fields.getLength()
    LET l_current_field_qbe = d_c.getQueryFromField(this.column[i].columnname)
    IF l_current_field_qbe IS NOT NULL THEN
        IF this.where IS NULL THEN
            LET this.where = l_current_field_qbe
        ELSE
            LET this.where = this.where, " AND ", l_current_field_qbe
        END IF
    END IF
END FOR
IF this.where IS NULL THEN
    LET this.where = "1=1"
END IF

Interesting code in the DISPLAY ARRAY triggers is when the user accepts, the code needs to get the values in the selected row(s), (multiple in case multiple row select is enabled), and then the EXIT WHILE to exit the while loop

WHEN "ON ACTION accept"
    LABEL lbl_accept:
    IF this.mode = "list" THEN
        IF this.multiplerow THEN
            FOR l_row = 1 TO l_row_count
                IF d_da.isRowselected("data", l_row) THEN
                    CALL this.add_row_to_result(l_row)
                END IF
            END FOR
        ELSE
            CALL this.add_row_to_result(arr_curr())
        END IF
    END IF
    LET this.mode = "accept"
    EXIT WHILE

Both dialogs when finished, then both set the dialog variable to NULL to signify done with the dialog.

LET d_c = NULL
...
LET d_a = NULL

I hope this has given you a good insight into dynamic dialogs and how they can be used to write generic code.  In FGLDIR/demo/dbbrowser you will find another instance of an example using Dynamic Dialogs.

You may look at generic code and decide it is complex but the key point is it reduces code duplication, and makes your application more consistent and simpler to maintain.  It allows the code calling the functions with the dynamic dialogs to be simpler.  If fgl_zoom replaces 100+ instances of lookups from a BUTTONEDIT, then if you subsequently want to make a change, you do this once in your generic library, rather than 100+ times.  To code a new lookup using fgl_zoom requires 10-30 unique lines of simple code that is just setting parameters.  You would not write all your dialogs using Dynamic Dialogs.

If you look through your application, you may find many other patterns or groupings where you might be able to use a generic library using Dynamic Dialogs rather than coding many similar looking static dialogs.