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.
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 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
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
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
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).
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
<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
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.
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
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!
17th December 2001
|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.|