Ask Reuben

Dynamic Forms

How does fgl_zoom work without a form defined?

Can I generate a form on the fly?

You may have noticed that the last 3 Ask Reuben’s have all featured new functionality that made fgl_zoom, the repository I illustrated in an earlier Ask-Reuben possible.  Today I cover the last piece of the puzzle, and that is the concept of Dynamic Forms, or In-memory Forms.  This is not a concept that was new to 3.20, these techniques have been available from early on in Genero’s lifetime.

With the fgl_zoom library you might notice that there is no form defined.   Where is the .per, or .4fd. and the resultant .42f ?  The answer is there isn’t one, I create it in memory as we go along.

First a little history, experienced 4gl programmers might recognise the code pattern of

IF some-condition THEN
    LET form_name = "form-name_1"
ELSE
    LET form_name = "form-name_2"
END IF
OPEN WINDOW w WITH FORM form_name

That is two or more forms are maintained and based on some condition the appropriate form is opened.  An example might be that only users with a certain role are able to view an employees salary so there are two forms, one with a salary field and one without.

Maintaining two or more forms with only a slight variation is both expensive and dumb, another solution at the time was to only have one form and then do a …

DISPLAY integer SPACES AT line,column

… and blank out the data you weren’t supposed to see.  Again looking back, you think equally dumb but that is what pre-Genero developers had to do with the tools available.

With Genero the introduction of the hidden attribute meant that you could start hiding fields and information that you did not want users to see.  You could use the ui.Form methods setFieldHidden and setElementHidden to hide fields and elements you did want the user to see e.g

CALL f.setFieldHidden("employee.salary", user_cant_see_salary())

This technique was good for a minor adjustment to a form, create the form with maximum content, and then hide fields or containers as required.

However for generic forms, you don’t want a technique of putting every field there and hiding the ones you don’t want, the better technique is to build up the form from the bottom-up with just the fields you want to see in this particular instance.

Techniques to do this have evolved from having your 4gl code creating a .per file, compiling it, and then opening it.  Genero developers soon recognised that the .42f file is an XML text file and that you could eliminate the compilation step by simply creating a .42f file on the fly by writing out the correct XML.

What I did in fgl_zoom was taking this one step further and instead of creating a .42f file, writing it to disk, and then opening it, I did this all in memory.  This is the concept of creating forms dynamically as introduced here.

To illustrate by example, with the fgl_zoom repository, in the file fgl_zoom.4gl, find the OPEN WINDOW syntax …

OPEN WINDOW fgl_zoom WITH 1 ROWS, 1 COLUMNS ATTRIBUTES(STYLE = IIF(this.combobox, "combobox fgl_zoom","fgl_zoom"), TEXT = % "fgl_zoom.window.title")
LET this.window = ui.Window.getCurrent()
CALL this.create_form()

The  WITH 1 ROWS, 1 COLUMNS is syntax that was in Informix-4gl that says to open an empty window of the specified size.  The number of rows and columns was useful in a TUI environment but is ignored in a GUI environment, so ignore the fact it says 1 row, 1 column, perhaps one day we will enhance the compiler to allow a clause like OPEN WINDOW fgl_zoom WITH NO FORM, as that is more appropriate and is more indicative of what is going on.  For now we are simply opening an empty window and we are going to display content to it later.  (If you are wondering about the combobox style, it simply refers to opening the zoom window with less decoration so that it can be made to look like a combobox dropdown)

It is in the create_form() function that I then create the form in memory and attach it to this window.  So next find the function create_form().

The first lines create a Form node beneath our Window node …

LET this.form = this.window.createForm("fgl_zoom")
LET form_node = this.form.getNode()

… and then define some attributes about the form …

CALL form_node.setAttribute("width", 10 * this.column.getLength() + 2)
CALL form_node.setAttribute("height", "16")

… then we get into the business of adding some containers for the form and defining attributes about them, in this case a VBox and a Table

LET vbox_node = form_node.createChild("VBox")
CALL vbox_node.setAttribute("name", "vbox")

