Friday 6 August 2021

Refresh of CL_SALV_TREE

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.

Functionality

This is what it will look like.

SAP ABAP Exam Prep, SAP ABAP Tutorial and Materials, SAP ABAP Study Material, SAP ABAP Career, SAP ABAP Learning

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
      IMPORTING
        i_container     TYPE REF TO cl_gui_container
      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.
    IF i_container IS BOUND.
      custom_container = CAST #( i_container->children[ 1 ] ).
    ELSE.
      custom_container = CAST #( cl_gui_container=>screen0->children[ 1 ] ).
    ENDIF.
    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!

Source: sap.com

No comments:

Post a Comment