Dynamic Menu Service for PFC PowerBuilder Applications
Back in my PB 7 days I was tasked with creating a Requisition System for the Purchasing department to replace an extremely cumbersome manual process. Essentially it was taking the data from a multi-thousand page report (which was produced daily) into a client-server application. It was a task most business developers should relish since, if done properly, it will produce instant productivity gains for the company.
I decided on an MDI model for the app but when it came to the menus for the windows, I really did not want to create a bunch of objects – even though the app itself only had about forty five windows. With some research in the PFC documentation on the pfc_n_cst_menu this is what I came up with.
My basic idea was to have a single application menu which was used on all screens rather than the standard method of using one menu for each window. This ‘master’ menu would be initialized prior to the opening of a window and options enabled / disabled dynamically as the user progressed through their workflow. Now PB menu handling was always known to be memory intensive but based on the hardware the end users would be on I felt the app should still perform at an acceptable level.
The initial database set up for this service involves entry of the rows which differ from the menu defaults (i.e., items enabled / disabled when window opened). Each window has a set of entries for all the applicable menu entries.
Objects to get this working
<strong>Single Menu object (used by forty odd windows)</strong>
m_app_frame (inherited from the PFC m_master object)
This object has all the various options for all windows within the application. If there are menu options you won’t be using in your application, make them invisible on this object.
<strong>Table definition (MS SQLServer)</strong>
CREATE TABLE dbo.cmnMenuOptions (menuOptionKey int NOT NULL , menuName varchar(50) NOT NULL , windowName varchar(50) NOT NULL , initialSetting char(1) NOT NULL , CONSTRAINT pkmenuoptionkey PRIMARY KEY CLUSTERED (menuOptionKey)) ; CREATE UNIQUE INDEX idx_cMO_1 ON dbo.cmnMenuOptions (windowName , menuName ) ;
<strong>Datawindow object (d_menu_settings) based on cmnMenuOptions table with retrieval argument of 'as_windowname'</strong>
table(column=(type=char(50) updatewhereclause=yes name=menuname dbname="cmnMenuOptions.menuName" )
column=(type=char(1) updatewhereclause=yes name=initialsetting dbname="cmnMenuOptions.initialSetting" )
retrieve=" SELECT cmnMenuOptions.menuName,
cmnMenuOptions.initialSetting
FROM cmnMenuOptions
WHERE cmnMenuOptions.windowName = :as_windowname
" arguments=(("as_windowname", string)) )
Menu service object code (from the export)
forward
global type n_cst_menuservice from n_base
end type
end forward
global type n_cst_menuservice from n_base autoinstantiate
end type
type variables
datastore ids_menuoptions
menu im_menu, im_item
string is_menuoptions[]
window iw_parent
n_cst_menu inv_menu // PFC menu service
n_cst_string inv_string // PFC string service
end variables
forward prototypes
public function integer of_setmenuoptions ()
public subroutine of_setmenu (boolean ab_enable, string as_menuitem)
public function integer of_changemenuoption (string as_option, string as_enabled)
public function integer of_findmenuoption (string as_option)
public subroutine of_menuinit (string as_window)
end prototypes
public function integer of_setmenuoptions ();
// sets the is_menuoptions array for the
// menu assigned to this window.
// only visible options are included.
integer li_limit, li_cnt, li_submenulimit, li_smcnt
integer li_count
string ls_option
li_count = 1
li_limit = UpperBound (im_menu.item)
// loop through every menu item on the menu
FOR li_cnt = 1 TO li_limit
// skip any invisible menu items
IF (im_menu.item[li_cnt].visible) THEN
// check for sub menus
li_submenulimit = Upperbound(im_menu.item[li_cnt].item)
CHOOSE CASE lower(im_menu.item[li_cnt].classname())
CASE 'm_window','m_help'
// skip these
CASE ELSE
FOR li_smcnt = 1 to li_submenulimit
IF (im_menu.item[li_cnt].item[li_smcnt].Visible) THEN
ls_option =lower(im_menu.item[li_cnt].item[li_smcnt].classname())
IF (Pos(ls_option,'dash') = 0) THEN // skip any dash entries
ls_option +='='+string(im_menu.item[li_cnt].item[li_smcnt].enabled)
is_menuoptions[li_count]=ls_option
li_count++
END IF
END IF
NEXT
END CHOOSE
END IF
NEXT
RETURN li_limit
end function
public subroutine of_setmenu (boolean ab_enable, string as_menuitem);
// enables or disables a menuitem
IF IsNull(as_menuitem) or as_menuitem = '' THEN RETURN
im_menu = iw_parent.MenuID
// get reference to menu item so it's properties can be changed
inv_menu.of_GetMenuReference (im_menu, as_menuitem, im_item)
im_item.Enabled = ab_enable
end subroutine
public function integer of_changemenuoption (string as_option, string as_enabled);
// set the menuoptions array to the proper format (ie, "m_save=true")
integer li_rc
integer li_ac
li_rc = 0
li_ac = this.of_findmenuoption(as_option)
IF (li_ac > 0) THEN
is_menuoptions[li_ac] = lower(as_option + '=' + as_enabled)
li_rc = li_ac
END IF
RETURN li_rc
end function
public function integer of_findmenuoption (string as_option);
// find the specified menu item in the array of menu options
integer li_ac
integer li_i
integer li_rc
li_rc = 0
li_ac = upperbound(is_menuoptions)
FOR li_i = 1 to li_ac
IF (Pos(is_menuoptions[li_i],as_option) > 0) THEN
li_rc = li_i
EXIT
END IF
NEXT
RETURN li_rc
end function
public subroutine of_menuinit (string as_window);
// initialize menu for specified window from the database table
long ll_rc
long ll_i
string ls_option
string ls_enabled
// get the menu options for the specified window
ll_rc = ids_menuoptions.retrieve(as_window)
FOR ll_i = 1 to ll_rc
ls_option = ids_menuoptions.getitemstring(ll_i,'menuname')
ls_enabled = ids_menuoptions.getitemstring(ll_i,'initialsetting')
IF (Trim(ls_enabled) = 'T') THEN
ls_enabled = 'true'
ELSE
ls_enabled = 'false'
END IF
of_changemenuoption(ls_option,ls_enabled)
NEXT
end subroutine
on n_cst_menuservice.create
call super::create
end on
on n_cst_menuservice.destroy
call super::destroy
end on
event constructor;call super::constructor;
// set up menu service datastore
ids_menuoptions = CREATE datastore
ids_menuoptions.dataobject = 'd_menu_settings'
ids_menuoptions.settransobject(SQLCA)
end event
event destructor;call super::destructor;
// destroy datastore
IF IsValid(ids_menuoptions) THEN DESTROY ids_menuoptions
end event
Code from window(sheet) ancestor
Instance Variable
n_cst_menuservice inv_ms
pfc_postopen event code
inv_ms.iw_parent = THIS // sets parent on menu service inv_ms.im_menu = this.MenuID inv_ms.of_setmenuoptions() event trigger ue_menuinit() wf_menu(inv_ms.is_menuoptions)
ue_menuinit event code
//Initial menu for this window string ls_classname ls_classname = this.classname() inv_ms.of_menuinit(ls_classname)
wf_menu function code
// call the menu processing service for each item // in the as_items array. // format of as_items is: "m_new=true" // indicating this item should be enabled integer li_return integer li_rc integer li_i string ls_option boolean lb_enabled li_return = 1 li_rc = upperbound(as_items) FOR li_i = 1 to li_rc ls_option = inv_ms.inv_string.of_gettoken(as_items[li_i],'=') lb_enabled = (Pos(as_items[li_i],'true') > 0) inv_ms.of_setmenu(lb_enabled,ls_option) NEXT RETURN li_return
Example descendant window code used to enable / disable menu items based on user input, processing, work flow changes, etc.
string ls_menu[] // this could be made an instance variable too ls_menu[1] = 'm_save=true' //enable the save option on the window // add additional array elements as needed wf_menu(ls_menu) // now the menu option is changed
Example cmnMenuOptions rows for window ‘w_inv_action_list’
Key menuName windowName initialSetting 1 m_save w_inv_action_list F 2 m_saveas w_inv_action_list F 3 m_printpreview w_inv_action_list F 4 m_pagesetup w_inv_action_list F 5 m_first w_inv_action_list F 6 m_priorpage w_inv_action_list F 7 m_nextpage w_inv_action_list F 8 m_lastpage w_inv_action_list F 9 m_zoom w_inv_action_list F 10 m_processdetail w_inv_action_list F 11 m_sort w_inv_action_list F 12 m_uploadreq w_inv_action_list F 13 m_selectall w_inv_action_list F 14 m_clear w_inv_action_list T 15 m_paste w_inv_action_list F 16 m_copy w_inv_action_list F 17 m_cut w_inv_action_list F 18 m_undo w_inv_action_list F 19 m_insertrow w_inv_action_list F 20 m_addrow w_inv_action_list F 21 m_deleterow w_inv_action_list F 22 m_nextitem w_inv_action_list F 252 m_reset w_inv_action_list F
Now if a new menu option is added to the application you add rows to the cmnMenuOptions for each window. Similar to this:
INSERT cmnMenuOptions (menuName, windowName, initialSetting) SELECT DISTINCT 'm_newmenuname', windowName, 'F' from cmnMenuOptionsYou might also be interested in