Contexte und Tabellen-Exits

In Fortsetzung des Themas Tabellenpräsentation (nach den Artikeln über Tabellengestaltung mit CSS und über die Datenquellen) geht es nun um die Table Exits. Es zeigt sich, dass es ein allgemeineres, dominierendes Oberthema gibt, von dem die Table Exits nur eine praktische Anwendung darstellen: Die View-Contexte.

View und Viewcontext: Das dynamische Duo

Die Präsentationsschicht - der View-Teil - ist im MVC-Muster nur eine von drei Schichten der Webanwendung. Durch konsequenten Einsatz von MVC im Entwurf erreicht man bereits eine klarere Aufgabentrennung der Anwendungsteile: Das eigentliche Know-How der Anwendung liegt im Model, die Darstellung der Anwendung im View und steuernde, vermittelnde Aufgaben im Controller.

In den Views einer Webanwendung können wir die eigentliche Präsentierungsinformation in Form von HTML-Code vorsehen. Dazu kommt Style in Form von CSS-Angaben und Behaviour - clientseitige Logik - in Form von JavaScript-Code. In Business Server Pages kommt schliesslich noch die serverseitige Präsentierungslogik in Form von ABAP-Code hinzu.

Es wäre ungünstig, diese Sprachvielfalt in den Entwicklungsobjekten zu mischen. Wir müssen daher, über das MVC-Modell hinaus, auch die Präsentierungsschicht weiter aufgliedern:

  1. Das Präsentierungsmodell (HTML oder XML) codiert die Grundstruktur der Seite: Die anzuzeigenden Daten, ihre Container und Eingabeelemente. In einer Business Server Page sehen wir die BSP-Elemente als Erweiterungen des HTML- oder XML-Codes ebenfalls in dieser Ebene. BSP-Elemente verbergen Anweisungen an den Server, aus den angegebenen Daten weiteren HTML- oder XML-Code zu generieren und an der angegebenen Stelle in das Gerüst der Seite einzubinden. Auch enthalten BSP-Elemente serverseitig auszuführende Angaben über die Datenquellen (Datenbindung), aus denen die im HTML-Code zu präsentierenden Daten letztlich gewonnen werden sollen.
  2. Der Stil (CSS oder XSLT) macht konkretere Angaben über das Wie der Darstellung. Dazu gehören Dinge wie Hintergrundfarben und -Bilder, Positionierung von Datencontainern, Schriftarten etc. Stile lassen sich in externe CSS- oder XSLT-Dateien auslagern, die vom Browser beim Seitenaufbau dazugelesen werden. Das macht nicht nur den View lesbarer, sondern macht auch den Seitenaufruf effizienter, da die Stil-Informationen sich eher selten ändern und in den temporären Dateien des Browsers vorgehalten werden, so dass sie nicht bei jedem Request wieder neu über die Leitung gesendet werden müssen.
  3. Das clientseitige Verhalten enthält alle Arten von Präsentierungslogik, die sich nicht im Modell und im Stil darstellen lassen, deren Ausführung man aber dem Browser überlässt. Man könnte Verhalten auch als Reaktionen auf Events im User Agent definieren. Da es aber auch ein onload-Event gibt, mit dem man beliebige Präsentationslogik verknüpfen kann, sind die Übergänge hier fliessend. Die Sprache der Wahl, um clientseitiges Verhalten zu implementieren, ist JavaScript. Auch JavaScript-Dateien lassen sich in externe Dateien auslagern, mit denselben Vorteilen wie beim Stil.
  4. Das serverseitige Verhalten enthält Logik, die bereits auf dem Server ausgeführt werden kann - bevor der Benutzer die Seite zu Gesicht bekommt. Das ist in Business Server Pages ABAP-Code, der mit den Scripting-Direktiven <%...%> und <%=...%> in den View eingebaut wird.
Im Gegensatz zu Stil und clientseitigem Verhalten lässt sich das serverseitige Verhalten nicht in externe Dateien auslagern, das liegt im Entwurf der Server Pages begründet. Was kann man also tun, um der wartungsunfreundlichen Durchmischung von HTML-Code und ABAP-Code zu begegnen?

Man kann die Präsentierungslogik in eine Klasse auslagern - solche Klassen nenne ich Viewcontexte. Im hier vorgestellten MVC-Framework ist ein Viewcontext dadurch gekennzeichnet, dass er das Interface zif_view_context implementiert. Trifft der Controller beim Aufruf eines Views auf ein Seitenattribut, das sich auf den Typ zif_view_context abbilden lässt, so erzeugt er automatisch eine Instanz und ruft die einzige Interfacemethode set_view( ) auf, in der Initialisierungscode des Contexs hinterlegt werden kann. Der View enthält in Form des Viewattributs nun eine Referenz auf den Viewcontext. So wie der Stil eine Referenz auf eine externe Datei enthält und die Stilinformationen durch Verweise auf Klassennamen abgerufen werden können, so kann nun der View mit der Referenz auf den Context, beispielsweise <%=ctx%> operieren und Methoden und Attribute des Contexts einbinden. Hier bestehen auch Ähnlichkeiten mit den BSP-Extensions. Aber der Context ist viewspezifischer, er enthält keinen für beliebige Views nützlichen Präsentierungscode.

