IN THIS CHAPTER
This chapter describes some of the tips and techniques in Forms application development. The power of Forms can be leveraged fully when one moves from GUI to "beyond GUI"—that is, to GUI combined with programming. The tricks of the trade I explore in this chapter include quality-assured front ends such as implementing a standard toolbar, creating dynamic and floating toolbars, implementing timers, and displaying a clock. The chapter also provides special tips for list items, including simulating drill-down Lists of Values (LOVs), ordering by foreign key items, and implementing object-oriented methods in Forms. Finally, I'll present some tips for standard GUI practices. Wherever possible, code segments are provided to help you implement these techniques.
A common
application-specific feature is a
standard iconic toolbar that provides icons to carry out common form functions such as Save,
Undo,
Enter Query,
Execute Query,
Next Block,
Previous Block,
Next Record,
Previous Record,
Insert Record,
Delete Record,
Exit, Clear,
List Values,
and Close Application.
Toolbars also let users invoke functions such as Notepad,
Diary,
Calculator,
and so on. Such a toolbar is useful especially when the application uses a
customized menu bar to replace the default menu provided by Oracle Forms runtime. In addition, a toolbar enhances the GUI's look and feel.
In Forms 5.x and above, the
default toolbar comes with Forms runtime (by adding the suffix &SMARTBAR
to the DEFAULT
value of the Menu Module
property). However, think of a function like Close Application,
mentioned earlier. This function might refer to closing all forms invoked in a multiform application—a task that cannot be achieved with the normal Exit toolbar icon unless it is clicked repeatedly.
In Forms 5.x and above, toolbars can be associated with a form at three levels at runtime:
At the menu level—On an MDI platform, this toolbar is similar to the MDI toolbar in Forms 4.5. This toolbar also comes in an SDI platform. Forms supplies it by default with the name SMARTBAR
with the DEFAULT
menu.
At the form level—This toolbar is new to Forms 5.x and above and is available for all windows within that form. It is not equivalent to the MDI toolbar.
At the individual window level—This toolbar is specific to a particular window to which it is attached.
To create a menu-level toolbar, specify &SMARTBAR
after the DEFAULT
menu module name in the Form Module
property palette. Dynamically hiding the menu using REPLACE_MENU('')
in the WHEN_NEW_FORM_INSTANCE
trigger will also suppress the display of the associated toolbar.
To create a menu-level toolbar for any
customized menu, set either the
Visible In Horizontal Menu Toolbar
or
Visible In Vertical Toolbar
property to Yes
for each menu item that should appear in the menu toolbar.
Figure 1.1 shows a menu-level toolbar.
To create a
form-level or
window-level toolbar, you create a horizontal toolbar canvas view with iconic buttons. Then, specify it as the value for the
Form Horizontal Toolbar
property or the
Window Horizontal Toolbar
property. To do so, follow these steps:
Create a canvas view of type horizontal toolbar (that is, CANVAS_TOOLBAR
).
Assign to the canvas view a property class specifying the standard canvas characteristics (such as PC_CANVAS
). The height of the canvas should be just enough to enclose an iconic button of standard size. Set the Visible
property of this canvas to Yes
and the Bevel
property to None.
Create a property class named PC_ICONIC_BUTTON
with the following property values: Iconic
set to Yes,
Mouse Navigate
set to Yes,
Keyboard Navigable
set to Yes,
Width
set to 10,
Height
set to 10,
and ToolTip Visual Attribute Group
set to DEFAULT.
Construct a block named TOOLBAR
with items as iconified push buttons for each of the individual functions. The buttons inherit their properties from the PC_ICONIC_BUTTON
property class.
A WHEN-BUTTON-PRESSED
trigger for each of the buttons contains the appropriate Forms built in as arguments to the DO_KEY
procedure. For example, the Save button can have the following line of code in its WHEN-BUTTON-PRESSED
trigger:
WHEN-BUTTON-PRESSED DO_KEY('COMMIT_FORM'),
You can achieve the Close Application
functionality described earlier by calling a procedure in the corresponding WHEN-BUTTON-PRESSED
trigger. This technique is explained in the sub-section
"Simulating a CLOSE ALL Forms"
in Chapter 3, "Multi-form Applications."
ToolTips can be specified
for each iconic button. This is done by specifying the respective text for the ToolTip Text
property for each of the iconic buttons. For example, the ToolTip text for the iconic button corresponding to the Save
function can be given in the button property palette by entering
Save Changes
in the ToolTip Text
property.
Specifying ToolTips provides a visual hint feature when the cursor enters the icon. This feature is available from Forms 5.x onwards. In Forms 4.5, this functionality has to be simulated with extra coding, as illustrated later in this section.
Specify this horizontal toolbar canvas CANVAS_TOOLBAR
as the value for the Form Horizontal Toolbar
property in Forms 5.x and above. In Forms 4.5, specify the Horizontal MDI Toolbar
property in the Forms Module property sheet.
A sample toolbar constructed in this way looks like the one shown in Figure 1.2.
In Forms 4.5, you can display
ToolTips by creating
a display item, TOOLTIP_ITEM,
with the following properties:
Char
data type
The same visual attribute as the ToolTip visual attribute group
The Bevel
property set to None
and Displayed
property set to False
Specify the ToolTip text as the value for the Label
property for each iconic button
In addition, the property class PC_ICONIC_BUTTON
discussed earlier can have the following two triggers attached to it:
WHEN-MOUSE-ENTER
and
WHEN-MOUSE-LEAVE.
The code for these triggers is explained in the steps that follow.
Dynamically populate this item with text equivalent to the Label
in the WHEN-MOUSE-ENTER
trigger, as follows:
WHEN-MOUSE-ENTER DECLARE tooltip_x_pos NUMBER; tooltip_y_pos NUMBER; tooltip_text VARCHAR2(30); item_id ITEM; BEGIN tooltip_x_pos := GET_ITEM_PROPERTY(:SYSTEM.MOUSE_ITEM,X_POS); tooltip_y_pos := GET_ITEM_PROPERTY(:SYSTEM.MOUSE_ITEM,Y_POS); tooltip_text := GET_ITEM_PROPERTY(:SYSTEM.MOUSE_ITEM,LABEL); :toolbar.tooltip_item := tooltip_text; item_id := FIND_ITEM('TOOLBAR.TOOLTIP_ITEM'), IF NOT ID_NULL(item_id) THEN SET_ITEM_PROPERTY(item_id, DISPLAYED, PROPERTY_TRUE); SET_ITEM_PROPERTY(item_id, POSITION tooltip_x_pos+10, tooltip_y_pos+10); ELSE MESSAGE('Invalid Tooltip Item'), RAISE FORM_TRIGGER_FAILURE; END IF; END;
A WHEN-MOUSE-LEAVE
trigger will hide the dynamic ToolTip by
setting its DISPLAYED
property to FALSE
as follows:
WHEN-MOUSE-LEAVE DECLARE item_id ITEM; BEGIN item_id := FIND_ITEM('TOOLBAR.TOOLTIP_ITEM'), IF NOT ID_NULL(item_id) THEN :toolbar.tooltip_item := NULL; SET_ITEM_PROPERTY(item_id, DISPLAYED, PROPERTY_FALSE); END IF; END;
In Forms 5.x and above, the ToolTip appears below the iconic button, whereas in Forms 4.5 it appears on the side. This has been intentionally done because the ToolTip in Forms 4.5 is attached to the MDI horizontal toolbar; there is no way to display it outside the toolbar canvas without making the canvas height more than the icons'height. This leaves a view space on the MDI toolbar.
The following tips provide the tidbits often ignored while developing form-level and window-level toolbars. Also, the points to remember while associating toolbars with forms in multiform applications are highlighted.
Use a source template form in which the toolbar can be constructed. Then, you can base customized forms on this template.
Toolbars attached at the form level are not available to all forms in an application. In a multiform
application, especially when you're using OPEN_FORM,
you can subclass the CANVAS_TOOLBAR
from the source template form. Then, set the Form Horizontal Toolbar
property
to CANVAS_TOOLBAR
for each form. Doing so creates an application with a common toolbar for each independent form.
To simulate the same functionality while using Forms 4.5, the CANVAS_TOOLBAR
can be referenced from the source template form. Then, set the MDI Horizontal Toolbar
property to CANVAS_TOOLBAR
for each form.
A modal window should have a window-level toolbar associated with it. Form-level toolbar icons are not accessible from a modal window. The same is true with toolbars associated with menu, SDI, or MDI windows. Figure 1.3 shows a form-specific toolbar with a Personal Info. icon. If you click on this icon, you get the window-specific toolbar for a modal window, shown in Figure 1.4.
This section discusses the techniques to implement dynamic toolbars. It starts by looking at the various ways a toolbar can be "dynamic."
Dynamic toolbars can be implemented several ways. You can create toolbars that dynamically shrink or expand, based on the current mouse position or the current item, record, block, canvas, or form; you hide, unhide, and resize proportionately the toolbar's iconic buttons, depending on the current cursor position. Examples are as follows:
Hiding (or disabling) the List button when the cursor is in a nonupdateable item or an item that has no LOV attached to it, and showing it otherwise.
Hiding (or disabling) the Next Record button when the cursor is in the last record of a Query Only
block, and showing it otherwise.
Hiding (or disabling) the Enter Query and Execute Query buttons when a control block receives input focus, and showing them otherwise.
Generating a new toolbar by selecting a subset of buttons from the original set when a form is being run in Query Only mode.
Other dynamic toolbars are scrollable for forms that require greater functionality to be provided by means of application-specific buttons. Floating toolbars can appear almost anywhere—that is, they "float" in an application or appear where you right-click. You can resize toolbars so that the icons fit proportionately. Finally, you can generate toolbars from a global set of iconic buttons, using point-and-click.
In this section, I will discuss the implementation of a floating toolbar. It has the flexibility of being able to float, or be displayed almost anywhere when initiated by a user action. It can also have dynamic resizing capability. It has the same functionality as a regular toolbar but remains displayed until closed by another user action. For example, the user action initiating the display of the toolbar can be a right mouse click on an empty part of the canvas (that's why it's floating), and the user action closing it can be a left mouse click in an empty area of the can vas.
The form objects including triggers can be included in an Object Group, which in turn can be included in an Object Library and shared (subclassed) across form modules.
Alternatively, a template form can be created with the described objects, and customized application-specific forms can be based on this template form.
Follow these steps to create the floating toolbar:
Create a new WINDOW_FLOATING_TOOLBAR
that is nonmodal and is not part of the normal windows being used in any form. Make this window's width and height the same as the canvas view width and height of the window-specific toolbar created in the last section's example.
Set the window's window style
to Dialog,
its Modal
property to No,
its Closeable
property to Yes,
its Minimizable
and
Maximizable
properties to No,
and
its Bevel
property to None.
Create a canvas named CANVAS_FLOATING_TOOLBAR.
This canvas is created in the same manner as the standard toolbar described in the earlier example. The only difference is that the number of icons is less here. This number has been chosen at random and can be application specific. Note that this can also be a stacked canvas.
Specify the canvas view properties:
Set the Window
property to WINDOW_FLOATING_ TOOLBAR,
the
Canvas Type
to Horizontal Toolbar,
the
Visible
property to No,
and
the Bevel
property to None.
Set
the Horizontal Toolbar
property of the WINDOW_FLOATING_TOOLBAR
window to CANVAS_FLOATING_TOOLBAR.
Initiate the window display by writing
a WHEN-MOUSE-CLICK
trigger at the form level and capturing the right mouse click event. The code for this trigger has the following logic:
Check for the cursor not being positioned inside any item, including non–data-bearing items. This is done using the system variable
:SYSTEM.MOUSE_ITEM.
Check whether the mouse button pressed is not the left mouse button. This is done by checking that the value of the Forms
system variable :SYSTEM.MOUSEBUTTON_PRESSED
does not equal the constant 1.
This system variable
can take values 1,
2,
or 3.
The value 1
is for left, 2
for middle, and 3
for right mouse buttons. All these values are returned as character values. Checking for a value other than 1
ensures that the user-pressed mouse button is not the left one.
Position the window WINDOW_FLOATING_TOOLBAR
at the current (X,Y) position of the cursor. This is done by dynamically
setting the X_POS
and
Y_POS
properties of the window to the values :SYSTEM.MOUSE_X_POS
and :SYSTEM.MOUSE_Y_POS,
respectively, using SET_WINDOW_PROPERTY.
The WHEN-MOUSE-CLICK
trigger code is as follows:
WHEN-MOUSE-CLICK IF NAME_IN('SYSTEM.MOUSE_ITEM') IS NULL THEN IF NAME_IN('SYSTEM.MOUSEBUTTON_PRESSED') != '1'THEN SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR', X_POS, TO_NUMBER(NAME_IN('SYSTEM.MOUSE_X_POS'))); SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR',Y_POS, TO_NUMBER(NAME_IN('SYSTEM.MOUSE_Y_POS'))) SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR', VISIBLE,PROPERTY_TRUE); END IF; END IF;
The floating toolbar will look like Figure 1.5.
The actual (X,Y) position of the displayed toolbar is an offset from the actual cursor (X,Y) position because the former is relative to the MDI Window and the latter is relative to the primary canvas of WINDOW0.
You can initiate
the window close in several ways. First, you can set
the Hide On Exit
property to Yes
for this toolbar window. This is not a standard practice because the user might want to perform multiple functions in the form using this toolbar and would therefore want the toolbar to remain floating until explicitly closed. Applications demanding user-specific requests for such a feature are an exception to this rule.
Second, the window can close when the user clicks the upper-right ×. This is the recommended practice, unless a user requirement specifies a different approach. To use this technique, write a WHEN-WINDOW-CLOSED
trigger
at form level that checks for the currently active window and closes it if it is WINDOW_FLOATING_TOOLBAR
:
WHEN-WINDOW-CLOSED IF NAME_IN('SYSTEM.EVENT_WINDOW') = 'WINDOW_FLOATING_TOOLBAR'THEN SET_WINDOW_PEROPERTY('WINDOW_FLOATING_TOOLBAR', VISIBLE, PROPERTY_FALSE); END IF;
Third, the window can be made to disappear by creating a timer at the time of window initia tion and writing a WHEN-TIMER-EXPIRED
trigger
to hide it after a certain interval of time. You provide this second way of closing by specifying an appreciably large time interval—a minute, for example. Closing the floating toolbar by means of a timer involves the following steps:
Create a
timer in the WHEN-MOUSE-CLICK
trigger
immediately before displaying the toolbar window. The modified WHEN-MOUSE-CLICK
trigger is as follows:
WHEN-MOUSE-CLICK DECLARE timer_id TIMER; one_minute NUMBER := 60000; BEGIN IF NAME_IN('SYSTEM.MOUSE_ITEM') IS NULL THEN IF NAME_IN('SYSTEM.MOUSE_BUTTON_PRESSED') != '1'THEN SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR', X_POS, NAME_IN('SYSTEM.MOUSE_X_POS')); SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR', Y_POS, NAME_IN('SYSTEM.MOUSE_Y_POS')); BEGIN timer_id := FIND_TIMER('TIMER_HIDE_FT'), IF ID_NULL(timer_id) THEN timer_id := CREATE_TIMER('TIMER_HIDE_FT',one_minute, NO_REPEAT); END IF; END; SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR', VISIBLE, PROPERTY_TRUE); END IF; END IF;
Write a WHEN-TIMER-EXPIRED
trigger
at the form level to close the window after the time period elapses, in one minute in this case:
WHEN-TIMER-EXPIRED IF GET_APPLICATION_PROPERTY(TIMER_NAME) = 'TIMER_HIDE_FT'THEN IF GET_WINDOW_PROPERTY('WINDOW_FLOATING_FT', VISIBLE) = 'FALSE'THEN SET_WINDOW_PROPERTY('WINDOW_FLOATING_TOOLBAR', VISIBLE, PROPERTY_FALSE); END IF; END IF;
Another convenient way of displaying a floating toolbar is by providing a pop-up menu on a right mouse button click. This pop-up can have at least two menu options: Show Toolbar and Hide Toolbar. This can be achieved by the following steps:
Create a pop-up menu
in the Object Navigator with two items: SHOW_TOOLBAR
(labeled Show Toolbar) and HIDE_TOOLBAR
(labeled Hide Toolbar).
The WHEN-MOUSE-CLICK
trigger is still necessary because the :system.mouse_x_pos
for a pop-up menu is 0
when the cursor is in the pop-up. Therefore, you capture the (mouse_x_pos,
mouse_y_pos
) as soon as the user right-clicks to display the pop-up menu. You can write a WHEN-MOUSE-CLICK
trigger
, in addition to the menu item code, to capture the current (X,Y) of the cursor on right click before the cursor is positioned on one of the items of the pop-up menu:
WHEN-MOUSE-CLICK IF :SYSTEM.MOUSE_BUTTON_PRESSED != '1'THEN IF :SYSTEM.MOUSE_ITEM IS NULL THEN COPY(NAME_IN('SYSTEM.MOUSE_X_POS'), 'CTRL_BLK.MOUSEXPOS'), COPY(NAME_IN('SYSTEM.MOUSE_Y_POS'), 'CTRL_BLK.MOUSEYPOS'), END IF; END IF;
Here, CTRL_BLK.MOUSEXPOS
and CTRL_BLK.MOUSEYPOS
are two control items to hold the current values of SYSTEM.MOUSE_X_POS
and SYSTEM.MOUSE_Y_POS
as defined in the preceding code snippet.
The menu item code for the SHOW_TOOLBAR
item is as follows:
IF :SYSTEM.MOUSE_ITEM IS NULL THEN SET_WINDOW_PROPERTY('window_ft', X_POS, TO_NUMBER(:ctrl_blk.mousexpos)); SET_WINDOW_PROPERTY('window_ft', Y_POS, TO_NUMBER(:ctrl_blk.mouseypos)); SET_WINDOW_PROPERTY('window_ft', VISIBLE, PROPERTY_TRUE); END IF;
The menu item code for the HIDE_TOOLBAR
item is as follows:
SET_WINDOW_PROPERTY('window_ft', VISIBLE, PROPERTY_FALSE); :ctrl_blk.mousexpos := NULL; :ctrl_blk.mouseypos := NULL;
The pop-up menu appears somewhat like the one shown in Figure 1.6.
An external clock can be constructed
using timers. Timers correspond to internal clocks, which have a specific time period. When the specified duration expires, the timer can either perform an action once and stop or repeat the action regularly every time the timer expires. Timer duration
is always in milliseconds. Timers are
created using the CREATE_TIMER
built in and require
a WHEN-TIMER-EXPIRED
trigger to be written at the form level. This trigger fires every time the timer expires.
Let's create
a display item, CURRENT_TIME,
in the horizontal toolbar canvas CANVAS_TOOLBAR
created earlier. This item shows the time in HH24:MI:SS
format and updates itself every second (the timer duration).
In the WHEN-NEW-FORM-INSTANCE
trigger,
create a timer named CLOCK_TIMER,
which iterates after every one second and populates the CURRENT_TIME
item with the system date in HH24:MI:SS
format. The code is as follows:
DECLARE timer_id TIMER; one_second NUMBER := 1000; BEGIN timer_id := FIND_TIMER('CLOCK_TIMER'), IF NOT ID_NULL(timer_id) THEN DELETE_TIMER(timer_id); ELSE timer_id := CREATE_TIMER('CLOCK_TIMER',one_second, REPEAT); END IF; SELECT TO_CHAR(SYSDATE,'HH24:MI:SS') INTO :toolbar.current_time FROM DUAL; EXCEPTION WHEN OTHERS THEN MESSAGE(TO_CHAR(SQLCODE)||''||SQLERRM); END;
Create a WHEN-TIMER-EXPIRED
trigger as follows:
DECLARE timer_name VARCHAR2(30); BEGIN timer_name := GET_APPLICATION_PROPERTY(TIMER_NAME); IF timer_name = 'CLOCK_TIMER' THEN SELECT TO_CHAR(SYSDATE,'HH24:MI:SS') INTO :toolbar.current_time FROM DUAL; END IF; EXCEPTION WHEN OTHERS THEN MESSAGE(TO_CHAR(SQLCODE)||''||SQLERRM); END;
In this section, I shall discuss some special tips and techniques offered by Forms with respect to lists and list items.
List items appear as either drop-down list boxes, T-lists, or combo boxes in Forms. Mostly, list items have static values as their elements, created during design time. However, there arises a need to populate a list item with elements based on runtime criteria or from a database table. For example, one might populate a drop-down list box with all the state codes from a look-up table named STATE_TAB.
This can be done either using a query or adding elements one by one at runtime. This way of populating list items programmatically without hard-coding their values at design time is what is termed
dynamically populating list items.
This technique involves populating the list with a query or without a query. I will discuss populating by means of a query because it is very powerful and efficient.
One elegant way to populate list items dynamically is to use programmatically created records groups. This record group should have a two-column structure, both being of character data type. The first column should correspond to the label of the list item, and the second column, to the corresponding value.
The record group has to be created programmatically. However, it can either query or nonquery as long as it follows the two-column structure mentioned here.
Never try to populate a list item using a query directly. Always create and populate a query record group, and use this record group to do the job.
The code performs these tasks:
The sample code is given here:
DECLARE rg_list_id RECORDGROUP; rg_name VARCHAR2(20) := 'RG_LIST'; ret_code NUMBER; --The following holds a SELECT query from which the list elements are derived. v_select VARCHAR2(300); BEGIN v_select := 'SELECT state_code, state_code FROM state_tab ORDER BY 2'; rg_list_id := FIND_GROUP(rg_name); IF NOT Id_Null(rg_list_id) THEN DELETE_GROUP(rg_list_id); END IF; rg_list_id := CREATE_GROUP_FROM_QUERY(rg_name, v_select); ret_code := POPULATE_GROUP(rg_list_id); POPULATE_LIST('LIST_ITEM','RG_LIST'), DELETE_GROUP(rg_list_id); END;
Sometimes, it might be necessary to populate NUMBER
and DATE
columns
as list item element values. Because list items always retain character values only, for both the label and value, it is necessary to perform
conversion to VARCHAR2
from NUMBER
and DATE.
As an illustration, consider a drop-down list box showing all departments in an organization. You can assume that the DEPT
table has the following structure:
CREATE TABLE DEPT (ID NUMBER(6) PRIMARY KEY, NAME VARCHAR2(30) NOT NULl);
The label column has its values derived from the NAME
column. The value corresponding to each NAME
is derived from the ID
column and should be stored in the list item as a
character value. This requires the use of TO_CHAR.
Therefore, the query text in the preceding example changes to
v_select := 'SELECT name, TO_CHAR(id) FROM dept ORDER BY 1';
After populating the list, the value can be
accessed by doing a reconversion to NUMBER
using the TO_NUMBER
function. The following shows how to access the ith element from the list discussed in the preceding example:
DECLARE v_id NUMBER; v_id_char VARCHAR2(6); item_id ITEM; BEGIN item_id := FIND_ITEM('LIST_ITEM'), IF ID_NULL(item_id) THEN MESSAGE('Invalid List'), RAISE FORM_TRIGGER_FAILURE; END IF; FOR I IN 1..GET_LIST_ELEMENT_COUNT(item_id) LOOP v_id_char = GET_LIST_ELEMENT_VALUE(item_id, i); v_id := TO_NUMBER(v_id); . . . . use this value for further processing END LOOP; END;
Use conversion functions TO_CHAR,
TO_DATE,
and TO_NUMBER.
On input to the list, use TO_CHAR
for both date and numeric values that correspond to the Value
column of the list.
On output from the list, use TO_DATE
and TO_NUMBER,
respectively, for date and numeric values.
Use these in the SELECT
column list, which is used to populate the list.
How many of you are aware of the fact that in Forms, you can add elements to the beginning of an already populated list item without repopulating it? This seems trivial at first thought but is a very powerful and flexible technique to be used in many demanding situations.
Use ADD_LIST_ELEMENT
and specify the constant 1
for the element index. This displaces all the existing elements to one position below and makes room for the new element with index number 1.
Do not specify the constant 0
instead of 1.
The code you will use is as follows:
ADD_LIST_ELEMENT(list_id, 1, <label>, <value>);
So far, I have discussed how to add elements dynamically to an empty list, how to access noncharacter list elements, and how to add to the beginning of an existing list. Now I will present a simple method to insert elements in the middle of an existing list and to append elements to the end of an existing list.
Use
ADD_LIST_ELEMENT
and specify the index number of the new element as
current element index + 1
where
current element
is the element after which the new element should be added. This displaces
all the remaining elements to one position below and makes room for the new element.
To add to the end of the list, specify the constant returned by GET_LIST_ELEMENT_COUNT,
which gives
the number of existing elements, and then specify the index of the new element as the value of this constant incremented by 1.
The following is the code for this:
DECLARE cnt NUMBER := 0; BEGIN cnt := GET_LIST_ELEMENT_COUNT(list_id); ADD_LIST_ELEMENT(list_id, (cnt+1), <label>, <value>); END;
In this example, you take the count of the list elements and increment it by 1. This value serves as the index for the new list element to be added. This works even when the list is wholly empty, because you initialize the count to zero before incrementing it by 1. Therefore, it adds the first element in case of an empty list.
LOV is the Forms acronym for List of Values. It functions in a manner similar to a pick list of choices. Drill-down LOV refers to descending through the LOV to its sublevels, starting from a highlighted element in the LOV.
Drill-down LOVs are very useful for tables involving recursive and/or parent-child relationships. Examples of such relationships are those involving a department and its employees, a manager and employees, or a company and its hierarchical representation.
In each of these cases,
a foreign key is involved that is either self-referential or parent- referential. For example, the department-employee relationship involves a parent-referential foreign key from the Department
table (the parent table). The manager-employees relationship is self-referential, with the primary and foreign keys being selected from the same table. Also, the information is hierarchical. The company information is another example of a
hierarchical representation.
LOVs are a commonly required feature of any application involving this kind of look-ups. The features of LOVs supported by Forms are limited in the sense that
There is no way to do multiselection from an LOV.
There is no way to drill down an LOV into its sublevels.
There is no way to add new rows to the look-up table using an LOV.
Out of these limitations, the most required functionality in case of parent-child relationships, especially tree-oriented, is the drill-down.
Drill-down functionality can be incorporated in an LOV directly using a Forms-provided LOV or using list items. This section discusses the implementation details of building a drill-down LOV using list items. The same method can be followed when using a Forms-supplied LOV.
You will use a T-list and dynamically populate it using a record group. The drill-down is achieved by dynamic replacement of the same record group query again and again till there is no further drill-down. Double-clicking a parent element will show this parent element and all its children one level below. The user "drills down" the LOV to reach a deeper level. The operation is repeatable until leaf elements are reached. The string '- Parent'
is appended to the parent element label to identify it as the parent. This element is always the first element of the list, irrespective of whether it has children.
Double-clicking a parent element will show its parent and all its children, that is, one level above. The user "drills up" the
LOV to reach a higher level. The operation is repeatable until the root element is reached. Once at the root, the original list is restored; that is, all EMPNO
s that exist as MGR
s. The string '- Parent'
is still appended to the parent element label until the initial list is restored.
The same T-list and the same record group are reused for the drill-down and drill-up operations.
The selection of an element is done by pressing Shift and double-clicking on the element. The normal double-clicking being reserved for drill-down, the Shift+double-click is used as an alternative for selecting an element from the LOV.
Create a block, BLOCK2,
having an item named LOV
in it. The properties
for the block are set as follows: Database Data Block
property set to No,
Number Of Records Displayed
set to 1,
Insert Allowed
set to Yes,
and Update Allowed
set to Yes.
The properties for the LOV item are as follows: Subclass Information property
set to PC_TLIST
(property class).
The property class PC_TLIST
has its properties set as shown in Figure 1.7.
Remember to create a NULL
list element; that is, both the label and its value are NULL.
NULL
means a null value, not the string 'NULL'.
The EMP
table is used to project the hierarchical relationship between manager (MGR
column) and employees (EMPNO
column). Assume that the EMP
table has the following structure:
CREATE TABLE EMP (EMPNO NUMBER(10) PRIMARY KEY, ENAME VARCHAR2(30) NOT NULL, MGR NUMBER(10) REFERENCES EMP (EMPNO), HIREDATE DATE, SAL NUMBER(11,2), JOB VARCHAR2(20), DEPTNO NUMBER);
The LOV functions the following way. At first, all the employees at the parent level are displayed. The query for doing this follows:
SELECT ename, TO_CHAR(empno) empno FROM emp WHERE empno IN (SELECT mgr FROM emp a)
This SELECT
statement also guarantees that the first-level EMPNO
is selected.
A dynamic record group, 'RG_'||'LOV',
is created from the preceding query and then populated and repopulated using rows returned by the query. The T-list is populated using this record group. You use a generic procedure for this step. The procedure is named p_populate_list
:
PROCEDURE p_populate_list(item_name_in VARCHAR2, query_in varchar2, o_retcd OUT NUMBER) IS rg_id RECORDGROUP; retcd NUMBER; rg_name VARCHAR2(100); item_id ITEM; BEGIN item_id := FIND_ITEM(item_name_in); IF ID_NULL(item_id) THEN o_retcd := -1; RETRUN; END IF; rg_name := 'RG_'||substr(item_name_in, INSTR(item_name_in, '.', 1)+1, LENGTH(item_name_in)); rg_id := FIND_GROUP(rg_name); IF NOT ID_NULL(rg_id) THEN DELETE_GROUP(rg_id); END IF; rg_id := CREATE_GROUP_FROM_QUERY(rg_name, query_in); retcd := POPULATE_GROUP(rg_id); IF (retcd <> 0) THEN o_retcd := retcd; RETURN; END IF; POPULATE_LIST(item_name_in, rg_id); IF NOT FORM_SUCCESS THEN o_retcd := -2; END IF; o_retcd := 0; END;
The appropriate trigger for calling the p_populate_list
procedure is WHEN-NEW-FORM-INSTANCE
:
WHEN-NEW-FORM-INSTANCE DECLARE query_in VARCHAR2(32767) := 'SELECT ename, TO_CHAR(empno) empno FROM emp WHERE empno IN (SELECT mgr FROM emp); retcd NUMBER; BEGIN p_populate_list('BLOCK2.LOV', query_in, retcd); IF (retcd <> 0) THEN MESSAGE('ERR: Could not populate list item.'), RAISE FORM_TRIGGER_FAILURE; END IF; END;
The right place for the drill-down code is the WHEN-LIST-ACTIVATED
trigger:
DECLARE query_in VARCHAR2(32767); item_name VARCHAR2(100) := NAME_IN('SYSTEM.TRIGGER_ITEM'), retcd NUMBER; current_rows_mgr NUMBER; current_rows_empno NUMBER; BEGIN query_in := 'select LPAD(ename, ((LEVEL-1)*4+LENGTH(ename)), '''')|| DECODE(TO_CHAR(empno), '|| NAME_IN(item_name)||', ''- Parent'', NULL) ename, TO_CHAR(empno) '|| ' FROM emp '||'WHERE empno = '||TO_NUMBER(NAME_IN(item_name))|| 'or mgr = '||TO_NUMBER(NAME_IN(item_name))|| ' START WITH empno = '|| TO_NUMBER(NAME_IN(item_name))||'CONNECT BY PRIOR empno = mgr'; || p_populate_list(item_name, query_in, retcd); END;
The WHEN-LIST-ACTIVATED
trigger is modified as follows (the following listing shows the complete code) to accomplish both drill-down and drill-up:
WHEN-LIST-ACTIVATED DECLARE query_in VARCHAR2(32767); item_name VARCHAR2(100) := NAME_IN('SYSTEM.TRIGGER_ITEM'), retcd NUMBER; current_rows_mgr NUMBER; current_rows_empno NUMBER; BEGIN IF INSTR(GET_LIST_ELEMENT_LABEL(item_name, 1),'Parent', 1) = 0 THEN -- if current element is in the initial list query_in := 'SELECT LPAD(ename, ((LEVEL-1)*4+LENGTH(ename)), '''') || DECODE(TO_CHAR(empno), '|| NAME_IN(item_name)||',''- Parent'',NULL) ename, TO_CHAR(empno)'|| 'FROM emp '|| 'WHERE empno = '||TO_NUMBER(NAME_IN(item_name))||'or mgr = '|| TO_NUMBER(NAME_IN(item_name))|| 'START WITH empno = '||TO_NUMBER(NAME_IN(item_name))|| 'CONNECT BY PRIOR empno = mgr '; ELSIF INSTR(GET_LIST_ELEMENT_LABEL(item_name, 1),'Parent', 1) > 0 and (TO_NUMBER(GET_LIST_ELEMENT_VALUE(item_name,1)) != TO_NUMBER(NAME_IN(item_name))) THEN -- if current is a child of a parent query_in := 'SELECT LPAD(ename, ((LEVEL-1)*4+LENGTH(ename)), '''') || DECODE(TO_CHAR(empno), '|| NAME_IN(item_name)||', ''- Parent'',NULL) ename, TO_CHAR(empno)'|| 'FROM emp '|| 'WHERE empno = '||TO_NUMBER(NAME_IN(item_name))||'or mgr = '|| TO_NUMBER(NAME_IN(item_name))|| 'START WITH empno = '||TO_NUMBER(NAME_IN(item_name))|| 'CONNECT BY PRIOR empno = mgr '; ELSIF INSTR(GET_LIST_ELEMENT_LABEL(item_name, 1),'Parent', 1) > 0 AND (TO_NUMBER(GET_LIST_ELEMENT_VALUE(item_name,1)) = TO_NUMBER(NAME_IN(item_name))) THEN -- if current element is a parent BEGIN current_rows_empno := TO_NUMBER(NAME_IN(item_name)); MESSAGE(TO_CHAR(current_rows_empno), ACKNOWLEDGE); SELECT mgr INTO current_rows_mgr FROM emp WHERE empno = current_rows_empno; EXCEPTION WHEN NO_DATA_FOUND THEN NULL; END; IF current_rows_mgr IS NOT NULL THEN query_in := 'SELECT LPAD(ename, ((LEVEL-1)*4+LENGTH(ename)), '''') || DECODE(TO_CHAR(empno), '||TO_CHAR(current_rows_mgr)|| ', ''- Parent'', NULL) ename, TO_CHAR(empno) '|| 'FROM emp '|| 'WHERE empno = '||current_rows_mgr||'or mgr = '||current_rows_mgr|| 'START WITH empno = '||current_rows_mgr|| 'CONNECT BY PRIOR empno = mgr '; ELSE query_in := 'SELECT ename, TO_CHAR(empno) empno FROM emp WHERE empno IN (SELECT mgr FROM emp)'; END IF;; END IF; p_populate_list(item_name, query_in, retcd); END;
Figures 1.8 through 1.10 depict the drill-down operation on the sample list discussed here. The parent for the second level is suffixed with the phrase '- Parent'
to mark it as the parent element for the children displayed below it.
The Find feature
of LOVs enables you to enlarge
or reduce the LOV list as the user types in characters of the LOV element value. This feature can be simulated using COMBO BOX
and T-LIST
type list items in Forms 4.5. Although it doesn't use the exact mechanism offered by LOVs, the method described here imitates the same functionality. You will assume that the list to be displayed is SITE_NAME,
based on the SITE_TAB
table. The SITE_TAB
table has the following structure:
SITE_NO NUMBER(6) NOT NULL, SITE_NAME VARCHAR2(20) NOT NULL.
Follow these steps:
Create a combo box–type list item. Name it LIST_ITEM_COMBO.
The trick lies in using a combo box so that as the user types in characters, the resulting list can be populated with a record group, which is created dynamically, based on user input. A WHEN-LIST-CHANGED
trigger would suffice for this.
Create a T-list–type list item below LIST_ITEM_COMBO.
Name it LIST_DEST_ITEM.
This holds the destination list based on the characters the user types in LIST_ITEM_COMBO.
Create a WHEN-LIST-CHANGED
trigger for LIST_ITEM_COMBO
as follows:
DECLARE rg_list_id RECORDGROUP; ret_code NUMBER; BEGIN rg_list_id := FIND_GROUP('RG_LIST'), IF NOT ID_NULL(rg_list_id) THEN DELETE_GROUP(rg_list_id); END IF; rg_list_id := CREATE_GROUP_FROM_QUERY('RG_LIST', 'SELECT site_name, site_name FROM site_tab WHERE site_name LIKE '||''''||:LIST_ITEM_COMBO||'%'||''''), ret_code := POPULATE_GROUP(rg_list_id); POPULATE_LIST('LIST_DEST_ITEM','RG_LIST'), DELETE_GROUP(rg_list_id); END;
This reduces or enlarges the list, based on user input in LIST_ITEM_COMBO.
Items based on
foreign key look-ups often figure in Forms applications. One of the features most admired in such applications
is a sort order option for users to choose the data item
to base the sorting order. This is often a feature required by end users and MIS managers alike. For example, an MIS manager might want to choose a sort order by the department name while tracking changes to his employees, department-wise. This seems so trivial on first thought but is not, when it comes to actual coding. The department name is generally a foreign key look-up item in the Employees screen and, therefore, a direct ORDER BY
is not possible. This section presents a special technique to implement such ORDER
-ing.
Use a stored function in the database, which retrieves the look-up value based on the foreign key column; specify this function in the ORDER BY
clause for the corresponding block.
Consider a block based on the EMPLOYEE
table with a nonbase table item DEPT_NAME
in it. You want to order by DEPT_NAME,
which is a look-up column (a nonbase table item in the block under consideration), from the DEPT
table based on DEPT_ID
in the EMPLOYEE
table. The follow ing steps will do the job:
CREATE
a stored function named POPULATE_DEPT_NAME(ip_dept_id IN NUMBER)
that returns the DEPT_NAME
corresponding to the parameter ip_dept_id.
CREATE
a nonbase item DEPT_NAME
in the EMPLOYEE
block (which is based on the EMPLOYEE
table).
In the block properties, for the ORDER BY
clause specify POPULATE_DEPT_NAME(DEPT_ID).
Note that the argument passed is the actual column name (that is, DEPT_ID
) in the DEPT
table.
Create a POST-QUERY
trigger for the EMPLOYEE
block as follows:
:employee.dept_name := populate_dept_name(:employee.dept_id);
On querying, it can be seen that the records in the block are ordered by DEPT_NAME.
As a continuation to the sorting capability by foreign key look-up items discussed in the preceding section, I explore here the functionality of querying by nonbase items. The DEPT_NAME
item in the EMPLOYEE
block (illustrated in the preceding section) is a nonbase table, and Forms default query processing does not apply to it. A typical requirement would be to query on all employees belonging to a particular department, based on DEPT_NAME.
The DEPT_ID
is not as suggestive as the DEPT_NAME
in identifying a department.
With the introduction of the capability to change the DEFAULT_WHERE
of a base table block dynamically, querying by nonbase table items can be accomplished with a PRE-QUERY
trigger containing an explicit cursor for determining the foreign key column values corresponding to the nonbase table item value.
An alternative way to order by foreign key items or query by nonbase table items involves specifying an inline SELECT
in place of the base table for the block, based on a stored procedure returning a result set. See Chapter 2, "Advanced GUI Development: Developing Beyond GUI,"
for more details regarding this technique.
Consider the standard DEPT
and EMP
application. You will outline the technique of querying by DEPT_NAME
that is a nonbase
table item in the EMPLOYEE
block. Follow these steps:
Set the Query Allowed
property to YES
/TRUE
for the item DNAME.
Derive the foreign key item DEPT_ID
based on the ad hoc query input of :EMPLOYEE.DEPT_NAME.
Use the POPULATE_DEPT_NAME
procedure to populate the DEPT_NAME
item for every DEPT_ID
retrieved. Use a POST-QUERY
trigger on the EMPLOYEE
block to do this:
POST-QUERY :employee.dept_name := populate_dept_name(:employee.dept_id);
In the PRE-QUERY
trigger, set the DEFAULT_WHERE
of the EMP
block dynamically based on the resulting DEPTNO
:
PRE-QUERY DECLARE v_dyn_where VARCHAR2(100) := NULL; BEGIN v_dyn_where := 'WHERE deptno IN (SELECT deptno FROM dept WHERE dname = NVL(:emp.dname, dname) '), SET_BLOCK_PROPERTY('EMP', DEFAULT_WHERE, v_dyn_where); END;
Performing an Enter Query
in the EMPLOYEE
block and specifying a criteria like S%
in the DEPT_NAME
item followed by Execute Query
would retrieve only those employees belonging to departments whose names start with uppercase S.
PRE-QUERY
is fired even before the SELECT
is constructed and is the best place to dynamically change the DEFAULT_WHERE
and ORDER BY
clauses. PRE-SELECT
is fired after the SELECT
is constructed and before it is issued for execution. The system variable :SYSTEM.LAST_QUERY
holds this SELECT
at this point in time. POST-SELECT
fires after the SELECT
has been executed but before records have been FETCHED.
POST-FETCH
fires repeatedly for each set of records fetched.
Any Forms application should conform to a specific set of GUI standards, which can be frozen for that application. The following serves as a selected list of guidelines that can prove helpful:
Use a sign-on form for each application. A sign-on form helps in two ways. First, an added level of security can be provided. For example, in addition to the Oracle login and password, there can be a secret code for an application particular to each user. This second level of checking can be easily done using a sign-on form. Second, added functionality for the Oracle Forms login can be incorporated, for example, a pick-list of database names for the user to choose from.
Use a standard iconic toolbar and button bar across forms by subclassing from a tem-plate form. You can have a vertical toolbar for function-specific buttons and a horizontal toolbar for application-specific or module-specific feature buttons. Also, common application-specific toolbars and common form-functionality toolbars can figure as menu-level toolbars for forms sharing common menus. Specific icons particular to a form can figure as form-level toolbars. Within a single form, window-specific icons can be organized as window-level toolbars.
Use ToolTips or balloon help for iconic buttons. ToolTips provide onscreen visual hints and also improve the GUI look-and-feel.
Highlight current and cursor records in multirecord blocks and blocks having master-detail relationships. This preserves the focus of the record the user is working on, as well as any of its related record(s).
Use alerts, and standardize the alert categories. This provides a visual way of displaying messages, as well as a distinct categorization of messages according to which ones are real errors, which ones are warnings, and which ones are just informative. For example, it is worthwhile to display a save confirmation alert having Yes, No and Cancel buttons when the user is navigating out of a form or a subwindow.
Use list items for more than five or six choices. Use radio groups in case of fewer choices.
Leverage the full potential of List Items by populating list items dynamically. This helps in customizing the list choices according to user-input runtime criteria.
Use drill-down LOVs in case of multiple levels of data in look-up tables, whether hierarchical or not. This eliminates the need for using multiple LOVs in case of multilevel look-ups.
Control record access at the block level by specifying records-buffered and records-fetched properties for the block. This is a performance-related point and should be done with proper care. The cost of fetch time versus memory should be analyzed carefully. (This tip is not meant to be taken as a rule.)
Exit child windows by means of buttons, such as OK or Close. Also, program the × for each window. This improves the flexibility, in addition to conforming to standard windows conventions.
Use KEY
triggers only to override default functionality of the corresponding physical function key. This is because KEY
triggers are the only directly related components corresponding to a physical function key.
As an example, to check for user selection from an LOV, writing a KEY-LISTVAL
trigger is recommended to redefine the List Values functionality. The following piece of code illustrates this:
KEY-LISTVAL DECLARE Selected BOOLEAN := FALSE; BEGIN LOOP Selected := SHOW_LOV('<lov_name'), IF selected THEN EXIT; ELSE MESSAGE('A value has to be selected from the List of Values'), END IF; END LOOP; END;
The KEY-LISTVAL
trigger can then be invoked using the DO_KEY
built in at a single place or more than one place, if required. By doing this, you provide code that is both appropriate and sharable.
DO_KEY('LIST_VALUES'),
Use POST-QUERY
to populate nonbase items during query time. This ensures that a database read is done only to query the look-up item based on the foreign key value and not also for the actual foreign key item.
Use changing LOVs dynamically at runtime rather than creating multiple LOVs.
Use Default Where
and Order By
to dynamically change query criteria rather than create multiple blocks for each criterion.
Simulate the GUI window-specific standard functionality in forms wherever not supported—for example, closing a window using the × in the upper-right corner.
As another example, you can use the Cut and Paste operations for duplicating an item, regardless of the instance of the record from where data is cut and pasted. The default Forms duplicate item functionality cuts data only from the immediately preceding record and pastes in the new (current) record. An easy way of implementing this is by means of pop-up menus.
Implement multiform applications because they increase modularity and provide ease of use.
Use timers for duration-specific applications such as balloon help.
Implement object and code reusability. Make effective use of object groups and property classes.
Do not set the Keyboard Navigable
and Mouse Navigate
properties to TRUE
/YES
for push buttons present in an exclusive BUTTON_PALETTE
block. This prevents the control from shifting, that is navigating, to the buttons by using either the keyboard or the mouse. One very important reason for doing this is to prevent invalid operations in the button block.
For example, assume that the BUTTON_PALETTE
has a button labeled List to invoke the LOV corresponding to the cursor item, which is a text item. Specifying the Keyboard Navigable
and/or Mouse Navigate
properties to Yes
would shift the input focus from the cursor item to the List button. This causes Forms to invoke the LOV for the button instead, which does not make sense. As a result, you get the error FRM-41026: Field does not understand operation.
Use text items with Insert Allowed
and Update Allowed
properties negated, to behave like display items for items with data length greater than the item width. This enables you to scroll to the end of the data present in the item
. Secondly, it provides consistency by giving a standard look-and-feel to all data-bearing items.
Do not set the current record attribute property for single record blocks and button-only blocks. There is no need to do so, because only one record is displayed at any point in time.
Programmatically populating default values for items is a better practice than setting them through object properties, because it prevents the risk of messing up validation. Default values set through object properties in a record do not initiate validation unless there is either user input or a programmatic setting of some item in a record.
Imagine a record with two items, BATCH_NO
and BATCH_DATE,
with BATCH_SEQNO
being display only (because it will be populated through PRE-INSERT
later on) and a default value of $$DATE$$
set for BATCH_DATE.
Doing so ignores the default form-level validation. As a consequence, consider a detail block for this master BATCH
record, say, BATCH_ITEMS,
where masterless operations are not allowed (because they should not be allowed). There is no way that the user can input data in this detail block unless there is user input—here it will be overwriting the same date. To control this programmatically, use WHEN-CREATE-RECORD.
If there are items that can be entered besides the first item, use WHEN-DATABASE-RECORD.
Alternatively, you can use WHEN-NEW-RECORD-INSTANCE
with :SYSTEM.RECORD_STATUS = 'NEW'.
Use standard form-specific libraries and application-specific libraries. Eliminate SQL from the Forms side as far as possible. This can be achieved by having a set of packaged procedures and functions, which can be called by the Forms Libraries.
The purpose of this chapter was to highlight some tips and techniques in Forms application development. The topics considered were standard toolbar implementation, including dynamic and floating toolbars, timers and a clock display, special tips for list items, querying and ordering by foreign key items in Forms, and object-oriented methods in forms. Last, this chapter also touched on tips for standard GUI practices and forms development. The concepts were illustrated with examples, and alternatives to each concept were discussed. These tips are based on the author's individual experience in the related field, and any code segments presented herein are the author's own.
3.12.162.65