Demonstrates callback techniques using RECORD methods, dynamic Dialogs and Reflection.
Presented by Leo Schubert (Senior software engineer, Four Js) at WWDC 20 Online, November 17, 2020. Talk title: "Less code with Dynamic Dialogs, Interface Methods and Reflection"
This project is a variation of the $FGLDIR/demo/dbbrowser sample. A combination of
DISPLAY ARRAY / INPUT / CONSTRUCT dynamic dialogs is applied to different tables
(customers, orders, items).
The key difference from the dbbrowser sample: application code can register callbacks
for validation and events. Each module using the new dialog code is accompanied by a
classic-dialog equivalent for comparison.
- Classic dialogs are strongly tied to particular data — no generic reuse
- Reusing a classic dialog for different data types requires a code generator
- Difficult to isolate business logic from UI logic
- Custom UI rules in each dialog cause code duplication / boilerplate
Dynamic Dialogs (introduced in Genero 3.10) were created to overcome these limitations.
Raw dynamic dialogs still have shortcomings:
- Only
setFieldValue()/getFieldValue()for data access — tedious in application code - Field-name and event registration requires even more boilerplate than classic dialogs
- Events from
nextEvent()are plain strings — typos are possible - No built-in callback mechanism; a plain
WHILEloop is required
The goal of this PoC is to achieve all of the following:
- Write customizable generic UI dialog code encapsulated in a reusable library
- Use plain 4GL
RECORDtypes as data in the dialogs - Have a type-safe callback mechanism back into application / business logic
- Have compile-time checks for data column names used in dialogs
- Use parsed SQL in application code (library code computes prepared statements)
- Have a fail-safe mechanism comparable to
ON ACTION
| Feature | Since |
|---|---|
| Dynamic Dialogs | Genero 3.00 |
base.SqlHandle |
Genero 3.00 |
INTERFACE + methods for RECORDs |
Genero 3.20 |
Reflection (reflect module) |
Genero 4.00 |
| File | Description |
|---|---|
| classicINPUT.4gl | Classic INPUT dialog for a customer record |
| classicDA.4gl | Classic DISPLAY ARRAY dialog |
| File | Description |
|---|---|
| dynINPUT.4gl | Raw dynamic INPUT — shows the boilerplate without the library |
| File | Description |
|---|---|
| libINPUT.4gl | Generic INPUT library: encapsulates the dynamic dialog, event loop, field sync via reflection |
| appINPUT.4gl | Application code: implements event() callback on TM_Customer |
| File | Description |
|---|---|
| libI3.4gl | Library with 3-method interface and CONSTANT definitions to prevent typos |
| appI3.4gl | Application code using the 3-method interface (BeforeInput, event, AfterField) |
| File | Description |
|---|---|
| libINPUTIM.4gl | Library using multiple single-method interfaces, checked at runtime via reflection |
| appINPUTIM.4gl | Application code; includes CheckInterfaces() for compile-time conformance check |
| multiple_interfaces.4gl | Standalone demo of the multiple-interface dispatch pattern |
| File | Description |
|---|---|
| libDA.4gl | Generic DISPLAY ARRAY library using reflection to introspect and fill array data |
| appDA.4gl | Application code for the DISPLAY ARRAY demo |
| File | Description |
|---|---|
| customer_reflect.4gl | Short intro to the reflect module: iterating fields of a RECORD LIKE customer.* |
| test_interface.4gl | Testing at runtime whether a RECORD implements an INTERFACE via canAssignToVariable() |
| File | Description |
|---|---|
| sql2array.4gl | readIntoArray(): uses base.SqlHandle + reflection to fill a dynamic array from a SQL query for any RECORD type |
| File | Description |
|---|---|
| interface.4gl | Short INTERFACE / RECORD methods recapitulation |
| utils.4gl | Shared utilities (e.g. dbconnect) |
| utils_customer.4gl | Customer-specific utilities (e.g. updateCustomer) |
| cols_customer.4gl | Column name constants for the customer table |
The library defines an interface that application RECORDs must implement:
PUBLIC TYPE I_DynamicINPUT INTERFACE
event(d ui.Dialog, event STRING)
END INTERFACEThe TM_Input.input() method:
- Uses reflection on the passed
recValto discover field names and types from the current form - Creates the dynamic dialog via
ui.Dialog.createInputByName() - Runs the event loop and calls back
delegate.event()on each event - Syncs dialog field values back to the record via
reflect.Value.set()after eachAFTER FIELD
VAR formNode = ui.Window.getCurrent().getForm().getNode()
VAR l = formNode.selectByTagName("FormField")
-- for each FormField node: get colName attribute,
-- then look up the type in the record via recType.getFieldTypeByName(name)This means the library needs zero knowledge of the concrete RECORD type.
VAR dlgVal = reflect.Value.copyOf(self.d.getFieldValue(name))
VAR fieldval = recVal.getFieldByName(name)
CALL fieldval.set(dlgVal)PUBLIC CONSTANT ON_ACTION_accept = "ON ACTION accept"
PUBLIC CONSTANT AFTER_FIELD = "AFTER FIELD"
CONSTANT C_zipcode = "zipcode"Using constants enables code completion and compiler checks instead of raw string literals.
Rather than forcing every application RECORD to implement every callback method, the library
accepts a reflect.Value delegate and tests at runtime:
IF delegate.canAssignToVariable(iBI) THEN
CALL delegate.assignToVariable(iBI)
CALL iBI.BeforeInput(d)
END IFA never-called CheckInterfaces() method on the RECORD provides compile-time verification:
FUNCTION (self TM_Cust) CheckInterfaces()
DEFINE iBI I_BeforeInput
RETURN
LET iBI = self -- compiler checks conformance
END FUNCTIONbase.SqlHandle and reflection work together to populate a typed dynamic array from
an arbitrary SQL query without per-column assignment code:
CALL readIntoArray(reflect.Value.valueOf(a), "select * from customer", FALSE)Supports SELECT, INSERT, and UPDATE. Performance is slower than parsed SQL;
keep result sets under ~300 rows.
| Goal | Result |
|---|---|
| Generic reusable UI dialog library | Yes |
| Use plain 4GL RECORD types as data | Yes |
| Type-safe callback into business logic | Yes |
| Compile-time checks for column names | Partial — requires CONSTANT or generated constants |
| Parsed SQL in application code | Yes |
Fail-safe mechanism like ON ACTION |
No — adding and checking an action still requires 2 steps |
Comparing classic vs. new approach for this use case, the new code needs roughly half the lines in application code and carries significantly less boilerplate. The pattern (WHILE loop, CONSTRUCT filter, INPUT/append) is written once in the library and reused across tables.
The library can additionally handle things that classic dialogs require per-dialog boilerplate for:
-
Row count updates in
BEFORE ROW -
Toolbar state management
-
Filter loop (
WHILEaroundDISPLAY ARRAY+CONSTRUCT) -
Global actions without a preprocessor
-
ON FILLBUFFERand multiple selection -
Adaptive layout: multiple dialog on desktop, cascaded single dialog on mobile
See https://github.com/leopatras/generic_dlg_reflect with a more advanced version of the demo program.
- Slides / original repository: https://github.com/leopatras/generic_dlg_reflect
- This repository: https://github.com/FourjsGenero/ex_wwdc2020_generic_dialogs
- Dynamic Dialogs docs: http://4js.com/online_documentation/fjs-fgl-manual-html/?path=fjs-fglmanual#fgl-topics/c_fgl_dynamic_dialogs.html
- Ask Reuben ig-24 (Dynamic Dialogs): https://4js.com/ask-reuben/ig-24/
- base.SqlHandle docs: http://4js.com/online_documentation/fjs-fgl-manual-html/?path=fjs-fglmanual#fgl-topics/c_fgl_ClassSqlHandle_create.html
- Ask Reuben ig-25 (SqlHandle): https://4js.com/ask-reuben/ig-25/
- INTERFACE docs: http://4js.com/online_documentation/fjs-fgl-manual-html/?path=fjs-fglmanual#fgl-topics/c_fgl_Interface.html