Plugins für APEX programmieren - Teil 2

In diesem Blog betrachten wird die PL/SQL-Seite eines Regions- oder Item-Plugins. Da wir uns nicht mit einer konkreten Aufgabenstellung beschäftigen, kommen hier keine Hilfsmethoden etc. zum Einsatz, sondern der Blick geht auf den grundsätzlichen Aufbau sowie einiger hilfreicher Hilfsmethoden, die ich in meinen Plugins regelmäßig verwende.

 

Plugins für APEX programmieren - Teil 2

Ich bevorzuge für den PL/SQL-Teil eines Plugins immer ein Package innerhalb der Datenbank. Alternativ ist es zwar auch möglich, diesen Code direkt in der Entwicklungsumgebung eines Plugins zu hinterlegen, doch sind mir hier die Möglichkeiten der Formatierung, des Debuggers und allgemein der Editierung nicht ausreichend. Zudem mag ich keine großen Code-Mengen in APEX-Anwendungen, dort beschränke ich mich im Regelfall auf das absolute Minimum, weil ich die einzelnen Anwendungsseiten eher dem View- als dem Control-Layer zuordne. Beim Deployment, das muss ich zugeben, ist ein solches Vorgehen allerdings leicht aufwändiger, denn es muss nun auch ein Package installiert werden. Doch werden wir sehen, dass auch die Auslieferung der zugeordneten Dateien als separate Dateien Sinn macht (sie können so durch den WebServer gecacht werden), und in diesem Zusammenhang fällt das zusätzliche Package aus meiner Sicht nicht ins Gewicht.

Das PL/SQL-Package

Grundlage ist ein Package, dass als Stub bei mir stets alle möglichen Methoden eines Plugin-Typs implementiert, bei einem Region-Plugin also zwei Funktionen für das Rendern und Refreshen einer Region, bei einem Item-Plugin zusätzlich noch eine Validate-Funktion. Ich habe früher die Namen dieser Funktionen an das Plugin angepasst, bei einem Passwort-Item also zum Beispiel RENDER_PASSWORT_ITEM, doch bin ich davon abgekommen: Die Funktion heißen nun einheitlich einfach RENDER, REFRESH oder VALIDATE. Stattdessen nenne ich das Package dann PLUGIN_PASSWORD_ITEM. Die Deklaration dieser Funktion folgt der Vorgabe für diese Funktionen, so wie sie in der Dokumentation hinterlegt sind.

Ein Plugin erhält Zugriff auf eine ganze Reihe Eigenschaften der Region, die über die Assistenten der Entwicklungsumgebung gesetzt werden. Einige der wichtigen Eigenschaften betreffen den SQL-Source der Region oder des Items, Angaben zur Darstellung, aber insbesondere natürlich auch die für das Plugin definierten Parameter mit ihren aktuell eingestellten Werten. Seit Version 4.2 ist die Zahl der Parameter auf 15 angehoben worden, vorher betrug sie 10. In vielen Plugins sehe ich eine kopierte Version von Patrick Wolfs Beispielen (inklusive seiner Kommentare), die als einen der ersten Schritte die übergebenen Parameterwerte auf sprechend bezeichnete Variablen vornimmt. Zwar ist das aus meiner Sicht eine gute Idee, doch wird oft vergessen, dass diese Arbeit zweimal durchgeführt werden muss: Einmal beim Rendern, ein zweites Mal beim Refresh. Zudem sind oft Variablenwerte zu setzen, deren Werte nicht als Parameter übergeben, sondern aus anderer Quelle besorgt werden. Für beide Anwendungsfälle sehe ich daher normalerweise eine kleine Hilfsfunktion vor, die übergebene Parameterwerte, den Quellcode der Region und andere Werte auf einen Record kopiert, den ich normalerweise g_ctx nenne. Nachfolgend zeige ich einmal eine solche Hilfsfunktion:

type ctx_t is record(
    report_source apex_application_page_regions.attribute_01%type,
    report_item apex_application_page_regions.attribute_02%type,
    report_description hierarchy_definition.description%type,
    render_template apex_application_page_regions.attribute_03%type,
    render_options apex_application_page_regions.attribute_04%type,
    module apex_application_page_regions.attribute_05%type,
    title_option apex_application_page_regions.attribute_06%type,
    refresh_interval number,
    use_description_as_title apex_application_page_regions.attribute_08%type,
    hide_if_empty apex_application_page_regions.attribute_09%type,
    reuse_filter boolean,
    no_data_found_message varchar2(32767),
    report_static_id apex_application_page_regions.static_id%type,
    report_id number,
    region_sql varchar2(32767)
  );
  g_ctx ctx_t;
  
  procedure copy_parameters(
    p_region apex_plugin.t_region)
  as
  begin
    g_ctx.report_source := p_region.attribute_01;
    g_ctx.report_item := p_region.attribute_02;
    g_ctx.render_template := p_region.attribute_03;
    g_ctx.render_options := p_region.attribute_04;
    g_ctx.module := p_region.attribute_05;
    g_ctx.title_option := p_region.attribute_06;
    g_ctx.refresh_interval := to_number(p_region.attribute_07);
    g_ctx.use_description_as_title := p_region.attribute_08;
    g_ctx.hide_if_empty := p_region.attribute_09;
    g_ctx.reuse_filter := p_region.attribute_10 = 'Y';
    g_ctx.no_data_found_message := p_region.no_data_found_message;
    g_ctx.region_sql := p_region.source;
    g_ctx.region_static_id := p_region.static_id;
    get_report_id;
    if g_ctx.report_id is not null then
      select description
        into g_ctx.report_description
        from hierarchy_definition
       where id = g_ctx.report_id
         and rownum = 1;
    end if;
  end copy_parameters; 

Dieser Ausriss stammt aus einem Plugin, das als Wrapper um JQuery DataTables fungiert, es ist in der Lage, beliebige Berichte in SQL (oder auch gespeichert in der Datenbank) aufzurufen und auf einer APEX-Seite darzustellen. Wenn Sie die einfache Hilfsmethode durchsehen, stellen Sie fest, dass eben auch etwas aufwändigere Arbeiten als nur das einfache Umkopieren von Parameterwerten hier durchgeführt wird. Diese Hilfsprozedur wird nun bei den Funktionen RENDER und REFRESH aufgerufen und setzt den Kontext auf das aktuell verwendete Plugin.

Instrumentierung des Codes

In jedem Fall sinnvoll ist es, das eigene Plugin in den Debug-Strom von APEX zu integrieren. Dies erreichen Sie, indem Sie eine Standardprozedur aus dem Package APEX_PLUGIN_UTIL aufrufen:

-- APEX-Debug instrumentieren
if wwv_flow.g_debug then
  apex_plugin_util.debug_region(
    p_plugin => p_plugin,
    p_region => p_region,
    p_is_printer_friendly => p_is_printer_friendly);
end if;

Zu dieser Prozedur ist nicht viel zu sagen, außer, dass der Aufruf dazu dient, Debug-Meldungen aus dem Plugin zu schreiben, sobald diese Option durch die Oberfläche angefordert wird.

Einbindung von externen Dateien

Im vorigen Teil des Blogs hatte ich schon darauf hingewiesen, dass in den Einstellungen ein Pfad zu den Dateien des Plugins angegeben werden kann. Der nachfolgende Schritt besteht nun darin, die extern benötigten JavaScript- und CSS-Dateien in die Ausgabe einzubinden. Auch hierfür hält das Package APEX_JAVASCRIPT bzw. APEX_CSS vorgefertigte Prozeduren bereit, die einfach verwendet werden können:

-- --------------------
-- JQUERY DATA TABLE
-- --------------------
apex_javascript.add_library (
	p_name           => 'jquery.dataTables.min',
	p_directory      => c_path || 'js/',
	p_version        => null,
	p_skip_extension => false
);
apex_javascript.add_library (
	p_name           => 'TableTools.min',
	p_directory      => c_path || 'extras/TableTools/js/',
	p_version        => null,
	p_skip_extension => false
);
...
apex_css.add_file (
	p_name           => 'TableTools_JUI',
	p_directory      => c_path || 'extras/TableTools/css/',
	p_version        => null,
	p_skip_extension => false
);
apex_css.add_file (
	p_name           => 'TableTools',
	p_directory      => c_path || 'css/',
	p_version        => null,
	p_skip_extension => false
);

Seit 4.2 existiert hier die neue Option, ein Tag #MIN# einzufügen. Dies sorgt dafür, dass beim Deployment (ohne Entwicklungsumgebung) eine entsprechende minified-Version der Datei ausgeliefert wird, wohingegen während der Entwicklung die Langversion verwendet wird. Dies erleichtert die Entwicklung nicht unerheblich. Achten Sie zudem darauf, dass die Dateiendung jeweils nicht geschrieben wird. Wie immer in JavaScript sind Verzeichnis- und Dateibezeichner case sensitive und müssen daher genau so geschrieben werden, wie sie auf Platte stehen.

Erzeugung des HTML-Rahmens

Zu Beginn der Beschäftigung mit dieser Technik hat mich irritiert, "wohin" nun eigentlich der HTML-Code kommt und wer ihn vorgibt. Kommt der HTML-Content aus einem Template? Oder aus dem Region-Source? Beides ist grundsätzlich möglich, die meisten Plugins, die ich bislang gesehen habe (und alle eigenen) erzeugen den HTML-Code jedoch innerhalb der RENDER-Funktion selbst. Dadurch ist die Kenntnis dessen, was das Plugin benötigt, außerhalb des Plugins nicht erforderlich. Allerdings kann es Situationen geben, wo Sie hiervon abweichen müssen. Daher ist die Folgende Implementierung nur der Normalfall. Da ich es nicht mag, sehr viel HTML-Code innerhalb eines PL/SQL-Packages zu schreiben, beschränke ich mich auf das allernötigste. Hier sind es einige Regionen, die anschließend die einzelnen Teile des Berichtes aufnehmen sollen:

-- Initialisierung
l_standard_content := 
  '  <div id="' || g_ctx.region_static_id || '_error" style="display:none;" class="red"></div>' || 
  '  <div id="' || g_ctx.region_static_id || '_info" style="display:none;"></div>' || 
  '  <div id="' || g_ctx.region_static_id || '_help" style="display:none;"></div>' ||    
  '  <div id="' || g_ctx.region_static_id || '_filter"></div>' || 
  '  <div id="' || g_ctx.region_static_id || '_report" style="width:100%;overflow:auto;"></div>';

In meinem Beispiel habe ich verschiedene Regionen, die Fehlermeldungen, Benachrichtigungen etc. sowie den eigentlichen Bericht aufnehmen sollen. Diese werden hier erzeugt und in den Body des Region-Templates eingefügt. Wichtig für die spätere Arbeit mit dem Plugin ist, eine eindeutige ID zu erteilen, damit Sie später mehrere Instanzen des Plugins auf der Seite unterscheiden können. Ich verwende hier die Region Static ID, die ich aus dem "normalen" Eingabefeld für die statische ID entnehme. Ist diese nicht vergeben, wird hier ein durch APEX vergebener, numerischer Wert verwendet.

Erzeugung des initialen JavaScripts

Der nächste Schritt ist nun die Erzeugung des Onload-Codes, des Codes also, der ausgeführt wird, wenn das Plugin auf der Seite geladen wurde. Die Aufgabe ist es, das JavaScript-Pendant dieses Plugins aufzurufen und ihm die relevanten Parameter zu übergeben:

-- Onload Code erzeugen
l_js_code := 
  'apex.widget.DataTables.init("' ||  g_ctx.region_static_id || '", {' || g_return ||
  '  "id":"'|| p_region.static_id || '",' ||  g_return ||
  '  "ajaxCallback": "' || apex_plugin.get_ajax_identifier  || '",' || g_return ||
  '  "reportItem":"' || g_ctx.report_item || '",' || g_return ||
  '  "reportId":"' || g_ctx.report_id || '",' || g_return ||
  '  "reportDescription": "' || g_ctx.report_description || '",' || g_return ||
  '  "hideIfEmpty": "' || g_ctx.hide_if_empty || '",' || g_return ||
  '  "useDescriptionAsTitle": "' || g_ctx.use_description_as_title || '",' || g_return ||
  '  "render":"' || g_ctx.render_template || '",' || g_return ||
  '  "renderOptions": "' || g_ctx.render_options || '",' || g_return ||
  '  "standardContent": ''' || l_standard_content || ''',' || g_return ||
  '  "pageItems": "' || p_region.ajax_items_to_submit || '", ' || g_return ||
  '  "renderStatus": "", ' || g_return ||
  '  "dataTable": {}, ' || g_return ||
  '  "refreshInterval": ' || g_ctx.refresh_interval || g_return || ',' || g_return ||
  '  "noDataFoundMessage": "' || g_ctx.no_data_found_message || '",' ||
  '  "reuseExistingFilter": "' || case when g_ctx.reuse_filter then 'true' else 'false' end || '"' ||
  '});';

Besonders wichtig ist Zeile 5, in der ein eindeutiger Kennzeichner für diese Instanz des Plugins erzeugt wird. Hier hat Patrick Wolf nicht gekleckert, sondern geklotzt und einen wirklich eindeutigen Identifier erzeugen lassen ;-). Der Sinn dieses Identifiers liegt darin, dass APEX im Falle eines Refreshs wissen muss, welche (JavaScript)-Instanz des Plugins durch das Refresh angesprochen werden soll. Dieser Identifier ist nun mindestens seitenweit (de facto wohl eher weltweit) eindeutig und erlaubt diese Identifikation.

Eine andere, wichtige Eigenheit des Codes sehen Sie in Zeile 3. Das JavaScript-Gegenstück unseres Plugins wird hier im Einklang mit den anderen APEX-Plugins programmiert (so, wie das auch das APEX-Team selbst tut), und das bedeutet, dass der Namensraum apex.widget erweitert wird. Dies tun wir hier und rufen zudem die  init-Methode auf, die einen Identifier und ein Options-Objekt erwartet. Details hierzu finden Sie im nächsten Teil des Blogs.

Kommentar schreiben


Sicherheitscode
Aktualisieren