A demonstration on the use of Tab forms

Tony Marston - 17th December 2001
Amended - 3rd January 2002

Although the tab widget can be very useful some developers have a difficult time including it in their software as the documentation is very sparse and does not cover all the issues. The purpose of this article is to identify some of those issues and to explain how I deal with them.

A working demonstration of my code can be downloaded from here (23KB zipped). This code retrieves data from two related database entities, shows different parts of the data on three different tab pages, and allows any changes to be stored on the database.

Each tab page is a separate component

The first issue to grasp is the fact that the tab widget is defined on one component while the contents of each of the tabs is supplied on a separate component which is activated as a child instance. Each of these child components should have the 'Window Type' in Window Properties set to 'Tab Page'. This forces 'modality and attachment' to be set to 'Non-Modal, Attached'. The tab parent and each of its children must be non-modal.

The tab parent and its tab pages form a single logical transaction

The second issue is that each tab page should not be treated as a separate transaction. The tab parent and all of its tab pages should be considered as a single logical unit performing a single logical transaction. Each tab page is a child of the tab parent, it is under the control of the tab parent and should not act independently of the tab parent. The tab parent should be responsible for retrieving the data from the database and distributing it to each tab page as and when required. When a store is performed it should apply all updates which are pending in any of the tab pages as well as the tab parent. The store should normally be done within the tab parent after it has obtained any changes from each of its child pages.

The figures below are taken from my sample software. Figure 1 shows the tab parent while figure 2 shows one of the 3 tab pages. Figure 3 shows how the two actually look when they are combined at run time.

Figure 1 - The parent form containing the tab widget

tip25_01.gif

Figure 1 shows the tab widget with 3 tabs. These can be defined with the widget properties as 'Page Name' and 'Tab Label Text' or provided at run time as an associative list using code similar to the following:

putitem/id $valrep(tab_field), "TAB_PAGE_1", "Page 1"
putitem/id $valrep(tab_field), "TAB_PAGE_2", "Page 2"
putitem/id $valrep(tab_field), "TAB_PAGE_3", "Page 3"

It is even possible to have this associative list obtained from the message file so that the tab labels can be provided in different languages.

Another possibility when setting $valrep(tab_field) dynamically is the ability to remove the reference to any tab page if the user has not been granted access rights to it.

Note that although it is possible for the tab parent to contain some data in this example it is all distributed among the tab pages. It does not matter where any item of data appears, but it should not appear in more than one place as it would be difficult for any changes to be synchronised across all components.

Figure 2 - One of the child tab pages

tip25_02.gif

The tab pages contain database entities with all the triggers disabled as all I/O (except perhaps for foreign entities and other lookups) is handled within the tab parent. Note also that the tab pages do not contain any 'OK', 'Cancel' or 'Store' buttons as these are all handled within the tab parent.

Figure 3 - The two forms combined at run time

tip25_03.gif

At run time the body of the tab widget will show the contents of one of its tab pages. If a different tab is selected then the display will change accordingly. Note that if the dimensions of any tab page exceed those of the tab widget then scroll bars will be visible. The tab page is not aligned centrally within the tab widget - the top left-hand corner of the tab page is position in the top left-hand corner of the tab widget (below the tab labels).

Getting data into each tab form

In my sample software the data is retrieve'd within the tab parent and passed to each tab page component when that component is first activated. This is accomplished using local proc LP_ACTIVATE_TAB which contains the following code:

entry LP_ACTIVATE_TAB
params
   string  pi_TabComponent   : IN
endparams
variables
   string  lv_Data
endvariables

; does this component already exist as a child instance?
getitem/id lv_Data, $instancechildren, pi_TabComponent
if ($status > 0)
   setformfocus pi_TabComponent   ; yes, switch focus
   return(0)
endif

; obtain data required by this component
selectcase pi_TabComponent
case "TAB_PAGE_1"
   putlistitems/occ lv_Data, "X_PERSON"
case "TAB_PAGE_2"
   putlistitems/occ lv_Data, "X_PERSON"
case "TAB_PAGE_3"
   putlistitems/occ lv_Data, "X_PERS_ADDR"
endselectcase

; activate component with its data
activate pi_TabComponent.EXEC(lv_Data)
#include STD:FATAL_ERROR

setformfocus pi_TabComponent

return(0)

end LP_ACTIVATE_TAB

Note that each tab form is activated only once. Once an component instance has been created this proc takes no action other than switching focus to that instance. Each tab page remains in existence until the tab parent terminates.

