Pflegeview mit Datennavigation

Pflegeviews kennt jeder. Sie werden zu einer Tabelle oder einem View generiert und erlauben eine mehr oder weniger komfortable Dateneingabe. Mit Pflegeviews sind die meisten Customizingfunktionen realisiert worden.

Da der Tabellenpflegedialog generiert wird und von SAP seit Jahren nicht weiterentwickelt wird – ich hätte eine Menge einfacher Verbesserungsvorschläge – muss man mit dem Leben, was vorhanden ist. Die Eingabe oder die Funktionen können durch Zeitpunkte angepasst werden.

Ab einer bestimmten Größe, also wenn ziemlich viele Schlüsselfelder vorhanden sind, wird die Eingabe und die Kontrolle der vorhandenen Daten sehr mühselig.

Datennavigation

Um die Daten besser sichten zu können und sozusagen durch die Daten surfen zu können, hatte ich die Idee, einen ganz bestimmten Tree-Control anzubinden, der die Daten hierarchisch darstellt. Die Darstellung der Daten funktioniert natürlich mit allen Tree-Arten, aber es gibt eine Klasse, die eine ganz besondere Fähigkeit hat: Bei der Klasse CL_GUI_ALV_TREE_SIMPLE kann die Hierarchie zur Laufzeit geändert werden.

Der Anwender kann sich so also eine ganz eigene Sicht auf die Tabelle zusammenklicken. Ein Klick auf den entsprechenden Knoten soll dann die SM30 aufrufen. Die Anzeige wird auf die Daten eingeschränkt, die durch die Hierarchie gegeben sind.

Um das Ganze zu verdeutlichen, habe ich eine Demotabelle mit vielen Schlüsselfeldern gebaut und ein paar fiktive Daten eingefügt. Die Tabelle stellt eine typische Customizingtabelle dar, wo zu einer bestimmten Kombination von organisatorischen Werten Optionen aktiv sind oder nicht:

Wenn man sich hier mit ein paar tausend Einträgen, die durchaus realistisch sind, zurecht finden möchte, dann braucht man schon etwas Geduld und Wissen, wie man die einzelnen Einträge Filtern kann.

Vorbereitung

Um die Daten zu lesen und anzeigen zu können, musste ich zwei grundsätzliche Dinge tun, die, wenn man weiß wie, nicht schwer sind:

  • Erzeugen einer Tabelle mit genau der Struktur der vorgegebenen Tabelle
  • Daten zu einer beliebigen Tabelle/ View lesen

Dynamisch Tabelle erzeugen

Das Erzeugen der Tabelle geht extrem einfach:

DATA mr_data TYPE REF TO data.
FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
CREATE DATA mr_data TYPE STANDARD TABLE OF (tabellenname).
ASSIGN mr_data->* TO <lt_data>.

In TABELLENNAME steht der Name des Views. Im Feldsymbol <LT_DATA> steht nun die Tabelle zur Verfügung, die genau die gleichen Eigenschaften hat, als hätte ich sie direkt im Programm angegeben:

DATA lt_data TYPE STANDARD TABLE OF tabellenname.

Viewdaten lesen

Wenn es sich um eine Tabelle handelt, dann kann ich die Daten einfach mit SELECT ermitteln. Bei einem Tabellenpflegeview geht das nicht. Dieser ist nur für die Verwendung in der SM30 gedacht, nicht für die Datenselektion.

Aber das Problem hatte wohl vor mir auch schon jemand und hat den Funktionsbaustein VIEW_GET_DATA geschrieben.

    CALL FUNCTION 'VIEW_GET_DATA'
      EXPORTING
        view_name = tabellenname
      TABLES
        data      = <lt_data>
      EXCEPTIONS
        OTHERS    = 6.

Die Selektion der Daten ist also auch kein Problem.

Klasse CL_GUI_ALV_TREE_SIMPLE

