Quantcast
Channel: Programmierung – Tricktresor
Viewing all 166 articles
Browse latest View live

Login in anderer Sprache

$
0
0

In einigen Firmen gibt es immer wieder das Problem, dass man sich schnell mal eine Maske in einer anderen Sprache anschauen möchte. Dafür muss man sich jedoch abmelden und in der anderen Sprache anmelden, da häufig Mehrfachanmeldungen verboten sind.

Martin hat dafür einen kleinen Report gemacht:

REPORT ZLOGON_LANGUAGE.

DATA lv_destination type RFCDEST.

*== Sprachwahl über Transaktion herausfinden

CONCATENATE sy-sysid sy-mandt '_' sy-tcode+2(2) into lv_destination.

call function 'SYSTEM_REMOTE_LOGIN'
 EXPORTING
 SYSTEM = SY-SYSID
 HOST = ' '
 SERVICE = '00'
 DESTINATION = lv_destination
 EXCEPTIONS
 CANNOT_START = 1
 PARAMETER_INCOMPLETE = 2
 OTHERS = 3.

Dieser Funktionsbaustein wird über die Transaktionen Z_EN, Z_FR usw. aufgerufen.

Außerdem wurden in Transaktion SM59 RFC-Verbindungen eingerichtet, die immer wie folgt lauten:
SysidMandt_Sprache, Z.B. T01901_EN

Unter Anmeldung und Sicherheit im SM59 gibt man dann die Sprache an und hakt aktuellen Benutzer an. Das ist alles. Schon funktioniert es. Mit aufrufen der Transaktion öffnet sich ein neues Fenster in der gewünschten Sprache .

Vielen Dank, Martin!


ABAP System Deep Dive

$
0
0

Das SAP-System ist uralt und steckt voller Geheimnisse. Einige alte Artefakte sind immer noch vorhanden. Außerdem gibt es einige versteckte Kommandos. In diesem Artikel möchte ich dir einiges von dem geheimen Wissen vorstellen.

2016-09-26_12-39-10

1. R/2, R/3-Präprozessor

Es besteht die Möglichkeit, bestimmte Anweisungen nur auf R/2- bzw. R/3-Systemen auszuführen. Dies geht mithilfe des Präprozessor-Befehls #rx, wobei x für das entsprechende System steht.

R/2-Systeme interpretieren die Zeichenfolge #r3 am Anfang der Zeile als Kommentar, während R/3-Systeme die Zeilen ignorieren, die mit #r2 anfangen. Dies wird z.B. im Pretty-Printer ausgenutzt.

#r2 WRITE 'Ich bin ein R/2-System'.
#r3 WRITE 'Ich bin ein R/3-System'.

2. Operator IS %_SWITCHED_ON

Mit diesem versteckten Schlüsselwort kann man prüfen, ob ein bestimmter Switch im Switch Framework gesetzt ist. So kann man etwa ein Verhalten implementieren, das sich zwischen Industrie- und Retail-Systemen unterscheidet. Man kann den Operator u.A. in den Anweisungen CHECK oder IF verwenden.

3. Laufzeitmessung mit +REP/+ENDREP

Es besteht die Möglichkeit, eine Laufzeitmessung in ABAP durchzuführen. Dies funktioniert mit einer speziellen Schleife, der +REP/+ENDREP-Schleife. Die Anweisung hat noch einige Zusatzparameter, z.B. dass man nur zu einem bestimmten Befehl messen oder weitere interne Statisiken mit auslesen möchte. Das möchte ich hier nicht im einzelnen wiedergeben. Falls Sie Fragen hierzu haben, kann ich hierzu weitere Informationen liefern.

DATA ls_rep_results TYPE rep_s_results.

+REP 100 times.
 "hier das Statement einfügen (z. B. Methodenaufruf)
 +ENDREP RESULTS ls_rep_results.

WRITE: (30) 'Brutto-Laufzeit', ls_rep_results-rtime,
 /(30) 'Datenbankzeit', ls_rep_results-dbtime,
 /(30) 'Anzahl der Datenbankaufrufe', ls_rep_results-dbcount,
 /(30) 'Netzwerkzeit', ls_rep_results-ictime,
 /(30) 'Anzahl der Netzwerkzugriffe', ls_rep_results-iccount.

CLEAR ls_rep_results. "Clear nicht vergessen!!!

Scheinbar gibt es noch die Möglichkeit, die Performance von einzelnen ABAP-Opcodes (zu sehen im Debugger mit Rechtsklick ABAP-Bytecode) zu messen. Die Syntax ist:

DATA lv_opcode TYPE char4.
  DATA lv_flags  TYPE x.

  lv_opcode = 'WRIB'.
  lv_flags  = '01'.
  BREAK-POINT.
  +REP 1 TIMES UP TO lv_opcode lv_flags.
  WRITE 'foo'.
  +ENDREP RESULTS gs_rep_results.

  lv_opcode = 'TIME'.
  lv_flags  = '00'.
  +REP 100 TIMES REPEATING lv_opcode lv_flags.
  WRITE 'bar'.
  GET TIME.
  WRITE 'baz'.
  +ENDREP RESULTS gs_rep_results.

Allerdings ist dies sehr technisch und vor allen Dingen undokumentiert und führt zu Fehlern wie CX_SY_REP_PARAMETER_ERROR und CX_SY_REP_INTERNAL_ERROR.

Man kann außerdem

+REP 100 TIMES WITH KERNELINFO.

benutzen, dann stehen in den Feldern gs_rep_results-bboxinfo, gs_rep_results-kinfo und gs_rep_results-inlayinfo zudem weitere Infos.

Es handelt sich offenbar um Informationen zu Kernel-Funktionen wie vMemcpyR (Kopieren von Speicherbereichen).

4. Globale Variable in Form-Routine oder Methode einer lokalen Klasse definieren

Es ist möglich, eine lokale Variable innerhalb einer Form-Routinen oder in der Methode einer lokalen Klasse als global zu definieren. Fragt nicht, wozu das sinnvoll ist, aber die Anweisungen DATA unterstützt den Zusatz %_NON-LOCAL, der die Variable global deklariert.

5. Internen Modus bei SUBMIT explizit angeben.

Man kann mit dem Zusatz %_INTERNAL_%_SUBMODE_% bei der Anweisung SUBMIT explizit angeben, in welchem internen Modus ein Programm ausgeführt werden soll. Hiermit kann Interprozesskommunikation implementieren. Die Anweisung wird z.B. im Systemprogramm RDSBRUNT (dient der Steuerung von Reports) verwendet.

6. ABAP-Assembler2016-09-26_14-36-30

Sicherlich bist du schon einmal im Debugger oder in der ST22 über den ABAP-Bytecode gestolpert. Es besteht die Möglichkeit, Bytecodebefehle direkt in ABAP einzugeben. Die interne Anweisung RSYN ermöglicht es mit dem Zusatz >SCONT beliebige Bytecode-Befehle auszuführen. Das erste Argument entspricht dem Namen des Bytecodebefehls, das zweite Argument ist der erste Parameter (binär) und das zweite Argument der zweite Parameter (dezimal).

RSYN >SCONT time 0 0.

ist gleichbedeutend mit dem ABAP-Statement

GET TIME.

RSYN >SCONT wird im Standard in der Komponente zum Aufruf von RFC-Calls verwendet, um Spezialformen der Anweisung SYSTEM-CALL FREE MODE aufzurufen. Welche weiteren Funktionalitäten das Schlüsselwort RSYN bietet, konnte ich noch nicht herausfinden.

7. zusätzliche Vergleichsoperatoren

Aufgrund eines Fehlers im Compiler wurden vor ABAP 7.40 beliebige Zeichen als Vergleichsoperatoren erkannt. Dies ist mittlerweile gefixt, aber aus Kompatibilitätsgründen wird der Operator I (der gleichbedeutend mit EQ ist) weiterhin unterstützt.

Folgende Anweisung ist für den ABAP-Compiler eine gültige Syntax:

CHECK 1 i 1.

8. Vorsicht!

All die hier erwähnten Befehle oder Zusätze sollten nie in einem produktiven SAP-System eingesetzt werden. Die Behandlung dieser Befehle dient nur dem besseren Verständnis des SAP-Systems und um beim nächsten SAP-Stammtisch ein bisschen angeben zu können.

Keiner der hier vorgestellten Befehle ist dem Code Inspector auch nur ein Hinweis wert. Die Befehle könnten also weitestgehend unbemerkt eingesetzt werden.

Der Befehl CHECK 1 i 1 bringt die erweiterte Programmprüfung - die auch im Code Inspector aufgerufen wird - dazu, mit einem Kurzdump abzustürzen.

9. Beispielcode

Ein paar der oben genannten Beispiele sind in folgendem Listing untergebracht:

REPORT.

IF ea-glt IS %_SWITCHED_ON.
 RSYN >SCONT time 0 0. "GET TIME
 WRITE: / 'Global Trade Management aktiv'.
ENDIF.

IF 1 I 1. "=> Makes Code Inspector dump
#r3 PERFORM run.
 PERFORM out.
ENDIF.

*&---------------------------------------------------------------------*
*& Form run!
*&---------------------------------------------------------------------*
FORM run.

DATA ls_rep_results TYPE rep_s_results %_NON-LOCAL. "=>GLOBALISIEREN

 +REP 10 TIMES.
 WRITE: / sy-uzeit.
 SELECT COUNT( * ) FROM t001.
 +ENDREP RESULTS ls_rep_results.

ENDFORM. "run!

*&---------------------------------------------------------------------*
*& Form out
*&---------------------------------------------------------------------*
FORM out.

  WRITE: (30) 'Brutto-Laufzeit', ls_rep_results-rtime EXPONENT 0 DECIMALS 0,
        /(30) 'Datenbankzeit', ls_rep_results-dbtime EXPONENT 0 DECIMALS 0,
        /(30) 'Anzahl der Datenbankaufrufe', ls_rep_results-dbcount EXPONENT 0 DECIMALS 0,
        /(30) 'Netzwerkzeit', ls_rep_results-ictime EXPONENT 0 DECIMALS 0,
        /(30) 'Anzahl der Netzwerkzugriffe', ls_rep_results-iccount EXPONENT 0 DECIMALS 0.

ENDFORM. "out

 

Dynamische Suchhilfe

$
0
0

Suchhilfen geben immer wieder Anlass zu viel Diskussionen. Es gibt viele Wege zum Ziel; häufig sind diese jedoch umständlich oder zumindest nicht ideal. Für den Fall, dass du mal eine Suchhilfe brauchst, bei der die Felder erst zur Laufzeit bekannt sind, kannst du folgendes Vorgehen nutzen:

Suchhilfe-Exit Funktionsbaustein kopieren

Als erstes benötigst du einen Suchhilfe-Exit. Dieser Exit besteht aus einem Funktionsbaustein mit definierter Schnittstelle. Am besten kopierst du den Funktionsbaustein F4IF_SHLP_EXIT_EXAMPLE auf z. B. Z_F4IF_SHLP_EXIT_NEU1.

Hierfür benötigst du eine eigene Funktionsgruppe. Diese muss vor dem Kopieren vorhanden sein. In diesem Beispiel heißt der Funktionsbaustein z_f4ifshlp_exit_4_enno.

Funktionsbaustein Suchhilfe-Exit

FUNCTION z_f4ifshlp_exit_4_enno.
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*" TABLES
*" SHLP_TAB TYPE SHLP_DESCT
*" RECORD_TAB STRUCTURE SEAHLPRES
*" CHANGING
*" REFERENCE(SHLP) TYPE SHLP_DESCR
*" REFERENCE(CALLCONTROL) LIKE DDSHF4CTRL STRUCTURE DDSHF4CTRL
*"----------------------------------------------------------------------

DATA: rc TYPE sy-subrc.

IF callcontrol-step = 'SELONE'.
EXIT.
ENDIF.

IF callcontrol-step = 'PRESEL'.
EXIT.
ENDIF.

*"----------------------------------------------------------------------
* STEP SELECT (Select values)
*"----------------------------------------------------------------------
* This step may be used to overtake the data selection completely.
* To skip the standard seletion, you should return 'DISP' as following
* step in CALLCONTROL-STEP.
* Normally RECORD_TAB should be filled after this step.
* Standard function module F4UT_RESULTS_MAP may be very helpfull in this
* step.
IF callcontrol-step = 'SELECT'.
PERFORM shlp_4_enno USING shlp_tab[]
CHANGING callcontrol
shlp
record_tab[]
rc.
IF rc = 0.
callcontrol-step = 'DISP'.
ELSE.
callcontrol-step = 'EXIT'.
ENDIF.
EXIT. "Don't process STEP DISP additionally in this call.
ENDIF.

