Dynamic Menu Service for PFC PowerBuilder Applications

Posted on Friday, August 21st, 2009 at 8:05 am in

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

Single Menu object (used by forty odd windows)

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.

Table definition (MS SQLServer)

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 ) ;

Datawindow object (d_menu_settings) based on cmnMenuOptions table with retrieval argument of 'as_windowname'

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 cmnMenuOptions
You might also be interested in

Top