Export static workspace files in APEX5

In APEX 5 the functionality for the workspace- and application files is improved tremendously.

The most important changes are:
1. It is possible to upload zip files which will be extracted in the database.
2. The files can be organized within directories.

With this new functionality it is a lot easier to push your static files from development to other environments like acceptance and production.

Although it is here where a problem arises. At our site we deliver our applications via SQL scripts. When APEX exports an application it does include the static application files nicely into the export file. But what if you want to deliver your static workspace files as well? There is no check-box or what so ever to tell APEX to include these files in the export. I have searched but could not find functionality in APEX to achieve this. Normally google and twitter are your best friends in this situations but to my disappointment there was also no solution available on the internet. I had to figure this one out for myself.

In the wwv_flow_api package in the apex schema is a procedure to create a static workspace file.

[code language=”sql”]
procedure create_workspace_static_file (
p_id in number default null,
p_file_name in varchar2,
p_mime_type in varchar2,
p_file_charset in varchar2 default null,
p_file_content in blob );
[/code]

With this procedure you can upload a BLOB to the APEX static workspace files. All we need to do is write a script which will take all the static workspace files and create a PL/SQL block for each of them with the above procedure. There are several ways to create a script. In this example we will use dbms_output to get it done. After running the script, save the dbms output data to create the script.

The script looks as follows, in this script it is explained what is done and why.

[code language=”sql”]
declare
— workspace to export the static file from
l_workspace varchar2(255) := ‘APEX5’;

— cursor for all static workspace files
cursor c_file is
select w.id
, w.security_group_id
, w.file_name
, w.mime_type
, w.file_charset
, w.file_content
from apex_050000.wwv_flow_company_static_files w
join apex_050000.wwv_flow_companies c
on c.provisioning_company_id = w.security_group_id
where c.display_name = l_workspace;

begin
— loop throug all files
for r_file in c_file loop
— start pl/sql block
dbms_output.put_line(‘begin’);
— set the security group id to make it possible to use the script outside APEX (toad,sqldeveloper)
dbms_output.put_line(‘ wwv_flow_api.set_security_group_id(p_security_group_id=> ‘||r_file.security_group_id||’);’);
— create varchar table to hold the data for the BLOB
dbms_output.put_line(‘ wwv_flow_api.g_varchar2_table := wwv_flow_api.empty_varchar2_table;’);
— loop through LOB in blocks of 200 to improve readabiliy of the end script
for i in 1 .. trunc(dbms_lob.getlength(r_file.file_content)/200)+sign(mod(dbms_lob.getlength(r_file.file_content),200))
loop
— save to raw file data from the blob to a varchar table
dbms_output.put_line(‘ wwv_flow_api.g_varchar2_table(‘||i||’) := ”’
||utl_raw.cast_to_varchar2(utl_raw.cast_to_raw(dbms_lob.substr(r_file.file_content,200,((i-1)*200)+1)))
||””||’;’);
end loop;
— create the create_workspace_static_file procedure with the correct parameters
dbms_output.put_line(‘ wwv_flow_api.create_workspace_static_file(‘
dbms_output.put_line(‘ p_file_name => ”’||r_file.file_name||””);
dbms_output.put_line(‘ , p_mime_type => ”’||r_file.mime_type||””);
dbms_output.put_line(‘ , p_file_charset => ”’||r_file.file_charset||””);
— convert the varchar table to a blob as input for the procedure
dbms_output.put_line(‘ , p_file_content => wwv_flow_api.varchar2_to_blob(wwv_flow_api.g_varchar2_table)’);
dbms_output.put_line(‘ );’);
dbms_output.put_line(‘end;’);
end loop;
— commit your work
dbms_output.put_line(‘commit;’);
end;
[/code]

This script will deliver output that looks like this:

[code language=”sql”]
begin
wwv_flow_api.set_security_group_id(p_security_group_id=> 5325975770000001);
wwv_flow_api.g_varchar2_table := wwv_flow_api.empty_varchar2_table;
wwv_flow_api.g_varchar2_table(1) := ‘406368617273657420225554462D38223B0A0A756C2E64726F70646F776E2C0A756C2E64726F70646F776E206C692C0A756C2E64726F70646F776E20756C207B0A206C6973742D7374796C653A206E6F6E653B0A206D617267696E3A20303B0A2070616464696E673A20303B0A7D0A0A756C2E64726F70646F776E207B0A20706F736974696F6E3A2072656C61746976653B0A207A2D696E6465783A203539373B0A20666C6F61743A206C6566743B0A7D0A0A756C2E64726F70646F776E206C69207B0A20666C6F’;
wwv_flow_api.g_varchar2_table(2) := ‘61743A206C6566743B0A206D696E2D6865696768743A203170783B0A206C696E652D6865696768743A20312E33656D3B0A20766572746963616C2D616C69676E3A206D6964646C653B0A7D0A0A756C2E64726F70646F776E206C692E686F7665722C0A756C2E64726F70646F776E206C693A686F766572207B0A20706F736974696F6E3A2072656C61746976653B0A207A2D696E6465783A203539393B0A20637572736F723A2064656661111111232334555532212111126F70646F776E20756C207B0A20766973’;
wwv_flow_api.g_varchar2_table(3) := ‘6962696C6974793A2068696464656E3B0A20706F736974696F6E3A206162736F6C7574653B0A20746F703A20313030253B0A206C6566743A20303B0A207A2D696E6465783A203539383B0A7D0A0A756C2E64726F70646F776E20756C206C69207B0A20666C6F61743A206E6F6E653B0A7D0A0A756C2E64726F70646F776E20756C20756C207B0A20746F703A203170783B0A206C6566743A203939253B0A7D0A0A756C2E64726F70646F776E206C693A686F766572203E20756C207B0A207669736962696C697479’;
wwv_flow_api.g_varchar2_table(4) := ‘3A2076697369626C653B0A7D’;
wwv_flow_api.create_workspace_static_file(p_file_name => ‘css/xxxxx.css’
, p_mime_type => ’text/css’
, p_file_charset => ‘utf-8’
, p_file_content => wwv_flow_api.varchar2_to_blob(wwv_flow_api.g_varchar2_table)
);
end;
begin
wwv_flow_api.set_security_group_id(p_security_group_id=> 5325975770000001);
wwv_flow_api.g_varchar2_table := wwv_flow_api.empty_varchar2_table;
wwv_flow_api.g_varchar2_table(1) := ‘406368617273657420225554462D38223B0D0A0D0A756C2E64726F70646F776E2C0D0A756C2E64726F70646F776E206C692C0D0A756C2E64726F70646F776E20756C207B0D0A20206865696768743A206175746F3B0D0A20206261636B67726F756E642D636F6C6F723A20236638663866383B0D0A7D0D0A0D0A756C2E64726F70646F776E207B0D0A2020666F6E742D7765696768743A20626F6C643B0D0A2020666F6E742D73697A653A20313270783B0D0A2020706F736974696F6E3A2072656C61746976653B’;
wwv_flow_api.g_varchar2_table(2) := ‘0D111110746F703A203070783B0D0A20206C6566743A203070783B0D0A2020626F726465722D72696768743A2031707820736F6C6964207267626128302C302C302C302E3135293B0D0A20206D617267696E3A203020302031367078203670783B0D0A7D0D0A0D0A756C2E64726F70646F776E206C69207B0D0A202070616464696E673A20303B0D0A7D0D0A0D0A756C2E64726F70646F776E206C693A686F766572207B0D0A20206261636B67726F756E642D636F6C6F723A20236565653B0D0A7D0D0A0D0A756C’;
wwv_flow_api.g_varchar2_table(3) := ‘2E64726F70646F776E206C692061207B0D0A20646973706C61793A20626C6F636B3B0D0A2070616464696E673A203670782031307078203470783B0D0A2077686974652D73706163653A206E6F777261703B0D0A7D0D0A0D0A756C2E64726F70646AFFAF12aA5A6C696E6B2C0D0A756C2E64726F70646F776E20613A76697369746564207B0D0A2020636F6C6F723A20233535353B0D0A2020746578742D6465636F726174696F6E3A206E6F6E653B0D0A7D0D0A0D0A756C2E64726F70646F776E20613A686F7665’;
wwv_flow_api.g_varchar2_table(4) := ‘722C0D0A756C2E64726F70646F776E20613A616374697665207B0D0A2020636F6C6F723A20233535353B0D0A7D0D0A0D0A756C2E64726F70646F776E20756C207B0D0A2020626F726465723A2031707820736F6C6964207267626128302C302C302C302E3135293B0D0A7D0D0A0D0A2E6E61762D6172726F77207B0D0A2020666C6F61743A2072696768742021696D706F7274616E743B0D0A2020706F736974696F6E3A2072656C61746976653B0D0A20206C6566743A203870783B0D0A2020746F703A20307078’;
wwv_flow_api.g_varchar2_table(5) := ‘3B0D0A7D0D0A0D0A236E61762D6C332C0D0A236E61762D6C34207B0D0A2020626F726465722D6C6566743A2031707820736F6C6964207267626128302C302C302C302E3135293B0D0A2020626F726465722D626F74746F6D3A2031707820736F6C6964207267626128302C302C302C302E3135293B0D0A7D0D0A0D0A’;
wwv_flow_api.create_workspace_static_file(p_file_name => ‘js/xxxxx.js’
, p_mime_type => ‘application/x-javascript’
, p_file_charset => ‘utf-8’
, p_file_content => wwv_flow_api.varchar2_to_blob(wwv_flow_api.g_varchar2_table)
);
end;
commit;
[/code]

It is obvious that all settings like the directory, mime-type and character set are unchanged. Save the output as a script and add this script to your other install scripts like the application export SQL.

I hope in the future there will be a check-box in APEX which makes it possible to export the static workspace files with the application export. That would make this script unnecessary and life just a bit easier ;-).

