Ask Reuben – October 8, 2025
Maintaining a Sorted Table
Why is my Table no longer sorted when I modify a row?
How can I keep my Table sorted when an array is refreshed?
One of the dilemmas within Genero is to determine who controls the sorting when you allow a user to sort tables by clicking on the column header? If the program then modifies data in the array, or the user modifies a row in the array, should the sort be re-applied to respect what the arrow(s) in the column header are indicating? The decision made many years ago was to have the rule that can be found here https://4js.com/online_documentation/fjs-fgl-manual-html/#fgl-topics/c_fgl_prog_dialogs_list_sort.html When the program array is modified interactively, or by program with The motivation for the rule was that if a user or program modified a row, we did not want the modified row suddenly moving out of view. Better to have the freshly modified row still visible, even if out of position according to the indicated sort. The user can always reapply the sort by clicking on the column header(s) again to get the row into the sorted position. This approach works fine except in the case where you have an action in a DISPLAY ARRAY that refreshes the entire array. If the number of rows changes with this action, the visual array will be sorted as per the users sort. If the number of rows does not change, then the array will not be resorted as per the users sort. The expectation of the person asking this question is that the array should be visually sorted. There is a little trick you can do for this scenario. If you add a ui.dialog.deleteAllRows call then this will trick the runtime into reapplying the users sort. This can be illustrated with the attached program. With the program : if you examine the code you can hopefully see that once you click the “Add deleteAllRows call ” then each time you click “Smaller”, “Greater”, “Same”, a ui.Dialog.deleteAllRows call is executed before the arr.clear() call.
DIALOG
methods, the runtime does not perform row sorting, to keep new created rows visible. However, the runtime will perform row sorting, when the length of program array changes, after using DYNAMIC ARRAY
methods like appendElement()
#! askreuben298.4gl
IMPORT util
DEFINE arr DYNAMIC ARRAY OF RECORD
field1 INTEGER,
field2 STRING,
field3 INTEGER
END RECORD
DEFINE delete_all_rows_call_flag BOOLEAN
MAIN
DEFINE arr_length INTEGER
DEFINE program_name STRING
WHENEVER ANY ERROR STOP
DEFER INTERRUPT
DEFER QUIT
OPTIONS FIELD ORDER FORM
OPTIONS INPUT WRAP
LET program_name = base.Application.getProgramName()
CALL ui.Interface.loadStyles(program_name)
CALL ui.Dialog.setDefaultUnbuffered(TRUE)
CLOSE WINDOW SCREEN
OPEN WINDOW w WITH FORM program_name ATTRIBUTES(TEXT = program_name)
DISPLAY ARRAY arr TO scr.*
BEFORE DISPLAY
LET arr_length = 10
LET delete_all_rows_call_flag = FALSE
CALL populate_array(arr_length)
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
-- refresh the array, with less, the same, or more rows
ON ACTION smaller ATTRIBUTES(TEXT="Smaller")
LET arr_length = arr_length - 1
IF delete_all_rows_call_flag THEN
CALL DIALOG.deleteAllRows("scr")
END IF
CALL populate_array(arr_length)
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
ON ACTION same ATTRIBUTES(TEXT="Same")
IF delete_all_rows_call_flag THEN
CALL DIALOG.deleteAllRows("scr")
END IF
CALL populate_array(arr_length)
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
ON ACTION greater ATTRIBUTES(TEXT="Larger")
LET arr_length = arr_length + 1
IF delete_all_rows_call_flag THEN
CALL DIALOG.deleteAllRows("scr")
END IF
CALL populate_array(arr_length)
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
ON ACTION random ATTRIBUTES(TEXT="Random")
LET arr_length = util.Math.rand(26)+1
IF delete_all_rows_call_flag THEN
CALL DIALOG.deleteAllRows("scr")
END IF
CALL populate_array(arr_length)
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
-- also test these scenarios
ON DELETE
LET arr_length = arr_length - 1
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
ON INSERT
INPUT arr[arr_curr()].* FROM scr[scr_line()].*
IF int_flag THEN
LET int_flag = 0
ELSE
LET arr_length = arr_length + 1
END IF
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
ON UPDATE
INPUT arr[arr_curr()].* FROM scr[scr_line()].* ATTRIBUTES(WITHOUT DEFAULTS=TRUE)
IF int_flag THEN
LET int_flag = 0
END IF
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
-- flag to add / remove a call to ui.dialog.deleteAllRows
ON ACTION add_delete_all_rows_line ATTRIBUTES(TEXT="Add .deleteAllRows call")
LET delete_all_rows_call_flag = TRUE
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
ON ACTION remove_delete_all_rows_line ATTRIBUTES(TEXT="Remove .deleteAllRows call")
LET delete_all_rows_call_flag = FALSE
CALL state(DIALOG, arr_length, delete_all_rows_call_flag)
END DISPLAY
END MAIN
FUNCTION populate_array(len INTEGER)
DEFINE i INTEGER
CALL arr.clear()
FOR i = 1 TO len
LET arr[i].field1 = i
LET arr[i].field2 = ASCII (64 + i), ASCII (64 + i), ASCII (64 + i)
LET arr[i].field3 = util.Math.rand(10000)
END FOR
END FUNCTION
FUNCTION state(d ui.Dialog, len INTEGER, delete_all_rows_call_flag BOOLEAN)
CALL d.setActionActive("add_delete_all_rows_line", NOT delete_all_rows_call_flag)
CALL d.setActionActive("remove_delete_all_rows_line", delete_all_rows_call_flag)
CALL d.setActionActive("smaller", len > 1)
CALL d.setActionActive("greater", len <= 26)
END FUNCTION
#! askreuben298.per
LAYOUT
TABLE
{
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
[f01 ][f02 ][f03 ]
}
END
END
ATTRIBUTES
EDIT f01 = formonly.field1;
EDIT f02 = formonly.field2;
EDIT f03 = formonly.field3, TITLE="Sort This Column";
INSTRUCTIONS
SCREEN RECORD scr(field1, field2, field3);
<?xml version="1.0" encoding="ANSI_X3.4-1968"?>
<!-- askreuben298.4st -->
<StyleList>
<Style name="Window">
<StyleAttribute name="windowType" value="normal" />
</Style>
<Style name="Table">
<StyleAttribute name="headerAlignment" value="auto" />
</Style>
</StyleList>