Kommen wir nun zu dem spannenden Teil und meiner eigentlichen Idee zur Navigation in den Daten. Die Darstellung der Daten aus dem Tabellenpflegeview möchte ich hierarchisch darstellen. Die Klasse CL_GUI_ALV_SIMPLE_TREE erstellt die Hierarchie fast automatisch.

Die Klasse benötigt eine Tabelle und eine Information, nach welchen Tabellenfeldern der Aufriss erfolgen soll. Wie bereits erwähnt, hat die Klasse CL_GUI_ALV_TREE_SIMPLE die besondere Eigenschaft, dass der Aufriss zur Laufzeit geändert werden kann:

Wie bei einem normalen ALV üblich, kann das Layout auch gespeichert werden, so dass man sich häufig genutzte Hierarchien speichern und wieder laden kann.

Navigation

Nun ist die bloße Anzeige der Daten nicht sonderlich hilfreich. Deswegen habe ich einen Doppelklick auf die Knoten und Items des Baumes programmiert. Mit einem Doppelklick sollen die Daten bis zu dieser Hierarchiestufe angezeigt werden. Wenn ich also einen Doppelklick auf die oberste Ebene, die Verkaufsorganisation 1000 mache, dann sollen im View nur die Daten mit Verkaufsorganisation 1000 angezeigt werden. Wenn ich einen Doppelklick auf den untergeordneten Vertriebsweg 10 mache, sollen nur die Daten von VkOrg 1000 und Vertriebsweg 10 angezeigt werden:

Das funktioniert auch ganz gut, denn den Tabellenpflegedialog kann man nicht nur über die Transaktion SM30 aufrufen, sondern auch über den Funktionsbaustein VIEW_MAINTENANCE_CALL. Diesem Funktionsbaustein gibt man grob die folgenden Daten mit:

  • Tabellenname
  • Aktion (Anzeige oder Ändern)
  • Selektionstabelle

Der Clou hierbei ist die Selektionstabelle, in der ich anhand der jeweiligen Doppelklick-Position im Baum genau die zugrunde liegenden Daten übergebe. Beim Doppelklick werden folgende beiden Werte geliefert:

  • Die Hierarchiestufe
  • Der Tabellenindex der zugrunde liegenden Datentabelle

Ich ermittele dafür beim Doppelklick die aktuelle Hierarchiedefinition, lese den zugrunde liegenden Tabelleneintrag und nehme dann die Werte aus der aktuellen Hierarchiestufe in die Selektionstabelle auf.

Beispiel

Obige Hierarchie zeigt

  • Verkaufsorganisation
    • Vertriebsweg
      • Sparte

Ich mache einen Doppelklick auf den Eintrag Vertriebsweg 10 der VkOrg 1000. Das Doppelklickereignis des Trees sagt mir als Hierarchiestufe VTWEG und Tabellenzeile 2.

Ich mache einen Loop über die aktuelle Hierarchie und weise per ASSIGN COMPONENT dieses Feld der Tabellenzeile einem weiteren Feldsymbol zu. Den Feldnamen und den Wert dieses Feldes wird an die Selektionstabelle angehängt. So lange, bis ich die aktuelle Hierarchiestufe erreicht habe.

Hierarchie ändern

Wenn ich nun nicht über die Verkaufsorganisation an die Daten ran möchte, sondern zum Beispiel über das Material, dann kann ich einfach die Hierarchie ändern:

Die Darstellung im Baum ist entsprechend und ich kann mit einem Doppelklick auf ein Material schnell alle Einträge auswählen, die dieses Material enthalten:

Wo bin ich

Eine Schwäche der Baumdarstellung ist, dass ich nicht genau, bzw. nicht gut erkennen kann, wo ich mich gerade befinde. Leider sind die Methoden, die den Aufbau der Hierarchie steuern als PRIVATE Methoden angelegt. Es ist also nicht möglich, die Klasse zu beerben und entsprechend anzupassen.

Ich fände es sinnvoll, wenn ich diesem Falle der Eintrag nicht 1000, 2000 usw. heißen würde, sondern “Verkaufsorganisation 1000” usw. Das würde deutlich machen, welche Hierarchiestufe es ist.

