Reference a program’s internal table from ALV event handler – 2 Methods
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.
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.
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?
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.
Did not think about that! I guess it makes sense that it actually has to contents as opposed to the reference. In case of the reference being global it would no longer be usable outside the procedure. If we start thinking in terms of pointers, this makes so much more sense.
Hello Raghu,
this is a common design discussion while trying to assign responsibilities to objects. In this case, I see two competing architectures:
And, about the fastest way to display data? try cl_demo_output.
best regards,
JNN
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.
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!
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:
- 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.
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.
best regards,
JNN
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....
+1