Skip to Content

What was the issue?

When using ALVs, in this case using CL_SALV_TABLE, the event handler has to be defined as a method in a class that points to an event in the interface IF_SALV_EVENTS_ACTIONS_TABLE. This is all wonderful in keeping with the object oriented approach to programming.

However, when you try to use or manipulate the data from the displayed ALV grid, the class gets in the way. If you look at the SALV_DEMO_TABLE_EVENTS program, it gets around this issue by having the event handler method calling a Form which is back in the program. While this works, you are still declaring the data table as a global, and not really using the object oriented methodology. Here are two ways to get that data and use object oriented methodology without falling back to a Form in the report.

Method 1 – Reference to the table in the class

The very first thing I want to do is acknowledge that this post borrows heavily from this blog post on the website www.kerum.pl. I looked around the site, and unfortunately I can’t acknowledge the name of the person other than “Kris”. Well thank you Kris! From Kris’ example, I have added some newer ABAP constructs and converted the older style FORM to an object oriented METHOD. The key piece that makes this work is declaring the reference to table in the class…

ref_alv_table TYPE REF TO tt_usr,

and then sending the reference of table used in the ALV to that attribute with this statement.

GET REFERENCE OF lt_usr INTO event_handler->ref_alv_table.

Here is the full refactoring of Kris’s code.

REPORT z_my_test.

*----------------------------------------------------------------------*
*       CLASS lcl_event_handler DEFINITION
*----------------------------------------------------------------------*
CLASS lcl_event_handler DEFINITION.

  PUBLIC SECTION.
    TYPES:
*     Define a table type, you can use it to store the reference
*     on the internal table. In this case it is the same as the
*     table (in which case it is superfluous), but this can be used
*     to define only a sub-set of the fields.
      tt_usr TYPE TABLE OF usr02 WITH DEFAULT KEY.

*   Static attributes to store references on your local variables
    DATA:
*     Reference on the internal table with data
      ref_alv_table TYPE REF TO tt_usr,
*     Reference on the ALV object
      ref_alv       TYPE REF TO cl_salv_table.

    METHODS constructor
      IMPORTING
        i_alv TYPE REF TO cl_salv_table.

    METHODS on_link_click
      FOR EVENT if_salv_events_actions_table~link_click
                  OF cl_salv_events_table
      IMPORTING row column.

ENDCLASS.                    "lcl_event_handler DEFINITION

*----------------------------------------------------------------------*
*       CLASS lcl_event_handler IMPLEMENTATION
*----------------------------------------------------------------------*
CLASS lcl_event_handler IMPLEMENTATION.

  METHOD constructor.
    ref_alv = i_alv.
  ENDMETHOD.                    "constructor

  METHOD on_link_click.
    "... find clicked row using the table reference
    TRY.
        DATA(usr_record) = ref_alv_table->*[ row ].

        cl_abap_browser=>show_html( html = VALUE #( ( usr_record-bname && |</br>| )
                                                    ( |Created by - | && usr_record-aname )  ) ).
      CATCH cx_sy_itab_line_not_found.
        "Error message goes here
    ENDTRY.

    " If something changed...
    DATA(something_changed) = abap_false.
    IF something_changed = abap_true.
      "...then refresh.
      ref_alv->refresh( ).
    ENDIF.
  ENDMETHOD.                    "on_link_click
ENDCLASS.                    "lcl_event_handler IMPLEMENTATION

*----------------------------------------------------------------------*
*       CLASS lcl_routines DEFINITION
*----------------------------------------------------------------------*
CLASS lcl_routines DEFINITION.
  PUBLIC SECTION.
    METHODS display_alv.

ENDCLASS.