IF callcontrol-step = 'DISP'.
EXIT.
ENDIF.

ENDFUNCTION.

Unterroutine

In dieser Unterroutine wird die Ergebnistabelle aufgebaut. Wir verwenden hier zwar eine feste Struktur (zshlp_4_enno_structure), aber das Ergebnis wird dynamisch an die Ergebnistabelle übergeben.
Die Struktur sieht folgendermaßen aus:

2016-09-26_15-41-50

FORM shlp_4_enno USING it_shlp TYPE shlp_desct
CHANGING i_callcontrol TYPE ddshf4ctrl
i_shlp TYPE shlp_descr
xt_records TYPE ddshreslts
e_rc TYPE sy-subrc.

DATA: lt_data TYPE STANDARD TABLE OF zshlp_4_enno_structure WITH NON-UNIQUE DEFAULT KEY,
*--------------------------------------------------------------------*
* Für jedes Feld das in der Selopt als Importparaemter gekennzeichnet ist eine Range erstellen
* Kann man sicher auch dynamisch machen - aber dann wird's schwerer lesbar
*--------------------------------------------------------------------*
lt_r_datum TYPE RANGE OF zshlp_4_enno_structure-datum,
lt_r_uname TYPE RANGE OF zshlp_4_enno_structure-uname,
ls_data LIKE LINE OF lt_data,
ls_address TYPE bapiaddr3,
lt_return TYPE bapiret2_t.
.

FIELD-SYMBOLS: <ls_selopt_line> TYPE any.

*--------------------------------------------------------------------*
* Ranges für Daten-Selektion füllen
*--------------------------------------------------------------------*
LOOP AT i_shlp-selopt ASSIGNING FIELD-SYMBOL(<ls_selopt>).

CASE <ls_selopt>-shlpfield.
WHEN 'DATUM'.
APPEND INITIAL LINE TO lt_r_datum ASSIGNING <ls_selopt_line>.
WHEN 'UNAME'.
APPEND INITIAL LINE TO lt_r_uname ASSIGNING <ls_selopt_line>.
WHEN OTHERS.
CONTINUE.
ENDCASE.

MOVE-CORRESPONDING <ls_selopt> TO <ls_selopt_line>.

ENDLOOP.

*--------------------------------------------------------------------*
* Die so gefüllten Ranges jetzt für irgend eine Datenselektion nehmen
*--------------------------------------------------------------------*
SELECT bname AS uname, trdat AS datum
FROM usr02
WHERE bname IN @lt_r_uname
AND trdat IN @lt_r_datum
INTO CORRESPONDING FIELDS OF TABLE @lt_data.

LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<ls_data>).

CALL FUNCTION 'BAPI_USER_GET_DETAIL'
EXPORTING
username = <ls_data>-uname
IMPORTING
address = ls_address
TABLES
return = lt_return.
<ls_data>-freitext = ls_address-fullname.

ENDLOOP.

*--------------------------------------------------------------------*
* Und SAP das Ganze so aufbereiten lassen, wie man es kennt
*--------------------------------------------------------------------*
CALL FUNCTION 'F4UT_RESULTS_MAP'
EXPORTING
source_structure = 'ZSHLP_4_ENNO_STRUCTURE'
TABLES
shlp_tab = it_shlp
record_tab = xt_records[]
source_tab = lt_data[]
CHANGING
shlp = i_shlp
callcontrol = i_callcontrol
EXCEPTIONS
OTHERS = 0.
ENDFORM.

Suchhilfe anlegen

In Transaktion SE11 musst du nun noch eine Suchhilfe anlegen, die den Suchhilfe-Exit verwendet. Normalerweise gibst du in der Suchhilfe direkt eine Selektionsmethode an, also die Tabelle oder den View, aus dem gelesen wird. Da wir die Suchhilfe dynamisch aufbauen, bleibt dieses Feld leer und wir tragen nur den Baustein für den Suchhilfe-Exit ein:

2016-09-26_15-43-42

Das war auch schon alles. Die Suchhilfe kann direkt getestet werden:

2016-09-26_15-46-31

Anwendungsbeispiele

Der Anwendungsfall, für den man eine dynamische Suchhilfe benötigt, ist sicherlich nicht allzu oft vorhanden. Es ist aber schön, es zu können, wenn man mal vor dieser Herausforderung steht. Eine Möglichkeit wäre zum Beispiel jeweils andere Felder zu zeigen, je nachdem welche Partnerart der Anwender für die Selektion ausgewählt hat:  Kunde, Lieferant oder Sachbearbeiter. Auch könnte ich mir vorstellen, dass zu Materialnummern unterschiedliche Felder je Materialart oder Anwendergruppe angezeigt werden.

Vielen Dank an Stefan für das Beispiel!!

Feldname mit doppeltem Feldnamen

$
0
0

Hä?

Die nordfriesisch höfliche Nachfrage ist berechtigt. Wie kann ein Feldname einen doppelten Feldnamen besitzen? Aber der Reihe nach. Erst das Problem, dann die Idee und zum Schluss die Lösung.

MOVE-CORRESPONDING

Der MOVE-CORRESPONDING-Befehl ist sehr bequem. Man kann einfach alle Felder einer Struktur in die gleichnamigen Felder einer anderen Struktur kopieren. Wird ein neues Feld in die Strukturen eingefügt, wird es automatisch berücksichtigt.

<offtopic>Eigentlich müsste der Befehl dann ja COPY-CORRESPONDING heißen, denn der Feldinhalt wird ja nicht verschoben, sondern kopiert...</offtopic>.

Der Befehl birgt aber auch Tücken, denn die Feldnamen müssen immer gleich sein. Häufig hat man jedoch den Fall, dass Feldnamen - trotz gleicher Funktion und gleichem Datenelement - in verschiedenen Strukturen anders heißen. Zum Beispiel könnte das Feld in der Quellstruktur LIFNR heißen, in der Zielstruktur heißt es jedoch LIEFERANT.

Ein nachträgliches MOVE ist in diesem Fall erforderlich:

MOVE quell_struktur-lifnr TO ziel_struktrur-lieferant.

Das ist nicht weiter schlimm. Wenn man jedoch eine dynamische Struktur als Quellstruktur hat, dann muss man umständlich mittels ASSIGN COMPONENT den Quellwert lesen und dann zuweisen. Das ist umständlich und gegebenenfalls auch zeitkritisch.

ASSIGN COMPONENT 'LIFNR' OF STRUCTURE dynamische_quell_struktur TO FIELD-SYMBOLS(<lifnr>).
IF sy-subrc = 0.
  ziel_struktur-lieferant = <lifnr>.
ENDIF.

Sofern es sich um kundeneigene Tabellen handelt, kann man den folgenden Trick anwenden und quasi einen ALIAS vergeben:

ALIAS-Feldname

Stichwort: Benanntes Include. Wir machen uns den Umstand zu Nutze, dass man ein Include in eine Tabelle einbinden kann und dieses Include benennen kann. Das ist eigentlich für inkludierte Strukturen gedacht, um zum Beispiel die komplette Tabelle MARA per Include einbinden zu können und diese auch MARA benennen zu können. Die komplette Struktur MARA steht dann in der inkludierten Struktur zur Verfügung:

SELECT * FROM MARA INTO my_struc-mara WHERE...

Wir verwenden diese Gruppe nun nicht für eine inkludierte Struktur, sondern nur für ein inkludiertes Feld. Die Technik ist die gleiche.

Die Tabelle bestand vorher aus den Feldern

  • MANDT
  • MATNR
  • KENNZ

Ich möchte diese Tabelle nun so abändern, dass das Feld KENNZ auch unter dem Namen KENNZEICHEN ansprechbar ist. Dafür lege ich einen Include an, der nur das eine Feld KENNZ enthält. Dieses Include wird in die Tabelle eingebunden:

2016-09-26_18-01-48

Die Struktur der Tabelle ist hinterher genau so, wie vorher:

2016-09-26_18-02-48

Die richtige Ansprache

Den Vorteil, den ich jetzt habe: ich kann das Feld mit dem richtigen Namen KENNZ und mit dem Alias KENNZEICHEN, dem Gruppennamen des Includes ansprechen:

DATA lt_tt1 TYPE STANDARD TABLE OF ztt1.
DATA ls_tt1 TYPE ztt1.


SELECT * FROM ztt1 INTO TABLE lt_tt1.

LOOP AT lt_tt1 INTO ls_tt1.
 IF ls_tt1-kennz = 'X'.
   ls_tt1-kennzeichen = 'F'.
 ENDIF.
 WRITE: / ls_tt1-matnr, ls_tt1-kennz.
ENDLOOP.

Autosumme in ALV-Grid – Version 1

$
0
0

In einem Projekt wurde eine Anwendung programmiert, in der viele numerische Daten berechnet und ausgegeben wurden. Die Berechnungsergebnisse wurden auf mehrere Zellen verteilt. Um überprüfen zu können, ob die Verteilung richtig programmiert war, musste man die Summe über die verteilten Werte bilden und mit dem Ausgangswert vergleichen. Dies war immer relativ mühselig, da man entweder die Werte per Copy&Paste nach Excel kopieren musste (dann gab es aber Probleme bei negativen Zahlen...!) oder man musste die komplette Tabelle in Excel öffnen. Alles natürlich möglich, aber mir doch etwas zu umständlich.

Ich erinnerte mich an das Event delayed_changed_sel_callback, mit dem man eine Selektion im Grid verzögert auswerten konnte. Mit Hilfe der Tastenkombination STRG+Y kann man einzelne Zellen markieren. Diese Funktionen wollte ich nutzen, um die markierten Zellenwerte auszulesen und zu summieren.

Herausgekommen ist das unten stehende Programm. Der eigentliche Teil, die Summierung der Zellen, steht komplett in der Methode HANDLE_DELAYED_SELECTION.

2016-10-07_19-06-25

Vorgehen

Event delayed_changed_sel_callback registrieren und einen Eventhandler in der Klasse zuordnen (Handle_Delayed_Selection).

Wie kommt man an die markierten Zellen heran? Dafür gibt es die Methode Get_Selected_Cells. Sie liefert eine Tabelle zurück, in der die markierten Feldnamen und der Zeilenindex stehen. Mittels READ TABLE und ASSIGN COMPONENT kann man also auf einen Zellwert zugreifen.

Nun muss noch mittels DESCRIBE FIELD geprüft werden, ob es sich um ein Feld mit einem numerischen Wert handelt. Wenn das der Fall ist, kann der Wert der Zelle aufsummiert werden.

 

Normale Zellen vs. Summenzeilen

Das Vorgehen bei einem ALV-Grid, in dem keine Summen oder Zwischensummen gebildet wurden, ist einfach und erfolgt nach dem oben genannten Schema. Eine Herausforderung sind jedoch die (Zwischen-) Summenstufen gewesen. Diese werden im ALV-Grid in eigenen - geschützten - Tabellen verwaltet:

Tabelle Bedeutung Bemerkung
MT_CT00 Summetabelle Sie enthält in der Regel nur einen Eintrag. Ausnahme: Es sind in der Summierung der Feldwerte unterschiedliche Einheiten vorhanden.
MT_CT01 Zwischensummentabelle 1 Erste Zwischensummenstufe
MT_CTnn Zwischensummentabelle n Tabellen für Zwischensummenstufe nn
MT_CT09 Zwischensummentabelle 9 9 ist die höchste Stufe. Mehr Zwischensummen können nicht erstellt werden.

Immerhin gibt es in diesem Fall eine Methode, mit der man sich die Zwischensummentabellen - bzw. eine Referenz auf diese - besorgen kann: Get_Subtotals. Im Feld ROW_ID-ROWTYPE der Zellen-Tabelle steht, ob es sich um die Totals-Tabelle handelt (1. Zeichen = T) oder eine Zwischensumme (1. Zeichen = S). Nicht gruppierte Zellen haben den Eintrag SPACE.

Eine Selektion auf ein Summenfeld liefert zum Beispiel diesen ROWTYPE: S 0101X0000000001. Der Vierstellige Code nach dem S sagt aus, um welche Hierarchiestufe es sich handelt (Stellen 1 und 2 des Codes). Die Stellen 3 und 4 des Codes sagen aus, in welcher Tabelle das markierte Feld steht. In diesem Fall ist es Level 1 der Hierarchie und Tabelle MT_CT01.

Anhand dieses Code kann man also herausfinden, in welcher Zwischensummentabelle nachgeschaut werden muss. Dies tue ich hier:

lv_index = ls_cell-row_id-rowtype+4(2).
lv_tablename = 'LD_CT' && lv_index.
ASSIGN (lv_tablename) TO <ref_data>.

Nachdem wir nun wissen, in welcher Tabelle wir nachsehen müssen um den markierten Zellwert zu finden, müssen wir nun noch den richtigen Index ermitteln. Dieser wird leider nicht mitgegeben, sondern muss aus der Tabelle GROUPLEVELS, die über Get_Subtotals geliefert wird, ermittelt werden:

READ TABLE lt_grouplevels INTO ls_grouplevel INDEX ls_cell-row_id-index.
IF sy-subrc = 0.
  ls_cell-row_id-index = ls_grouplevel-cindx_from.
ENDIF.

Achtung! Das Programm funktioniert nur, wenn es sich um reine (Zwischen-) Summen handelt! Zwischensummen, die aus mehreren Zeilen bestehen weil sich die zugehörige Einheit unterscheidet, können (noch) nicht erkannt werden. Hier muss ich noch etwas forschen...

Zwischensummen bilden

Ein kurzer Hinweis, wie man im ALV-Grid Zwischensummen bildet:

Wähle als erstes mindestens eine Spalte über die du dann mit Hilfe des Summenicons 2016-10-07_19-48-00 eine Summe bildest. Danach kannst du weitere Spalten markieren und mit dem Zwischensummenicon 2016-10-07_19-49-31 die Spalten definieren, über die zusätzlich eine Zwischensumme erstellt werden soll.

Aufrisssummenstufe

Über die Aufrisssummenstufe kannst du einfach festlegen, dass nur Zwischensummenzeilen einer bestimmten Hierarchie angezeigt werden sollen:

Auswahl im Menü:

2016-10-07_19-41-49

Auswahl der Hierarchieebene:

2016-10-07_19-06-45

Anzeige der gewählten Zwischensummen:

2016-10-07_19-22-39

Code

REPORT zz_alv_autosumme.

PARAMETERS p_total TYPE p DECIMALS 2.

CLASS lcl_main DEFINITION.

 PUBLIC SECTION.
 METHODS start.
 PROTECTED SECTION.
 DATA mr_grid TYPE REF TO cl_gui_alv_grid.
 DATA mt_data TYPE STANDARD TABLE OF spfli.
 DATA mv_data_table TYPE tabname VALUE 'SPFLI'.
 DATA mr_dock TYPE REF TO cl_gui_docking_container.
 METHODS create_docker.
 METHODS create_grid.
 METHODS handle_delayed_selection
 FOR EVENT delayed_changed_sel_callback
 OF cl_gui_alv_grid
 IMPORTING sender.
 METHODS register_events.
 METHODS select_data.
ENDCLASS.

CLASS lcl_main IMPLEMENTATION.

 METHOD start.
 select_data( ).
 create_docker( ).
 create_grid( ).
 register_events( ).
 ENDMETHOD.

 METHOD create_docker.
 "Create Docking container at bottom
 CREATE OBJECT mr_dock
 EXPORTING
 side = cl_gui_docking_container=>dock_at_bottom
 ratio = 90
 no_autodef_progid_dynnr = abap_false.

 ENDMETHOD.

 METHOD create_grid.
 "Create ALV-Grid
 CREATE OBJECT mr_grid
 EXPORTING
 i_appl_events = abap_true
 i_parent = mr_dock.

 "and display data
 mr_grid->set_table_for_first_display(
 EXPORTING
 i_structure_name = mv_data_table
 CHANGING
 it_outtab = mt_data ).

 "Set focus on grid so user can directly scroll and select cells via CTRL+Y
 cl_gui_container=>set_focus( mr_grid ).

 ENDMETHOD.
 METHOD handle_delayed_selection.

 "Local data
 DATA lt_cells TYPE lvc_t_cell.
 DATA ls_cell LIKE LINE OF lt_cells.
 DATA lv_total TYPE p DECIMALS 2.
 DATA lv_val_type TYPE c.
 DATA lv_index TYPE n LENGTH 2.
 DATA lv_tablename TYPE string.
 DATA lt_grouplevels TYPE lvc_t_grpl.
 DATA ls_grouplevel LIKE LINE OF lt_grouplevels.

 FIELD-SYMBOLS <ref_data> TYPE REF TO data.
 FIELD-SYMBOLS <table> TYPE table.
 FIELD-SYMBOLS <warea> TYPE any.
 FIELD-SYMBOLS <val> TYPE any.

 "data references to sub totals tables
 DATA ld_ct00 TYPE REF TO data.
 DATA ld_ct01 TYPE REF TO data.
 DATA ld_ct02 TYPE REF TO data.
 DATA ld_ct03 TYPE REF TO data.
 DATA ld_ct04 TYPE REF TO data.
 DATA ld_ct05 TYPE REF TO data.
 DATA ld_ct06 TYPE REF TO data.
 DATA ld_ct07 TYPE REF TO data.
 DATA ld_ct08 TYPE REF TO data.
 DATA ld_ct09 TYPE REF TO data.

 "get selected cells (selection via CTRL + Y)
 sender->get_selected_cells( IMPORTING et_cell = lt_cells ).

 "If there is only one cell selected, we do not need to sum that...
 CHECK lines( lt_cells ) > 1.

 "Read all cell values
 LOOP AT lt_cells INTO ls_cell.

 "in case of rowtype (normal cell, total or subtotal) assign correct data table
 CASE ls_cell-row_id-rowtype(1).
 "Total sum of all
 WHEN 'T'.
 sender->get_subtotals( IMPORTING ep_collect00 = ld_ct00 ).

 ASSIGN ld_ct00 TO <ref_data>.
 ls_cell-row_id-index = 1.
 "assign specified data table
 ASSIGN <ref_data>->* TO <table>.

 "subtotals
 WHEN 'S'.
 sender->get_subtotals( IMPORTING
 ep_collect01 = ld_ct01
 ep_collect02 = ld_ct02
 ep_collect03 = ld_ct03
 ep_collect04 = ld_ct04
 ep_collect05 = ld_ct05
 ep_collect06 = ld_ct06
 ep_collect07 = ld_ct07
 ep_collect08 = ld_ct08
 ep_collect09 = ld_ct09
 et_grouplevels = lt_grouplevels ).

 lv_index = ls_cell-row_id-rowtype+4(2).
 lv_tablename = 'LD_CT' && lv_index.
 ASSIGN (lv_tablename) TO <ref_data>.

 READ TABLE lt_grouplevels INTO ls_grouplevel INDEX ls_cell-row_id-index.
 IF sy-subrc = 0.
 ls_cell-row_id-index = ls_grouplevel-cindx_from.
 ENDIF.
 "assign specified data table
 ASSIGN <ref_data>->* TO <table>.

 "Normal cell value
 WHEN space.
 ASSIGN mt_data TO <table>.
 ENDCASE.


 "Only read table line when index changes
 AT NEW row_id.
 READ TABLE <table> ASSIGNING <warea> INDEX ls_cell-row_id-index.
 ENDAT.
 "Assign selected fieldname of workarea
 ASSIGN COMPONENT ls_cell-col_id OF STRUCTURE <warea> TO <val>.
 IF sy-subrc = 0.
 "check correct type of field: Only numeric fields will be taken
 DESCRIBE FIELD <val> TYPE lv_val_type.
 CASE lv_val_type.
 WHEN 'P' "Packed
 OR 'N' "Numchar
 OR 'b' "Integer
 OR 'a' "decfloat
 OR 'e' "decfloat
 OR 'F'. "Float?
 "add cell value to total
 ADD <val> TO lv_total.
 ENDCASE.
 ENDIF.
 ENDLOOP.

 IF lv_total IS NOT INITIAL.
 "There were numeric fields selected and therefor we have a total to show:
 MESSAGE s000(oo) WITH 'TOTAL:' space lv_total.
 "Parameterfeld ebenfalls füllen
 p_total = lv_total.
 ENDIF.
 ENDMETHOD.

 METHOD register_events.
 "Set handler
 SET HANDLER handle_delayed_selection FOR mr_grid.
 "register event for delayed selection
 mr_grid->register_delayed_event( mr_grid->mc_evt_delayed_change_select ).
 ENDMETHOD.

 METHOD select_data.
 "Select data
 SELECT * FROM (mv_data_table) INTO TABLE mt_data UP TO 100 ROWS.
 ENDMETHOD.

ENDCLASS.

INITIALIZATION.
 DATA(gr_main) = NEW lcl_main( ).
 gr_main->start( ).

Autosumme in ALV-Grid – Version 2

$
0
0

Im vorherigen Artikel Autosumme in ALV-Grid (Version 1) habe ich euch gezeigt, wie man das Ereignis set_delay_change_selection nutzen kann, um die Summe von markierten Zellen eines ALV-Grids herausfinden kann. Diese Methode hat leider zwei Nachteile:

  1. Die Reaktionszeit des Ereignisses Delayed_Changed_Sel_Callback ist auf 1,5 Sekunden voreingestellt. Das ist viel zu lange, um schnell mal eben einige Werte zu prüfen
  2. Es funktioniert nur, wenn die Datentabelle bekannt ist, denn die Datentabelle des ALV-Grid MT_OUTTAB ist geschützt. Auf sie kann also nicht zugegriffen werden. Es ist demnach nicht möglich, eine Methode zu schreiben, die die Grid-Instanz einfach entgegen nimmt und darauf die Funktion Autosumme anwendet.

Wie kann man nun dieser Herausforderung begegnen? Es gibt zwei Varianten. Die erste Variante habe ich hier bereits beschrieben. Die Lösung ist in diesem Fall, dass man eine eigene Klasse von CL_GUI_ALV_GRID ableitet (erben lässt) und dann eine neue Methode erstellt, die diese geschützte Methode aufruft.

Die andere Variante ist hier von Lukasz Pegiel beschrieben: http://abapblog.com/articles/tricks/105-how-to-access-private-or-protected-data-and-methods-of-cl-gui-alv-grid

In dieser Variante wird einfach das Interface if_alv_rm_grid_friend eingebunden. Hierdurch wird die eigene Klasse als "Freund des ALV-Grid" bekannt gemacht. Und als Freund ist es möglich, auf die geschützten Attribute und Methoden zuzugreifen!

Vielen Dank an dieser Stelle an Lukasz, der mir sehr geholfen hat, diesen Trick Wirklichkeit werden zu lassen! Ich kann jedem nur empfehlen einen oder besser: mehrere Blicke auf seinen ABAPBlog zu werfen.  Besonders hervorzuheben ist seine Entwicklung Fast ALV-Grid

2016-10-07_19-06-25

1. Verbesserung: Änderung der Reaktionszeit

Um das Event zu behandeln und die markierten Zellen auszulesen, bedarf es keiner großen Tricks. Allerdings ist die Reaktionszeit Das ist deutlich zu lange. Mit Hilfe der Methode set_delay_change_selection kann die Reaktionszeit geändert werden. Diese Methode ist allerdings PROTECTED, also geschützt.

Wir müssen also das oben genannte Interface einbinden und haben so Zugriff auf die Methode set_delay_change_selection mit der man die Reaktionszeit in Millisekunden einstellen kann.

2. Verbesserung: Zugriff auf die Datentabelle des ALV-Grid

Zusätzlich wollte ich an das geschützte Attribute MT_OUTTAB heran, um die aktuellen Daten auszulesen. Auf diese Weise ist es möglich, ein universelles Tool zu schreiben, dass mit jedem ALV-Grid zusammen arbeitet und nicht nur lokal.

Wie sieht's aus?

Beide Verbesserungen sind in dieser Version 2 des Codes enthalten. Das Programm besteht nun aus zwei Klassen:

  1. Die Klasse, LCL_MAIN, die den Grid verwendet und darstellt.
  2. Die Klasse LCL_AUTOSUMME, die die eigentliche Funktionalität zur Verfügung stellt.

LCL_AUTOSUMME kann nun also als globale Klasse universell eingesetzt werden. Jedes Grid, dass die Autosummenfunktion verwenden möchte, muss sich in der Klasse mit der Methode REGISTER registrieren. Und dann kann's los gehen...!

Code

 REPORT zz_grid_autosumme2.

PARAMETERS p_total TYPE p DECIMALS 2.

CLASS lcl_autosumme DEFINITION.
 PUBLIC SECTION.
 INTERFACES if_alv_rm_grid_friend .
 CLASS-METHODS register IMPORTING ir_grid TYPE REF TO cl_gui_alv_grid.
 PROTECTED SECTION.
 CLASS-METHODS handle_delayed_selection
 FOR EVENT delayed_changed_sel_callback
 OF cl_gui_alv_grid
 IMPORTING sender.