-- Create table node
LET table_node = vbox_node.createChild("Table")
CALL table_node.setAttribute("pageSize", 15)
CALL table_node.setAttribute("name", "tablist")
IF NOT this.header THEN
    CALL table_node.setAttribute("style", "fgl_zoom noheader")
ELSE
    CALL table_node.setAttribute("style", "fgl_zoom")
END IF
CALL table_node.setAttribute("height", "15ln")
CALL table_node.setAttribute("tabName", "data")
CALL table_node.setAttribute("doubleClick", "accept")

and then for each of the columns we want in the table, add a TableColumn node as a child of the Table node

-- TableColumn nodes
FOR i = 1 TO this.column.getLength()
    LET tablecolumn_node = table_node.createChild("TableColumn")
    CALL tablecolumn_node.setAttribute("name", SFMT("formonly.%1", this.column[i].columnname)) #TODO
    CALL tablecolumn_node.setAttribute("sqlTabName", "formonly")
    CALL tablecolumn_node.setAttribute("colName", this.column[i].columnname) #TODO
    CALL tablecolumn_node.setAttribute("fieldId", (i - 1) USING "<&")
    CALL tablecolumn_node.setAttribute("text", this.column[i].title)

and with each of these table column nodes adding a widget ( one of my TODO's is to enhance this to use different widgets e.g. DateEdit for dates) .

LET widget_node = tablecolumn_node.createChild("Edit")
CALL widget_node.setAttribute("width", this.column[i].width)

IF this.column[i].format IS NOT NULL THEN
    CALL widget_node.setAttribute("format", this.column[i].format)
END IF

The question you are asking is how do you know what nodes and attribute values to use?  There are two closely related techniques, one is to look at a .42f in a text editor, so experiment creating the form you are trying to generically create by writing out in a .per / .4fd and compiling it.  Have a look at the resulting .42f.  The second is to do the same but look in the GUI Debug Tree at the nodes and attribute values created.  The form you are creating in memory is going to have to have similar node and attribute values. (I showed how to look at the GUI Debug Tree in an earlier Ask-Reuben)

So learn by creating a simple .per, add the containers, widgets, and attributes you are interested in, and then look in the resultant .42r, and/or look in the GUI Debug Tree to see what the actual node names are, what the actual attribute names are, what the actual attribute values are, and what the relationship between the nodes is.  Take baby steps, start with a form with one widget, add attributes, add extra fields, different containers, different widgets, learn what is required.

Some key things to remember,

  • the GUI Debug Tree will bold the attribute names that have been changed from the default, so you don't necessarily have to explicitly define every attribute you see in the GUI Debug Tree.  Keep your code simple and rely on the defaults
  • where a GUI widget is used to enter a value, there will be a parent node that will have the attributes that are independent of the widget used e.g. name, sqlTabName, colName, fieldId, text etc, whilst the child widget node will have the attributes that are unique to that widget.
  • with name attribute, take care to see if you can specify a simple name e.g. field1 or if you need to prefix it with table or formonly e.g. formonly.field1
  • the syntax used in the .per or Genero Studio Form Designer is not necessarily the node or attribute name or value.  In most cases it is but sometimes it is not.

The last un-explained piece of the code ...

-- Create record view
LET recordview_node = form_node.createChild("RecordView")
CALL recordview_node.setAttribute("tabName", "formonly")

-- Link nodes
FOR i = 1 TO this.column.getLength()
    LET link_node = recordview_node.createChild("Link")
    CALL link_node.setAttribute("colName", this.column[i].columnname)
    CALL link_node.setAttribute("fieldIdRef", (i - 1) USING "<&")
END FOR

... is for internal use, but you can think of it as the information in the INSTRUCTIONS, SCREEN RECORD syntax of the .per, and it being added into the form in memory.  if you look in the .42f or GUI Debug Tree you should see the entries where I got this from.

The final point is that every major release, the contents of a compiled form, and what you see in the GUI Debug Tree MAY change.  I would say unlikely to change but if we are going to make any changes, it will be at the time of  a major release.  So with these code techniques you should be prepared to participate in Early Access Programs and if there are changes, catch them early, and provide feedback or adapt your code.