Ein View bildet mit seinem Context ein dynamisches Duo: Der HTML-Code entsteht in einem Wechselspiel, einem PingPong zwischen View und Context. Der View enthält statischen HTML-Code, BSP-Elemente und Referenzen auf Attribute und Methoden von Contexten. Immer wenn er beim Prozessieren des Views auf eine Context-Referenz stösst, übergibt er die Kontrolle an den Context. Die folgende Abbildung verdeutlicht schematisch den Zusammenhang bei der Abarbeitung des Views:

Ein Beispiel

Hier ein Beispiel aus der Praxis: Ein leicht gekürzter View aus einer Applikation zur Anzeige einer Liste von Anlieferungen. Er arbeitet mit einem Context ctx zusammen, der auf dem Attribute-Reiter als Instanz der Klasse zcl_ctx_lili deklariert ist. Das der Anwendung zugrundeliegende Model zcl_lili ist hier ebenfalls als Attribut aufgeführt. Da es in der Flow Logic mit der ID lili deklariert wurde, wird es hier aufgrund der Namensgleichheit automatisch vom MVC-Framework an den View weitergereicht. Wenn die Viewverarbeitung beginnt, sind also beide Attribut an reale Objekte gebunden, ohne dass hierfür auch nur eine Zeile Anwendungscode nötig war.

Unmittelbar vor dem Aufruf des Views wird nun - ebenfalls automatisch - die Contextmethode zif_view_context~set_view() aufgerufen. Da die Instanz des Views als Parameter übergeben wird, hat der Viewcontext Gelegenheit, Viewattribute zu übernehmen und bereits gewisse Vorbereitungen für das Rendern zu treffen:
method zif_view_context~set_view.

* Viewattribut lili enthält Referenz auf Lieferlisten-Model
* Referenz in globales Attribut übernehmen
iv_view->get_attribute( exporting name = 'lili' importing value = go_lili ). * Lieferlisten fürs Rendern vorbereiten build_blocks( ). endmethod.
Doch nun zum Präsentierungscode des Views lili.htm selbst. Leicht vereinfacht, hat er folgende Struktur:
<%@page language="abap"%>
<%@extension name="Z" prefix="z"%>
<div class="buttons">
    <z:button fcode     = "expa"
              href      = "javascript:expandAll();"
              text      = "<%=otr(ZSSCC/ALLE_AUF)%>"/>
    <z:button fcode     = "coll"
              href      = "javascript:collapseAll();"
              text      = "<%=otr(ZSSCC/ALLE_ZU)%>"/>
    <span class="separated">
      <z:button fcode="save"
                href="javascript:doSave();"
                text="<%= otr(ZSSCC/BELEG_SICHERN) %>"/>
    </span>
</div>
<div class="blocks">
<% while ctx->has_more_tables( ) eq 'X'. %>

<%-- Überschrift für neuen Lieferanten --%>
<%   if ctx->begin_of_block( ) eq 'X'.        %>
  <div class="listheader title">
      <img class = "link"
           id    = "img_<%=ctx->block_id( )%>"
           src   = "../public/srs/arrow_open.gif"
           title = "<%=otr(ZSSCC/AUF_ZU)%>">&nbsp;<%=ctx->block_caption( )%>
  </div>
  <div class="blockContainer" id="bc_<%=ctx->block_id( )%>">
<% endif. %>

<%-- Eine Tabelle der Lieferungen je Lieferant --%>
      <div class="tableCaption"><%=ctx->table_caption( )%></div>
      <z:table class    = "block listcontainer"
               data     = "<%=ctx->gt_table->*%>"
               exit     = "<%=ctx%>"
               table_id = "block_<%=ctx->table_id( )%>">
        <z:column name="vbeln"
                  header="<%=zcl_bsp_util=>dtel_text_s('vbeln_vl')%>"/>
        <z:column header  = "<%=otr(ZSSCC/WARENEINGANGSSTATUS_KURZ)%>"
                  name    = "statu"/>
        <z:column name="wert" tdClass="right"/>
        <z:column header  = "<%=zcl_bsp_util=>dtel_text_s('zanzpos')%>"
                  name    = "anzpo"/>
      </z:table>

  <% if ctx->end_of_block( ) eq 'X'. %>
    </div>
  <% endif. %>

<% endwhile. %>
</div>
Der View hat die Aufgabe, eine grosse Tabelle von Lieferungen, die im Model vorgehalten wird, in mehrere kleine Tabellen je Lieferant aufzuspalten. Jede dieser kleinen Tabellen wird dann mit dem <z:table>-Element ausgegeben. Ohne auf alle Details eingehen zu wollen, folgen hier einige Erläuterungen:

Delayed Rendering

Beim Entwurf eines BSP-Elements wie <z:table>, das seine Daten aus einer internen Tabelle ableitet, hat man grundsätzlich zwei Möglichkeiten: Nun gibt es beim Rendern einer internen Tabelle normalerweise keine Abhängigkeit der Darstellung von der durchlaufenen Tabellenzeile: Das Element benötigt die interne Tabelle selbst - die kann bereits beim Öffnen des Elements angegeben werden - und allenfalls ein bisschen Customizing zur Darstellung der einzelnen Spalten, z.B. die Texte der jeweiligen Spaltenüberschriften. Der nicht-iterierende Entwurf erscheint daher zunächst als der natürlichere: Einmal, nämlich bei do_at_beginning(), wird die Tabelle an das Element übergeben, und einmal wird der Inhalt des Elements auf der Suche nach Spaltencustomizing durchgelesen: Danach, bei do_at_end() kann sich das Element an seine Arbeit machen, indem es die Tabellenzeilen durchläuft und jede Zeile gemäss dem angegebenen Spaltencustomizing präsentiert.

Aus diesen Gründen wurde für das Element <z:table> der nicht-iterierende Entwurf gewählt. Die Tabelle wird als Attribut an das Element übergeben, und der Elementinhalt enthält verschiedene Sub-Elemente wie <z:toolbar>, <z:column> und <z:tableBody>, mit denen man das Rendering beeinflussen kann. Das Rendering selbst erfolgt dann in der Methode do_at_end( ). Der Elementinhalt, etwa das Spaltencustomizing, wurde also nur einmal prozessiert, dann erfolgt die Iteration über die Tabellenzeilen. Damit lässt sich eine grosse Anzahl von Präsentierungsanforderungen abdecken.

Wie es aber keine Regel ohne Ausnahme gibt, hängt die Präsentierung in manchen Fällen eben doch von der gerade durchlaufenen Tabellenzeile ab. Es stellt sich dann die Frage, wie solche Abhängigkeiten in einen nicht-iterierenden Entwurf aufgenommen werden können. Die Anforderung nenne ich delayed rendering: Der im View vorgesehene Code soll nicht sofort in die HTML-Ausgabe geschrieben werden - sonst würde er ja nur einmal durchlaufen - sondern zu einem späteren Zeitpunkt, hier beim Verarbeiten der Tabellenzeilen in zcl_bsp_z_table->do_at_end( ).

Templates

Die technisch einfachste Lösung für das delayed rendering sind die altbewährten Templates: Strings mit Platzhaltern, die nach festgelegten Spielregeln zum Renderzeitpunkt aufgelöst werden. Diese Lösung deckt bereits den grössten Teil der Anforderungen an ein dynamisches Rendering ab.

Ich will das Vorgehen am Beispiel einer Aktienkurstabelle demonstrieren: Nehmen wir an, die Tabelle hat eine Spalte kurs und eine weitere Spalte change, die die Änderung zum Vortag angibt. Es kann eine Anforderung sein, dass man den Kurs zusammen mit der Änderung in einer Spalte ausgeben will, in der Form "116.20 (+13.05)". Diese Form wird durch das Template #kurs# (#change#) beschrieben, das man in der Spaltendefinition für den Kurs unterbringen kann:

  <z:table binding="//aktien/kurse">
    <z:column name="kurs">#kurs# (#change#)</z:column>
    <z:column name="change" invisible="true"/>
  </z:table>
Was geschieht bei der Verarbeitung dieses Codes? Nach zcl_bsp_z_table->do_at_beginning() wird zunächst der Elementinhalt geparsed und verarbeitet. Hier enthält das Element <z:table> nur Spaltencustomizing, das mit den Elementen <z:column> festgelegt wird. Das <z:column> hat selbst fast keine Logik, es ist ein reines Durchreiche-Element: Die Attribute und auch der Inhalt werden an das <z:table>-Element weitergereicht und dort in einer Hashtabelle mit dem Spaltennamen als Schlüssel abgelegt. In diesem konkreten Beispiel wird das <z:table>-Element also darüber in Kenntnis gesetzt, dass die Spalte kurs ein HTML-Template aufweist und die Spalte change nicht als eigene Spalte gerendert werden soll (invisible = "true").

Erst bei zcl_bsp_z_table->do_at_end() beginnt die Ausgabe der Tabelle. Nun liegen alle Informationen über die gewünschte Darstellung vor, und die Schleife über die Tabellenzeilen kann ausgeführt werden. Intern werden in der Methode substitute() die Templates pro Tabellenzeile aufgelöst, so dass zeilenweise ein anderer HTML-Code entsteht.