ENDCLASS.

CLASS lcl_autosumme IMPLEMENTATION.
 METHOD register.
 "Set handler
 SET HANDLER handle_delayed_selection FOR ir_grid.
 "set delayed selection time
 ir_grid->set_delay_change_selection( time = 100 ). " Time in Milliseconds
 "register event for delayed selection
 ir_grid->register_delayed_event( ir_grid->mc_evt_delayed_change_select ).

 ENDMETHOD.

 METHOD handle_delayed_selection.

 "Local data
 DATA lt_cells TYPE lvc_t_cell.
 DATA ls_cell LIKE LINE OF lt_cells.
 DATA lv_total TYPE p DECIMALS 2.
 DATA lv_val_type TYPE c.
 DATA lv_index TYPE n LENGTH 2.
 DATA lv_tablename TYPE string.
 DATA lt_grouplevels TYPE lvc_t_grpl.
 DATA ls_grouplevel LIKE LINE OF lt_grouplevels.

 FIELD-SYMBOLS <ref_data> TYPE REF TO data.
 FIELD-SYMBOLS <table> TYPE table.
 FIELD-SYMBOLS <warea> TYPE any.
 FIELD-SYMBOLS <val> TYPE any.

 "data references to sub totals tables
 DATA ld_ct01 TYPE REF TO data.
 DATA ld_ct02 TYPE REF TO data.
 DATA ld_ct03 TYPE REF TO data.
 DATA ld_ct04 TYPE REF TO data.
 DATA ld_ct05 TYPE REF TO data.
 DATA ld_ct06 TYPE REF TO data.
 DATA ld_ct07 TYPE REF TO data.
 DATA ld_ct08 TYPE REF TO data.
 DATA ld_ct09 TYPE REF TO data.

 "get selected cells (selection via CTRL + Y)
 sender->get_selected_cells( IMPORTING et_cell = lt_cells ).

 "If there is only one cell selected, we do not need to sum that...
 CHECK lines( lt_cells ) > 1.

 "Read all cell values
 LOOP AT lt_cells INTO ls_cell.

 "in case of rowtype (normal cell, total or subtotal) assign correct data table
 CASE ls_cell-row_id-rowtype(1).
 "Total sum of all
 WHEN 'T'.
 ASSIGN sender->mt_ct00 TO <ref_data>.
 ls_cell-row_id-index = 1.
 "subtotals
 WHEN 'S'.
 sender->get_subtotals( IMPORTING
 ep_collect01 = ld_ct01
 ep_collect02 = ld_ct02
 ep_collect03 = ld_ct03
 ep_collect04 = ld_ct04
 ep_collect05 = ld_ct05
 ep_collect06 = ld_ct06
 ep_collect07 = ld_ct07
 ep_collect08 = ld_ct08
 ep_collect09 = ld_ct09
 et_grouplevels = lt_grouplevels ).

 lv_index = ls_cell-row_id-rowtype+4(2).
 lv_tablename = 'LD_CT' && lv_index.
 ASSIGN (lv_tablename) TO <ref_data>.

 READ TABLE lt_grouplevels INTO ls_grouplevel INDEX ls_cell-row_id-index.
 IF sy-subrc = 0.
 ls_cell-row_id-index = ls_grouplevel-cindx_from.
 ENDIF.
 "Normal cell value
 WHEN space.
 ASSIGN sender->mt_outtab TO <ref_data>.
 ENDCASE.

 "assign specified data table
 ASSIGN <ref_data>->* TO <table>.

 "Only read table line when index changes
 AT NEW row_id.
 READ TABLE <table> ASSIGNING <warea> INDEX ls_cell-row_id-index.
 ENDAT.
 "Assign selected fieldname of workarea
 ASSIGN COMPONENT ls_cell-col_id OF STRUCTURE <warea> TO <val>.
 IF sy-subrc = 0.
 "check correct type of field: Only numeric fields will be taken
 DESCRIBE FIELD <val> TYPE lv_val_type.
 CASE lv_val_type.
 WHEN 'P' "Packed
 OR 'N' "Numchar
 OR 'b' "Integer
 OR 'a' "decfloat
 OR 'e' "decfloat
 OR 'F'. "Float?
 "add cell value to total
 ADD <val> TO lv_total.
 ENDCASE.
 ENDIF.
 ENDLOOP.

 IF lv_total IS NOT INITIAL.
 "There were numeric fields selected and therefor we have a total to show:
 MESSAGE s000(oo) WITH 'TOTAL:' space lv_total.
 p_total = lv_total.
 ENDIF.
 ENDMETHOD.

ENDCLASS.

CLASS lcl_main DEFINITION.

 PUBLIC SECTION.
* INTERFACES if_alv_rm_grid_friend .
 METHODS start.
 PROTECTED SECTION.
 DATA mr_grid TYPE REF TO cl_gui_alv_grid.
 DATA mt_data TYPE STANDARD TABLE OF spfli.
 DATA mv_data_table TYPE tabname VALUE 'SPFLI'.
 DATA mr_dock TYPE REF TO cl_gui_docking_container.
 METHODS create_docker.
 METHODS create_grid.
 METHODS select_data.
 METHODS register_autosumme.
ENDCLASS.

CLASS lcl_main IMPLEMENTATION.

 METHOD start.
 select_data( ).
 create_docker( ).
 create_grid( ).
 register_autosumme( ).
 ENDMETHOD.

 METHOD create_docker.
 "Create Docking container at bottom
 CREATE OBJECT mr_dock
 EXPORTING
 side = cl_gui_docking_container=>dock_at_bottom
 ratio = 90
 no_autodef_progid_dynnr = abap_false.

 ENDMETHOD.

 METHOD create_grid.
 "Create ALV-Grid
 CREATE OBJECT mr_grid
 EXPORTING
 i_appl_events = abap_true
 i_parent = mr_dock.

 "and display data
 mr_grid->set_table_for_first_display(
 EXPORTING
 i_structure_name = mv_data_table
 CHANGING
 it_outtab = mt_data ).

 "Set focus on grid so user can directly scroll and select cells via CTRL+Y
 cl_gui_container=>set_focus( mr_grid ).

 ENDMETHOD.

 METHOD select_data.
 "Select data
 SELECT * FROM (mv_data_table) INTO TABLE mt_data UP TO 100 ROWS.
 ENDMETHOD.

 METHOD register_autosumme.
 lcl_autosumme=>register( mr_grid ).
 ENDMETHOD.

ENDCLASS.

INITIALIZATION.
 DATA(gr_main) = NEW lcl_main( ).
 gr_main->start( ).

Zugriff auf nicht unterstützte Ereignisse des CL_SALV_TABLE

$
0
0

Der SALV wird inzwischen gerne von Programmierern verwendet, da einiges einfacher und einleuchtender ist, als beim ALV-Grid. Allerdings ist auch vieles komplizierter und überhaupt nicht einleuchtend. Zudem hat der SALV einige Funktionen nicht, die der ALV-Grid schon lange zur Verfügung stellt, wie zum Beispiel Editierbarkeit. Ich brauchte neulich in einem Projekt das Ereignis "Verzögertes Reagieren auf Markierungen" in einem SALV-Grid. Leider bietet der SALV dieses Event nicht an. Developer Hero 2016 Łukasz Pęgiel hat mir das unten stehende Coding zur Verfügung gestellt. Vielen Dank dafür!

Was macht das Ereignis Delayed_Changed_Sel_Callback?

Das Ereignis wird immer dann - verzögert! - ausgelöst, wenn der Anwender die Markierung im Grid ändert, also zum Beispiel Zeilen, Spalten oder einzelne Zellen markiert. Standardmäßig ist die Verzögerung von DELAYED_CHANGED_SEL_CALLBACK auf 1,5 Sekunden eingestellt. Die Funktionalität kann dafür verwendet werden, um Daten anhand der markierten Zellen nachzulesen und anzuzeigen, ohne dass der Anwender eine weitere Taste drücken muss. 1,5 Sekunden sind allerdings häufig eine zu lange Zeit, denn häufig wechselt ein Anwender kurz vor dieser Zeitspanne die Markierung wieder, weil er der Meinung war, dass nun eh nichts mehr passiert. Wie diese Einstellung geändert werden kann, habe ich in dem Beispiel AUTOSUMME beschrieben.

Was ist nun der Trick?

Der Trick, um an die Events des unterliegenden ALV-Grids zu gelangen, liegt darin, das Ereignis AFTER_REFRESH FOR ALL INSTANCES zu registrieren, also auf alle aktuell instanziierten Grids anzuwenden. Zusätzlich muss der implizit immer vorhandene Importing-Parameter SENDER bei der Definition angegeben werden:

METHODS evh_refresh FOR EVENT after_refresh
      OF cl_gui_alv_grid
      IMPORTING sender.

In dem unten stehenden Programm wird ein einfacher SALV mit Daten aus der Flugdatenbank erzeugt. Es wird das Ereignis AFTER_REFRESH registriert um in diesem Ereignis für den SENDER - also das im SALV verwendete ALV-Grid - das eigentliche Ereignis Delayed_Change_Sel_Callback zu registrieren. Wird die Markierung im Grid geändert, so wird diese Meldung ausgegeben:

2016-10-10_09-46-32

Wer eine Alternative zu SALV und ALV-Grid sucht, der sollte sich das Open-Source-Tool FALV von Łukasz ansehen!

Achtung

Der Zusatz FOR ALL INSTANCES greift wirklich bei ALLEN Instanzen des ALV-Grid!! Deswegen sollte er wirklich nur mit Bedacht benutzt werden und du solltest dir der möglichen Seiteneffekte bewusst sein, denn viele interne Funktionen des SALV und ALV-Grids nutzen ebenfalls wieder ein Grid (Auswahl Feldkatalog, Auswahl Layout etc.). So kann es zu ungewollten Reaktionen - zum Beispiel bei der Änderung des Layouts - kommen:

2016-10-10_09-11-13

Code

REPORT zsalv_grid_events.

CLASS lcl_grid_trick DEFINITION.

 PUBLIC SECTION.

 DATA spfli TYPE STANDARD TABLE OF spfli.
 DATA salv TYPE REF TO cl_salv_table.

 METHODS create_salv.
 METHODS evh_refresh FOR EVENT after_refresh
 OF cl_gui_alv_grid
 IMPORTING sender.
 METHODS evh_delayed_selection
 FOR EVENT delayed_changed_sel_callback
 OF cl_gui_alv_grid
 IMPORTING sender.
 PRIVATE SECTION.
 DATA mv_event_registered TYPE boolean.

ENDCLASS.

CLASS lcl_grid_trick IMPLEMENTATION.

 METHOD create_salv.

 SELECT * UP TO 100 ROWS
 INTO CORRESPONDING FIELDS OF TABLE @spfli
 FROM spfli.

 cl_salv_table=>factory(
 IMPORTING
 r_salv_table = salv
 CHANGING
 t_table = spfli ).

 salv->get_functions( )->set_default( abap_true ).

 SET HANDLER evh_refresh FOR ALL INSTANCES.

 salv->display( ).

 ENDMETHOD.

 METHOD evh_refresh.

 CHECK mv_event_registered = abap_false.
 SET HANDLER evh_delayed_selection FOR sender.
 sender->register_delayed_event( i_event_id = cl_gui_alv_grid=>mc_evt_delayed_change_select ).
 mv_event_registered = abap_true.

 ENDMETHOD.

 METHOD evh_delayed_selection.

 DATA lr_type_description TYPE REF TO cl_abap_typedescr.
 lr_type_description = cl_abap_typedescr=>describe_by_object_ref( sender ).
 MESSAGE i001(00)
 WITH 'Delayed Selection; SENDER ist vom Typ '
 lr_type_description->absolute_name.

 ENDMETHOD.

ENDCLASS.

START-OF-SELECTION.

 DATA(output) = NEW lcl_grid_trick( ).
 output->create_salv( ).
 

If you wanna be my lover – Das FRIENDS-Konzept

$
0
0

Ich programmiere nun schon einige Zeit objektorientiert. Trotzdem - und das ist ja auch das schöne daran - gibt es immer wieder Themen, die in meiner Vorstellung anders sind, als sie sich dann tatsächlich darstellen. Das Friends-Konzept bedeutet, dass Klassen befreundet sein können. Dies muss in den Klassen definiert werden und bedeutet dann, dass die befreundeten Klassen Zugriff auf die geschützten und privaten Attribute und Methoden der Freunde haben.

Das Friends-Konzept kannte ich zwar, hatte aber bisher noch keine Anwendungsmöglichkeiten dafür, da die Friends-Beziehung immer von zwei Seiten ausgehen muss: Klasse A muss Klasse B als Freund eintragen und Klasse B muss Klasse A als Freund bekannt geben.

