Apex page call stack

Page Call Stack Implementation

Purpose

The goal is to have a way to navigate back to the previous page. Typically this is done by a button, sometimes also by a page brache or a link column. A global application item A_LAST_PAGE is used to hold the numberof the last page.

The link inside the “Back to previous page button” then is rendered using this item with a page target: &A_LAST_PAGE.

This post describes how to setup the logic for populating this item using a complete page call stack. Thereby allowing to navigate several pages forth and back as wanted and always having some logical choice set as the last page.

Setup

Needed are two apex application items, one application process and a plsql module. The page stack is implemented using an apex collection.

The names of the items are declared as constants in the plsql module. So if you use a different item name, then you need to change the constant value there too.
Also if your homepage is not number 1 then you must change that constant value.

application items

A_LAST_PAGE => Will hold the page number. This item can be referred wherever you need to go back to the last page.

A_PAGESTACK_POINTER => the current position in the page stack. Entries are never deleted if we move back in the stack, just overwritten. So in theory the stack can also be used to “go forward” again.

application page process
The application process needs to run in the page head for each page. You can / should set a condition so that it doesn’t run for some special pages like Login, Help, Feedback or certain Modal pages. Although modal pages are taken into account if you use the default Apex 5 mechanism.

Apex_pageStack

database code

The logic should work in all apex versions but for the rule 4. The isModalPage subfunction should only be used if you are on Apex 5.0 yet. Older version will raise an error because the page_mode column does not exists in previous versions of the apex_application_pages view.

  -- adds the current apex page to the page stack
  procedure managePageStack
  is
    -- constants
    co_modul_name          constant varchar2(100) := $$PLSQL_UNIT||'.managePageStack';
    c_appItem_last_page    constant varchar2(30)  := 'A_LAST_PAGE';
    c_appItem_page_pointer constant varchar2(30)  := 'A_PAGESTACK_POINTER';
    c_PageStack            constant varchar2(30)  := 'PAGESTACK';
    c_PageStack_max_size   constant number        := 500;
    c_Homepage             constant varchar2(30)  := '1';

    -- types
    type pageStack_t      is table of apex_collections.c001%type index by binary_integer; 

    -- variables
    v_pageStack           pageStack_t;
    v_current_page        varchar2(30);
    v_ps_pointer          number;
    v_last_page           varchar2(30);
    v_new_last_page       varchar2(30);

    -- sub modules
    function isModalPage(p_page in varchar2) return boolean
    is
      v_page_mode APEX_APPLICATION_PAGES.page_mode%type;
    begin

      select page_mode
      into v_page_mode
      from APEX_APPLICATION_PAGES
      where application_id = v('APP_ID')
      and page_id = p_page;

      return (v_page_mode='Modal Page');

    exception
      when no_data_found then
        return null;
    end isModalPage;

  begin
    ----------------------------------------------------------------------------
    -- read the current application items
    ----------------------------------------------------------------------------
    v_current_page  := v('APP_PAGE_ID');
    v_ps_pointer    := coalesce(to_number(v(c_appItem_page_pointer)),0);
    v_last_page     := coalesce(v(c_appItem_last_page),c_Homepage);

    ----------------------------------------------------------------------------
    -- make sure the collection exists
    ----------------------------------------------------------------------------
    if v_ps_pointer <= 0 then       -- create the collection       -- first page is always the homepage       --if not apex_collection.collection_exists(c_PageStack) then       apex_collection.create_collection(c_PageStack);       --end if;       -- just in case add the homepage       apex_collection.add_member(c_PageStack, p_c001 => c_Homepage);
      -- point to first page
      v_ps_pointer := 1;
    end if;  

    ----------------------------------------------------------------------------
    -- load the apex_collection into a plsql collection
    ----------------------------------------------------------------------------
    select c001
    bulk collect into v_pageStack
    from apex_collections
    where collection_name = c_PageStack;

    ----------------------------------------------------------------------------
    -- implement rules
    ----------------------------------------------------------------------------
    case 

    -- rule 0
    -- if something is wrong with the collection and we can not transfer it to a plsql collection
    when v_pageStack.count = 0
    then v_new_last_page := c_Homepage;
         if not apex_collection.collection_exists(c_PageStack) then
           apex_collection.create_collection(c_PageStack);
         end if;
         apex_collection.add_member(c_PageStack, p_c001 => c_Homepage);
         v_ps_pointer := 1;

    -- rule 1
    -- if the current page is the same as we have currently in stack, then do nothing
    -- this happens during a redirect to the same page
    when v_current_page = v_pageStack(v_ps_pointer)
    then v_new_last_page := v_last_page;

    -- rule 2
    -- if the new page is the same as the last page
    -- then go back in the stack one step. But never go below the first page
    -- we probably went back to the previous page, therefore last page needs to be even one more back in the stack
    when v_current_page = v_last_page
    then v_ps_pointer    := greatest(v_ps_pointer-1,1);
         -- the new last page is even one more page back
         v_new_last_page := v_pageStack(greatest(v_ps_pointer-1,1));

    -- rule 3
    -- if we are back to the home page reset everything!
    when v_current_page = c_homepage
    then v_ps_pointer := 1;
         v_new_last_page := v_pageStack(v_ps_pointer);
    -- rule 4
    -- ignore modal pages
    when isModalPage(v_current_page)
    then v_new_last_page := v_last_page;

    -- rule 5
    -- the new page is not under the current or last page, so we need to add it to the stack and increase pointer
    else
      v_new_last_page := v_pageStack(v_ps_pointer);
      v_ps_pointer:=v_ps_pointer+1;

      -- check to lessen the impact of endless loops and other nasty things
      if v_ps_pointer > c_PageStack_max_size then
        v_new_last_page := v_pageStack(1);
        v_ps_pointer := 2;
      end if;

      -- are we at the end of the stack already?
      if v_PageStack.count >= v_ps_pointer then
        -- change page in stack
        -- use the update_member_attribute function,
        -- because that is slightly faster than the update_member function
        apex_collection.update_member_attribute(c_PageStack, p_seq => v_ps_pointer, p_attr_number => 1, p_attr_value => v_current_page);
      else
        -- add page to stack
        apex_collection.add_member(c_PageStack, p_c001 => v_current_page);
        -- no need to add it to the plsql collection too!
      end if;
    end case;

    ----------------------------------------------------------------------------
    -- set the last page item
    apex_util.set_session_state(c_appItem_last_page    , coalesce(v_new_last_page,c_Homepage));
    apex_util.set_session_state(c_appItem_page_pointer ,v_ps_pointer);

  exception
     when others then
       -- add your own custom logging framework here
       logger.logError( co_modul_name, 'Problem during management of page call stack !'||'pointer='||v_ps_pointer||', current page='||v_current_page||', last page ='||v_last_page);
       raise;
  end managePageStack;