Eine einfache Möglichkeit habe ich jedoch gefunden, um die Darstellung anzupassen. Es kann ein Gruppenstufen-Layout definiert werden. Hier ist es möglich, für jede Stufe der Hierarchie ein Icon zu definieren. Da man im Icon auch eine Quickinfo mitgeben kann, lässt sich folgende Ausgabe erzeugen:

Wenn man im Layout des SAPGUI einstellt, dass die Quickinfo sofort angezeigt wird, ist das eine akzeptable Lösung.

Doppelklick

Um die Navigation so einfach und intuitiv wie möglich zu machen, habe ich nicht nur NODE_DOUBLE_CLICK ausprogrammiert, sondern auch ITEM_DOUBLE_CLICK. Ich finde es immer nervig, wenn man irgendwo draufklickt und nichts passiert. Oder wenn man nur ein Element angeklickt hat und dann die Meldung kommt: “Bitte markieren Sie einen Knoten”.

Call Screen

Leider hat die Lösung eine große Macke: Da mit jedem Doppelklick der Tabellenpflegedialog erneut aufgerufen wird, wird mit jedem Aufruf ein CALL SCREEN ausgeführt. Das ist jedoch nur etwa 50 mal möglich.

Ein LEAVE TO SCREEN 0 sorgt zwar dafür, dass die Aufrufhierarchie wieder abgebaut wird, allerdings gibt es bei der Verwendung von LEAVE TO SCREEN 0 in der Doppelklick-Eventhandlermethode merkwürdige Seiteneffekte beim Blättern im Pflegedialog.

Ich habe leider keine Möglichkeit gefunden, um die Daten direkt im View zu aktualisieren, ohne den VIEW_MAINTENANCE_CALL erneut auszuführen.

Weitere Infos

Um möglichst viele Informationen über den Tabellenpflegedialog zu bekommen – und auch um zu wissen, ob überhaupt ein Pflegedialog existiert 🙂 – rufe ich den Baustein VIEW_GET_DDIC_INFO auf. In der Tabelle TVDIR, die der Baustein unter anderem liest, steht zum Beispiel, in welcher Funktionsgruppe der Pflegedialog erstellt wurde. Das ist wichtig für externe Perform-aufrufe, mit denen man evtl. Daten manipulieren möchte. Es gibt zum Beispiel die Routine VIM_SET_GLOBAL_FIELD_VALUE, mit der globale Felder geändert werden können:

DATA(prog) = |SAPL{ ms_tvdir-area }|.
DATA(rc) TYPE i.
PERFORM vim_set_global_field_value 
     IN PROGRAM (prog) 
  USING 'VIM_NEXT_SCREEN' 
        'N' 
        '0' 
         rc.

Das funktioniert aber nur, wenn auch der Aufruf “extern” erfolgt. Für einen externen Aufruf müssen ein paar sehr intime Infos übergeben werden, die aber fast alle vom VIEW_GET_DDIC_INFO ermittelt werden.

Mit der Routine TABLE_CALL_INFO und der Funktion “READ” werden die Daten gelesen und mit der Funktion “EDIT” werden die Daten im Änderungsmodus dargestellt.

    DATA(prog) = |SAPL{ ms_tvdir-area }|.
    PERFORM table_call_function IN PROGRAM (prog)
     TABLES lt_dba_sellist
            lt_dpl_sellist
            mt_x_header
            mt_x_namtab
            lt_excl_func
      USING 'READ'
            'VERY_SHORT'
            lv_updflag.

    PERFORM table_call_function IN PROGRAM (prog)
     TABLES lt_dba_sellist
            lt_dpl_sellist
            mt_x_header
            mt_x_namtab
            lt_excl_func
      USING 'EDIT'
            'VERY_SHORT'
            lv_updflag.

Ich habe es, wie gesagt, leider nicht geschafft, die Daten nur zu aktualisieren, nachdem der View einmal dargestellt wurde.

