4

[Lengthy post, apologies]

I am looking for confirmation regarding CL_GUI_TIMER: Must each program group (of an internal session) have its own dedicated instance of a CL_GUI_TIMER object? I am finding that if the reference to the CL_GUI_TIMER object is shared with another program group, then calling run() from within the second program group will not trigger a finished event?

I will try to summarize the relevant code:

There is a class that implements a timer (to leave the main program when it expires). Note: Below the class is stripped of the error-checking, but no errors/exceptions occur in the actual code during instantiation or method calls:

CLASS zcl_cs_gui_timer_leave_prog DEFINITION.
  PUBLIC SECTION.
    METHODS constructor.
    METHODS set_timer.

  PROTECTED SECTION.
    DATA      go_gui_timer              TYPE REF TO cl_gui_timer .
    CONSTANTS c_default_timeout_seconds TYPE        i VALUE 6 ##NO_TEXT.

  PRIVATE SECTION.
    METHODS timer_handler FOR EVENT finished OF cl_gui_timer .
ENDCLASS.


CLASS zcl_cs_gui_timer_leave_prog IMPLEMENTATION.
  METHOD constructor.
    go_gui_timer = NEW #( ).
    SET HANDLER me->timer_handler FOR go_gui_timer.
    go_gui_timer->interval = c_default_timeout_seconds.
  ENDMETHOD.

  METHOD set_timer.
    go_gui_timer->cancel( ).
    go_gui_timer->run( ).
  ENDMETHOD.

  METHOD timer_handler.
    MESSAGE 'Transaction ended due to inactivity' TYPE 'S'.
    LEAVE PROGRAM.
  ENDMETHOD.
ENDCLASS.

Function group Z_FGROUP has a function module Z_CS_SET_GUI_TIMEOUT with a static instance of the class above (note: behaviour is the same if you make the static variable a function group "global" instead):

FUNCTION z_cs_set_gui_timeout.

  STATICS lo_zcs_gui_timer TYPE REF TO zcl_cs_gui_timer_leave_prog.

  TRY.
      IF NOT lo_zcs_gui_timer IS BOUND.
        lo_zcs_gui_timer = NEW #( ).
      ENDIF.
      lo_zcs_gui_timer->set_timer( ).
    CATCH cx_abap_context_info_error INTO DATA(go_exc).
      MESSAGE |{ go_exc->get_longtext( ) }| TYPE 'S'.
  ENDTRY.
ENDFUNCTION.

The function group Z_FGROUP also has another function module, Z_FGROUP_CALL_SCREEN which calls screen 100, defined in Z_FGROUP. In the PBO of that screen we have the module

MODULE set_gui_timeout_100 OUTPUT.
* actually is in a subroutine (not the module), skipping for brevity:
  CALL FUNCTION 'Z_CS_SET_GUI_TIMEOUT'.
ENDMODULE.

Now a main program, say Z_MAIN also has its own screen 200, with the same call to Z_CS_SET_GUI_TIMEOUT in its PBO:

MODULE set_gui_timeout_200 OUTPUT.
* actually is in a subroutine (not the module), skipping for brevity:
  CALL FUNCTION 'Z_CS_SET_GUI_TIMEOUT'.
ENDMODULE.

In this execution sequence:

  1. Z_MAIN calls its screen 200
    then possibly,
  2. Z_MAIN calls function module Z_FGROUP_CALL_SCREEN (which then calls its screen 100)

the first step does properly start the timer. If you don't do step #2, this first timer will expire causing a LEAVE PROGRAM as intended. But if you also do step #2 (obviously before the #1 timer expiration) what will happen is that no timer event gets triggered for step #2. So then you can stay on screen 100 for as long as you wish. Meanwhile the timer that was started with #1 expires "silently", i.e., does not trigger a finished event which can be handled while you are on screen 100.

If in the code above you change Z_MAIN's screen 200 timer to call its own CL_GUI_TIMER instance (instead of the function group's one):

MODULE set_gui_timeout_200 OUTPUT.
* actually is in a subroutine (not the module), skipping for brevity:

**  CALL FUNCTION 'Z_CS_SET_GUI_TIMEOUT'.

  STATICS lo_zcs_gui_timer TYPE REF TO zcl_cs_gui_timer_leave_prog.

  TRY.
      IF NOT lo_zcs_gui_timer IS BOUND.
        lo_zcs_gui_timer = NEW #( ).
      ENDIF.
      lo_zcs_gui_timer->set_timer( ).
    CATCH cx_abap_context_info_error INTO DATA(go_exc).
      MESSAGE |{ go_exc->get_longtext( ) }| TYPE 'S'.
  ENDTRY.
ENDMODULE.

then everything works properly and both screens get a timer (that triggers the finished event).


Addition, following Sandra's answer pointing to SAP note 2679117. Program demonstrates that an underlying timer keeps running and will trigger if a modal dialog is closed prior to expiration:

PROGRAM ztimer_event.


* +-------------------------------------------------------------------------------------------------+
PARAMETERS:
* +-------------------------------------------------------------------------------------------------+
  p_time TYPE i DEFAULT '6'.