Check scripts

A DBA can execute the following statements to see what is happening.

alter session set current_schema = apex_050000;
execute wwv_flow_security.g_security_group_id := 10;
select * from wwv_flow_collections$;
select * from wwv_flow_collection_members$
where collection_id in (select id from wwv_flow_collections$ 
                       where collection_name = 'PAGESTACK');

Side Notes

Ideas

When we have such a call stack it opens up some other possibilities.
For example we can implement a dynamic breadcrumb bar that shows not only the static way to one page, but instead shows the way we used in our session. And if we go back in the call stack, we could even show the pages that we just left.

Tuning

While implementing the collection part I wondered
which is better (=faster) to use.
apex_collection.update_member or apex_collection.update_member_attribute

They both work slightly differently, but for my purpose (only one column) they are identical.

Here is the performance test that I did. Result is that apex_collection.update_member_attribute is almost a second faster when calling it 10000 times. This matched my expectation.

-------------------------------
-- test member_attribute 
-- performance test
set serveroutput on
declare
  v_time timestamp := systimestamp;
begin 
 apex_collection.create_collection('PageStack');
 -- add new page 10 to the stack
 apex_collection.add_member('PageStack', p_c001 => '10');

 v_time := systimestamp;
 -- update the member 10000 times
 for i in 1..10000 loop
    apex_collection.update_member('PageStack', p_seq => 1, p_c001 => '20');
 end loop;    
 dbms_output.put_line('Member           updated 10000 times: '||to_char(systimestamp-v_time));

 -- update the member 10000 times using attribute
 v_time := systimestamp;
 for i in 1..10000 loop
    apex_collection.update_member_attribute('PageStack', p_seq => 1, p_attr_number => 1, p_attr_value => '10');
 end loop;    
 dbms_output.put_line('Member attribute updated 10000 times: '||to_char(systimestamp-v_time));

 apex_collection.delete_collection('PageStack');
end;
/
Elapsed: 00:00:05.048
Member           updated 10000 times: +000000000 00:00:02.980000000
Member attribute updated 10000 times: +000000000 00:00:02.043000000

To run this yourself, you need to create a valid apex session state.
For example using Martin D’Souzas logic: http://www.talkapex.com/2012/08/how-to-create-apex-session-in-plsql.html

Nasty surprises

When you create an apex collection using the apex_collection package, the collection name will always be written in UPPERCASE. This cost me some time to identify the issue, because when reading it from apex_collections I used a lowercase name. And the collection was never found. So remember to always write collection names in UPPERCASE.

Advertisements

2 thoughts on “Apex page call stack

  1. Hi! I have used same approach, but with page-specific items. It helps with going deeper in one page and it can handle multi-tab page execution.

    But be aware of APEX 5.1. Session cloning for multi-tab support will be done with collection-cloning. Waiting for release to get deal with this dynamic breadcrumbs breaking change.

    • Hi kuz,

      thanks for the reminder at Apex 5.1. I believe that this solution should work also for cloned sessions. Of cause from the time of the cloning, the two call stacks would start to differ. But I think this matches the users expectations. Your solution using multi-tab page executions surely sounds interesting. Did you share that solution somewhere?

      Regards
      Sven

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s