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 …

    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.*
    ... some logic ...

DEFINE recB LIKE tableB.* 
    ... some logic ...

DEFINE recC LIKE tableC.* 
    ... some logic ...

… 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, and then also defining the triggers which is the first half of the next page

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.

     name STRING,
     datatype STRING

So at this point you have the equivalent of

    ON ACTION action-name

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

   LET trigger = d.nextEvent()
   CASE trigger

and respond to what the user does i.e the  triggers, that is explained in the second half of this page

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

CALL d.close()

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 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"
            LET this.fields[i].type = "STRING"

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,[l_row, l_column])
CALL d_da.setCurrentRow("data", 1)


… 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

        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 …

    CASE d_c.nextEvent()


    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.

    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
            LET this.where = this.where, " AND ", l_current_field_qbe
        END IF
    END IF
IF this.where IS NULL THEN
    LET this.where = "1=1"

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

    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
            CALL this.add_row_to_result(arr_curr())
        END IF
    END IF
    LET this.mode = "accept"

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.