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 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()

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 :

  1. click on the column header marked “Sort This Column” (the 3rd column) to sort the data.
  2. click “Smaller” and “Greater” and note how the data is sorted as per this column.  (These buttons are repopulating the array with a different number of rows)
  3. click “Same” and note how the data is NOT sorted as per this column (This button is repopulating the array with the same number of rows)
  4. click the “Add deleteAllRows call” button
  5. click “Smaller”, “Greater”,”Same” and note how the data now remains sorted as per this column.

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.


#! 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>