Filterung

Normalerweise kann man in einem ALV Daten filtern. Der CL_GUI_ALV_SIMPLE_TREE basiert auf einem ALV aber leider kann hier nicht gefiltert werden. Die Funktion müsste aber leicht nachgestellt werden können. Eventuell kümmere ich mich da später noch mal drum.

Select-Options

Sinnvoll wäre es natürlich auch, ein Selektionsbild für den View anzubieten, so dass der Anwender eine Vorauswahl treffen kann.

Dies müsste mit den freien Selektionsbedingungen abbildbar sein, aber da hatte ich bisher noch keine Lust zu. In diesem Beitrag steht jedoch, wie diese zu verwenden sind: Dynamisches Selektionsbild

Mit dem Funktionsbaustein VIEW_RANGETAB_TO_SELLIST können die Selektionsoptionen einfach in die für den Pflegedialog notwendige Selektionstabelle überführt werden.

AbapGit

Der gesamte Code inklusive Tabellendefinition und Tabellenpflegedialog steht bei github.com:

https://github.com/tricktresor/blog

Coding

REPORT ztrcktrsr_sm30_navigation.

PARAMETERS p_table TYPE tabname DEFAULT 'ZTT_DEMO1'.

CLASS lcl_tree DEFINITION.
  PUBLIC SECTION.
    TYPES tt_sellist           TYPE STANDARD TABLE OF vimsellist.

    DATA mo_tree               TYPE REF TO cl_gui_alv_tree_simple.
    DATA mt_sort               TYPE lvc_t_sort. "Sortiertabelle
    DATA mr_data               TYPE REF TO data.
    DATA ms_tvdir              TYPE tvdir.
    DATA mv_callstack_counter  TYPE i.

    DATA mt_sellist            TYPE STANDARD TABLE OF vimsellist.
    DATA mt_x_header           TYPE STANDARD TABLE OF vimdesc.
    DATA mt_x_namtab           TYPE STANDARD TABLE OF vimnamtab.

    METHODS handle_node_double_click
                  FOR EVENT node_double_click OF cl_gui_alv_tree_simple
      IMPORTING grouplevel index_outtab.
    METHODS handle_item_double_click
                  FOR EVENT item_double_click OF cl_gui_alv_tree_simple
      IMPORTING grouplevel index_outtab fieldname.
    METHODS build_sort_table.
    METHODS register_events.
    METHODS set_view IMPORTING viewname TYPE clike RAISING cx_axt.
    METHODS get_view_data.
    METHODS init_tree.
    METHODS constructor.
    METHODS view_maintenance_call IMPORTING it_sellist TYPE tt_sellist.

ENDCLASS.

DATA main TYPE REF TO lcl_tree.