Download Interactive Report in de achtergrond

Bij de klant waar ik momenteel werk wil men de data van meerdere Apex interactive reports (IR) downloaden. Een IR heeft hiervoor een standaard functionaliteit, die over het algemeen prima toereikend is. In dit geval betreft het echter rapporten met soms wel honderdduizenden regels. Mijn eerste reactie hierop was dat dit soort rapportages in een BI-systeem thuis horen, maar de klant wil dit toch met de IR doen.
De applicatie is bereikbaar via IBM Tivoli Access Manager en WebSEAL. Deze WebSEAL heeft een zeer korte time-out periode. Daardoor komt het geregeld voor dat terwijl de data nog wordt opgebouwd aan de serverkant deze time-out periode al voorbij is. Dit resulteert dan in een “no respond error”. Deze time-out periode laten aanpassen blijkt geen optie. Om de downloadfunctionaliteit toch aan te kunnen bieden, heb ik een oplossing bedacht om dit voor elkaar te krijgen.
Daarvoor moeten de volgende stappen genomen worden:

  1. Verzamel IR gegevens.
  2. Creëer Oracle scheduler job om export te maken.
  3. Procedure om csv-bestand aan te maken.
  4. Procedure om csv-bestand op te halen.

Verzamel IR gegevens
Wat is er nodig om het rapport buiten Apex om te kunnen opbouwen?

Apex voorziet in diverse API’s. Ook voor IR zijn er een aantal (kijk hier voor alle API’s). Voor het opvragen van de query worden de volgende API’s gebruikt:

Op de pagina met het IR is een button gecreëerd die het IR ID ophaalt en dit doorgeeft aan de procedure die met dit ID de overige gegevens kan ophalen.

l_report_id := apex_ir.get_last_viewed_report_id (p_page_id => l_page_id, p_region_id => l_region_id);
ir_package.get_ir(l_page_id, l_region_id, l_report_id, l_report_name);

In de procedure ir_package.get_ir worden de query en filtergegevens opgehaald vervolgens wordt er een Oracle job gestart die zorgt voor de opbouw van het csv-bestand.

De query en bind variabelen worden in een BLOB kolom gezet. Indien er alleen tekst in deze kolom komt kan dit natuurlijk ook in een CLOB kolom gezet worden, met het voordeel dat je geen CLOB-BLOB conversie hoeft uit te voeren. In onze applicatie kunnen er echter ook andere documenten worden weggeschreven. Vandaar de keuze voor een BLOB.
APP_USER_FILES
USF_USR_LOGIN_NAME" VARCHAR2(30 CHAR)
USF_REP_CODE" VARCHAR2(10 CHAR)
USF_FILE_NAME" VARCHAR2(255 CHAR)
USF_FILE_MIMETYPE" VARCHAR2(255 CHAR)
USF_FILE_CHARSET" VARCHAR2(128 CHAR)
USF_FILE_BLOB" BLOB
USF_FILE_COMMENTS" VARCHAR2(4000 CHAR)