One point to note about the putlistitems/occ statements - as none of the fields from these entities are referenced within the tab parent it is necessary to set the field list to ALL, otherwise a lot of the fields simply won't be available.

The LP_ACTIVATE_TAB proc is activated by the following code in the <exec> trigger which appears between the data retrieval and the edit statement:

<exec> trigger
retrieve

; tell the widget which tab to display
tab_field = "TAB_PAGE_1"

; activate component for this tab page
call LP_ACTIVATE_TAB(tab_field)
#include STD:FATAL_ERROR

edit

This proc is also activated by the following code in the <value changed> trigger of tab widget:

<value changed> trigger
call LP_ACTIVATE_TAB(@$fieldname)
#include STD:FATAL_ERROR

The data is received and loaded by each tab page by the following code in the <EXEC> trigger:

<EXEC> trigger
params
   string  pi_Data  : IN
endparams

getlistitems/occ/init pi_Data, "<MAIN>"

edit

Getting data out of each tab form

All database updates are performed within the tab parent, so any changes captured by any child tab pages must be brought into the tab parent before the store command is executed. This is accomplished by using local proc LP_STORE_PROC which contains the following code:

entry LP_STORE_PROC
variables
   string  lv_list, lv_instance
endvariables

lv_list = $instancechildren     ; get list of child instances
while (lv_list != "")
   getitem lv_instance, lv_list, 1  ; get first entry
   delitem lv_list,1            ; delete from list

   if ($instancemod(lv_instance))
      ; get the child to pass back all changes
      call LP_CHILD_VALUES(lv_instance)
      #include STD:FATAL_ERROR
   endif
endwhile

call STORE_PROC
if ($status < 0) return($status)

lv_list = $instancechildren     ; get list of child instances
while (lv_list != "")
   getitem lv_instance, lv_list, 1  ; get first entry
   delitem lv_list,1            ; delete from list

   if ($instancemod(lv_instance))
      ; unset modification flags in child instance
      activate lv_instance.STORE()
      #include STD:FATAL_ERROR
   endif
endwhile

return(0)

end LP_STORE_PROC

Local proc LP_CHILD_VALUES contains the following code:

entry LP_CHILD_VALUES   ; get changed values from a child process
params
   string  pi_Instance  : IN
endparams
variables
   string  lv_Data
endvariables

; retrieve values from child form (associative list)
activate pi_Instance.PASS_BACK_VALUES(lv_Data)
#include STD:FATAL_ERROR

; update corresponding values in this form
selectcase pi_Instance
case "TAB_PAGE_1"
   getlistitems/occ lv_Data,"x_person"
case "TAB_PAGE_2"
   getlistitems/occ lv_Data,"x_person"
case "TAB_PAGE_3"
   getlistitems/occ lv_Data,"x_pers_addr"
endselectcase

return(0)

end LP_CHILD_VALUES

LP_CHILD_VALUES requires the following operation within each of the tab pages:

operation PASS_BACK_VALUES     ; pass updated values back to parent
params
   string  po_Data     : OUT
endparams

; put all changed data into an associative list
putlistitems/occ/modonly po_Data,"<MAIN>"

return(0)

end PASS_BACK_VALUES

LP_STORE_PROC requires the following operation within each of the tab pages:

operation STORE ; unset all modification flags

store

return(0)

end STORE

The only reason to perform a store within a tab page is to clear any modification flags. No database updates are performed as all <write> triggers are disabled.

Firing of <ACCEPT>, <QUIT> and <STORE> triggers

One point to remember when you are dealing with a collection of components, as we are here with a tab parent and several tab pages, is that only one of them has focus at any one time. This means that when one of the <ACCEPT>, <QUIT> or <STORE> triggers is fired, either by use of one of the Ctrl key shortcuts or by a button on the session panel, it is only fired in the component that currently has focus.

In the case where a tab page has focus the following trigger code is executed:

<accept> trigger of tab page

setformfocus $instanceparent         ; switch focus to parent
postmessage $instanceparent,"ACCEPT" ; tell parent to accept
return(-1)  ; cancel this trigger in this component
<quit> trigger of tab page

setformfocus $instanceparent         ; switch focus to parent
postmessage $instanceparent,"QUIT"   ; tell parent to quit
return(-1)  ; cancel this trigger in this component
<store> trigger of tab page

setformfocus $instanceparent            ; switch focus to parent
postmessage $instanceparent,"STORE"     ; tell parent to store
return(0)