CLASS lcl_tree IMPLEMENTATION.
  METHOD constructor.
  ENDMETHOD.

  METHOD set_view.
    SELECT SINGLE * FROM tvdir INTO ms_tvdir WHERE tabname = viewname.
    IF sy-subrc > 0.
      RAISE EXCEPTION TYPE cx_axt.
    ENDIF.
  ENDMETHOD.

  METHOD handle_item_double_click.
    "Pass click on item to handle_node_double_click
    handle_node_double_click(
      grouplevel   = grouplevel
      index_outtab = index_outtab ).

  ENDMETHOD.

  METHOD handle_node_double_click.

    FIELD-SYMBOLS <lt_data>            TYPE STANDARD TABLE.
    ASSIGN mr_data->* TO <lt_data>.
    DATA lt_dba_sellist                TYPE STANDARD TABLE OF vimsellist.
    DATA ls_dbasellist                 TYPE  vimsellist.

    "Get current hierarchy
    mo_tree->get_hierarchy( IMPORTING et_sort = DATA(lt_sort) ).

    IF grouplevel = space.
      "clicked on entry
      ASSIGN <lt_data>[ index_outtab ] TO FIELD-SYMBOL(<ls_data>).
      CHECK sy-subrc = 0.

      LOOP AT lt_sort INTO DATA(ls_sort).
        ASSIGN COMPONENT ls_sort-fieldname OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_value>).
        IF sy-subrc <> 0.
          EXIT.
        ENDIF.
        APPEND INITIAL LINE TO lt_dba_sellist ASSIGNING FIELD-SYMBOL(<ls_sellist>).
        <ls_sellist>-viewfield = ls_sort-fieldname.
        <ls_sellist>-operator  = 'EQ'.
        <ls_sellist>-value     = <lv_value>.
        <ls_sellist>-and_or    = 'AND'.
        READ TABLE mt_x_namtab TRANSPORTING NO FIELDS WITH KEY viewfield = ls_sort-fieldname.
        <ls_sellist>-tabix     = sy-tabix.
      ENDLOOP.

    ELSE.
      "Clicked on hierarchy node
      ASSIGN <lt_data>[ index_outtab ] TO <ls_data>.
      IF sy-subrc = 0.
        LOOP AT lt_sort INTO ls_sort.
          "Fill up all field from start of hierarchy to clicked node
          ASSIGN COMPONENT ls_sort-fieldname OF STRUCTURE <ls_data> TO <lv_value>.
          IF sy-subrc <> 0.
            EXIT.
          ENDIF.
          APPEND INITIAL LINE TO lt_dba_sellist ASSIGNING <ls_sellist>.
          <ls_sellist>-viewfield = ls_sort-fieldname.
          <ls_sellist>-operator  = 'EQ'.
          <ls_sellist>-value     = <lv_value>.
          <ls_sellist>-and_or    = 'AND'.
          READ TABLE mt_x_namtab TRANSPORTING NO FIELDS WITH KEY viewfield = ls_sort-fieldname.
          <ls_sellist>-tabix     = sy-tabix.
          IF ls_sort-fieldname = grouplevel.
            EXIT.
          ENDIF.
        ENDLOOP.
      ENDIF.
    ENDIF.

    CHECK <ls_data> IS ASSIGNED.

    IF mv_callstack_counter > 50.
      MESSAGE 'Navigation not possible anymore. Sorry' TYPE 'I'.
      RETURN. "handle_double_click
    ENDIF.

    ADD 1 TO mv_callstack_counter.

    view_maintenance_call( lt_dba_sellist ).

  ENDMETHOD.


  METHOD get_view_data.

    FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
    CREATE DATA mr_data TYPE STANDARD TABLE OF (ms_tvdir-tabname).
    ASSIGN mr_data->* TO <lt_data>.


    "Get info about table/ view
    CALL FUNCTION 'VIEW_GET_DDIC_INFO'
      EXPORTING
        viewname        = ms_tvdir-tabname
      TABLES
        sellist         = mt_sellist
        x_header        = mt_x_header
        x_namtab        = mt_x_namtab
      EXCEPTIONS
        no_tvdir_entry  = 1
        table_not_found = 2
        OTHERS          = 3.
    IF sy-subrc = 0.
      "Get data of view
      CALL FUNCTION 'VIEW_GET_DATA'
        EXPORTING
          view_name = ms_tvdir-tabname
        TABLES
          data      = <lt_data>
        EXCEPTIONS
          OTHERS    = 6.
    ENDIF.

  ENDMETHOD.                               " BUILD_OUTTAB

  METHOD build_sort_table.

    DATA ls_sort TYPE lvc_s_sort.
    DATA lv_idx  TYPE i.

    LOOP AT mt_x_namtab INTO DATA(ls_namtab)
    WHERE keyflag   = abap_true
      AND datatype <> 'CLNT'.
      ADD 1 TO lv_idx.
      ls_sort-fieldname = ls_namtab-viewfield.
      ls_sort-seltext   = ls_namtab-scrtext_l.
      ls_sort-spos      = lv_idx.
      ls_sort-up        = abap_true.
      APPEND ls_sort TO mt_sort.
    ENDLOOP.

  ENDMETHOD.                               " BUILD_SORT_TABLE


  METHOD register_events.

    mo_tree->set_registered_events( VALUE #(
          "Used here for applying current data selection
          ( eventid = cl_gui_column_tree=>eventid_node_double_click )
          ( eventid = cl_gui_column_tree=>eventid_item_double_click )
          "Important! If not registered nodes will not expand ->No data
          ( eventid = cl_gui_column_tree=>eventid_expand_no_children ) ) ).

    SET HANDLER handle_node_double_click FOR mo_tree.
    SET HANDLER handle_item_double_click FOR mo_tree.

  ENDMETHOD.                               " register_events


  METHOD init_tree.

    get_view_data( ).
    build_sort_table( ).

    DATA(docker) = NEW cl_gui_docking_container(
                            ratio = 25
                            side  = cl_gui_docking_container=>dock_at_left
                            dynnr = CONV #( ms_tvdir-liste )
                            repid = |SAPL{ ms_tvdir-area }| "'SAPLSVIM'
                            no_autodef_progid_dynnr = abap_false ).

