Ask Reuben
How to allow the user to safely stop a long running operation.
How to allow the user to safely stop a long running operation?
How to interrupt a FOR, WHILE, or FOREACH loop?
How to interrupt a long running database query?
When a program performs something that takes some time to run, such as a loop, a report, a long running database query etc, the front-end has no control. The runtime is not sitting waiting for an INPUT, MENU etc to be responded too. However you might want to allow the user to stop the report, database query, batch update etc, typically in case they have entered the wrong parameters, or it is taking longer than expected.
The technique is to have an action-view, typically a button, with the special action name “interrupt”. When the runtime is executing the long running processes, this button will become active. The user can then click the button, and an asynchronous interruption request will be sent to the program.
When the run-time receives this event, it will set the INT_FLAG variable to TRUE, and so your code can test if an interrupt has occurred by checking the value of the INT_FLAG variable.
This is covered in the documentation in this section on User Interruption Handling.
I like to create a library to handle the interruption window. So I’d start with a little test like this …
#! interrupt.per LAYOUT GRID { [l01 ] [b01 ] } END END ATTRIBUTES LABEL l01 = formonly.txt, JUSTIFY=CENTER; BUTTON b01: interrupt, TEXT="Stop", IMAGE="stop";
#! interrupt.4gl FUNCTION open(title STRING) OPEN WINDOW interrupt WITH FORM "interrupt" ATTRIBUTES(STYLE="dialog", TEXT=title) CALL ui.Interface.refresh() END FUNCTION FUNCTION display(txt STRING, refresh BOOLEAN) DISPLAY BY NAME txt IF refresh THEN CALL ui.Interface.refresh() END IF END FUNCTION FUNCTION close() LET int_flag = 0 CLOSE WINDOW interrupt CALL ui.Interface.refresh() END FUNCTION
#! main.4gl IMPORT FGL interrupt MAIN DEFER INTERRUPT MENU "" COMMAND "Run" CALL do_loop() COMMAND "Exit" EXIT MENU END MENU END MAIN FUNCTION do_loop() DEFINE l_count INTEGER = 0 CALL interrupt.open("Interrupt infinite loop test") WHILE TRUE CALL interrupt.display(SFMT("%1 seconds have elapsed", l_count), TRUE) IF int_flag THEN -- User has pressed interrupt button EXIT WHILE END IF LET l_count = l_count + 1 SLEEP 1 END WHILE CALL interrupt.close() CALL FGL_WINMESSAGE("Info", SFMT("Loop interrupted after %1 seconds", l_count), "info") END FUNCTION
… the key things to note include …
- the button in interrupt.per has the special name interrupt
- the use of ui.Interface.refresh() in interrupt.4gl so that the window is open and closed immediately
- the use of DEFER INTERRUPT in our main program so that we can respond to the int_flag being set
- the use of IF int_flag to test if the user has clicked the interrupt button
- setting the int_flag back to 0 inside the close() function in the library. You could allow the developer to set it back each time outside the library but they tend to forget. Alternatively you could set it to 0 as part of the open(). It is upto you as to what fits best in your coding standard.
By setting in a library like I have done, you can reuse in many places easily. Wether that is for loops, while loops, foreach loops, or an SQL operation.
I would expect most Genero applications to need and have this ability to interrupt something long running.