Refresh of CL_SALV_TREE
I’m developing for the first time a CL_SALV_TREE simple tree. The tree is way of configuring data that is grouped together. I could have done it with table maintenance, but I wanted something rather nicer.
The structure of my data (for the purposes of this blog is:
|Group name||Count||Active flag|
The three operations I want to do are:
- Create a new entry
- Delete a current entry
- Toggle the activation flag
The problem I had was that after updating the records – how to refresh the screen. With some great help from Sandra Rossi and with the participation of Frederic Girod this is what I’ve managed to achieve.
This is what it will look like.
Double click on the create icon, and a new record will be created under that node.
Double click on the delete icon and that record is removed.
Double click on an active/inactive icon and the state will be toggled.
Here’s the final code
It’s pretty clean, but there’s room for improvement!
REPORT zsalv_tree. CLASS lcl_main DEFINITION. PUBLIC SECTION. CLASS-METHODS class_constructor. METHODS: constructor, go RAISING cx_salv_error, get_tree RETURNING VALUE(r_result) TYPE REF TO cl_salv_tree, set_tree IMPORTING i_tree TYPE REF TO cl_salv_tree. PRIVATE SECTION. TYPES: BEGIN OF ty_tree_record, group TYPE string, count TYPE i, active TYPE boolean, active_icon TYPE salv_de_tree_image, action_icon TYPE salv_de_tree_image, END OF ty_tree_record. TYPES ty_tree_records TYPE STANDARD TABLE OF ty_tree_record WITH NON-UNIQUE KEY group count. CLASS-DATA: BEGIN OF _icons, active TYPE salv_de_tree_image, inactive TYPE salv_de_tree_image, delete TYPE salv_de_tree_image, create TYPE salv_de_tree_image, END OF _icons. DATA tree TYPE REF TO cl_salv_tree. DATA tree_records TYPE ty_tree_records. DATA actual_data TYPE ty_tree_records. DATA: expanded_groups TYPE string_table. METHODS populate_tree RAISING cx_salv_error. METHODS add_group_node IMPORTING i_record TYPE ty_tree_record RETURNING VALUE(r_result) TYPE lvc_nkey RAISING cx_salv_msg. METHODS add_leaf_node IMPORTING i_record TYPE ty_tree_record i_group_key TYPE lvc_nkey RAISING cx_salv_msg. METHODS set_columns. METHODS set_events. METHODS expand_nodes RAISING cx_salv_msg. METHODS get_salv_tree_gui_control RETURNING VALUE(r_result) TYPE REF TO cl_gui_column_tree. METHODS save_expansions RAISING cx_salv_msg . METHODS toggle_activation IMPORTING i_node_key TYPE any OPTIONAL RAISING cx_salv_msg. METHODS do_action IMPORTING i_node_key TYPE salv_de_node_key RAISING cx_salv_msg. METHODS delete_node IMPORTING i_leaf TYPE lcl_main=>ty_tree_record RAISING cx_salv_msg. METHODS create_node IMPORTING i_group TYPE string RAISING cx_salv_msg. METHODS on_double_click FOR EVENT double_click OF cl_salv_events_tree IMPORTING node_key columnname. CLASS-METHODS get_icon IMPORTING i_icon TYPE string RETURNING VALUE(r_result) TYPE salv_de_tree_image. ENDCLASS. CLASS lcl_main IMPLEMENTATION. METHOD class_constructor. _icons-active = get_icon( 'ICON_ACTIVATE' ). _icons-inactive = get_icon( 'ICON_DEACTIVATE' ). _icons-delete = get_icon( 'ICON_DELETE' ). _icons-create = get_icon( 'ICON_CREATE' ). ENDMETHOD. METHOD constructor. actual_data = VALUE #( ( group = 'A' count = 1 active = abap_true ) ( group = 'A' count = 2 active = abap_true ) ( group = 'B' count = 1 active = abap_true ) ( group = 'B' count = 2 active = abap_true ) ). ENDMETHOD. METHOD go. populate_tree( ). set_columns( ). set_events( ). tree->display( ). ENDMETHOD. METHOD populate_tree. IF tree IS NOT BOUND. cl_salv_tree=>factory( IMPORTING r_salv_tree = tree CHANGING t_table = tree_records ). ENDIF. SORT actual_data BY group count. LOOP AT actual_data INTO DATA(actual_record). DATA group_key TYPE lvc_nkey. AT NEW group. group_key = add_group_node( actual_record ). ENDAT. add_leaf_node( i_record = actual_record i_group_key = group_key ). ENDLOOP. ENDMETHOD. METHOD get_icon. DATA icon TYPE c LENGTH 255. CALL FUNCTION 'ICON_CREATE' EXPORTING name = i_icon add_stdinf = space IMPORTING result = icon EXCEPTIONS icon_not_found = 0 outputfield_too_short = 0 OTHERS = 0. r_result = icon. ENDMETHOD. METHOD add_group_node. DATA(node) = tree->get_nodes( )->add_node( related_node = '' data_row = VALUE ty_tree_record( group = i_record-group ) relationship = cl_gui_column_tree=>relat_last_child ). node->set_folder( abap_true ). node->get_item( 'ACTION_ICON' )->set_icon( _icons-create ). r_result = node->get_key( ). ENDMETHOD. METHOD add_leaf_node. DATA(node) = tree->get_nodes( )->add_node( related_node = i_group_key data_row = i_record relationship = cl_gui_column_tree=>relat_last_child ). node->get_item( 'ACTIVE_ICON' )->set_icon( SWITCH #( i_record-active WHEN abap_true THEN _icons-active ELSE _icons-inactive ) ). node->get_item( 'ACTION_ICON' )->set_icon( _icons-delete ). ENDMETHOD. METHOD set_columns. tree->get_columns( )->set_optimize( ). LOOP AT tree->get_columns( )->get( ) INTO DATA(column). CASE column-columnname. WHEN 'ACTIVE'. column-r_column->set_technical( ). WHEN 'ACTIVE_ICON'. column-r_column->set_short_text( 'Active' ). ENDCASE. ENDLOOP. ENDMETHOD. METHOD get_tree. r_result = me->tree. ENDMETHOD. METHOD set_tree. me->tree = i_tree. ENDMETHOD. METHOD set_events. DATA(events) = tree->get_event( ). SET HANDLER on_double_click FOR events. ENDMETHOD. METHOD on_double_click. TRY. CASE columnname. WHEN 'ACTIVE_ICON'. toggle_activation( node_key ). WHEN 'ACTION_ICON'. do_action( node_key ). WHEN OTHERS. RETURN. ENDCASE. save_expansions( ). CLEAR tree_records. tree->get_nodes( )->delete_all( ). populate_tree( ). expand_nodes( ). CATCH cx_salv_msg cx_salv_error INTO DATA(error). MESSAGE error TYPE 'I' DISPLAY LIKE 'E'. ENDTRY. ENDMETHOD. METHOD expand_nodes. LOOP AT tree->get_nodes( )->get_all_nodes( ) INTO DATA(node). DATA(record_ref) = node-node->get_data_row( ). FIELD-SYMBOLS <record> TYPE ty_tree_record. ASSIGN record_ref->* TO <record>. CHECK line_exists( expanded_groups[ table_line = <record>-group ] ) AND <record>-count EQ 0. node-node->expand( ). ENDLOOP. CLEAR expanded_groups. ENDMETHOD. METHOD get_salv_tree_gui_control. " all this should be in a TRY-CATCH block because there's a lot of assumption... DATA: splitter TYPE REF TO cl_gui_splitter_container, splitter_2 TYPE REF TO cl_gui_container, custom_container TYPE REF TO cl_gui_custom_container. custom_container = CAST #( cl_gui_container=>screen0->children[ 1 ] ). splitter = CAST #( custom_container->children[ 1 ] ). splitter_2 = CAST #( splitter->children[ 2 ] ). r_result = CAST #( splitter_2->children[ 1 ] ). ENDMETHOD. METHOD save_expansions. DATA(gui_control) = get_salv_tree_gui_control( cl_gui_container=>screen0 ). DATA(node_key_table) = VALUE treev_nks( ). gui_control->get_expanded_nodes( CHANGING node_key_table = node_key_table EXCEPTIONS OTHERS = 4 ). LOOP AT node_key_table INTO DATA(node_key). DATA(node) = tree->get_nodes( )->get_node( node_key ). DATA(record_ref) = node->get_data_row( ). FIELD-SYMBOLS <record> TYPE ty_tree_record. ASSIGN record_ref->* TO <record>. INSERT <record>-group INTO TABLE expanded_groups. ENDLOOP. ENDMETHOD. METHOD toggle_activation. DATA(record_ref) = tree->get_nodes( )->get_node( i_node_key )->get_data_row( ). FIELD-SYMBOLS <record> TYPE ty_tree_record. ASSIGN record_ref->* TO <record>. IF <record>-active = abap_true. <record>-active = abap_false. <record>-active_icon = _icons-inactive. ELSE. <record>-active = abap_true. <record>-active_icon = _icons-active. ENDIF. READ TABLE actual_data ASSIGNING FIELD-SYMBOL(<actual_line>) WITH TABLE KEY group = <record>-group count = <record>-count. <actual_line>-active = <record>-active. ENDMETHOD. METHOD do_action. DATA(record_ref) = tree->get_nodes( )->get_node( i_node_key )->get_data_row( ). FIELD-SYMBOLS <record> TYPE ty_tree_record. ASSIGN record_ref->* TO <record>. IF <record>-count EQ 0. " Groups have count 0 create_node( <record>-group ). ELSE. delete_node( <record> ). ENDIF. ENDMETHOD. METHOD delete_node. DELETE actual_data WHERE group = i_leaf-group AND count = i_leaf-count. ENDMETHOD. METHOD create_node. DATA(count) = 0. DO. count = count + 1. CHECK NOT line_exists( actual_data[ group = i_group count = count ] ). EXIT. ENDDO. INSERT VALUE ty_tree_record( group = i_group count = count ) INTO TABLE actual_data. INSERT i_group INTO TABLE expanded_groups. ENDMETHOD. ENDCLASS. end-of-selection. NEW lcl_main( )->go( ).
It turned out that a simple refresh is really easy! Just:
METHOD on_double_click. ... CLEAR tree_records. tree->get_nodes( )->delete_all( ). populate_tree( ). ... ENDMETHOD.
The trouble is that if I’ve opened any nodes, the tree will be redisplayed with all nodes collapsed. It looks like I can either do first display with all nodes collapsed or all nodes expanded.
What I want to do is for those nodes already expanded, they remain expanded. Collapsed nodes remain collapsed. And when I create a new node in an collapsed group I want it to be expanded.
How to do that?!
A bit of Rossi magic
Sandra Rossi is one of those people who digs deep to find out how things work, and in this case has come up with a clever solution.
The trick is that CL_SALV_TREE is built upon the rather more powerful and flexible CL_GUI_COLUMN_TREE. And that class has a method GET_EXPANDED_NODES which returns a table of the node keys of the standard nodes!
And the way to get a reference to that, is not TREE->GET_ACTUAL_TREE( ). That would be nice, but, yeah, it kind of smashes the idea of CL_SALV_TREE to bits. No. Instead what you have to do is somehow find out that the container in which your CL_SALV_TREE is placed is structured like this:
- Your main container‘s first child is a container. We’ll call that one child of container.
- Child of container‘s is a splitter.
- Splitter has a second child which is also a splitter. We’ll call that one child of splitter.
- Child of splitter‘s first child is the CL_GUI_COLUMN_TREE.
You can see that encoded above in method GET_SALV_TREE_GUI_CONTROL. It’s a bit of magic that I’m not entirely happy with, but I’ve tried to find other ways to get the CL_GUI_COLUMN_TREE reference, but there really doesn’t seem a way. I do think the implementation is very unlikely to change, so it’s fairly safe.
Once that was in place, I could save the expanded nodes and then go through them and rexpand them.
The annoying thing is that when you start, the node keys are the number of your data table record. But when you update, you get a new set of node keys. What is why in EXPAND_NODES. I have to get the data for the node and compare it with the actual data.
There is a bug, in that if you delete all the entries in a group, the group disappears and there’s no way of getting back. I’ll leave the fixing of that as an exercise for the reader!
And once more, huge thanks to Sandra Rossi.
One of the issues with CL_SALV_TREE is that the screen is completely redrawn, which doesn’t look very nice. The way around this is to use CL_GUI_ALV_TREE – it’s more powerful, and a little less easy to use.
You can find working code in the comments Sandra’s answer here: https://answers.sap.com/questions/13455438/adding-events-to-cl-gui-alv-tree-prevents-expansio.html
A little word about method GET_SALV_TREE_GUI_CONTROL. It's different from what I proposed initially: I took into account that the SALV tree could be displayed in 2 flavors, in a container or in full screen (decided by the parameter r_container of cl_salv_tree=>factory). In your case, your SALV tree is full screen, so getting the reference to the CL_GUI_COLUMN_TREE object will always be (*):
When it's in a container, there's no custom container implied, it will always (*) be:
Currently, the way the method GET_SALV_TREE_GUI_CONTROL is written cannot work with a SALV tree in a container, it would short dump because there's no custom container. Either you simplify it to work only with full screen mode (you may remove the parameter and the condition) or you fix it.
(*) I did not test it extensively, so "always" is a little bit optimistic.
Thanks for that. I'll remove it for the example. Anyone interested in a container in a custom container, see Sandra's example in the question.
As a hint (although I'm no longer entirely sure):
In the past I had problems with the ALV layout set up by the user as the standard layout. It couldn't be loaded automatically when the tree was displayed in a container (part of a bigger dynpro) and the tree was displayed for the first time. The user has to load the layout manually. That was not so nice for the user, but acceptable.
As I said, I'm not sure. Just in case someone has a bug like this and spends hours debugging ...
Matthew Billingham: Thanks for the blog. I'm a big fan of the class CL_SALV_TREE. It's limited but really fast and nice to work with.
Sandra Rossi: As always, thanks for sharing your knowledge 🙂
I converted my code to use CL_GUI_ALV_TREE. It was pretty straightforward really. I needed to make it update smoothly as it's for a commercial product my employer sells. Although the end users are all techies, it helps if it looks nice!