Die Templates können nun verschiedene Grade von Dynamik aufweisen. Wieder wird die grosse Mehrzahl der Anforderungen bereits durch die Substitution von Komponenten und des Zeilenindex (#tabix#) abgedeckt. Eine weitere grosse Zahl von Fällen kann mit einer einfachen "Wenn-Dann-Logik" erledigt werden - beispielsweise die folgende Anforderung: Wenn das Flag LOEVM den Wert X hat, soll statt dieses Wertes die Trash-Ikone angezeigt werden. Wenn es den Wert space hat, soll die Zelle leer bleiben. Genau für diese Anforderung gibt es das Muster #isfilled:...#. Hinter dem : kann man den HTML-Code für die Ikone hinterlegen. Er wird nur verwendet, wenn die Komponente in der aktuellen Zeile nicht ihren typgemässen Initialwert hat.

Manchmal wird aber auch zeilenweise eine Logik benötigt, die sich nur noch programmieren lässt. Da das Spaltencustomizing nur einmal durchlaufen wird, vor dem eigentlichen Rendern der Tabelle, kann man für diesen Zweck kein ABAP-Scripting verwenden. Statt der in Methode substitute() vordefinierten Regeln soll beliebiger Custom Code beim Rendern der Zellen durchlaufen werden.

Das ist die grosse Stunde des View-Contexts! Es gibt zwei mögliche Wege, um während des Renderns der Tabelle Custom Code zu prozessieren, die wir im folgenden genauer beschreiben werden.

Template Bedeutung
#comp# Eine von # eingerahmte Zeichenfolge wird als Komponentenname der aktuellen Tabellenzeile interpretiert. Diese Methode ersetzt die Folge #comp# durch den Inhalt der Komponente comp der aktuellen Tabellenzeile. Der Inhalt wird dabei ins externe Format des Datentyps konvertiert.
#!comp# Die Folge wird literal durch den Inhalt der Komponente comp ersetzt. Es erfolgt keine Konvertierung ins externe Format.
#tabix# Pseudokomponente TABIX: Der Ausdruck #tabix# wird immer durch den vierstelligen Zeilenindex ersetzt.
#isfilled:...# Den auf ":" folgenden Text verwenden, wenn der Inhalt des Feldes nicht initial ist. Ist er initial, soll space gerendert werden. Typischer Anwendungsfall für dieses Konstrukt sind Flags: Wenn ein Flag aktiv ist, soll nicht einfach der Wert X erscheinen, sondern beispielsweise ein bestimmtes Icon. Dann setzt man den HTML-Code für das Icon nach den ":".
#render_cell:<id># Es wird das Ereignis ZIF_RENDER_EVENT~RENDER ausgelöst. Der Ereignisparameter iv_id enthält den nach dem ':' folgenden String. Der Ereignisparameter iv_context enthält aktuelle Tabellenzeile und den Komponentennamen. Das Schreibgerät iv_out kann genutzt werden, um das Feld nach eigenen Regeln zu rendern: Sein Inhalt ersetzt nach Ausführung den Ausdruck #render_cell:<id>#
\# Das Fluchtsymbol # wird maskiert.

Table Exits

Ein illustratives Beispiel hierzu können Sie sich live in der Tag Library Demo-Applikation ansehen: Die Rotfärbung von Fehlermeldungen. Hier sollen die Texte von Fehlermeldungen in roter Schrift angezeigt werden. Ausserdem soll der Meldungstyp durch eine passende Ikone visualisiert werden. Hier noch einmal der Auszug aus der Implementierung der Methode render_cell(), der einzigen Methode des Interface ZIF_TABLE_EXIT:
method zif_table_exit~render_cell .

  data: lv_gif type string value '<img src="s_s_led#.gif" width="16" height="14">',
        lv_text type string value '<td style="color:red">#</td>'.

* DDIC-Zeilentyp
  field-symbols: <ls_row> type zt100.

* Nur für die gewünschte Tabelle
  check iv_table_id eq 'M__test__gt_test'.

* Feldzugriff ermöglichen
  assign is_row to <ls_row>.

  case is_field-fieldname.
    when 'TYPE'.
* Regel 1: Fehlertyp als GIF darstellen (nur Inhalt geändert)
      replace '#' in lv_gif with <ls_row>-type.
      ev_html = lv_gif.
      ev_done = zif_table_exit~co_content_done.
    when 'TEXT'.
      if <ls_row>-type eq 'E'.
* Regel 2: Nur im Fehlerfall: Meldung rot anzeigen (ganze Zelle geändert)
        replace '#' in lv_text with <ls_row>-text.
        ev_html = lv_text.
        ev_done = zif_table_exit~co_all_done.
      endif.
  endcase.

endmethod.
Der Code ist wie folgt aufgebaut:

Das Render-Event

Auch beim Thema "Render-Events" möchte ich an den Anfang ein Beispiel zur Illustration des Problems stellen. Im Retail Store gibt es die Anzeige der für einen Betrieb gelisteten Warengruppen:

Was hier auffällt: Die Spalten sind unterschiedlich gross - andererseits ist die Reihenfolge der Spalten aber frei customizebar. Das bedeutet, jede Spalte muss eine width gemäss dem Komponentennamen bekommen, den der User für die Anzeige in dieser Spalte jeweils gewählt hat. Wie lässt sich dies erreichen?

Das gerenderte HTML zeigt, dass nach dem einleitenden <table> die Deklaration der Spaltenbreiten mit dem <colgroup>-Element erfolgt:

<table id="WARENGRUPPEN" class="listcontainer" style="table-layout:fixed;" > 
  <colgroup>
    <col width="100">
    <col width="80">
    <col width="180">
    <col width="20">
    <col width="180">
    <col width="35">
  </colgroup>
  <tr>
    <td class="listheader">Boss-Thema</td>
    <td class="listheader" colspan="02" >Warengruppe</td>
    <td class="listheader" colspan="02" >Sortimentsstufe</td>
  </tr>
  ...
Es stellen sich zwei Fragen:
  1. Wie bekommt man den <colgroup>-Block in das <table>-Element hinein?
  2. Wie kann man erreichen, dass bei Umkonfigurieren der Spaltenanordnung durch den Benutzer die entsprechenden width-Angaben im <colgroup>-Block angepasst werden?

ad 1. Um zusätzlichen Code an bestimmten Stellen in das HTML-Element <table> einzuschleusen, bietet das BSP-Element <z:table> ein besonderes Subelement namens <z:tableBody>. Mit dem Attribut position gibt man an, wo der zusätzliche HTML-Code erscheinen soll. position="top" beispielsweise bedeutet: Unmittelbar nach Rendern des <table>-Elements. Eine statische Deklaration der Spaltenbreiten könnte man daher auf folgende Weise erreichen:

<z:table table_id = "WARENGRUPPEN"
         rows = "<%= lv_rows %>"
         data = "<%= et_wgrp %>"
         pagingButtonPos = "top"
         tableTagAttributes = 'style="table-layout:fixed;"' >
 <z:toolbar toolbar_id="WARENGRUPPEN"
            strucname="ZSRS_WGRP_BOSSNR" />
 <z:tableBody position="top">
   <colgroup>
    <col width="100">
    <col width="80">
    <col width="180">
    <col width="20">
    <col width="180">
    <col width="35">
  </colgroup>
 </z:tableBody>
 <z:column name  = "WGBEZ"    joinHeader = "true" />
 <z:column name  = "RQGRPBEZ" joinHeader = "true" />
 ...
</z:table>
Übrigens verwenden Sie das Element <z:tableBody> natürlich nicht für zusätzlichen HTML-Code innerhalb von Zellen, denn dort haben Sie ja den Elementinhalt des betreffende <z:column>-Elements für HTML-Templates zur Verfügung (sowie Table Exits, falls einfache Substitutionen nicht ausreichen). Das Element wird nur für Code benötigt, der ausserhalb von Zellen, aber innerhalb des <table>-Elements eingeschleust werden soll.

ad 2. Nun bekommen Sie zwar die gewünschte <colgroup> ins HTML – aber leider stimmen die Breiten nur, wenn der User die Anordnung der Spalten nicht ändert. Der <colgroup>-Block muss dynamisiert werden. Dies können Sie durch folgende leichte Änderungen des obigen Codes erreichen:

 <z:tableBody position="top" renderer="<%=renderer%>">
 <colgroup>
 #zzbossnr:<col width="100">
 #matkl:<col width="80">
 #wgbez:<col width="180">
 #sstuf:<col width="20">
 #sstufbez:<col width="180">
 #wdaus:<col width="35">
 #rqgrp:<col width="20">
 #rqgrpbez:<col width="180">
 #default:<col width="25">
 #</colgroup>
 </z:tableBody>
Hier übergeben Sie also nicht einen festen HTML-Codeblock, sondern eine Liste von Name/Wert-Paaren mit einer denkbar einfachen Syntax: Jeder Name wird von einem # eingeleitet, und jeder Wert beginnt nach dem :. Die Namen dieser Paare sind natürlich die Spaltennamen der Tabelle, und der zugeordnete Wert ist der zu wählende HTML-Code, der für diese Spalte in die <colgroup>-Definition zu setzen ist.

Das Objekt renderer, das hier an das Element <z:tableBody> übergeben wird, ist vom Typ zcl_srs_colgroup_renderer. Es ist eine wiederverwendbare Klasse, die nur auf das eine Problem der dynamischen <col>-Angaben spezialisiert ist. Durch Angabe dieses Renderers wird das Objekt als Behandler für das Ereignis zif_render_event~render registriert und kann in der Methode zif_render_exit~render darauf reagieren:

method zif_render_exit~render.

  constants: lc_col_default type string value '<col width="20">'.

  data: lv_table    type ref to zcl_bsp_z_table,
        lv_toolbar  type ref  to zcl_bsp_z_toolbar,
        lv_field_it type ref to zif_iterator,
        lt_dict     type zsrs_dict_tab,
        lv_head     type string,
        lv_tail     type string.

* Nur bei Aufruf durch das Table Tag
  lv_table ?= sender.
  check lv_table is not initial.

* Nur für die richtige Phase
  check iv_phase = phase.

* Toolbar abholen
  lv_toolbar ?= lv_table->toolbar.

* Gibt es die Toolbar? Dann Toolbar-Iterator verwenden
  if lv_toolbar is not initial.
    lv_field_it = lv_toolbar->field_iterator( ).
  else.
    lv_field_it = lv_table->field_iterator( ).
  endif.

* Content analysieren....
  parse_content( exporting iv_content = content
                 importing ev_head    = lv_head
                           et_dict    = lt_dict
                           ev_tail    = lv_tail ).

* ... und aufbauen
  iv_out->print( lv_head ).
  iv_out->new_line( ).

  build_by_list( iv_iterator = lv_field_it
                 iv_out      = iv_out
                 it_dict     = lt_dict
                 iv_default  = lc_col_default ).

  iv_out->print( lv_tail ).
  iv_out->new_line( ).

endmethod.
Die Methode prüft zunächst, ob sie im richtigen Kontext gerufen wird: Wurde das Render-Ereignis wirklich vom <z:table>-Element ausgelöst, und ist dies auch die richtige "Phase" (in diesem Fall top)? Sodann wird ein Felditerator beschafft, der die aktuelle Anordnung der Spalten durchläuft. Der Elementinhalt wird in der Methode parse_content() analysiert. Dabei werden die Name/Wert-Informationen in eine Hashtabelle eingetragen, ein "Wörterbuch" lt_dict. Dieses wird dann schliesslich an die Methode build_by_list() übergeben, um die einzelnen <col>-Informationen in den Ausgabestream zu setzen - der Ausgabestream wird dem Ereignisbehandler Importparameter iv_out übergeben.

Wie man HTML-Code im Context vermeiden kann

Bislang haben sich unsere Darstellungen darauf konzentriert, wie man ABAP-Code im View entweder ganz eliminieren kann (Templates) oder mittels Render Events oder Exits in einen Viewcontext verlagert. So kann der View sich auf seine "Kernkompetenz" verlegen, den Präsentationscode, HTML- und BSP-Elemente. Nun soll noch die umgekehrte Frage behandelt werden: Wie kann man HTML-Code vom ABAP in die View-Ebene bekommen?

Zuerst: Warum sollte man das überhaupt versuchen? Man kann natürlich auch in ABAP stets mit HTML-Code auf der Stringebene arbeiten. Das ist jedoch nicht schön, weil der View dadurch unvollständig wird. In Exits kommen irgendwelche weitere HTML-Codefetzen in die Ausgabe, die im View selbst nicht zu sehen sind. Das beeinträchtigt die Nachvollziehbarkeit des Views. Besser wäre es, diese Codestücke auch im HTML-Code selbst vorzusehen und dem Viewcontext zugänglich zu machen. Aber wie?

Die Grundidee, dies möglich zu machen, ist, den HTML-Code als Elementinhalt eines speziellen BSP-Elements im View zu notieren. Dieses spezielle BSP-Element nimmt den Inhalt entgegen und parsed ihn ggf. nach bestimmten, im View Context definierbaren Regeln. Danach stehen die HTML-Codestücke in Exits oder an anderen Stellen des View-Contexts zur Verfügung, um in den Ausgabestrom gesetzt zu werden.

Zur Demonstration kommen wir wieder auf die Visualisierung mit Ikonen zurück: Eine Statusspalte soll einen bestimmten Status nicht als Wert anzeigen, sondern in Form einer passenden Ikone. Ausserdem soll je nach Statuswert ein anderer Kurztext, der als Domänenfestwert hinterlegt ist, als Quickinfo erscheinen. Wir haben oben gesehen, dass man derartige Anforderungen leicht mit Table Exits erfüllen kann. Die obige Lösung hat jedoch noch den Schönheitsfehler, dass sie HTML-Code im ABAP-Code versteckt, indem die Templatevariablen mit HTML-Codestücken initialisiert werden. Man könnte nun das HTML-Element <img> zum Rendern der passenden Ikone im Viewcontext als String generieren. Noch besser aber ist es, dieses HTML-Element im View zu belassen und nur die Auswahl des passenden Codestücks im Viewcontext zu übernehmen.

So kommen wir zu folgendem Code (kleine Randbemerkung: Für einen Quickinfo-Text verwendet man nicht das Attribut alt, das hat nämlich einen anderen Zweck, sondern das Universalattribut title):

<z:column name       = "STALD"
          tdClass    = "info"
          joinHeader = "true"
          header     = "<%= zcl_bsp_util=>dtel_ddtext('ZLDIFF_ITEM_STAT_LD') %>" >
  &no_difference:     <img src="../public/bc/icons/s_s_ledg.gif" title="#title#">
  &transmission_open: <img src="../public/bc/icons/s_s_ledr.gif" title="#title#">
  &feedback_open:     <img src="../public/bc/icons/s_s_ledy.gif" title="#title#">
  &confirmed:         <img src="../public/bc/icons/s_b_okye.gif" title="#title#">
  &rejected:          <img src="../public/bc/icons/s_b_okno.gif" title="#title#">
  &out_of_limit:      <img src="../public/bc/icons/s_s_wsdo.gif" title="#title#">
</z:column>
Auf dem View notieren wir die HTML-Templates für die verschiedenen zu verwendenden Ikonen. Um einzelne Werte zu unterscheiden, geben wir ihnen einen Schlüssel, der sie identifiziert, und verwenden – wie bereits im Abschnitt Render-Events – eine einfache ad hoc-Syntax für Name/Wert-Paare. Jeder Name wird mit dem Zeichen & eingeleitet. Der Doppelpunkt leitet dann den Wert ein, wobei wir führenden und schliessenden Leerraum ignorieren.

Im Viewcontext werten wir diesen Code nun aus und führen allfällige Ersetzungen durch. Dies geschieht in der Implementierung der Methode render_cell() des Interface zif_table_exit, die für jede anzuzeigende Zelle einmal aufgerufen wird:

      case is_field-fieldname.
*----------------------------------------------------------------------
*        Unterschiedliche Status werden durch Icons repräsentiert
*----------------------------------------------------------------------
        when 'STALD'.
* Dictionary aus dem View einlesen (einmal)
          if gt_dict is not initial.
            build_dictionary( exporting iv_content = is_column-value
                              importing et_dict    = gt_dict ).
          endif.

* Dictionary zum aktuellen Status lesen
          read table gt_dict assigning <ls_dict> 
               with table key name = <ls_row>-stald.
          if sy-subrc eq 0.
            ev_html = <ls_dict>-value.
          else.
            clear ev_html.
          endif.
          
* Quickinfo-Text einsetzen
          if ev_html ne space.
            lv_title = zcl_bsp_util=>domvalue_text( iv_domname = 'ZLDIFF_ITEM_STAT'
                                                    iv_value   = <ls_row>-stald ).
            if sy-subrc eq 0.
              replace '#title#' in ev_html with lv_title.
            endif.
          endif.

* Rendern des Inhalts wurde übernommen
          ev_done = zif_table_exit~co_content_done.


Der Elementinhalt ist im Exit im Feld is_column-value verfügbar - zunächst ein reiner String. Die Methode build_dictionary() macht aus diesem String dann ein "Wörterbuch", eine Hashtabelle. Hier hat man nicht viel selbst zu programmieren, denn für die wesentliche Arbeit, eine einfache Name/Wert-Syntax wie die oben im View verwendete zu analysieren und in eine Hashtabelle einzulesen, gibt es in der Klasse ZCL_RENDER_EXIT_BASE bereits die Methode parse_content(), die man im wesentlichen aufrufen kann, um eine eigene Hashtabelle zu befüllen (ich habe sie zu Release 7.00 in eine statische öffentliche Methode verwandelt, damit sie leichter in eigenem Code wiederverwendet werden kann). Dies nur als Beispiel, wie der im View hinterlegte HTML-Code in der Präsentationslogik des Viewcontexts weiterverarbeitet werden kann.

HTML-Codestücke an beliebiger Stelle im View

Es kann vorkommen, dass Sie einen ähnlichen Mechanismus wie den oben erklärten benötigen, sich aber nicht innerhalb einer Tabelle befinden, sondern irgendwo anders im View. Für diesen Fall habe ich kürzlich ein neues Element <z:span> in der Tag Library Z kreiert. Es hat eine ähnliche Funktion wie <z:tableBody> innerhalb einer Tabelle, kann aber an einem beliebigen Ort im View stehen. Es hat die Attribute renderer (das kennen Sie schon) und id, damit mehrere <z:span>-Elemente im selben View unterschieden werden können.

Wenn Sie keinen Renderer angeben, macht das Element schlicht überhaupt nichts. Das liegt daran, dass das Ereignis zif_render_event~render, das es am Ende auslöst, dann ungehört verpufft. Wenn Sie aber einen Renderer angeben (üblicherweise gibt man einen Viewcontext als Renderer an), so kommt dieser bei zcl_bsp_z_span->do_at_end() als Ereignisbehandler dran, und Sie können in der Methode zif_render_exit~render() festlegen, was nun eigentlich geschehen soll. Dabei bekommen Sie den Elementinhalt im Parameter iv_content mitgeliefert, die ID des Elements sowie ein Schreibgerät iv_out, in das Sie die gewünschte Ausgabe setzen können.

Damit haben Sie alles in der Hand, um wie oben beschrieben vorzugehen: Den iv_content können Sie parsen, um ihm HTML-Codefragmente bzw. -Templates zu entnehmen. Diese können Sie nun gemäss anderen Informationen, die der Viewcontext hat, bearbeiten, um den zu sendenden HTML-Code in die Ausgabe zu setzen.

Um den Einsatz des Elements <z:span> zu verstehen, können Sie sich den Unit Test der Elementbehandlerklasse zcl_bsp_z_span ansehen (wie ja Unit Tests überhaupt oft als die beste Entwicklerdoku fungieren. Denn sie beschreiben nicht in Worten, wie die Klasse wohl funktionieren sollte, sondern zeigen die Verwendung der Klasse an einigen typischen Beispielen - und stellen zugleich dank des "Modultest"-Menüpunkts unter Beweis, dass die Klasse auch wirklich wie beschrieben funktioniert!).

Die Methode test() in der lokalen Unit-Test-Klasse lcl_au_basic von zcl_bsp_z_span führt einen HTTP-Request auf eine speziell für Unit Tests der Tag Library vorgesehene BSP-Applikation ZAU_WTAG_TEST aus und prüft - der Einfachheit halber mittels Stringvergleich, ob das <z:span>-Element, das in den Views dieser BSP-Applikation enthalten ist, seine Arbeit korrekt ausgeführt hat. Das Element wird in der Testseite verwendet, um zwei Templates zu definieren, eines für gerade und eines für ungerade Zahlen. Die geraden Zahlen sollen in grüner Farbe, die ungeraden in roter Farbe dargestellt werden:

<!-- Testseite für ABAP Units                                        -->
<!-- Bei Änderungen bitte auch Modultests in ZCL_BSP_Z_SPAN anpassen -->

<z:span id="pairImpair" renderer="<%=ctx%>">
  &z0:<div style="color:green">#i# ist eine gerade Zahl</div>
  &z1:<div style="color:red">#i# ist eine ungerade Zahl</div>
</z:span>
Bevor wir sehen, wie das im Context implementiert ist, schauen wir uns erst einmal das Ergebnis an:

Der Renderer liest zunächst den Elementinhalt ein. Bei der oben im View verwendeten Syntax für Name/Wert-Paare kann ohne weiteres Coding die Klasse zcl_srs_dict zum Parsen verwendet werden. Sie liest die Paare in eine Hash-Tabelle ein, auf die dann bequem mit einer get()-Methode zugegriffen werden kann.

Danach interpoliert der Renderer die Schleifenvariable in fünf Wiederholungen (do 5 times) und stellt das Ergebnis in den Ausgabestrom:

method zif_render_exit~render.

  data: ls_key  type ty_key value 'z0',
        lv_i    type n,
        lv_word type string.

* Um nur Render-Events für <z:span>-Elemente zu behandeln
  check iv_phase eq 'span'.

* Um nur das Render-Event des <z:span>-Elements der ID 'pairImpair' zu behandeln
  check iv_id eq 'pairImpair'.

* HTML-"Wörterbuch" einlesen
  go_dict->parse( iv_context ).

* Nun kann gerendert werden, z.B. anhand von Modeldaten
* Oder hier mit einer sinnleeren DO-Schleife, um irgendetwas zu produzieren
  do 5 times.
* Irgendwelche Daten...
    lv_i = sy-index.
* ...ergeben bestimmte Schlüssel...
    ls_key-mod = sy-index mod 2.
* ... für den Zugriff auf die Tabelle der HTML-Templates
    lv_word = go_dict->get( ls_key ).
* Das Template wird nun noch interpoliert...
    replace '#i#' in lv_word with lv_i.
* ... und schliesslich ausgegeben
    iv_out->print( lv_word ).
  enddo.

endmethod.

Zusammenfassung

Sie haben in diesem Artikel weitere Beispiele dafür gesehen, was für starke Freiheiten im Entwurf die Business Server Pages bieten. Im Gegensatz zu anderen Frameworks erledigt das BSP-Framework nicht alles für Sie, sondern bietet Ihnen nur unaufdringlich für genau die Probleme Hilfe an, auf die es spezialisiert ist. Die von der BSP-Laufzeit bereitgestellte Präsentationsumgebung bietet ein offenes UI: Sie haben die Freiheit, Teile der Anwendung auch ohne jede Unterstützung durchs Framework zu entwickeln und jedes Byte selbst zu bestimmen, das in den Ausgabestrom gesetzt werden soll. Sie sparen jedoch Zeit, wenn Sie sich die Softwareteile zunutze machen, die gängige Probleme von Webanwendungen lösen.

Auch die Tag Library ist in diesem Sinne nur ein Angebot, das Sie nutzen können, aber nicht müssen. Die hier diskutierten Beispiele <z:span> und <z:table> zeigen, dass es möglich ist, BSP-Elemente mit sinnvoller Funktionalität zu entwerfen, die dennoch in ihrem Einsatz flexibel und anpassbar sind. Die View Contexte als ABAP-Code führende Begleiter von Views helfen Ihnen darüberhinaus, ABAP- und HTML-Code im Entwurf der Präsentation besser auseinanderzuhalten.

Zurück