*----------------------------------------------------------------------*
*       CLASS lcl_routines IMPLEMENTATION
*----------------------------------------------------------------------*
CLASS lcl_routines IMPLEMENTATION.
  METHOD display_alv.
    DATA:
      lt_usr     TYPE lcl_event_handler=>tt_usr.          " <- local internal table

    SELECT * FROM usr02
      UP TO 30 ROWS
      INTO CORRESPONDING FIELDS OF TABLE @lt_usr
      ORDER BY bname.

    TRY.
        CALL METHOD cl_salv_table=>factory
          IMPORTING
            r_salv_table = DATA(alv)
          CHANGING
            t_table      = lt_usr.

        " Instantiate the event handler passing a reference
        " To the ALV object which can be used within the event handler
        DATA(event_handler) = NEW lcl_event_handler( alv ).

        " Register event handler
        DATA(lo_events) = alv->get_event( ).
        SET HANDLER event_handler->on_link_click FOR lo_events.

        " Get and store the reference on your local internal table
        GET REFERENCE OF lt_usr INTO event_handler->ref_alv_table.
        " Also store the reference on the ALV object, it can be useful
        event_handler->ref_alv = alv.

        " Set column as hotspot
        DATA(columns) = alv->get_columns( ).
        DATA(column) = CAST cl_salv_column_list( columns->get_column( 'BNAME' ) ).
        column->set_cell_type( if_salv_c_cell_type=>hotspot ).

        alv->display( ).

      CATCH cx_salv_msg.             " cl_salv_table=>factory()
        "Ideally raise a message instead of WRITE statements
        WRITE: / 'cx_salv_msg exception'.
      CATCH cx_salv_not_found.       " cl_salv_columns_table->get_column()
        WRITE: / 'cx_salv_not_found exception'.
    ENDTRY.
  ENDMETHOD.    "display_alv
ENDCLASS.


*----------------------------------------------------------------------*
*       START-OF-SELECTION
*----------------------------------------------------------------------*
START-OF-SELECTION.

  DATA(lcl) = NEW lcl_routines( ).
  lcl->display_alv( ).

Method 2 – Single class

Now that I have moved the display_alv code into a class… why not move it into the same class as the event handler? This works wonderfully! No more passing references and dereferencing them. The table and the alv object are declared as private attributes. That way any method in the class can access them and you don’t have to pass any references around. I think it is both easier to understand the context of each variable and also reduces the overall code.

Here is the same code refactored again…

REPORT z_my_salv_template.

*----------------------------------------------------------------------*
*       CLASS lcl_routines DEFINITION
*----------------------------------------------------------------------*
CLASS lcl_routines DEFINITION.

  PUBLIC SECTION.
    METHODS display_alv.

  PRIVATE SECTION.
*   Attributes to store references on your local variables
    DATA:
*     Reference on the internal table with data
      data_table TYPE STANDARD TABLE OF usr02 WITH DEFAULT KEY,
*     ALV object
      alv        TYPE REF TO cl_salv_table.

    METHODS on_link_click
      FOR EVENT if_salv_events_actions_table~link_click
                  OF cl_salv_events_table
      IMPORTING row column.

ENDCLASS.                    "lcl_routines DEFINITION

*----------------------------------------------------------------------*
*       CLASS lcl_routines IMPLEMENTATION
*----------------------------------------------------------------------*
CLASS lcl_routines IMPLEMENTATION.

  METHOD display_alv.

    SELECT * FROM usr02
      UP TO 30 ROWS
      INTO CORRESPONDING FIELDS OF TABLE @data_table
      ORDER BY bname.

    TRY.
        CALL METHOD cl_salv_table=>factory
          IMPORTING
            r_salv_table = alv
          CHANGING
            t_table      = data_table.

        " Register event handler
        DATA(lo_events) = alv->get_event( ).
        SET HANDLER me->on_link_click FOR lo_events.

        " Set column as hotspot
        DATA(columns) = alv->get_columns( ).
        DATA(column) = CAST cl_salv_column_list( columns->get_column( 'BNAME' ) ).
        column->set_cell_type( if_salv_c_cell_type=>hotspot ).

        alv->display( ).

      CATCH cx_salv_msg.             " cl_salv_table=>factory()
        "Ideally raise a message instead of WRITE statements
        WRITE: / 'cx_salv_msg exception'.
      CATCH cx_salv_not_found.       " cl_salv_columns_table->get_column()
        WRITE: / 'cx_salv_not_found exception'.
    ENDTRY.
  ENDMETHOD.    "display_alv

  METHOD on_link_click.
    "... find clicked row using the table reference
    TRY.
        DATA(usr_record) = data_table[ row ].

        cl_abap_browser=>show_html( html = VALUE #( ( usr_record-bname && |</br>| )
                                                    ( |Created by - | && usr_record-aname )  ) ).
      CATCH cx_sy_itab_line_not_found.
        "Error message goes here
    ENDTRY.

    " If something changed...
    DATA(something_changed) = abap_false.
    IF something_changed = abap_true.
      "...then refresh.
      alv->refresh( ).
    ENDIF.
  ENDMETHOD.                    "on_link_click

ENDCLASS.                    "lcl_routines IMPLEMENTATION

*----------------------------------------------------------------------*
*       START-OF-SELECTION
*----------------------------------------------------------------------*
START-OF-SELECTION.

  DATA(lcl) = NEW lcl_routines( ).
  lcl->display_alv( ).

 

