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
- Vertriebsweg
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.
- Interview mit Björn Schulz (Software-Heroes.com) - 3. September 2024
- Daten aus ALV ermitteln - 3. September 2024
- So lange es den SAPGUI noch gibt… - 27. Juni 2024