Create dbms_scheduler job en program
Het aanmaken van het csv-bestand wordt in de achtergrond uitgevoerd middels de Oracle scheduler. Hiervoor wordt eerst een program aangemaakt met een herkenbare naam en een aantal parameters.
l_user := v('APP_USER');
l_program_name := l_report_name||l_user;
dbms_scheduler.create_program
(program_name => l_program_name
,program_action => 'ir_package.download_ir'
,program_type =>'STORED_PROCEDURE'
,number_of_arguments => 2
,enabled => false);
dbms_scheduler.define_program_argument
(program_name => l_program_name
,argument_position => 1
,argument_type => 'varchar2');
dbms_scheduler.define_program_argument
(program_name => l_program_name
,argument_position => 2
,argument_type => 'varchar2');
dbms_scheduler.enable
(name => l_program_name);

Daarna wordt een job gestart die het daadwerkelijke csv-bestand aanmaakt. Hier wordt de user meegegeven omdat de apex user op het moment dat deze job draait niet meer bekend is. Je wil het rapport natuurlijk wel bij de juiste gebruiker afleveren.
l_job_name := dbms_scheduler.generate_job_name(l_report_name||'_');
dbms_scheduler.create_job
(job_name => l_job_name
,program_name => l_program_name
,comments => 'Create csv '||l_report_name||' for '||l_user
,enabled => false
,auto_drop => true);
dbms_scheduler.set_job_argument_value
(job_name => l_job_name
,argument_position => 1
,argument_value => l_user);
dbms_scheduler.set_job_argument_value
(job_name => l_job_name
,argument_position => 2
,argument_value => l_rep_code);
dbms_scheduler.enable
(name => l_job_name);

Procedure om csv-bestand aan te maken
In deze procedure wordt met behulp van dbms_sql de data opgehaald om het csv-bestand op te bouwen.
Open cursor
l_cursor := dbms_sql.open_cursor;
dbms_sql.parse(l_cursor, l_query, dbms_sql.native);

Activeer de bind variabelen
for i in 1..l_para_count
loop

dbms_sql.bind_variable(l_cursor, l_para_name, l_para_value);
end loop;

Open BLOB om het csv-bestand in op te slaan. Let bij het openen van de BLOB dat de cache parameter op TRUE staat. Indien deze op FALSE staat kan het, vooral bij grote rapporten, een grote performance impact hebben.
dbms_lob.createtemporary( l_csv, TRUE );
dbms_lob.open( l_csv, dbms_lob.lob_readwrite );

Definieer de rapport kolommen
dbms_sql.describe_columns2(l_cursor, l_col_count, l_desc_tbl );
for i in 1 .. l_col_count loop
dbms_sql.define_column(l_cursor, i, l_col_val, 32767 );
end loop;

Schrijf de kolomkoppen naar de BLOB
for i in 1 .. l_col_count loop
l_col_val := l_desc_tbl(i).col_name;
if i = l_col_count then
l_col_val := '"'||l_col_val||'"'||chr(10);
else
l_col_val := '"'||l_col_val||'";';
end if;
l_raw := utl_raw.cast_to_raw( l_col_val );
dbms_lob.writeappend( l_csv, utl_raw.length( l_raw ), l_raw );
end loop;

Schrijf de regels naar de BLOB. Om te zorgen dat het csv-bestand goed gelezen kan worden, worden alle kolommen tussen quotes geplaatst. Als scheidingsteken wordt  ;  gebruikt.
l_cursor_status := sys.dbms_sql.execute(l_cursor);
-- write result set to CSV file
loop exit when dbms_sql.fetch_rows(l_cursor) <= 0;
for i in 1 .. l_col_count loop
dbms_sql.column_value(l_cursor, i, l_col_val);
if i = l_col_count then
l_col_val := '"'||l_col_val||'"'||chr(10);
else
l_col_val := '"'||l_col_val||'";';
end if;
l_raw := utl_raw.cast_to_raw( l_col_val );
dbms_lob.writeappend( l_csv, utl_raw.length( l_raw ), l_raw );
end loop;
end loop;

Sluit cursor en BLOB
dbms_sql.close_cursor(l_cursor);
dbms_lob.close( l_csv );

Schrijf BLOB naar app_user_files
insert into app_user_files( usf_usr_login_name
, usf_rep_code
, usf_file_name
, usf_file_mimetype
, usf_file_charset
, usf_file_blob
, usf_file_comments)
values ( p_user
, l_rep_code
, l_file_name
, 'text/csv'
, null
, l_csv
, 'Ready');

Procedure om het csv-bestand op te halen
Op de “home” pagina van de applicatie staan diverse gebruikers specifieke gegevens. Daar is een rapport toegevoegd die alle beschikbare csv-bestanden voor de ingelogde gebruiker laat zien en die de mogelijkheid biedt om deze te downloaden. De link roept hiervoor de procedure get_user_file aan. In overleg met de klant is de keuze gemaakt om van ieder rapport maximaal één versie te bewaren, hierdoor zal er per rapport maximaal een download beschikbaar zijn.
procedure get_user_file
(p_usf_rep_code in varchar2,
p_usf_file_name in varchar2)
as
l_mime varchar2(2000) ;
l_length number;
l_file_name varchar2 (2000) ;
l_lob blob;
begin
select usf.usf_file_mimetype
, usf.usf_file_blob
, lower(usf.usf_file_name)
, dbms_lob.getlength(usf.usf_file_blob)
into l_mime
, l_lob
, l_file_name
, l_length
from app_user_files usf
where usf.usf_usr_login_name = v('APP_USER')
and usf.usf_rep_code = p_usf_rep_code
and usf.usf_file_name = p_usf_file_name;
/*--*/
/*-- set up HTTP header*/
/*--*/
/*-- use an NVL around the mime type and*/
/*-- if it is a null set it to application/octect*/
/*-- application/octect may launch a download window from windows*/
owa_util.mime_header(nvl(l_mime, 'application/octet'), false) ;
/*-- set the size so the browser knows how much to download*/
htp.p('Content-length: ' || l_length) ;
/*-- the filename will be used by the browser if the users does a save as*/
htp.p('Content-Disposition: attachment; filename="'||l_file_name||'"') ;
/*-- close the headers*/
owa_util.http_header_close;
/*-- download the BLOB*/
wpg_docload.download_file(l_lob) ;
exception
when others then htp.p('Fout'||SQLCODE||' -ERROR- '||SQLERRM);
end get_user_file;

