Introduction
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
frdric.girod this is what I've managed to achieve.
https://answers.sap.com/questions/13452021/cl-salv-tree-restoring-the-tree-expansioncollapse.html
Functionality
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( ).
First attempt
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.
Finally
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.
Addendum
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