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