Another goodie –

See how I have used CL_ABAP_BROWSER to quickly display a popup window with a couple of lines of data. While I could send it a full HTML table with style sheets and headers and tables, etc… Here I just pass the data separated by a line break </br>.

Just displaying a single piece of data can be written like this…

  cl_abap_browser=>show_html( html = VALUE #( ( usr_record-bname ) ).
    OR
  cl_abap_browser=>show_html( html = VALUE #( ( 'Here is my static text' ) ).

If anyone has a better and simple way of quickly displaying data, please do share. In the past, I have created screens and added tables using CL_DD_DOCUMENT. This seems to be much simpler, and if you have HTML knowledge, you will probably be able to do a lot more visually.

To report this post you need to login first.

9 Comments

You must be Logged on to comment or reply to a post.

  1. Sandra Rossi

    Be careful with declaring a local internal table while playing with control framework. It always relies on an ALV internal table which must be persistent (global scope, or anonymous data object (i.e. CREATE DATA) with at least one living reference).

    In your case, a local internal table works because the display is done full screen, i.e. the display is done inside the method, and the method never ends, so local internal table always exists. Even when a button is clicked, the method is still active (in the call stack), so the internal table still exists. You can see this by debugging it.

    That would not work if you’d display the ALV after leaving the method where you build.

    You could solve using CREATE DATA for creating the internal table. It would still exist after you leave the method, as long as there is at least one reference to it.

    (2) 
    1. Raghu Govindarajan Post author

      Very true! I do not know what the impact of containers or split screens or even using one of the other ways of creating an ALV.

      I am not sure I understand how using CREATE DATA for the internal table is going to help. Even in that case wouldn’t you have to create the data reference in a global scope? In that case, what is the advantage over just declaring a global variable?

      (0) 
      1. Sandra Rossi

        I wasn’t clear about the containers; in fact, it was more an example: screens with something else than just a full screen ALV grid, are handled using several methods, so probably the procedure where the ALV internal table is defined is not the same procedure where the main screen is displayed. It’s better to forget my example.

        If you create an “anonymous data object” in a procedure, it will persist after the procedure has ended if you have stored the data reference in a data object declared outside the procedure.

        I didn’t say that CREATE DATA was better than a global variable, especially for dynpros.

        I just saw that local alv internal table, and this made be react, because this is something which often leads to a short dump, at scrolling for instance (FREED STACK).

        I see that you have moved the alv table from local scope to an attribute. As long as the instance of LCL_ROUTINES exists, the alv table exists. So it’s much better now.

        (0) 
  2. Jacques Nomssi

    Hello Raghu,

    this is a common design discussion while trying to assign responsibilities to objects. In this case, I see two competing architectures:

    • Layered Architecture, with the Model-View-Controller as the grandfather of patterns. The solution (Method 1) looks like:

    • It could be improved to better match the pattern participants, e.g. CL_SALV_TABLE is the view, the LCL_ROUTINES can hold the model data (data selection), and the LCL_EVENT_HANDLER could implement the controller. With this, you will gain the needed flexibility if you have many views.
    • I used to create a public interface to get the model data, check this example
    • Nowadays I use the iterator pattern to traverse data in the model object without making it global, e.g. the model object can generate a new iterator for selected items.
    • Smart UI (Method 2) often seen as an Anti-Pattern because all the business logic in the user interface. But even Eric Evans (cf. chapter 4, Domain-Driven Design) considers it a legitimate pattern in the contexts of simple applications. You just have to know there is no graceful path to richer behavior.
    •  

    And, about the fastest way to display data? try cl_demo_output.

    best regards,

    JNN

    (3) 
    1. Raghu Govindarajan Post author

      Jacques,

      I think I need a beer or 10… we will have a lot to talk about differing philosophies 🙂 .You bring a lot to the table in your discussions of architecture – but, I think they have a place and a purpose. Just the fact that you mention two different architectures goes to show that there is no right or wrong – just benefits and detriments to different styles. And there is no saying that the a style which has a benefit in one set of circumstances will benefit in another – you have to pick and choose and maybe even mix it up based on your particular set of circumstances.

      The classical MVC pattern was created not just to assign responsibilities to object, but also so the objects can then be handed to different people with different skill-sets and knowledge areas. I think that still has a very important place where you are building almost anything in software – but for standard ABAP reports. I would approach it a bit different.

      I believe that in using the CL_SALV_* classes, the pattern that you are following the closest is that of “naked objects” because the user interface is automatically created. So in this case what we primarily dealing with in the entire custom code of the report is the Model.. and secondarily parts of the controller that are not pre-packaged. In that paradigm, I should have named my LCL_ROUTINES as LCL_MODEL and the LCL_EVENTHANDLER should have been named LCL_CONTROLLER. There is no view in the program, because the view is the CL_SALV_TABLE.

      One place in your examples is the model LCL_MARA. This being a fairly standard object that will have to dealt with in every SAP system, I would take the time to make this a stand alone class ZCL_MARA with its own error handling and related bits and pieces such as getting the material description. This would truly be a separation of works. If this is done, then the report just becomes the controller and nothing else… at that point do we really need to go through the pain a finding a place to put the M, V and C separately in the report?

      I did go over your examples, and I think unless you are making a program that will be stand alone and handle anything thrown at it without modifications, the iterator pattern is an overkill. If you do want to make your own version of SE16N with drill downs into value tables, it will be brilliant! Or, you could handle that with recursive calls too (Gasp!! 🙂 )

      The other example with the public interfaces, again I not sure what you gain from the level of splitting up of tasks into separate local classes gets you. If there is something that I am missing please fill me in – does it make it easier to maintain, easier to extend, easier to copy to a new report for a different data structure? Here is how I managed to rewrite it… I think the behavior of both programs is identical.

      REPORT ysdnblog_jnn.
      
        DATA: BEGIN OF gs_sel,
          matnr TYPE matnr,
          mtart TYPE mtart,
          matkl TYPE matkl,
        END OF gs_sel.
      
      SELECTION-SCREEN BEGIN OF SCREEN 200.
      
      SELECT-OPTIONS:
        so_matnr FOR gs_sel-matnr,
        so_mtart FOR gs_sel-mtart,
        so_matkl FOR gs_sel-matkl.
      
      PARAMETERS p_rows TYPE count DEFAULT '100'.
      
      SELECTION-SCREEN END OF SCREEN 200.
      
      INTERFACE lif_unit_test.
      ENDINTERFACE.
      
      CLASS lcl_model DEFINITION FRIENDS lif_unit_test.
        PUBLIC SECTION.
          CLASS-DATA mt_data TYPE mara_tt.
          CLASS-METHODS main.
      ENDCLASS.
      
      CLASS lcl_model IMPLEMENTATION.
        METHOD main.
          TRY.
              "Here is the model
              SELECT * FROM mara
                INTO CORRESPONDING FIELDS OF TABLE mt_data
                UP TO p_rows ROWS
                WHERE matnr IN so_matnr
                  AND mtart IN so_mtart
                  AND matkl IN so_matkl.
      
              "Instantiate controller and view object
              cl_salv_table=>factory(
                IMPORTING r_salv_table   = DATA(mo_alv)
                CHANGING  t_table        = mt_data ).
      
              "Controller set up
              DATA(lo_functions) = mo_alv->get_functions( ).
              lo_functions->set_all( abap_true ).
      
              "View
              mo_alv->display( ).
      
            CATCH cx_static_check INTO DATA(lx_error).
              MESSAGE lx_error TYPE 'E' DISPLAY LIKE 'I'.
          ENDTRY.
        ENDMETHOD.
      ENDCLASS.
      
      START-OF-SELECTION.
        CALL SELECTION-SCREEN 200.
        lcl_model=>main( ).

      Just to make sure I am not missing anything in your example, to get the table reference into an event handler, I had to go through the same gyrations as in my original blog post. Based on your split up of methods, where would I put the GET REFERENCE? My test had me putting the event handler method in the LCL_VIEW and taking the lr_data reference variable and declaring it as an attribute in the same class. Would you approach it the same way?

      PS – I love the cl_demo_output, though I feel like with Horst Keller’s disclaimer in this post, I am going to stay away from using it in a production environment.

      PPS – Though I disagree with some of your style, you are teaching me quite a bit and challenging me to think – Respect!

      (1) 
      1. Jacques Nomssi

        Hello Raghu,

        the question is always, which use case do you have? which concerns do you have?

        We can discuss code structure patterns. but for each concrete case, the starting point will be requirement gathering/use cases, a specification that can be validated with tests. From this vantage point we evaluate a design and select an architecture. The solution has to be correct, but we can only call it optimal with reference to a given specification/context.

        So the correct answer is always: it depends. Which use case do you have? Which concerns do you have?

        Your proposal works, but it conflates the view and the model for convenience. This is legitimate if you want to avoid complexity. In this case, I would object with naming the class LCL_MODEL, as most of the logic is ALV code and it is not your intent to create a pure MODEL object.

        My proposal reflects my concerns:

        • Modelling: I want to expressive code in the language of business objects (BO), i.e. in the ubiquitous language used in the domain model (note this does not mean plain english). I want to eliminate mappings between the objects in the specification and the code structure, so it really about a good domain model.In this case, LCL_MARA is the BO. Even with an existing ZCL_MARA BO, I think LCL_MARA would be needed as a facade to hide its complexity in a specific report. Helper objects were introduced because
          – the BO should not be tied to selection screens, so an abstraction LCL_PARAM was invented/extracted to express this concern
          – the BO should not be tied to the view, the LIF_DATA_SOURCE interface was extracted.

          So I want to separate concerns to make my intent visible in the code structure.

        • Enable Unit Tests: Creating a design where objects only have one reason to change really requires discipline, but it enables unit testing in isolation. I can use mock objects for the data source (LCL_MARA) or the user parameters (LCL_PARAM).

        Iterator Use Case

        Suppose you have custom functions in the ALV that must process the user selected entries. The following iterator will return a reference to the each selected entry. So the PRIVATE data can be changed.

        CLASS lcl_iterator DEFINITION CREATE PROTECTED
          FRIENDS lif_aim_client lif_output_client.
          PUBLIC SECTION.
            CLASS-METHODS new IMPORTING it_index_rows TYPE lvc_t_row
                              RETURNING VALUE(ro_iterator) TYPE REF TO lcl_iterator.
            CLASS-METHODS create RETURNING VALUE(ro_iterator) TYPE REF TO lcl_iterator.
        
            METHODS has_next RETURNING VALUE(rv_flag) TYPE flag.
            METHODS next RETURNING VALUE(rr_aim) TYPE REF TO ts_output_list
                         RAISING lcx_aim_error.
            METHODS get_size RETURNING VALUE(rv_lines) TYPE sytabix.
          PROTECTED SECTION.
            DATA mt_index_rows TYPE lvc_t_row.
            DATA mv_index TYPE sytabix.
            DATA mv_lines TYPE sytabix.
            CLASS-DATA gt_output_list TYPE tt_output_list.
        
            METHODS constructor IMPORTING it_index_rows TYPE lvc_t_row.
        ENDCLASS.
        
        CLASS lcl_iterator IMPLEMENTATION.
        
          METHOD create.
            ro_iterator = new( VALUE #( FOR j = 1 THEN j + 1
               WHILE j < lines( gt_output_list )  ( index = j ) ) ).
          ENDMETHOD.
        
          METHOD new.
            ro_iterator = NEW #( it_index_rows ).
          ENDMETHOD.
        
          METHOD constructor.
            super->constructor( ).
            mt_index_rows = it_index_rows.
            mv_lines = lines( mt_index_rows ).
            CLEAR mv_index.
          ENDMETHOD.
        
          METHOD get_size.
            rv_lines = mv_lines.
          ENDMETHOD.
        
          METHOD next.
            CLEAR rr_aim.
            IF mv_index LT mv_lines.
              ADD 1 TO mv_index.
              ASSIGN gt_output_list[ mt_index_rows[ mv_index ]-index ] TO FIELD-SYMBOL(<ls_out>).
              IF sy-subrc EQ 0.
                rr_aim = REF #( <ls_out> ).
                RETURN.
              ENDIF.
            ENDIF.
            RAISE EXCEPTION TYPE lcx_aim_error.
          ENDMETHOD.
        
          METHOD has_next.
            rv_flag = xsdbool( mv_index LT mv_lines ).
          ENDMETHOD.
        
        ENDCLASS.

         

        best regards,

        JNN

         

         

         

         

        (1) 
  3. Paul Hardy

    I always love to challenge the “it cannot be done” but sometimes, in fact, it cannot be done,

    In the SAP GUI DYNPRO world the business logic was right there in the UI. For so long people have fought against this, with varying degrees of success, ignoring the fact the framework was designed to be all messed up together (Business Logic and UI logic) like an omelette,

    You can break this up with some effort, and that is all good, but SAP never had a GUI way to do the MVC properly. Indeed Web DYNPRO did not really do MVC in a serious sense, UI5 does, however, as the UI is in a different language than the model…

    In this case if  the SAP framework wants a global variable, then you are best off supplying a global variable, and not banging your head against a brick wall….

     

     

    (4) 

Leave a Reply