* create tree control
    mo_tree = NEW #( i_parent              = docker
                     i_node_selection_mode = cl_gui_column_tree=>node_sel_mode_multiple
                     i_item_selection      = 'X'
                     i_no_html_header      = ''
                     i_no_toolbar          = '' ).



* register events
    register_events( ).


    FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
    ASSIGN mr_data->* TO <lt_data>.

    DATA lt_grouplevel        TYPE lvc_t_fimg.
    DATA ls_grouplevel        TYPE lvc_s_fimg.
    DATA lv_field_description TYPE text50.
    DATA lt_dba_sellist       TYPE STANDARD TABLE OF vimsellist.

    LOOP AT mt_sort INTO DATA(ls_sort).
      ls_grouplevel-grouplevel = ls_sort-fieldname.
      lv_field_description = mt_x_namtab[ viewfield = ls_sort-fieldname ]-scrtext_l.
      CALL FUNCTION 'ICON_CREATE'
        EXPORTING
          name       = 'ICON_OPEN_FOLDER'
          text       = ls_sort-fieldname
          info       = lv_field_description
          add_stdinf = ' '
        IMPORTING
          result     = ls_grouplevel-exp_image.
      CALL FUNCTION 'ICON_CREATE'
        EXPORTING
          name       = 'ICON_CLOSED_FOLDER'
          text       = ls_sort-fieldname
          info       = lv_field_description
          add_stdinf = ' '
        IMPORTING
          result     = ls_grouplevel-n_image.
      APPEND ls_grouplevel TO lt_grouplevel.
    ENDLOOP.

* create hierarchy
    CALL METHOD mo_tree->set_table_for_first_display
      EXPORTING
        i_save               = 'A'
        is_variant           = value #( report = sy-repid username = sy-uname )
        i_structure_name     = ms_tvdir-tabname
        it_grouplevel_layout = lt_grouplevel
      CHANGING
        it_sort              = mt_sort
        it_outtab            = <lt_data>.

    "expand first level
    mo_tree->expand_tree( 1 ).

    " optimize column-width
    CALL METHOD mo_tree->column_optimize
      EXPORTING
        i_start_column = mt_sort[ 1 ]-fieldname
        i_end_column   = mt_sort[ lines( mt_sort ) ]-fieldname.

    view_maintenance_call( lt_dba_sellist ).

  ENDMETHOD.

  METHOD view_maintenance_call.

    CALL FUNCTION 'VIEW_MAINTENANCE_CALL'
      EXPORTING
        action      = 'S'
        view_name   = ms_tvdir-tabname
      TABLES
        dba_sellist = it_sellist
      EXCEPTIONS
        OTHERS      = 15.

  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  CHECK main IS INITIAL.
  main = NEW #( ).
  TRY.
      main->set_view( viewname = p_table ).
      main->init_tree( ).
    CATCH cx_axt.
  ENDTRY.

 

Enno Wulff
Letzte Artikel von Enno Wulff (Alle anzeigen)