Ask Reuben

ComboBox Initializer Function

What is a ComboBox Initializer Function?

How can I populate my ComboBox from a database query? 

Why do I get a -1338 error with a ComboBox Initializer Function?

The list of entries that appear in a COMBOBOX can be populated programmatically.  That is rather than defining a static list using the ITEMS attribute,  you can populate it at runtime via 4gl code.  This can use the INITIALIZER  syntax in the form file to indicate what function to use to populate the ComboBox list if items when the form is loaded.  This function will use methods in the ui.ComboBox class to clear and add items to the list.   This function can also be called on demand to repopulate the list.

I won’t dive too deep this class, if you can understand ui.ComboBox.forName(fieldname) to get the ui.ComboBox object for a particular combobox and how to use the ui.ComboBox.addItem(code, text) method to add items to the ComboBox you will go a long way.  See these two code examples if you have not seen them before.

I have mentioned in a previous Ask-Reuben how you can take care with the second parameter for the addItem method to improve the User Experience with keystrokes.

With ComboBox Initializer functions, an improvement I can see is that rather than defining a database cursor inside each function, you can use  base.SqlHandle class to populate a ComboBox with the result of an SQL.  The function populate_sql() in this GitHub repository takes two arguments, a fieldname and an SQL string.  It populates the ComboBox corresponding to that field with the result of the SQL statement.  As a result you might end up with functions like the following …

FUNCTION combobox_populate_country(fieldname STRING)
    CALL combobox_populate_sql(fieldname, SELECT code, desc FROM country ORDER BY code","%2 (%1)")
END FUNCTION

FUNCTION combobox_populate_state(fieldname STRING)
    CALL combobox_populate_sql(fieldname, SELECT code, desc FROM state ORDER BY code","%2 (%1)")
END FUNCTION

FUNCTION combobox_populate_city(fieldname STRING)
    CALL combobox_populate_sql(fieldname, SELECT code, desc FROM city ORDER BY code","%2 (%1)")
END FUNCTION

and it makes it a simple task for a junior developer to quickly populate a ComboBox.

The above example has fieldname STRING as the first argument, it could easily be a cb ui.ComboBox object, thus making the function directly usable as an INITIALIZER parameter e.g.

FUNCTION combobox_populate_country(cb ui.ComboBox)
    CALL combobox_populate_sql(cb, SELECT code, desc FROM country ORDER BY code","%2 (%1)")
END FUNCTION

FUNCTION combobox_populate_state(cb ui.ComboBox)
    CALL combobox_populate_sql(cb, SELECT code, desc FROM state ORDER BY code","%2 (%1)")
END FUNCTION

FUNCTION combobox_populate_city(cb ui.ComboBox)
    CALL combobox_populate_sql(cb, SELECT code, desc FROM city ORDER BY code","%2 (%1)")
END FUNCTION

Once you get into the business of calling functions from the INITIALIZER function, you may encounter a -1338 error.

The function ‘function name’ has not been defined in any module in the program

There is a reason why this occurs and a simple solution.

When the runner fglrun is interpreting the current line of code being executed, if it encounters a function being called, it says …

Is this function in the current .42m ? 

If Yes, execute the lines in that function. 

If No, then it has to find (using FGLLDPATH) the .42m that has that function and load it….

If the function has been referenced using module.function then the runner will find, read and use module.42m.  

If there are IMPORT FGL defined then it will look inside those .42m for a function of that name

Otherwise it will look inside program.42r which will contain a list of functions and the modules they are in.

In this last step, there is potential for some optimisation to have occurred in the building of the .42r which has the undesired effect of removing a reference to a function and the module it can be found in.

What the link optimisation does is only include function/module references in the .42r where a path can be shown from MAIN to that function.   It says  why include functions in this big long lookup list if it is never called?   This link optimisation makes the list smaller and any lookups quicker.  The problem then arising, what if the function is never called in a path from MAIN but only from the use of INITIALIZER, this list item is removed.

The code at the end of the article can be used to quickly illustrate this.

At runtime, when the form is loaded in askreuben164_main.4gl, the INITIALIZER function combo_same() is in the current 4gl and can be found.  The other INTIALIZER function combo_different() is not in the current module, there is no reference to it in the .42r as no code ever calls it from MAIN, and so the -1338 error results.

One modern solution is to be explicit in the INITIALIZER defintion, that is use askreuben164_lib.combo_different (module.function) so the runtime knows to find and look inside askreuben164_lib.42m for combo_different().  Another solution is to trick the optimisation process into thinking that the function is indeed used.  That is achieved with some code like …

IF 1=0 THEN
    CALL combo_different(ui.ComboBox.forName("dummy"))
END IF

… positioned somewhere so that there is a path to it from MAIN.  The link optimisation does not evaluate the expressions to see if the path is ever executed.  It sees it the same as …

IF foo() THEN
     CALL combo_different(ui.ComboBox.forName("dummy")) 
END IF

It includes it in the .42r because there is a “path” to that function from MAIN.

In the example, you can uncomment some lines that illustrate the potential solutions.

This -1338 error has the potential to occur for any functions that are only ever called from an INITIALIZER attribute, as there is no path from MAIN to these functions.  So if you ever see the -1338 error when implementing a COMBOBOX INITIALIZER and and you think you have done everything right, ask yourself is there a path from MAIN to that function?


#! askreuben164_main.4gl
MAIN
DEFINE rec RECORD
    field1 CHAR(1),
    field2 CHAR(1)
END RECORD

    OPEN WINDOW w WITH FORM "askreuben164"
    INPUT BY NAME rec.*

    --IF 1=0 THEN
    --    CALL combo_different(ui.ComboBox.forName("dummy"))
    --END IF
END MAIN

FUNCTION combo_same(cb ui.ComboBox)
    CALL cb.addItem("Y","Y-Yes")
    CALL cb.addItem("N","N-No")
END FUNCTION

#! askreuben164_lib.4gl
FUNCTION combo_different(cb ui.ComboBox)
    
    CALL cb.addItem("N","N-No")
    CALL cb.addItem("Y","Y-Yes")
END FUNCTION

#! askreuben164.per

LAYOUT
GRID
{
Function in same module             [f01       ]
Function in different module        [f02       ]

}
END
END
ATTRIBUTES
COMBOBOX f01 = formonly.field1, INITIALIZER=combo_same, NOT NULL;
COMBOBOX f02 = formonly.field2, INITIALIZER=combo_different, NOT NULL;
--COMBOBOX f02 = formonly.field2, INITIALIZER=askreuben164_lib.combo_different, NOT NULL;

fglform askreuben164.per

fglcomp askreuben164_lib.4gl
fgllink -o askreuben164_lib.42x askreuben164_lib.42m

fglcomp askreuben164_main.4gl
fgllink -o askreuben164.42r askreuben164_main.42m askreuben164_lib.42x