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.