Tenslotte
Ik heb niet de complete code van de procedures in dit artikel gekopieerd, omdat er op meerdere plekken applicatie specifieke zaken gebeuren die niets toevoegen aan dit artikel. Wil je deze oplossing gebruiken, maar kom je er niet uit? Dan mag je mij altijd mailen en zal ik je proberen verder te helpen.

Progress database converteren naar Oracle

Inleiding

Zoals zoveel programmeurs heb ik in verleden als vriendendienst een applicatie voor een bevriende ondernemer gemaakt. In de tijd dat ik dit project heb uitgevoerd werkte ik hoofdzakelijk in een Progress omgeving. Het is dus niet verwonderlijk dat de applicatie daar ook in gemaakt is. Dit is een Progress 6, character based applicatie die nu hard aan vervanging toe is.  Aangezien ik Progress al jaren geleden achter me heb gelaten en gekozen heb om mij te specialiseren in Oracle APEX, ben ik van plan om de applicatie te herbouwen in Oracle APEX. Een belangrijke reden om in APEX te bouwen is dat de nieuwe applicatie platform en device onafhankelijk zal moet werken. Het is niet de bedoeling om de oude applicatie één op één te kopiëren. Omdat het zonde is om de kennis die de afgelopen jaren in de applicatie is opgebouwd zomaar weg te gooien, heb ik besloten om het schema opnieuw aan te maken en aan te passen waar nodig. Dit in een Oracle database, rekening houdend met de naam conventies zoals wij die gebruiken in ons Orcado ontwikkel framework (OFW) voor Oracle (er is ook een PHP versie). Dit schema moet zoveel mogelijk gevuld moeten worden met de data uit de Progress database. Het is uiteraard mogelijk om zelf een export programma in Progress te schrijven en een import programma in PL/SQL, maar waarom het wiel opnieuw uitvinden? Ik ben op zoek gegaan naar een beschikbare tool hiervoor en die heb ik gevonden in Pro2XMLSchema.

Pro2XMLSchema

Met deze tools is het mogelijk om het schema en de data van de Progress database te exporteren en in een Oracle database, maar ook vele andere databases, te importeren. Het gebruik van de tool is heel simpel. De tool werkt op basis van de Command Line Interface (CLI) versie van PHP (min versie 4.3). Deze zal dus aanwezig moeten zijn. Kijk hier om PHP te downloaden en te installeren. Ik ga geen uitgebreide uitleg geven over hoe je PHP en de tool moet gebruiken, dat staat in de documentatie die met de tools wordt meegeleverd. Wel zal ik kort de stappen doornemen die uiteindelijk voor de conversie zorgen. Pro2XMLSchema levert een aantal bestanden op voor de conversie van je database.

Progress

Start in de Progress ontwikkel omgeving het door Pro2XMLSchema Progress 4gl, pro2xml.p, programma voor de export van het schema en de data

Eerst wordt het schema en daarna de data geëxporteerd. Ik heb ervoor gekozen om geen indexdefinities mee te exporteren, hierover later meer. Dit programma moet 2 keer runnen. Één keer om het schema te exporteren en één voor de data. Selecteer voor het starten welke tabellen er allemaal mee moeten naar Oracle. Het eindresultaat, diversen XML-bestanden, staat in c:\temp\test directory (in mijn voorbeeld). Het TEST bestand wordt door pro2xml.p aangemaakt om te testen of in de doel directory kan worden geschreven, let er dus op dat deze directory wel aanwezig is.

 PHP

Nu moeten de bestanden die zijn aangemaakt worden ingelezen. Zoals al eerder gemeld is er een nieuw schema aangemaakt waar nieuwe velden zijn toegevoegd en andere zijn vervallen en waar gebruik gemaakt wordt van een andere naam conventie. De data kan dus niet één op één in het nieuwe schema worden geladen. Daarom is er gebruik gemaakt van een staging schema waar het Progress schema met data zal worden ingelezen. Dit is ook de reden waarom de indexen niet zijn meegenomen. In de stagin zijn we zijn alleen in de data geïnteresseerd. Voor het inlezen zijn 2 PHP programma’s beschikbaar

Loadschema.php zorgt voor het laden van het schema en loaddata.php voor het laden van de data. Zorg dat de juiste gegevens in dbconnect.inc staan zodat PHP de Oracle database kan benaderen. Indien er tijdens het inlezen fouten voorkomen dan worden deze in een bestand met de naam <table>.log in de directory waar de xml data bestanden staan geplaatst. Tijdens het inlezen heb ik 2 fouten vastgesteld.

  1. Data omdat er onleesbare tekens in de bron staan. Dit blijkt corrupte data in de Progress database. Dit probleem is handmatig opgelost.
  2. Data in de Progress database heeft een langer lengte dan in de datadictionary staat aangeven. In tegenstelling tot Oracle waar de veld beschrijving bepaald wat er in een kolom past is dit bij Progress slechts de standaard manier hoe een veld wordt weergegeven. In het nieuwe Oracle schema is dus te weinig ruimte gereserveerd, ook dit probleem is handmatig opgelost.

Het eind resultaat is een kopie van de Progress database in de Oracle database.

Oracle

De volgende stap is een PL/SQL package in Oracle maken waarin de data uit de staging omgeving netjes en gecontroleerd wordt overzet naar het nieuwe schema. In dit nieuwe schema wordt in tegenstelling tot het Progress schema met technische sleutels gewerkt. In dit package worden de volgende acties uitgevoerd:

Nu heb ik een prima uitgangssituatie waarmee ik de nieuwe applicatie in Oracle APEX kan gaan bouwen. Deze applicatie wil ik natuurlijk graag in APEX 5 maken maar deze versie is helaas nog beschikbaar voor download. Ik denk dat ik nu eerst ga starten in de op dit moment beschikbare versie en later de applicatie zal upgraden. Volgens mij moet dit voor de toekomst genoeg stof opleveren voor een vervolg op dit verhaal.

My First APEX plug-in

In an attempt to reach a broader audience, this will be my first blog in English. I hope this will help more people to read and use my tips and tricks. For me it will be an extra challenge!

For some time now (since version 4.0), APEX has offered the plug-in functionality and although I used several plug-ins, I still did not build any myself. This is mainly because existing plug-ins were adequate or I built the needed functionality directly into the application. To be able to reuse code, it is better to use plug-ins instead of coding everything into pages. In this blog I will build my first plug-in.