* +-------------------------------------------------------------------------------------------------+
CLASS zcl_cs_gui_timer_leave_prog DEFINITION.
  PUBLIC SECTION.
    METHODS:
      constructor,
      set_timer.
    CONSTANTS:
      c_default_timeout_seconds TYPE i VALUE 6 ##NO_TEXT.
    DATA:
      lo_gui_timer              TYPE REF TO cl_gui_timer .

  PRIVATE SECTION.
    METHODS timer_handler FOR EVENT finished OF cl_gui_timer .
ENDCLASS.


CLASS zcl_cs_gui_timer_leave_prog IMPLEMENTATION.
  METHOD constructor.
    lo_gui_timer = NEW #( ).
    SET HANDLER me->timer_handler FOR lo_gui_timer.
    lo_gui_timer->interval = COND #( WHEN p_time <= 0 THEN c_default_timeout_seconds
                                     ELSE p_time ).
  ENDMETHOD.

  METHOD set_timer.
    lo_gui_timer->cancel( ).
    lo_gui_timer->run( ).
  ENDMETHOD.

  METHOD timer_handler.
    MESSAGE 'Timer was triggered' TYPE 'I'.
*    LEAVE PROGRAM.
  ENDMETHOD.
ENDCLASS.
* +-------------------------------------------------------------------------------------------------+


* +-------------------------------------------------------------------------------------------------+
START-OF-SELECTION.
* +-------------------------------------------------------------------------------------------------+
  TYPES:
    BEGIN OF t_alv_row,
      text TYPE string,
    END OF t_alv_row.

  DATA:
    lr_salv TYPE REF TO            cl_salv_table,
    lt_alv  TYPE STANDARD TABLE OF t_alv_row.


  DATA(go_timer) = NEW zcl_cs_gui_timer_leave_prog( ).
  go_timer->set_timer( ).

  lt_alv = VALUE #( ( text = |Timer is running. To let it expire silently:| )
                    ( text = |Wait { go_timer->lo_gui_timer->interval } |
                           & |seconds before closing this popup'| ) ).

  cl_salv_table=>factory(
    EXPORTING
       list_display = abap_false
    IMPORTING
      r_salv_table = lr_salv
    CHANGING
       t_table     = lt_alv ).

  lr_salv->set_screen_popup(
    start_column = 10
    end_column   = 60
    start_line   = 5
    end_line     = 9 ).
  WRITE: / |This will not time out if approximately { go_timer->lo_gui_timer->interval } seconds |
         & |passed before you closed the popup.|,
         / |It will timeout (at approximately { go_timer->lo_gui_timer->interval } |
         & |seconds from execution) if you closed the popup earlier|.

  lr_salv->display( ).
Suncatcher
  • 10,355
  • 10
  • 52
  • 90
Pilot
  • 441
  • 2
  • 9

2 Answers2

3

It's explained in the SAP note 2679117 - CL_GUI_TIMER: Timer is aborted when a dialog is opened in SAP GUI:

CL_GUI_TIMER is not designed to handle switching screens while the timer is running.

As far as I understand this note, SAP says they don't support this class anymore.

So, it means "undefined behavior, use it at your own risk".

Sorry!

Sandra Rossi
  • 11,934
  • 5
  • 22
  • 48
  • 1
    Just for completeness, the note is not entirely accurate when it says that "the timer is aborted as soon as a modal dialog is opened in SAP GUI". If you close the modal dialog before the underlying timer expires, then the event is triggered and handled: – Pilot Jul 09 '19 at 11:50
  • On this, added sample program to the question – Pilot Jul 09 '19 at 12:00
1

Following up on Sandra's answer above, where the SAP note mentions "modal dialog box":

It turns out program groups are unrelated to the question. Creating a new CL_GUI_TIMER instance per program group happened to solve the issue because it just created a new system event binding for a separate GUI control. Referring to my example code, this separate control corresponded to the function group's Z_FGROUP screen 100. This screen was being called at a different "popup level" (with CALL SCREEN 0100 STARTING AT ...). It turns out this overlooked detail is key:

Within the control framework, system events are registered: enter image description here

Presumably the CL_GUI_TIMER is implemented with a GUI control. But each GUI control is bound to a popup level. If the control requires a "container control" then the container is also bound to a "popup level". When the GUI is at a different popup level, any control belonging to another level is "invisible".

So, I am guessing, the automation controller will trigger system events for any such invisible ("out of current popup level") controls (containers). But the control framework will not call the handler because the popup level is not current. The timer keeps running so if you get back to the right popup level in time, the automation controller will trigger the event and push it to the control framework (this explains the behavior in the latter part of my question).

The control framework was designed to handle user interaction and the assumption seems to be that when there is a popup, events at other levels (including the main window CL_GUI_CONTAINER=>screen0) are irrelevant or must be suppressed.

Thomas Jung has an old blog post which throws some light on the inner workings of CFW events. Also, "stackpos" in CL_GUI_CFW seems relevant.

Pilot
  • 441
  • 2
  • 9