So dachte ich bisher.

FRIENDS-Interface

Dass es auch einfacher geht, steht klar und deutlich in der SAP-Hilfe:

Geben Sie ein Interface unter Freunde an, dann bedeutet dies, dass alle Klassen, die dieses Interface implementieren, Zugriff auf die geschützten und privaten Komponenten haben.

Das bedeutet, dass man gar nicht die einzelnen befreundeten Klassen in allen Klassen angeben muss, sondern dass es reicht, ein befreundetes Interface zu verwenden.

Das folgende kleine Demoprogramm verwendet das Interface If_You_Wanna_Be_My_Lover (In Anlehnung an den Hit Wannabe der Spice Girls), damit die Klassen gegenseitig auf das geschützte Attribut der jeweils anderen Klasse zugreifen kann.

Ich hätte zwar gerne auch die anderen drei Spice Girls mit in den Code verarbeitet, aber Mel B. und Scary Spice reichen zum Verständnis aus.

Weiterführende Informationen zum FRIENDS-Konzept findest du im ZEVOLVING-Blog von Naimesh Patel.

Realer Nutzen

Wie man dieses Wissen für die tägliche Arbeit nutzen kann, kannst du im ABAPBLOG von Lukasz Pegiel sehen und auch hier im Tricktresor. In diesen Artikel wird beschrieben, dass man das Interface IF_ALV_RM_GRID_FRIEND in eigene Klassen einbinden kann, um dann Zugriff auf geschützte und private Methoden des ALV-Grid zu bekommen.

2016-10-10_15-34-03

Code

REPORT zz_spice_girls.

INTERFACE if_you_wanna_be_my_lover.
ENDINTERFACE.

CLASS lcl_mel_b DEFINITION FRIENDS if_you_wanna_be_my_lover.
 PROTECTED SECTION.
   DATA text TYPE string VALUE 'You gotta get with my friends'.
ENDCLASS.

CLASS lcl_scary_spice DEFINITION FRIENDS if_you_wanna_be_my_lover.
 PROTECTED SECTION.
   DATA text TYPE string VALUE 'If you wanna be my lover'.
ENDCLASS.

CLASS lcl_wannabe DEFINITION FRIENDS if_you_wanna_be_my_lover.
 PUBLIC SECTION.
   INTERFACES if_you_wanna_be_my_lover.
   CLASS-METHODS sing.
   CLASS-METHODS class_constructor.
 PROTECTED SECTION.
   CLASS-DATA mr_mel_b TYPE REF TO lcl_mel_b.
   CLASS-DATA mr_scary_spice TYPE REF TO lcl_scary_spice.
ENDCLASS.

CLASS lcl_wannabe IMPLEMENTATION.
 METHOD class_constructor.
   CREATE OBJECT mr_mel_b.
   CREATE OBJECT mr_scary_spice.
 ENDMETHOD.
 METHOD sing.
   WRITE: / mr_scary_spice->text.
   WRITE: / mr_mel_b->text.
 ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
 lcl_wannabe=>sing( ).

Welche Methode ist das eigentlich?

$
0
0

Heute ein wirklich kurzer Trick, um den Klassen- und Methodennamen der Methode heraus zu bekommen, in der man sich gerade befindet. Diese Information ist sehr nützlich, um genauere Protokolle zu schreiben oder dem Anwender oder Entwickler bessere Informationen bei einem Programmfehler geben zu können.

Eine Einschränkung gibt es bei diesem Trick: Der Name der Methode kann leider bei einer lokalen Klasse nicht ermittelt werden.

Achtung! Hier kommt er:

Type-Pool BCB in den Eigenschaften der globalen Klasse bekannt geben:

2016-10-18_10-23-19

In der Methode folgendes Coding aufrufen:

*== Klassennamen ermitteln
bcb_constant_class_name.
write: / 'Class:', class_name.

*== Klassennamen->Methodenname ermitteln
bcb_constant_method_name.
write: / 'Method:', method_name.

Das Original

Hier der Vollständigkeit halber das zu Grunde liegende Coding der beiden Makros aus dem Type-Pool BCB:

bcb_constant_class_name

data __class_descr__ type ref to cl_abap_typedescr.
__class_descr__ = cl_abap_classdescr=>describe_by_object_ref( me ).
data class_name type string.
class_name = __class_descr__->get_relative_name( ).

bcb_constant_method_name

data __exception__ type ref to cx_bcb_exception.
try.
* force an exception...
   raise exception type cx_bcb_exception
         exporting area_code = '0' error_code = '0' msg = '0'.
catch cx_bcb_exception into __exception__.
*  ... caught right after
  data __include__ type syrepid.
__exception__->get_source_position( importing include_name = __include__ ).
*   map include name to method name
  data __program_name__ type programm.
__program_name__ = __include__.
  data __method__ type seocpdkey.
  call function 'SEO_METHOD_GET_NAME_BY_INCLUDE'
       exporting
         progname = __program_name__
       importing
         mtdkey   = __method__.
*  get class and method names
*  data class_name  type string.
*  class_name = __method__-CPDNAME.
  data method_name type string.
  concatenate __method__-clsname '->' __method__-cpdname into method_name.
endtry.
Weiterhin funktionieren tut natürlich auch der Trick mit ABAP_CALLSTACK.

64-bit Programme mit dem SAP GUI für Windows ausführen

$
0
0

Immer mehr kommen 64-bit Programme im 64-bit Windows-Umfeld zum Einsatz. 32-bit Programme, manchmal auch mit der Extension x86 versehen, werden langsam aber sicher weniger. So stellte sich mir die Frage, ob es mit dem SAP GUI für Windows, der ja bekanntlich eine 32-bit Applikation ist, möglich ist auch x64-Programme zu starten. Um Programme auf dem Frontend-Server zu starten wird aus ABAP im Normalfall die Methode Execute der Klasse cl_gui_frontend_services verwendet. Diese Methode nutzt unter Windows native die API-Funktion ShellExecute und diese kann auch x64-Programme von einem 32-bit-Prozess starten, wie hier zu lesen ist. Also ist es kein Problem, und so ist es auch. Zum Testen habe ich den Editor notepad gestartet, wie im folgenden Code-Snippet zu sehen ist:

*-Begin-----------------------------------------------------------------
Program zExecuteTest.

  Data:
    lr_fe_serv Type Ref To CL_GUI_FRONTEND_SERVICES.

  Create Object lr_fe_serv.

  Check lr_fe_serv->Get_Platform( ) = 14.

  lr_fe_serv->Execute(
    Exporting

      "Das Verzeichnis Sysnative ist nur auf x64-Windows sichtbar und es
      "kann nur mit x86-Software darauf zugegriffen werden
      Application = '%windir%\Sysnative\notepad.exe'

      "Im Verzeichnis System32 sind alle x64 Applikationen zu finden
      "Application = '%windir%\System32\notepad.exe'

      "Im Verzeichnis SysWOW64 sind alle x86 Applicationen zu finden
      "Application = '%windir%\SysWOW64\notepad.exe'

    Exceptions
      CNTL_ERROR = 1
      ERROR_NO_GUI = 2
      BAD_PARAMETER = 3
      FILE_NOT_FOUND = 4
      PATH_NOT_FOUND = 5
      FILE_EXTENSION_UNKNOWN = 6
      ERROR_EXECUTE_FAILED = 7
      SYNCHRONOUS_FAILED = 8
      NOT_SUPPORTED_BY_GUI = 9
      Others = 10
    ).

    If sy-subrc <> 0.

    EndIf.

*-End-------------------------------------------------------------------

Nebenbei zeigt dieses Beispiel noch wie über das virtuelle System-Verzeichnis Sysnative auf Windows-eigene 64-bit Applikationen zugegriffen werden kann. Auf diesem Wege wird das automatische Redirectoring umgangen. Ein Versuch ein Programm direkt aus dem Verzeichnis System32 aufzurufen, wie in der auskommentierten Zeile zu sehen, funktioniert so nicht, da wird man automatisch zu SysWOW64 umgeleitet und der 32-bit Pendant wird verwendet. So steht der Nutzung von x64-Software im Kontext des SAP GUI für Windows nichts entgegen.

SAP GUI Scripting Rekorder mit Windows PowerShell

$
0
0

Das SAP GUI Scripting API ist eine Schnittstelle zur Automatisierung von Benutzerinteraktionen mit dem SAP GUI für Windows. Das SAP GUI Scripting kann das Leben der Anwender erheblich vereinfachen in dem sich immer wiederholende Arbeiten und Aufgaben einfach automatisiert werden können. Hinter dem SAP GUI Scripting steht ein Objektmodell das den SAP GUI abbildet. Über dieses Objektmodell kann auf fast alle Objekte des SAP GUI zugegriffen werden. Eine sehr gute Einführung in das SAP GUI Scripting ist hier zu finden.

Die im SAP-Standard verankerten Möglichkeiten nutzen VBScript als Scripting-Sprache. Und im SAP-Hinweis 592685 wird demonstriert wie mit Hilfe der angehängten Visual Basic Anwendung die Aktionen des Anwenders in SAP GUI aufgezeichnet werden können. Die Visual Basic Plattform ist jedoch veraltet und Ansätze mit anderen (aktuellen) Scripting-Sprachen sind nicht vorhanden, mit Ausnahme des Scripting Tracker. Aus diesem Grund habe ich einen Ansatz eines SAP GUI Scripting Rekorders in PowerShell, der aktuellen Windows Script-Sprache, erstellt. Mit dieser Basis ist es nun sehr einfach einen eigenen Rekorder zu erstellen. Hier der Code:

#-Begin-----------------------------------------------------------------

  #-Includes------------------------------------------------------------
    ."$PSScriptRoot\COM.ps1"

  #-Sub Main------------------------------------------------------------
    Function Main() {

      [Reflection.Assembly]::LoadFile("$PSScriptRoot\SAPFEWSELib.dll") > $Null

      $SapGuiAuto = Get-Object "SAPGUI"
      If ($SapGuiAuto -isnot [System.__ComObject]) {
        Break
      }

      $Application = Invoke-Method $SapGuiAuto "GetScriptingEngine"
      [SAPFEWSELib.GuiApplication]$Application =
        [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($Application,
        [SAPFEWSELib.GuiApplicationClass])
      If ($Application -isnot [System.__ComObject]) {
        Break
      }

      $Connection = $Application.Children.Item(0)
      If ($Connection -eq $Null) {
        Break
      }
      Else {
        [SAPFEWSELib.GuiConnectionClass]$Connection =
          [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($Connection,
          [SAPFEWSELib.GuiConnectionClass])
      }

      $Session = $Connection.Children.Item(0)
      If ($Session -eq $Null) {
        Break
      }
      Else {
        [SAPFEWSELib.GuiSession]$Session =
          [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($Session,
          [SAPFEWSELib.GuiSessionClass])
      }

      $Session.Record = $True
      Register-ObjectEvent -InputObject $Session -EventName "Change" -SourceIdentifier "Action" > $Null

      While ($true) {
        Write-Host "Waiting for event..."
        $Event = Wait-Event -SourceIdentifier "Action" -Timeout 10
        If ($Event -eq $Null) {
          Write-Host "No event received for 10 seconds."
          Break
        }

        [SAPFEWSELib.GuiSession]$RecSession =
          [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($event.SourceArgs[0],
          [SAPFEWSELib.GuiSessionClass])
        #Ohne die folgende Zeile liefer PowerShell im Folgenden einen Fehler
        $Dummy = $RecSession | Format-List | Out-String

        [SAPFEWSELib.GuiComponent]$RecComponent =
          [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($event.SourceArgs[1],
          [SAPFEWSELib.GuiComponentClass])
        Write-Host ( $RecComponent | Format-List | Out-String )
        #Write-Host ( $RecComponent | Select -ExpandProperty "ID" )
        Write-Host "Type / Method / Parameter: " $event.SourceArgs[2]

        Remove-Event -SourceIdentifier "Action"
      }

      Unregister-Event -SourceIdentifier "Action"
      $Session.Record = $False

    }

  #-Main----------------------------------------------------------------
    Main

#-End-------------------------------------------------------------------

Zur einfacheren Nutzung des Component Object Models (COM) habe ich ein Include erstellt, mit dem dies möglich ist.

#-Begin-----------------------------------------------------------------

  #-Load assembly-------------------------------------------------------
    [Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") > $Null

  #-Function Create-Object----------------------------------------------
    Function Create-Object {
      param([String] $objectName)
      try {
        New-Object -ComObject $objectName
      }
      catch {
        [Void] [System.Windows.Forms.MessageBox]::Show(
          "Can't create object", "Important hint", 0)
      }  
    }

  #-Function Get-Object-------------------------------------------------
    Function Get-Object {
      param([String] $objectName)
      [Microsoft.VisualBasic.Interaction]::GetObject($objectName)
    }

  #-Sub Free-Object-----------------------------------------------------
    Function Free-Object {
      param([__ComObject] $object)
      [Void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($object)
    }

  #-Function Get-Property-----------------------------------------------
    Function Get-Property {
      param([__ComObject] $object, [String] $propertyName)
      $objectType = [System.Type]::GetType($object)
      $objectType.InvokeMember($propertyName,
        [System.Reflection.Bindingflags]::GetProperty,
        $null, $object, $null)
    }

  #-Sub Set-Property----------------------------------------------------
    Function Set-Property {
      param([__ComObject] $object, [String] $propertyName, 
        $propertyValue)
      $objectType = [System.Type]::GetType($object)
      [Void] $objectType.InvokeMember($propertyName,
        [System.Reflection.Bindingflags]::SetProperty,
        $null, $object, $propertyValue)
    }

  #-Function Invoke-Method----------------------------------------------
    Function Invoke-Method {
      param([__ComObject] $object, [String] $methodName,
        $methodParameters)
      $objectType = [System.Type]::GetType($object)
      $output = $objectType.InvokeMember($methodName,
        "InvokeMethod", $NULL, $object, $methodParameters)
      if ( $output ) { $output }
    }

#-End-------------------------------------------------------------------

Weiterhin benötigt der Rekorder eine Interoperationsbibliothek für das SAP GUI Scripting. Mit Hilfe der Interop-Technik lassen sich alle klassischen, binär kompilierten Windows-Bibliotheken mit Wrappern versehen, so dass deren Programmfunktionen wie normale .NET-Programmfunktionen aufgerufen werden können. Entweder man erzeugt sich diesen Wrapper selbst mit TypLibImp, einem Tool aus dem .NET-Framework, oder man nutzt die Interoperationsbibliothek des NWBC.

Vom Ablauf her ist der Rekorder sehr einfach:

  • Es wird eine Verbindung zu einer SAP-Session hergestellt
  • Dann wird das Ereignis Change registriert
  • In einer Schleife, die nach 10 Sekunden ohne Ereignis beendet wird, werden die Informationen eines Ereignisses abgefangen
  • Im Objekt GuiComponent sind viele Informationen zu finden
  • Im dritten Argument des Ereignisses $event.SourceArgs[2] ist die Aktivität zu finden
SAP GUI Scripting Recorder in Windows PowerShell

SAP GUI Scripting Recorder in Windows PowerShell

Mit diesem Ansatz ist es nun sehr einfach möglich einen eigenen SAP GUI Scripting Rekorder zu implementieren.

Ist das gültig?

$
0
0

Das fragt man sich als Programmierer häufig. Speziell dann, wenn man aus zeitlich abgegrenzten Datensätzen den richtigen Datensatz herausfinden muss. Hier zeige ich dir, wie man einfach aber sicher ans Ziel kommt.

Gültigkeitszeitraum vs. Gültig ab

Wenn Datensätze zeitlich voneinander abgegrenzt werden sollen, hat man in der Regel die folgenden zwei Möglichkeiten:

  1. Exakte zeitliche Abgrenzung mit "Gültig ab" und "Gültig bis"
  2. Abgrenzung mit "Gültig ab"

Bei Variante (1) hat man den Vorteil, dass man einen Zeitraum auch "schließen" kann. Beziehungsweise im Umkehrschluss: Man muss den Zeitraum auch begrenzen. Anders bei Variante (2): Hier gibt es immer einen gültigen Eintrag (sofern mindestens ein Eintrag vorhanden ist).

Der große Nachteil bei Variante (1) ist ebenfalls, dass eine exakte zeitliche Abgrenzung schwer zu administrieren und zu programmieren ist. Besonders dann, wenn Zeiträume in einen oder zwei vorhandene Zeiträume eingefügt werden sollen. Hier ist Variante (2) deutlich einfacher und pflegeleichter.

Variante (2) eignet sich also besonders dann sehr gut, wenn zu einem "Objekt" immer ein gültiger Eintrag vorhanden sein soll. Deshalb werden wir uns diese Variante genauer ansehen.

Gültig ab-Tabelle2016-12-01_14-51-58

Als einfaches Beispiel habe ich mir eine Tabelle erstellt, die ein Gültig-Ab-Datum und eine Monatsbezeichnung hat. Es kann also für jedes Datum eine Bezeichnung vergeben werden.

Diese Tabelle ist zugegebener Weise einigermaßen Sinn frei, verdeutlicht aber sehr gut das Verfahren.

Ermittlung des gültigen Eintrags - herkömmlich

Um aus der vorgestellten Tabelle zu einem Datum den jeweils gültigen Eintrag zu lesen, geht man normalerweise folgendermaßen vor:

  1. Lies alle Einträge, deren Gültigkeit kleiner gleich dem gewünschten Datum ist.
  2. Sortiere die Einträge nach GUELTIG_AB absteigend, so dass der aktuellste Eintrag an erster Stelle steht
  3. Lies den Eintrag an erster Stelle
SELECT * FROM ztt_datum INTO TABLE lt_datum 
 WHERE gueltig_ab <= mein_datum
 ORDER BY gueltig_ab DESCENDING.
READ TABLE lt_datum INTO DATA(ls_datum) INDEX 1.
Der Nachteil von dieser Variante ist, dass man alle jemals gültig gewesenen Einträge lesen muss.

Ermittlung des gültigen Eintrags - Tricktresor-Style

Mithilfe eines Subqueries ist es möglich, exakt den einen richtigen Eintrag zu finden. Ein Code-Snippet sagt mehr als tausend Worte:

SELECT single * FROM ztt_datum INTO @DATA(ls_datum)
 WHERE datab = ( SELECT MAX( datab )
                   FROM ztt_datum
                  WHERE datab <= @p_datum ).

Ist das gültig? (2)

$
0
0

So. da bin ich im letzten Beitrag Ist das gültig? etwas über das Ziel hinaus geschossen... Ein Merkmal des Tricktresors ist es, dass ich die Beispiele auf das Wesentlich und nur absolut Notwendige reduziere. So bleiben die Tricks einfach und klar. Im letzten Beitrag habe ich jedoch etwas zu viel reduziert und der Trick ist für das Beispiel sogar komplizierter als notwendig.

Klarstellung

Natürlich kann man von einem definierten Eintrag sehr einfach den zuletzt - bzw. aktuell - gültigen Eintrag ermitteln. Ingo hat darauf hin gewiesen, dass die folgende Variante doch deutlich einfacher ist als die von mir vorgestellte:

SELECT * FROM ztt_datum
 UP TO 1 ROWS
 INTO ls_datum
 WHERE gueltig_ab <= p_datum
 ORDER BY gueltig_ab DESCENDING.
ENDSELECT.

Recht hat er.

Eigentlich wollte ich auch auf eine andere etwas komplexere Variante heraus. Nämlich die, bei der es nicht den einen Eintrag gibt, sondern eine unbestimmte Menge von Einträgen. Das könnten zum Beispiel alle gültigen Materialien eines Werkes sein oder Konten eines Buchungskreises sein.

Nächster Versuch

Für das neue Beispiel habe ich die folgende Tabelle erstellt:

ZTT_DATUM2

2016-12-05_10-51-52

Aus dieser Tabelle möchte ich alle zu einem Datum gültigen Gruppen ermitteln. Das funktioniert gut mit dem Befehl DELETE ADJACENT DUPLICATES:

SELECT * FROM ztt_datum2 INTO TABLE @DATA(lt_datum)
 WHERE datab <= @p_datum
 ORDER BY gruppe ASCENDING,
          datab  DESCENDING.

DELETE ADJACENT DUPLICATES FROM lt_datum COMPARING gruppe.

Es spricht nichts dagegen, diese Variante zu verwenden. Aber: Alles, was man mit einem Befehl anstelle von zweien lösen kann, ist irgendwie cooler. Zumal es immer schöner ist, nur die Daten zu lesen, die auch wirklich gebraucht werden.

Sub-query

Aus diesem Grund jetzt die wirklich hilfreiche und sinnvolle Variante mithilfe eines Sub-Queries:

SELECT * FROM ztt_datum2 AS z1 INTO TABLE @DATA(lt_datum)
 WHERE datab <= @p_datum
   AND datab  = ( SELECT MAX( datab ) FROM ztt_datum2
                   WHERE gruppe = z1~gruppe
                     AND datab <= @p_datum ).

Der Clou bei dieser Variante ist, dass man sich in der WHERE-Bedingung der Hauptselektion mit einem Sub-Query auf die jeweilige GRUPPE bezieht und hierfür das höchste Datum ermittelt.

Von der Performance her kann es sein, dass die herkömmliche  Variante sogar schneller ist. Da man diesen Select jedoch wahrscheinlich nur für sehr kleine Datenmengen verwenden wird, dürfte die Geschwindigkeit hier zu vernachlässigen sein.

Global. Lokal. Egal.

$
0
0

Im Rahmen eines Projektes haben wir uns mit der dynamischen Erzeugung von Klassen beschäftigt und Stefan hat dabei einen netten Trick herausgefunden.

Globale Klasse

Normalerweise gibt es in einem Report nur die folgenden zwei Möglichkeiten um eine Klasse zu instantiieren:

  1. Ein Objekt mit Referenz zu einer global definierten Klasse (SE24)
  2. Ein Objekt mit Referenz zu einer im selben Programm definierten lokalen Klasse

Stefan hat nun eine Möglichkeit gefunden, wie man lokale Klassen, die in beliebigen Programmen definiert sind, erzeugen kann. Und das geht so:

Lokale Klasse

Wir brauchen ein Programm mit einer lokal definierten Klasse. In dem folgenden Programm werden zwei lokale Klassen definiert. Beide haben die Methode INFO.

REPORT zttloc01.

CLASS lcl_local_01 DEFINITION.
 PUBLIC SECTION.
 METHODS info RETURNING VALUE(text) TYPE string.
ENDCLASS.

CLASS lcl_local_01 IMPLEMENTATION.

METHOD info.
 text = 'Ich bin Klasse 01'.
 ENDMETHOD.
ENDCLASS.

CLASS lcl_local_02 DEFINITION.
 PUBLIC SECTION.
 METHODS info RETURNING VALUE(text) TYPE string.
ENDCLASS.

CLASS lcl_local_02 IMPLEMENTATION.

METHOD info.
 text = 'Ich bin Klasse 02'.
 ENDMETHOD.
ENDCLASS.

Dynamische Erzeugung von Klasseninstanzen

Wenn man den Namen einer Klasse erst zur Laufzeit ermitteln kann, dann kann man ein Objekt dieser Klasse dynamisch wie folgt erzeugen:

DATA classname TYPE string.
DATA object    TYPE REF TO object.
classname = 'ZCL_CLASS_XYZ'.
CREATE OBJECT object TYPE (classname).

Sinnvoller Weise verwendet man hierfür ein Interface, dass alle in Frage kommenden Klassen implementiert haben.

Ihr Auftritt Herr Kollege

Unser Trick besteht nun darin, dass wir den Klassennamen genauer spezifizieren. Wir geben dem Klassennamen die Information mit, in welchem Programm die Klasse vorhanden ist. Der Klassenname sieht dann zum Beispiel wie folgt aus:

\PROGRAM=ZTTLOC01\CLASS=LCL_LOCAL01

Wir müssen also nur noch die intern verwendete Struktur zusammenbasteln. Das folgende Programm erzeugt je nach Auswahl ein Objekt der im Programm ZTTLOC01 lokale definierten Klasse LCL_LOCAL_01 oder LCL_LOCAL_02.

REPORT zttlocaccess.

PARAMETERS p_01 RADIOBUTTON GROUP cls DEFAULT 'X'.
PARAMETERS p_02 RADIOBUTTON GROUP cls.
PARAMETERS p_repid TYPE syrepid DEFAULT 'ZTTLOC01'.

START-OF-SELECTION.

 DATA classname TYPE string.
 DATA r_object TYPE REF TO object.
 DATA text TYPE string.

CASE 'X'.
 WHEN p_01.
   classname = 'LCL_LOCAL_01'.
 WHEN p_02.
   classname = 'LCL_LOCAL_02'.
 ENDCASE.

 CONCATENATE '\PROGRAM=' p_repid '\CLASS=' classname INTO classname.

 CREATE OBJECT r_object TYPE (classname).

 CALL METHOD r_object->('INFO') RECEIVING text = text.
 MESSAGE text TYPE 'I'.

Screenshot

Das rufende Programm muss natürlich genau wissen, was es tut. Je nachdem wie dynamisch der Aufruf einzelner Methoden sein soll, können ebenfalls dynamisch ermittelte Parameter über die Parameterliste übergeben werden.

Verwendet man ein Interface, dass die zu verwendenden Klassen implementieren, dann kann man verwendete Interface-Methoden direkt aufrufen.

Classname Utilities

$
0
0

Klassen sind inzwischen von ABAP nicht mehr wegzudenken. Die Verwaltung der Klassen erfolgt jedoch immer noch in INCLUDES. Diese Includes interessieren in der Regel niemanden. Hin und wieder tauchen diese Klassen-Include jedoch in Shortdumps auf. Der Name der Klasse selbst ist eindeutig aus dem Includenamen zu erkennen. Der Name der Methode jedoch nicht. Häufig gibt es zwar auch die zusätzlich notwendige Information, um welche Methode es sich handelt, aber das ist nicht immer der Fall.

Einzelne Includes sehen zum Beispiel so aus:

CL_OO_CLASSNAME_SERVICE=======CCDEF
CL_OO_CLASSNAME_SERVICE=======CCMAC
CL_OO_CLASSNAME_SERVICE=======CCIMP
CL_OO_CLASSNAME_SERVICE=======CCAU
CL_OO_CLASSNAME_SERVICE=======CU
CL_OO_CLASSNAME_SERVICE=======CO
CL_OO_CLASSNAME_SERVICE=======CI
CL_OO_CLASSNAME_SERVICE=======CP
CL_OO_CLASSNAME_SERVICE=======CT
CL_OO_CLASSNAME_SERVICE=======CS
CL_OO_CLASSNAME_SERVICE=======CM001
CL_OO_CLASSNAME_SERVICE=======CM002
CL_OO_CLASSNAME_SERVICE=======CM003
CL_OO_CLASSNAME_SERVICE=======CM004

CL_OO_CLASSNAME_SERVICE

Um die einzelnen intern verwendeten Objekte (Includes) verwalten zu können, gibt es die Klasse CL_OO_CLASSNAME_SERVICE. Mit dieser Klasse kannst du herausfinden, welche Methode zu welchem Include gehört und umgekehrt.

GET_METHOD_INCLUDE liefert zum Beispiel für die gleichnamige Methode den Includenamen CL_OO_CLASSNAME_SERVICE=======CM00B.

Umgekehrt liefert die Methode GET_METHOD_BY_INCLUDE die Methode zum Includenamen.

die Methode GET_ALL_CLASS_INCLUDES liefert eine komplette Liste der verwendeten Klassen-Includes.

 

Nummerierung

Interessant finde ich, dass die Methodenincludes nicht numerisch hochgezählt werden, sondern erst numerisch (1-9) und dann alphabetisch (A-Z). Bei einem drei-stelligen numerischen Feld, wie das verwendete (==CMnnn) würden 999 Methoden in eine Klasse passen. Eigentlich mehr als ausreichend, sollte man meinen.

 


Application-Log mit Kontext

$
0
0

Mit dem Application-Log lassen sich standardisiert Meldungen sammeln und ausgeben. Was viele nicht wissen: die Meldungen können um eigene Felder erweitert werden. Die Zusatzinformationen werden im KONTEXT gespeichert. Dieser Kontext muss definiert und natürlich separat gefüllt werden.

Im Demoprogramm SBAL_DEMO_02 kannst du nachvollziehen, wie die Programmierung erfolgen muss.

Kontext

Mit Kontext sind zusätzliche Felder gemeint, die zusätzlich zu jeder Meldung gesetzt werden können. Auf diese Weise lassen sich Meldungen besser gruppieren oder darstellen. Zudem können Meldungen an sich kürzer ausfallen. Häufig wird ein Protokoll für eine Vielzahl von gleichen Objekten erstellt (Materialnummern, Werke, Kunden, Belegnummer etc.) Anstelle der Meldung „Prüfung für Material &1 (Werk &2) und Charge &3 wurde erfolgreich beendet.“ könnten die Felder Materialnummer, Werk und Charge in einem Kontext gespeichert werden. Die Meldung lautet dann nur noch „Chargenprüfung erfolgreich.“.

Als Kontext muss eine eigene Struktur verwendet werden. Im Demoprogramm wird die Struktur BAL_S_EX01 verwendet:

Verwendung der Felder: Meldung hinzufügen

Um eine Meldung dem Meldungsprotokoll hinzuzufügen, ruft man den Baustein  BAL_LOG_MSG_ADD auf:

ls_msg-msgty = sy-msgty.
ls_msg-msgid = sy-msgid.
ls_msg-msgno = sy-msgno.
ls_msg-msgv1 = sy-msgv1.
ls_msg-msgv2 = sy-msgv2.
ls_msg-msgv3 = sy-msgv3.
ls_msg-msgv4 = sy-msgv4.
CALL FUNCTION 'BAL_LOG_MSG_ADD'
  EXPORTING
    i_s_msg = ls_msg.

In LS_MSG hat man die Meldungsnummer, Meldungs-ID und Meldungstyp sowie die vier Meldungsvariablen gespeichert. Möchte man den Kontext verwenden, dann muss auch das Feld LS_MSG-CONTEXT gefüllt werden:

DATA ls_msg     TYPE bal_s_msg.
DATA ls_context TYPE bal_s_ex01.

* define context information
 ls_context-carrid      = i_carrid.
 ls_context-connid      = i_connid.
 ls_context-fldate      = i_fldate.
 ls_msg-context-tabname = 'BAL_S_EX01'.
 ls_msg-context-value   = ls_context.

So kann für jede Meldung ein eigenes Set an Feldern mitgegeben werden.

Verwendung der Felder: Anzeige

Die Kontext-Felder werden nicht automatisch angezeigt, sondern müssen im Feldkatalog definiert werden. Damit die zusätzlichen Felder im Protokoll auftauchen, musst du sie im Display Profile hinzufügen. Mit dem Funktionsbaustein BAL_DSP_PROFILE_SINGLE_LOG_GET kannst du ein Standard-Anzeigeprofil laden und den Feldkatalog entsprechend erweitern:

* add passenger ID to message table
 l_s_fcat-ref_table = 'BAL_S_EX01'.
 l_s_fcat-ref_field = 'ID'.
 l_s_fcat-col_pos = 100.
 APPEND l_s_fcat TO l_s_display_profile-mess_fcat.

Verwendung der Felder: Baumanzeige

Ein Stärke des Application-Log ist die Darstellung der Meldung in einer Baumstruktur. Hierfür müssen die einzelnen Ebenen genau definiert werden. Das folgende Beispiel zeigt eine Aufteilung nach Fehlertyp und Kundennummer:

 

Das Coding dafür definiert den Feldkatalog und die Sortierung für die einzelnen Ebenen. LEV1_FCAT und LEV1_SORT definieren die Felder und die Sortierung für die oberste Ebene, LEV2* für die zweite und so weiter.

l_s_fcat-ref_table = 'BAL_S_EX01'.
 l_s_fcat-ref_field = 'ID'.
 l_s_fcat-col_pos   = 1.
 APPEND l_s_fcat TO l_s_display_profile-lev2_fcat.
 l_s_sort-ref_table = 'BAL_S_EX01'.
 l_s_sort-ref_field = 'ID'.
 l_s_sort-up        = 'X'.
 l_s_sort-spos      = 1.
 APPEND l_s_sort TO l_s_display_profile-lev2_sort.
 l_s_fcat-ref_table = 'BAL_S_SHOW'.
 l_s_fcat-ref_field = 'T_MSGTY'.
 l_s_fcat-col_pos   = 1.
 APPEND l_s_fcat TO l_s_display_profile-lev1_fcat.
 l_s_sort-ref_table = 'BAL_S_SHOW'.
 l_s_sort-ref_field = 'T_MSGTY'.
 l_s_sort-up        = 'X'.
 l_s_sort-spos      = 1.
 APPEND l_s_sort TO l_s_display_profile-lev1_sort.

Auf diese Weise ist eine schöne und ansprechende Darstellung von Meldungen leicht möglich.

Folgende Felder des Anzeigeprofils haben Einfluss auf die Baumdarstellung:

Das Feld TREE_ONTOP definiert, ob der Baum oben (TREE_ONTOP = X) oder links angezeigt wird (TREE_ONTOP = space).

Mit TREE_SIZE kann die Größe des Tree-Containers definiert werden.

Der Beitrag Application-Log mit Kontext erschien zuerst auf Tricktresor.

Application-Event stört gewaltig…

$
0
0

Ich bekam die Aufgabe, die Transaktion CO41 – Umsetzung Planaufträge um ein paar Funktionen zu erweitern. Nach einigem Hin- und Herüberlegen entschied ich mich dafür, an geeigneter Stelle (Include LCOUPF1N – Routine INIT_FBILD) mittels impliziter Erweiterung einen Docking-Container mit einem Dynamischen Dokument anzubinden. Der Docking Container war schnell erstellt und auch die ersten Zeichen inklusive Drucktaste waren zügig gezaubert.

Leider funktioniert dieser erste Wurf nicht, denn nach Drücken des Druckknopfes wurde nicht meine registrierte Methode angesprungen, sondern es kam die Meldung „Die angeforderte Funktion %_GC 1 ist hier nicht vorgesehen“.

Ich brauchte lange, um herauszufinden, dass die in einigen von der SAP verwendete FBS – Folgebildsteuerung der Grund für die Meldung war (Funktionsbaustein SCREEN_SEQUENCE_CONTROL). Beziehungsweise war das nicht in erster Linie der Grund, sondern der Umstand, dass bei den Dynamischen Dokumenten die zugehörigen Events alle standardmäßig als „application events“ registriert werden. Das hat zur Folge, dass die Ereignissteuerung nicht nur innerhalb der Klasse durchgeführt wird, sondern einen „Umweg“ über das Dynpro macht. Es wird in dem Fall ein Funktionscode ausgelöst, bei dem das PAI – Process After Input durchlaufen wird.

Meistens ist das egal, denn es werden vom Programm in der Regel nur die Funktionscodes abgeprüft, die auch definiert wurden. Man kann also in einer Transaktion „JIFDIJIFG“ eingeben und es passiert nichts. In der CO41 kann man das jedoch nicht, da für jeden Funktionscode in einer Tabelle nachgeprüft wird, wie bei diesem Funktionscode verfahren werden soll. Findet der Funktionsbaustein SCREEN_SEQUENCE_CONTROL keinen Eintrag, dann kommt die oben genannte Meldung.

Lösung

Ich machte mich schon darauf gefasst, die Klasse CL_DD_DOCUMENT abzuleiten, um die Methode DISPLAY, in der die Ereignisregistrierung stattfindet, zu redefinieren. Glücklicherweise ist das HTML-Objekt, in dem das Dynamische Dokument angezeigt wird und für das die Ereignisse registriert werden, öffentlich. Dadurch konnte ich das Ereignis SAPEVENT erneut, aber diesmal ohne APPL_EVENT, registrieren:

document->merge_document( ).
document->display_document( parent = docker ).
DATA(html) = document->html_control.

DATA: myevent_tab TYPE cntl_simple_events,
myevent TYPE cntl_simple_event.
myevent-eventid = html->m_id_sapevent.
myevent-appl_event = ' '.
APPEND myevent TO myevent_tab.
html->set_registered_events( events = myevent_tab ).

Nun konnte ich endlich meine Tasten drücken, ohne dass mir die Folgebildsteuerung in die Quere kam.

Der Beitrag Application-Event stört gewaltig… erschien zuerst auf Tricktresor.

REDUCE ABAP750 FOR x = u IN n = 1 THEN brainf*ck

$
0
0

Hoijoijoi. Ich gebe zu, ich tue mich echt schwer mit den neuen Befehlen im ABAP-Sprachschatz. Besonders die ganz neuen Features im ABAP Release 7.50 (oder 7.40 – ich blick da nicht mehr durch) fordern mich ziemlich (REDUCE, COND, FILTER etc).

Angeregt durch den Artikel von Jerry Wang im neuen SCN über REDUCE habe ich mich mit dem Befehl-REDUCE näher beschäftigt. Über die ABAP-Doku bin ich dann auf die Demoprogramme DEMO_REDUCE* gestolpert und beim DEMO_REDUCE_SIMPLE hängen geblieben.

Das Programm ermittelt mit Hilfe des REDUCE-Befehls die Summe von Werten in einer Tabelle. Das Programm DEMO_REDUCE_COND_ITERATION erzeugt mit Hilfe einer FOR-Schleife zusammengesetzte Texte (1 2 3 4 usw).

Ich wollte dann ein bisserl mit den Features herumspielen und hatte die Idee, einen HTML-Text zusammen zusetzen.

Aus der Tabelle

<HTML>
<BODY>
<P>

wollte ich die einzelnen Elemente plus einem separaten Text zu einem String zusammenfügen. Das funktionierte auch noch sehr einfach:

DATA(text) = REDUCE string(
               INIT html = ``
                FOR command IN html_commands 
                NEXT html = |{ html }{ command }| ) 
            && 'Hallo Welt'.

Eigentlich nur zum Spaß habe ich versucht, ob ich mit einem erneuten && auch ein erneutes REDUCE benutzen kann. Obwohl ich einigermaßen überrascht war, dass es anstandslos funktionierte, wäre ich auch maßlos enttäuscht gewesen, wenn es nicht geklappt hätte… 😉

Der nächste Schritt war dann etwas komplizierter: Ich wollte die einzelnen Tags auch wieder schließen. Natürlich in umgekehrter Reihenfolge. Und mit dem SLASH, der ein Ende-Tag anzeigt. Hier brauchte es dann etliche Versuche und verwirrte Blicke in die Doku um zu dem folgenden Ergebnis zu gelangen:

Und hier der Quelltext dazu:

REPORT zdemo_reduce_simple.

CLASS demo DEFINITION.
  PUBLIC SECTION.
  CLASS-METHODS main.
ENDCLASS.

CLASS demo IMPLEMENTATION.
  METHOD main.

  DATA(html_commands) = VALUE string_table(
                              ( `<html>` )
                              ( `<body>` )
                              ( `<p>` ) ).
  cl_demo_output=>write( html_commands ).


  DATA(text) = REDUCE string(
                INIT html = ``
                 FOR command IN html_commands NEXT html = |{ html }{ command }| )
             && 'Hallo Welt'
             && REDUCE string( INIT html = ``
                FOR n = lines( html_commands )
                THEN n - 1
                WHILE n > 0
                 NEXT html = html && COND #( LET command = html_commands[ n ]
                                              IN WHEN command IS NOT INITIAL
                                                 THEN command(1) && '/' && command+1 ) ).

   cl_demo_output=>write( text ).
   cl_demo_output=>display( ).
 ENDMETHOD.

ENDCLASS.

START-OF-SELECTION.
 demo=>main( ).

Ich lasse es einfach so unkommentiert stehen. Wenn man weiß, was der Ausdruck macht, ist es einigermaßen klar.

Hier der Beitrag von Horst Keller zum Thema ABAP-740 Features: https://blogs.sap.com/2014/09/30/abap-news-for-740-sp08-iteration-expressions/

Table Expressions

Sehr gewöhnungsbedürftig für mich ist der Zusatz um die einzelne Tabellenzeile (die Tabelle hat ja keine Struktur) anzusprechen:

COND #( LET command = html_commands[ n ]
         IN WHEN command IS NOT INITIAL
            THEN command(1) && '/' && command+1 )

Mein erster Ansatz war Folgendes (was aber nicht funktionierte):

html_commands[ n ]-TABLE_LINE

Also bin ich zum COND-Ausdruck gekommen. Hier finde ich verwirrend, dass anscheinend zwingend eine WHEN-Klausel angegeben werden muss. Vielleicht gibt es auch eine einfachere Alternative?! Bestimmt. Lasst es mich gerne wissen.

Link zur Horst Kellers Blog: https://blogs.sap.com/2013/05/29/abap-news-for-release-740-table-expressions/

 

Ein komisches Gefühl, auf ein mal wieder Anfänger zu sein…

Der guten alten Zeiten Willen noch mal das Coding mit ABAPvor740. Viel Länger ist es auch nicht. Und ob die Programmierung mit ABAP740 eleganter oder besser ist, wage ich zu bezweifeln.

DATA text TYPE string.
LOOP AT html_commands INTO DATA(command).
  text = text && command.
ENDLOOP.
text = text && 'Hallo Welt'.

DATA line TYPE i.
line = lines( html_commands ).
DO lines( html_commands ) TIMES.
  DATA(cmd) = html_commands[ line ].
  text = text && cmd(1) && '/' && cmd+1.
  SUBTRACT 1 FROM line.
ENDDO.

Der Beitrag REDUCE ABAP750 FOR x = u IN n = 1 THEN brainf*ck erschien zuerst auf Tricktresor.

Ausnahmen mit T100-Nachricht [ABAP750]

$
0
0

Die Verwendung von T100-Nachrichten mit Klassen basierten Ausnahmen ist von Beginn an krampfig. Alt und Neu passte immer irgendwie nicht so richtig zusammen. Paul Hardy hat sich in seinem sehr guten Buch ABAP To The Future dafür ausgesprochen, ausschließlich die neue Variante – also ohne T100-Nachricht zu verwenden. Es sprechen aber im Kampf mit den täglichen Programmiermonstern viele Dinge dafür, die Exceptions mit T100-Nachricht zu verwenden.

Umständlich

Die Verwendung von Klassen basierten Ausnahmen in Verbindung mit T100-Nachrichten ist äußerst umständlich gewesen:

DATA exc_t100 TYPE scx_t100key.

 TRY.
     exc_t100-msgid = 'OO'.
     exc_t100-msgno = '000'.
     exc_t100-attr1 = 'Test 1'.
     RAISE EXCEPTION TYPE cx_demo_t100 EXPORTING textid = exc_t100.
   CATCH cx_demo_t100 INTO DATA(error).
     MESSAGE error TYPE 'I'.
 ENDTRY.

Ab ABAP740 ist es möglich, die T100-Struktur direkt mit VALUE zu füllen:

RAISE EXCEPTION TYPE cx_demo_t100
   EXPORTING
     textid = VALUE scx_t100key( 
                     msgid = 'OO'
                     msgno = '000'
                     attr1 = 'Test 2' ).

Beide Varianten haben jedoch den Nachteil, dass die Aufbereitung der Meldung mit GET_TEXT nicht korrekt ist:

Die &-Parameter sollten eigentlich nicht vorhanden sein. Meine Meinung nach handelt es sich dabei um einen Fehler in Methode CL_MESSAGE_HELPER->SET_SINGLE_MSG_VAR in der bei der Zuweisung der Attribute beim Assign die Ausnahme cx_sy_assign_cast_illegal_cast falsch interpretiert wird. Das Attribut wird dann extra mit den &-Zeichen aufgebaut:

CONCATENATE '&' arg '&' INTO target.

Rettung in Sicht

Als ich mich auf die Suche nach einer Lösung gemacht habe, bin ich über den Blog Post von Horst Keller gestoßen: ABAP News for Release 7.50 – Converting Messages into Exceptions. In diesem Beitrag beschreibt Horst die Lösung, die ab Release ABAP750 angewendet werden soll:

RAISE EXCEPTION TYPE cx_demo_t100
   MESSAGE ID 'OO'
   NUMBER '000'
   WITH 'Test 3'.

Hierdurch wird die korrekte Ausgabe erzeugt:

Allerdings scheint das erst für Ausnahmen zu gehen, die auch in Release ABAP750 erzeugt wurden. Für die bisher verwendete Ausnahmeklasse CX_DEMO_T100 wird jedenfalls die Meldung ausgegeben:

Die Ausnahmeklasse muss das Interface „IF_T100_DYN_MSG“ haben, um den Zusatz „WITH“ verwenden zu können.

 

Der Beitrag Ausnahmen mit T100-Nachricht [ABAP750] erschien zuerst auf Tricktresor.

Switch für Ausnahmen eines Funktionsbausteins [ABAP740]

$
0
0

Seit Release ABAP740 gibt es die Anweisung SWITCH. Sehr anschaulich beschrieben im Blog von Horst Keller: ABAP News for Release 7.40 – Constructor Operators COND and SWITCH

Was mich bei Funktionsbausteinen schon immer gestört hat ist, dass man bei der sinnvollen Protokollierung von Returncodes von Funktionsbausteinen umständliche CASE-Konstrukte benötigt. Natürlich nur, wenn der Funktionsbaustein bei einem Fehler lediglich RAISE EXCEPTION verwendet aber nicht MESSAGE RAISING. Und Ersteres ist leider sehr häufig der Fall.

Mit dem neuen Feature SWITCH jedenfalls lässt sich ein Konstrukt bauen, das immer noch nicht schön ist, jedoch einigermaßen gut verwendbar. Hier am Beispiel von Funktionsbaustein CO_RU_CONFIRMATION, der schön viele Ausnahmen hat:

"Rückmeldung buchen
 CALL FUNCTION 'CO_RU_CONFIRMATION'
   EXPORTING
     afrud_imp = ls_afrud
     aktyp_imp = 'H'
     commit_flag = 'X'
     no_dialog_flag = 'X'
     rueck_imp = ls_afrud-rueck
     split_imp = ls_afrud-split
     call_from_list = ' '
   EXCEPTIONS
     confirmation_not_allowed = 1
     conf_canceled = 2
     different_key = 3
     interrupt_by_user = 4
     key_not_defined = 5
     material_data_not_found = 6
     missing_authority = 7
     new_status_not_possible = 8
     not_allowed = 9
     not_found = 10
     operation_not_found = 11
     operation_not_selectable = 12
     order_already_locked = 13
     order_category_not_valid = 14
     order_deleted = 15
     order_not_found = 16
     order_without_operation = 17
     predec_not_confirmed = 18
     sequence_not_found = 19
     table_entry_not_found = 20
     unit_conversion_not_possible = 21
     conf_data_false = 22
     conf_date_in_future = 23
     standard_conf_not_possible = 24
     work_center_not_found = 25
     conversion_error = 26
     order_data_not_found = 27
     wrong_aktyp = 28
     split_not_found = 29
     OTHERS = 30.
 IF sy-subrc = 0.
   COMMIT WORK.
 ELSE.
   data(subrc) = sy-subrc.
   "Fehler
   RAISE EXCEPTION TYPE zcx_hkv_pp_mig_general
         MESSAGE ID 'OO'
         NUMBER '000'
         WITH 'Fehler CO_RU_CONFIRMATION:'
              |{ subrc }|
           SWITCH #( subrc
 WHEN 1 THEN 'confirmation_not_allowed '
 WHEN 2 THEN 'conf_canceled '
 WHEN 3 THEN 'different_key '
 WHEN 4 THEN 'interrupt_by_user '
 WHEN 5 THEN 'key_not_defined '
 WHEN 6 THEN 'material_data_not_found '
 WHEN 7 THEN 'missing_authority '
 WHEN 8 THEN 'new_status_not_possible '
 WHEN 9 THEN 'not_allowed '
 WHEN 10 THEN 'not_found '
 WHEN 11 THEN 'operation_not_found '
 WHEN 12 THEN 'operation_not_selectable '
 WHEN 13 THEN 'order_already_locked '
 WHEN 14 THEN 'order_category_not_valid '
 WHEN 15 THEN 'order_deleted '
 WHEN 16 THEN 'order_not_found '
 WHEN 17 THEN 'order_without_operation '
 WHEN 18 THEN 'predec_not_confirmed '
 WHEN 19 THEN 'sequence_not_found '
 WHEN 20 THEN 'table_entry_not_found '
 WHEN 21 THEN 'unit_conversion_not_possible '
 WHEN 22 THEN 'conf_data_false '
 WHEN 23 THEN 'conf_date_in_future '
 WHEN 24 THEN 'standard_conf_not_possible '
 WHEN 25 THEN 'work_center_not_found '
 WHEN 26 THEN 'conversion_error '
 WHEN 27 THEN 'order_data_not_found '
 WHEN 28 THEN 'wrong_aktyp '
 WHEN 29 THEN 'split_not_found '
 WHEN 30 THEN 'OTHERS' ).

Zusammen mit dem Blockmarkiermodus (ALT-Taste während der Markierung des Quelltextes gedrückt halten) lässt sich auf diese Weise einfach eine saubere Protokollierung programmieren. Leider trotzdem sehr schade, dass man nicht mit Hilfe eines ABAP-Befehls den Text bzw. die Ausnahme erhalten kann.

PS: Der Zusatz MESSAGE zur Anweisung RAISE EXCEPTION ist erst ab Release ABAP750 vorhanden… Siehe Beitrag Ausnahmen mit T100-Nachricht

Der Beitrag Switch für Ausnahmen eines Funktionsbausteins [ABAP740] erschien zuerst auf Tricktresor.

Viewing all 166 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>