The purpose of the plug-in is to display notifications that can be controlled by a dynamic action. As a basis I will use the noty jQuery plug-in (http://needim.github.io/noty/) which is a good start to build the APEX plug-in around.

How to start? First, go to the shared components of your application and click on plug-ins under the User Interface heading. So far for the easy part ;-), although building a plug-in is not that complicated I discovered to my surprise. I will guide you through the different steps below.

  1. Shared components
  2. Plug-ins
  3. Create a new plug-in
  4. Enter the name for the plug-in.

    The name you provide here is used within your application. It is good practice to use your domain as a prefix. This creates a globally unique name. With this name you are able to share the plug-in without worrying about duplicates. I bet there are more plug-ins called ‘notification’ out there ;-). Choose the Type and Category as shown in the screenshot.
  5. Now create the plug-in, we will finish it later.
  6. Add Custom Attributes. These are the parameters to pass on to the Javascript function that will start the real action on your page. I did not create parameters for all available options in noty. For a start I just took those that are essential.

    Label Type Required Depending on Description
    Theme Select list yes  Select one of the themes you have created or use the default.
    Layout Text yes Where does the notification pop-up
    Type Text yes  alert, succes, error, warning, information or confirm
    Text Textarea yes  The text for the notification.
    Sticky notification Text no  Will the notification stick on the page.
    Hide after x seconds Integer no  Hide the notification after x seconds, only for not sticky notifications.
    Max visible notifications Text no The maximum number of visible notifications. More will be queued and become visible when other notifications disappear.
    Item receiving confirm answer Page Item/ Column no Item for confirm answer (Ok/Cancel)
  7. Now we build a wrapper around the noty jQuery plug-in. This is a Javascript function that is executed by APEX. In the wrapper we will receive the Custom Attributes defined in step 6 and pass them on to the noty jquery plug-in as parameters. There is a possibility in this function to use the value of an item for the attributes. To enable this you just put the item name between ‘#’s. (eg #P1_TEXT#).

    /**
    * Notification plugin v1.0 – http://www.orcado.nl
    *
    * Based on jQuery plugin noty
    * noty – jQuery Notification Plugin v2.1.2
    * Contributors: https://github.com/needim/noty/graphs/contributors
    * Examples and Documentation – http://needim.github.com/noty/
    * Licensed under the MIT licenses:
    * http://www.opensource.org/licenses/mit-license.php
    *
    **/
    function com_orcado_notification(){

    function replacePlaceholders(pText) {
    // replace all #page_item_name# placeholders
    var lSearchPattern = new RegExp(“#w+#”, “g”),
    lFoundList,
    lPageItem,
    lFinalText = pText;
    // search as long as the text contains a placeholder
    while (lFoundList = lSearchPattern.exec(pText)) {
    // get value, but search without the #
    lPageItem = $x(lFoundList[0].replace(/#/g, “”));
    if (lPageItem) {
    lFinalText = lFinalText.replace(lFoundList[0], $v(lPageItem));
    }
    }
    return lFinalText;
    }; // replacePlaceholders

    // It’s better to have named variables instead of using
    // the generic ones, makes the code more readable
    var lTheme = this.action.attribute01,
    lLayout = this.action.attribute02,
    lType = this.action.attribute03,
    lText = this.action.attribute04,
    lSticky = ((this.action.attribute05===”Y”)?false:this.action.attribute06*1000),
    lMax = this.action.attribute07;
    lAnswer = this.action.attribute08;

    if (replacePlaceholders(lType)==’confirm’) {
    var n = noty({
    text: replacePlaceholders(lText).replace(/n/g,’
    ‘),
    type: replacePlaceholders(lType),
    dismissQueue: true,
    layout: replacePlaceholders(lLayout),
    theme: lTheme,
    timeout: lSticky,
    maxVisible: replacePlaceholders(lMax),
    buttons: [
    {addClass: ‘btn btn-primary btn-noty-ok’, text: ‘Ok’, onClick: function($noty) {
    $s(lAnswer, ‘Ok’);
    $noty.close();
    }
    },
    {addClass: ‘btn btn-danger btn-noty-cancel’, text: ‘Cancel’, onClick: function($noty) {
    $s(lAnswer, ‘Cancel’);
    $noty.close();}
    }
    ]
    });
    }
    else { var n = noty({
    text: replacePlaceholders(lText).replace(/n/g,’
    ‘),
    type: replacePlaceholders(lType),
    dismissQueue: true,
    layout: replacePlaceholders(lLayout),
    theme: lTheme,
    timeout: lSticky,
    maxVisible: replacePlaceholders(lMax)
    });
    }
    }

    First, we assign the generic variables to named ones. This is not required but it makes the code more readable. Then we call the noty plug-in with the right parameters. As you can see, a bit of Javascript knowledge is needed here.

  8. Upload Files used by the plug-in.

    Name Description
    com_orcado_jquery.noty.js Javascript wrapper around the noty jquery plug-in
    custom.js Custom theme (based on default.js)
    default.js Default theme
    jquery.noty.js noty jquery plug-in
    layout.js Javascript which will set the css for the available layouts
  9. What remains is the PL/SQL code for rendering and executing the plug-in. You can see that you need to register the Javascript libraries and the function with attributes in the dynamic action

    function render_noty_notification (
    p_dynamic_action in apex_plugin.t_dynamic_action,
    p_plugin         in apex_plugin.t_plugin )
    return apex_plugin.t_dynamic_action_render_result
    is
    l_theme          varchar2(4000) := p_dynamic_action.attribute_01;
    l_layout         varchar2(4000) := p_dynamic_action.attribute_02;
    l_type           varchar2(4000) := p_dynamic_action.attribute_03;
    l_text           varchar2(4000) := p_dynamic_action.attribute_04;
    l_sticky         varchar2(4000) := p_dynamic_action.attribute_05;
    l_wait_seconds   number         := to_number(p_dynamic_action.attribute_06);
    l_max            varchar2(4000) := p_dynamic_action.attribute_07;
    l_answer         varchar2(4000) := p_dynamic_action.attribute_08;
    l_result         apex_plugin.t_dynamic_action_render_result;
    begin
    — During plug-in development it’s very helpful to have some debug information
    if apex_application.g_debug then
    apex_plugin_util.debug_dynamic_action (
    p_plugin         => p_plugin,
    p_dynamic_action => p_dynamic_action );
    end if;
    — Register the javascript and CSS library the plug-in uses.
    apex_javascript.add_library (
    p_name      => ‘jquery.noty’,
    p_directory => p_plugin.file_prefix,
    p_version   => null );
    apex_javascript.add_library (
    p_name      => ‘com_orcado_jquery.noty’,
    p_directory => p_plugin.file_prefix,
    p_version   => null );
    apex_javascript.add_library (
    p_name      => ‘default’,
    p_directory => p_plugin.file_prefix,
    p_version   => null );
    apex_javascript.add_library (
    p_name      => ‘layout’,
    p_directory => p_plugin.file_prefix,
    p_version   => null );
    — Register the function and the used attributes with the dynamic action framework
    l_result.javascript_function := ‘com_orcado_notification’;
    l_result.attribute_01 := l_theme;
    l_result.attribute_02 := l_layout;
    l_result.attribute_03 := l_type;
    l_result.attribute_04 := l_text;
    l_result.attribute_05 := l_sticky;
    l_result.attribute_06 := l_wait_seconds;
    l_result.attribute_07 := l_max;
    l_result.attribute_08 := l_answer;

    return l_result;
    end render_noty_notification;

  10. Save the plug-in by clicking Apply Changes

  11. 1. You finished building the plug-in so now we are able to use it. To do this, create a Dyamic action based on the plug-in we just built.
  12. Enter the values in the parameter fields:
  13. And you are done. The plug-in provides for nice notifications the way you specify in the dynamic action.
  14. See here for a demo
Currency Converter APEX plug-in

Na een vraag op het oracle forum over het converteren van valuta, heb ik besloten om hier een plug-in voor te schrijven.
De plug-in is niet echt heel spannend maar doet wel wat die moet doen.

Op basis van de koers die opgehaald wordt bij rate-exchange.appspot.com wordt de berekening uitgevoerd. Dit is een gratis webservice die de koers als JSON formaat terug geeft. Hier zat meteen de grootste uitdaging. Door de “same-origin policy” standaard mag je in javascript geen sites buiten het aanroepend domein benaderen. Wat je doet is een “Cross-origin resource sharing” aanroep en dat mag dus niet.
De aanroep resulteert in een No ‘Access-Control-Allow-Origin’ header is present on the requested resource.” error.

Een uitzondering hierop wordt gemaakt indien de URL via JSONP kan worden aan geroepen. Door het toevoegen van “&callback=?” aan de URL werkt de site van rate-exange op die manier. Nu krijg je wel de JSON data binnen.
De code hiervoor ziet er dan alsvolgt uit

var url = “http://rate-exchange.appspot.com/currency?from=”+$v(action.attribute01)+”&to=”+$v(action.attribute02)+”&callback=?”;

jQuery.ajax({  type: “GET”,
url: url ,
dataType: “json”,
success: function(data) {
$s(action.attribute04,($v(action.attribute03)*data.rate).toFixed(2));
}
});

Waarbij action.attributeXX de parameters van de plug-in zijn.

Voor de selectlists van de valuta keuzes wilde ik ook een webservice gebruiken. Dit hoeft niet via javascript maar kan “gewoon” door een select die de (XML) data van de webservice kan vertalen. Er is een webservice met deze informatie beschikbaar bij de ISO organisatie via deze URL.

select distinct extractvalue(value(x), ‘CcyNtry/CcyNm/text()’) ccynm
, extractvalue(value(x), ‘CcyNtry/Ccy/text()’)   ccy
from table(xmlsequence(extract(sys.xmltype.createxml(sys.urifactory.geturi(‘http://www.currency-iso.org/dam/downloads/table_a1.xml’) .getclob()), ‘//ISO_4217/CcyTbl/CcyNtry’))) x
order by ccynm

Dit werkt op apex.oracle.com echter niet, omdat je er een ACL voor moet aanmaken die toestemming geeft om de externe site te benaderen. In de demo gebruik ik dan ook STATIC selectlists.

Kijk op apex-plugin.com om de plugin te downloaden en hier voor een demo ervan.

 

Notification plug-in

Today I published a new version (1.1) of my notification plug-in.

In this version I added the possibility using free form text or predefined select lists for a few parameters.

Visit apex-plugin.com to get it.

Het gemak van xmltype

Heel af en toe krijgt een 3rd party applicatie waar wij hier beheer voor doen een fout item via een xml message binnen.
Hiervoor biedt de applicatie een beheerscherm waar de xml aangepast kan worden en daarna opnieuw aangeboden kan worden.
Dat voldeed prima tot we er op een dag ineens 90 foute berichten kregen.

Een blik op de onderliggende Oracle database liet zien dat de berichten met foutmelding in een tabel opgeslagen worden.
Zowel de errorstack als de message staan in een clob veld.
Wat we wilden automatiseren was een node uit de xml knippen waar het betreffende item uit de foutmelding in voor kwam.

Het wordt dus een update statement op de betreffende message tabel waarbij we de payload aanpassen:

update message_error_log lg
set lg.payload = ?

Eerst identificeren we de juiste berichten in de where clause. We weten het messagetype en de foutmelding:

 where lg.message_type = 1234
and    lg.exception_type = ‘com.exception.ItemNotFoundException’

Het vinden van het itemnummer in de foutmelding kan met ouderwets knippen en plakken:

to_char (substr (lg.stacktrace
,                instr (lg.stacktrace, ‘item does not exist:’) + 19
,                  (  instr (lg.stacktrace
,                            ‘at com.backend.process’
)
– 2
)
– (instr (lg.stacktrace, ‘item does not exist:’) + 19)
)
)

Eventueel zou bovenstaande ook met een reguliere expressie kunnen.

Om zeker te zijn dat het gevallen van dit specifieke probleem zijn controleren we of het itemnummer in de xml op de juiste plaats voorkomt:

and    extractvalue (xmltype (lg.payload)
,                       ‘/Message/Payload/Forecast/ItemDemand/itemNo[.=”‘
|| to_char (substr (lg.stacktrace
,                                        instr (lg.stacktrace, ‘item does not exist:’) + 19
,                                          (  instr (lg.stacktrace
,                                                    ‘at com.backend.process’
)
– 2
)
– (instr (lg.stacktrace, ‘item does not exist:’) + 19)
)
)
|| ‘”]’
) is not null

Eerst wordt hier de clob in een xmltype gezet: xmltype (lg.payload)
Vervolgens wordt een xpath expressie gemaakt met het item uit de foutmelding:

‘/Message/Payload/Forecast/ItemDemand/itemNo[.=”<itemnummer>”]’
Hiermee wordt gecontroleerd of het betreffende nummer op de juiste plaats in de xml voorkomt.
Deze waarde wordt dan met extractvalue eruit geplukt.

Nu hebben we de juiste rijen te pakken en komt het knippen in de xml:

 update message_error_log lg
set lg.payload =
deletexml (xmltype (lg.payload)
,                       ‘/Message/Payload/Forecast/ItemDemand[itemNo=”‘
|| to_char (substr (lg.stacktrace
,                                        instr (lg.stacktrace, ‘item does not exist:’) + 19
,                                          (  instr (lg.stacktrace
,                                                    ‘at com.backend.process’
)
– 2
)
– (instr (lg.stacktrace, ‘item does not exist:’) + 19)
)
)
|| ‘”]’
).getclobval ()

Eerst weer converteren naar xmltype: xmltype (lg.payload)
Dan met een iets andere xpath expressie op zoek naar de node waarin het item voorkomt (let op de plaats van de blokhaken!):
‘/Message/Payload/Forecast/ItemDemand[itemNo=”<itemnummer>”]’
Met deletexml wordt de betreffende node eruit geknipt.
Wat rest is de xmltype weer terug in een clob te veranderen, dit kan met de methode getclobval van het xmltype: .getclobval ()

Het eindresultaat:

update message_error_log lg
set lg.payload =
deletexml (xmltype (lg.payload)
,                       ‘/Message/Payload/Forecast/ItemDemand[itemNo=”‘
|| to_char (substr (lg.stacktrace
,                                        instr (lg.stacktrace, ‘item does not exist:’) + 19
,                                          (  instr (lg.stacktrace
,                                                    ‘at com.backend.process’
)
– 2
)
– (instr (lg.stacktrace, ‘item does not exist:’) + 19)
)
)
|| ‘”]’
).getclobval ()
where lg.message_type = 1234
and    lg.exception_type = ‘com.exception.ItemNotFoundException’
and    extractvalue (xmltype (lg.payload)
,                       ‘/Message/Payload/Forecast/ItemDemand/itemNo[.=”‘
|| to_char (substr (lg.stacktrace
,                                        instr (lg.stacktrace, ‘item does not exist:’) + 19
,                                          (  instr (lg.stacktrace
,                                                    ‘at com.backend.process’
)
– 2
)
– (instr (lg.stacktrace, ‘item does not exist:’) + 19)
)
)
|| ‘”]’
) is not null

In de applicatie konden we nu alle berichten opnieuw aanbieden en ze waren verwerkt.
Deze generieke oplossing kan vervolgens voor alle toekomstige gevallen gebruikt worden.
Dit bespaart tijd en is minder foutgevoelig dan het handmatige aanpassen.

v() en nv() mooie functies maar….

Inleiding

 

Zoals zo vaak bij het schrijven van een blog item wordt deze meestal getriggerd door een probleem waar ik tijdens het werk tegen aanloop. Ook dit item is er weer een uit die categorie.
De foutmelding die ik doorkreeg was net zo simpel als kort “De zoek opdracht op pagina xx performed niet goed”.
Ik hou wel van dit soort meldingen omdat het altijd weer een leuke uitdaging is om een performance probleem op te lossen en het resultaat direct bemerkt wordt door de klant.

Elementen betrokken bij het probleem

En dan begint het zoeken…

 

De verwachting is dat het probleem niet in de hoofdpagina met de “iframe container”, waarin de tabbladen worden geopend, zou zitten want die werkt voor de overige tabbladen prima. Daarbij komt nog dat een performance probleem “meestal” in een query zit.  Uiteindelijk de query opgezocht die gebruikt wordt.
select ..
      , ..
      , its.its_oni_package.oni_radio_group_score(p_score_type =>ind.ind_score_type, .. )
   from its_onderzoek_indicatoren oni
  where ..

De query is voor de leesbaarheid versimpeld. De query heb ik naar sqldeveloper gekopieerd om wat makkelijker te kunnen testen. Als eerst maar eens die functie uit query gehaald en jawel hoor de query performde meteen prima. Weer een stapje dichter bij het probleem. Dan maar eens die functie uitpluizen. In de functie zit een cursor die gebruik maakt van de nv()functie.

select .. 
  from its_onderzoek_indicatoren oni 
       . 
       . 
  join ios_elementen elm 
    on oni.oni_one_elm_id = elm.elm_id 
 where oni.oni_one_ond_nr = nv('p10105_ond_nr') 
   and elm.elm_id         = nv('p10105_element')

Voor het gemak heb ik deze functies vervangen door bind variabelen in mijn sqldeveloper query en ik kon geen performance probleem vaststellen. huuh, hoe kan dat nu? Het blijkt dat ik het probleem al opgelost had voordat ik exact wist wat het was.

select .. 
  from its_onderzoek_indicatoren oni 
       . 
       . 
  join ios_elementen elm 
    on oni.oni_one_elm_id = elm.elm_id
 where oni.oni_one_ond_nr = :p10105_ond_nr 
   and elm.elm_id         = :p10105_element

het is duidelijk dat het gebruik van nv()  functie het performance probleem creëert.

En dan nog oplossen…

 

Nu had ik het probleem dat ik wel wist waar het probleem zat maar hoe ga ik het nu oplossen en toch gebruik maken van deze nv() functie? Wat dacht je van een scalar subquery? De scalar subquery expression is een subquery die exact één kolom waarde van één record terug geeft. Om dit te testen is de query als volgt aangepast
select .. 
  from its_onderzoek_indicatoren oni 
       . 
       . 
  join ios_elementen elm 
    on oni.oni_one_elm_id = elm.elm_id 
 where oni.oni_one_ond_nr = (select nv('p10105_ond_nr')  from dual) 
   and elm.elm_id         = (select nv('p10105_element') from dual)
Dit heb ik voor het gemak direct in het package gedaan zodat het wat makkelijker te testen is. Vervolgens dezelfde zoekopdracht gegeven als waar ik mee begonnen ben en jawel hoor het probleem is opgelost!

Conclusie

 

– Indien mogelijk gebuik dan in je apex applicatie altijd bind variabelen ipv de n() en nv() functies.
– Scalar subqueries zijn een zeer handige oplossing als je de n() en nv() functies wel wil/moet gebruiken.
– Nu nog even mijn eigen applicaties controleren op het gebruik van de n() en nv() functies en aanpassen!
Apex bug fixing tijdens applicatie release

Bij mijn huidige opdrachtgever hebben we een prachtig systeem vanaf scratch mogen opbouwen. Inmiddels zijn we in een stadium aanbeland dat we niet meer in een projectgroep werken maar volledig in productie. Dit verandert ook de werkwijze op veel vlakken. Één daarvan is de manier hoe we moeten omgaan met bug fixing terwijl er op hetzelfde moment ook gewerkt wordt aan een nieuwe release. Gelukkig heeft APEX hiervoor de Build Option.
Met deze optie kan je diversen applicatie onderdelen in- of excluden. Hoe je de Build Option nu moet gebruiken, zal ik hieronder stap voor stap uitleggen.

 

Creëer Build Option.

Dit doe je bij de Shared Components
Hier kan je een nieuwe Build Option aanmaken.

buildoption1

 

Gebruik van de Build Option.

Je kunt de Build Option op verschillende plaatsen gebruiken.

Kortom op alles wat je op een pagina plaatst en de pagina zelf kan de de Build Option gebruiken.
Je geeft bij het betreffende object aan wanneer die moet worden gebruikt, wanneer de Build Option aan of uit staat.

buildoption3

buildoption4.

 

Productie of niet?

Op de bovenstaande manier kun je dus nieuwe objecten aan een pagina toevoegen en daarbij aangeven dat die bij een bepaalde Build Option horen.
Dit is natuurlijk allemaal heel handig maar hoe kun je nu aangeven welke release wanneer actief is?
Ook daar is over nagedacht. Bij de Build Option in de shared components kun je de status en default on export opgeven.

buildoption2

Met de status geef je aan of deze Build Option in de applicatie moet worden gebruikt of niet (include/exclude).
met Default on Export geef je aan of de Build Option mee moet worden genomen tijdens een export of deploy (via sqldeveloper).
Hierdoor is het dus mogelijk om aan een release te werken en toch een pagina op te leveren om een bug op te lossen.

 

Niet declaratief

Wat nu als je in bijvoorbeeld in een LOV bepaalde regels wel of niet wil tonen, afhankelijk van de Build Option. Je zou dan kunnen kiezen om 2 verschillende item’s te maken met ieder zijn eigen LOV en het juiste item tonen afhankelijk van de Build Option. Dit is echter niet altijd even handig omdat je de 2 item niet dezelfde naam kan geven. dit kan weer problemen geven met de verdere afhandeling op de pagina.
Ik heb dit opgelost door de Build Option aan te roepen via een apex api. In deze api is de functie apex_util.get_build_option_status aanwezig.

select case when apex_util.get_build_option_status(p_application_id    => 200,
p_build_option_name => ‘IBIS 2.4 (Signalen)’) = ‘EXCLUDE’
then ‘(Signaal)’
else ‘Signaal’
end
,
case when apex_util.get_build_option_status(p_application_id    => 200,
p_build_option_name => ‘IBIS 2.4 (Signalen)’) = ‘EXCLUDE’
then ‘SIG_exclude’
else ‘SIG’
end
from dual

Op deze manier kan ik dus 1 item gebruiken en in de query van de LOV de juiste gegevens selecteren op basis van de Build Option. De api kan natuurlijk op vele manieren worden gebruikt dit dient slechts als voorbeeld.

 

Conclusie

Je kunt met de standaard apex tools dus “gewoon” bugs oplossen terwijl er ondertussen wordt gebouwd aan een nieuwe release.

Reset pagination van een regio

Waarom zou je de pagination willen resetten? Als je met APEX werkt zul je al snel een keer de volgende melding zien:

Pagination error

Als je in het voorbeeld in figuur 1 een nieuwe regel in de master selecteert, waarbij er minder dan 3 pagina’s detail gegevens zijn, dan zal je bij het detail rapport de eerder genoemde melding krijgen.

Figuur 1

———-                          ———-
| master |                          | detail |
——————————-     ——————————-
| ………                   |     | ………                   |
| ………                   |     | ………                   |
| ………                   |     | ………                   |
| ………                   |     | ………                   |
| ………                   |     | ………                   |
|                             |     |                             |
|                     pag 5/6 |     |                     pag 3/5 |
——————————-     ——————————-

Deze melding kan je voorkomen door zelf de reset pagination te doen. Het resetten van de pagination kun je in APEX doen via de URL.
De URL voor een APEX applicatie ziet er als volgt uit

URL opbouw : f?p=1:2:3:4:5:6:7:8:9
1 – Application ID or alias
2 – Page ID or Alias
3 – Session &SESSION. :APP_SESSION
4 – Request (eg PRINT_REPORT=REPORT_1) &REQUEST. :REQUEST
5 – Debug (YES or NO) &DEBUG. :DEBUG
6 – Cache setting

APP – Clear cache for whole application
SESSION – Clear cache for current user session
RP – Reset pagination
x – Clear cache for page x
y – Clear cache for item y

7 – Comma separated list of page items
8 – Comma separated list of values
9 – Printer friendly mode (either YES or blank)

Zoals je ziet kun je via de URL veel sturing geven aan je pagina. Het gaat nu echter alleen om het “Cache setting” gedeelte waarbij je de tekst “RP” kunt zetten om een reset van de pagination te doen.
Handig? Ja dat wel, alleen wordt op deze manier de pagination gereset voor ALLE regio’s op de pagina. Als je meerdere rapporten in een master-detail constructie op je pagina hebt staan dan worden deze dus allemaal gereset.

Dit kan je voorkomen door in het “Request” gedeelte de tekst “pg_R_XXX” op te nemen, waarbij XXX staat voor het ID van de regio. Het is niet goed om dit ID hard te programmeren omdat het kan wijzigen als je een import van de applicatie doet. Ik heb dit opgelost door een hidden item op te nemen. Dit hidden item geef ik als “Source type” op dat het een “SQL Query” is. In de Source value or expression komt de volgende code:

select ‘pg_R_’||to_char(region_id)
from apex_application_page_regions
where application_id = :app_id
and page_id        = :app_page_id
and static_id      = ‘static value opgegeven bij regio’

Nu kan je dit hidden item op verschillende manieren gebruiken bij het request. Via een javascipt aanroep of link in een rapport.

top.location.href=’f?p=&APP_ID.:125:&APP_SESSION.:&P125_HIDDEN_ITEM_WITH_ID.:&DEBUG.::’

Column linkxx

Hier komt de sidebar

Volg ons op

© Orcado B.V. | 1999 - 2016