You will note that these triggers do nothing but send a message to the parent saying what trigger was activated. These messages are then processed using the following code in the <async interrupt> trigger of the tab parent:

<async. interrupt> trigger of tab parent

selectcase $msgid
case "STORE"
   macro "^STORE"
case "ACCEPT"
   call LP_ACCEPT_PROC
case "QUIT"
   if ($formname = $formfocus$)
      call LP_QUIT_PROC
   endif
endselectcase

The <store> trigger executes LP_STORE_PROC which has been described previously.

LP_ACCEPT_PROC will use the following code to update the database and terminate:

entry LP_ACCEPT_PROC

call LP_STORE_PROC

if ($status = 0) exit(0)

end LP_ACCEPT_PROC

LP_QUIT_PROC is a little more complicated:

entry LP_QUIT_PROC

call LP_QUIT_TEST
if ($status) return(-1)

rollback

exit(1)

end LP_QUIT_PROC
entry LP_QUIT_TEST     ; test for changes before quitting
variables
   string  lv_List, lv_Instance
endvariables

;FIRST - test all children for changes

lv_list = $instancechildren            ; get list of child instances
while (lv_list != "")
   getitem lv_instance, lv_list, 1     ; get first entry
   delitem lv_list,1                   ; delete from list

   if ($instancemod(lv_instance))
       askmess/question "Changes found - do you wish to continue ?"
       if ($status = 0)                ; no
           ; set focus to this instance
           setformfocus(lv_instance) 
           $formfocus$ = lv_instance
           tab_field = lv_Instance
           return(-1)                  ; do not quit
       else
           return(0)                   ; stop after 1st question
       endif
   endif
endwhile

; SECOND - test the parent for changes

if ($formdbmod | $instancedbmod)
   askmess/question "Changes found - do you wish to continue ?"
   if ($status = 1)                    ; yes
       return(0)                       ; continue with <quit>
   else
       setformfocus                    ; set focus to this instance
       return(-1)                      ; no - stop
   endif
endif

return(0)  ; continue with <quit>

end LP_QUIT_TEST

The purpose of LP_QUIT_TEST is to find out if any unstored modifications have been made in any of the tab pages or even the tab parent itself. If any changes are found anywhere it will ask the standard message and either continue with the quit or cancel it depending on the response. Note that this question is only asked once irrespective of how many separate components actually contain unstored modifications.

NOTE: There is a component variable in the tab parent called $formfocus$ which is set in the <FORM GETS FOCUS> trigger, examined in the <ASYNC INTERRUPT> trigger, and cleared in local proc LP_QUIT_TEST. This is used to ensure that LP_QUIT_PROC is called only once if the user answers 'NO' to the 'Changes found - do you wish to continue ?' question.

<form gets focus> trigger of tab parent

$formfocus$ = $formname

Firing of <ACCEPT> and <QUIT> triggers in the tab parent

When a component has child instances the behaviour of the <ACCEPT> and <QUIT> triggers can catch some developers unawares. When one of these triggers is fired in the parent form the corresponding trigger in each of the child instances is fired first. If any of these child instances returns a zero status then that instance is terminated before processing moves on to the next child instance. If any child instance returns a non-zero status then that instance is not terminated, and although the trigger is fired in the remaining child instances the trigger will be aborted in the parent instance without executing any code that it may contain.

This means that when one of these two triggers is fired in the parent form none of the trigger code in the parent form is actually executed unless all the child instances return a zero status in the same trigger, and by returning a zero status each child instance is immediately terminated. It is therefore not possible to have any code in the <QUIT> trigger to check for pending modifications, or any code in the <ACCEPT> trigger to retrieve those pending modifications for the simple reason that by this time all of the child instances have been terminated.

It is for this reason that in my sample code the <ACCEPT> and <QUIT> triggers in the child tab pages will always return a negative status, thus aborting all further processing of that trigger. Processing is switched instead to the <ASYNC. INTERRUPT> trigger of the parent form which performs the relevant processing without going through its own <ACCEPT> or <QUIT> triggers.

If all this sounds confusing then play with my sample code and see for yourself. Good luck!


Tony Marston
17th December 2001

mailto:tony@tonymarston.net
mailto:TonyMarston@hotmail.com
http://www.tonymarston.net

Amendment history:

3rd Jan 2002 Removed the condition in the <ACCEPT> and <QUIT> triggers of the tab pages so that if any of these triggers are fired in the parent form, either via command buttons or panel buttons, they will continue to work.

counter