Using Toolbars in Visual FoxPro

Dave Lehr
Soft Classics, Ltd.

Visual FoxPro lets you create "real" toolbars - something you could never do in FPW 2.6. Toolbars provide a nice way to access commonly used features in your application, such as record navigation, saving changes, and opening forms. Toolbars can make your applications much more appealing, however there are some design considerations you need to consider before you will be able to use them effectively.

Toolbars are a special type of form, based on the VFP Toolbar class, or a subclass thereof. Toolbars differ from standard forms in that they can never receive focus, so clicking on a toolbar control will not take focus away from the current form. You can add standard CommandButton, Combobox, or graphical Checkbox controls to toolbars just like on a regular form. Most other controls, however, are not suitable for use in a toolbar.

Toolbars are typically used to implement a secondary interface to commonly used menu items. As such, the toolbar buttons need to be enabled or disabled in sync with any corresponding menu item. When you click on a menu pad, FoxPro evaluates all the SKIP FOR clauses to determine which menu items should be enabled and which should be disabled. You could take the same approach with your toolbar buttons: When someone clicks on the button, check to see if the feature is currently enabled and beep if not. However, since toolbar buttons are always visible to the user, it is much better if you disable the button whenever the associated action is not available. Typically, you will add code to the Refresh() method of each button to determine the correct enable state for the button. That way the user always has visual feedback indicating which buttons are available to select from.

In order for the toolbars to always accurately reflect the current enable/disable state of each button, you need to refresh them whenever your code does something that may affect their state. For example, after moving to a new record, or saving changes in a form. Another time toolbars need to be refreshed is when the active form changes, since toolbar buttons (especially record navigation buttons) will need to reflect the state of each new active form.

Deciding Where to Put Code

One of the more important steps in designing any system is delegation - deciding what objects or procedures are responsible for what actions. If you are careful when delegating responsibility, you can greatly extend the functionality and flexibility of your system.

If you are used to programming in FoxPro 2.6, you might be tempted to put code to execute a particular function in each buttons Click() event. You could, for example, create a "Next" button to advance to the next record in the current form. The Click() event code might look something like the following:

IF TYPE('_SCREEN.activeform.name') = 'C'
  IF USED() AND NOT EOF()
    SKIP 1
    _SCREEN.ActiveForm.Refresh()
  ENDIF
ENDIF

The above code will work fine for most simple data forms, as long as the active form can always safely skip to a new record. But what if the form has unsaved changes? What if the navigation buttons are supposed to operate on a child cursor rather than the current workarea? You could keep expanding the code in your Next button to take into account these possibilities, but you can be sure there will someday be a special case form that needs something your button didn't expect. Then there is the matter of the menu bar - what if you want to have a "Next" menu item to do the same thing. Clearly there are some problems with this approach.

A much better approach, used by nearly all popular application frameworks, is to delegate responsibility for toolbar actions to the target forms themselves. In this case, the toolbar button's Click() event code will simply call the active form's Next() method, and the form can handle it however it sees fit. Now you have a very simple Next button that will always work for any form that has a Next() method. The Click() event code for a simple Next button using this technique might look like the following:

* Check if there is an active form, and use PEMSTATUS() to see if it has a Next() method
IF TYPE('_SCREEN.activeform.name') = 'C' AND PEMSTATUS(_SCREEN.activeform, 'Next', 5)
  _SCREEN.ActiveForm.Next()
ENDIF

Integrating Forms and Toolbars

In Visual FoxPro, there are two general approaches you can take to add toolbars to your applications. You can use formsets to contain both forms and toolbars, or you can create independent stand-alone toolbars.

Formsets

One way is to create a form-set, and add the toolbar along with the associated forms to the formset. The toolbar will then be created whenever you open the formset, along with all the forms. You can then add code to the toolbar buttons to take appropriate actions when they are pushed. This approach is similar to the old Foundation READ examples provided with FPW 2.6. While you might be tempted to take this approach at first, there are a few problems with it.

For these reasons, using a formset to integrate toolbars and forms is usually not a very good approach.

Standalone Toolbars

The preferred technique is to create standalone toolbars that are independent from any particular form. You can define standalone toolbar classes using the VFP Class Designer, and then create them at runtime using the CREATEOBJECT() function. Typically an application will open one or more toolbars when it starts up, and they will remain open for the life of the application session. More advanced frameworks will allow the user to interactively open and close toolbars with some type of "View Toolbars" menu option.

With this approach, we can have any number of toolbars open at any given time, you can use them before any forms are created, and every form in the application can easily share the same toolbar. But now there are a few other small problems that turn up. How can the forms find references to the toolbars to refresh them when needed? And what happens if we want to add menu items to do the same thing as some of the toolbar buttons? The menu items will need to enable and disable at the proper times as well as the toolbars. There are many possible ways to solve those problems. One of the simplest, most elegant, and most flexible solutions is to use a global State Manager object to act as a mediator between menus, toolbars, forms, and any other service objects in your application.

Designing a State Manager

A state manager is a globally accessible object that menus and toolbars can use to communicate with forms and other service objects. Your application would create the state manager object before putting up the system menu and before creating any forms or toolbars.

Then, when a menu item or toolbar button needs to know if it should be enabled or disabled, it asks the state manager, rather than asking the active form directly. The state manager object will then determine what the active form is, and whether or not it supports the requested method. When a menu item or toolbar button is selected and needs to execute a method, it tells the state manager what method needs to be called, rather than calling the method directly. Typically, the state manager object will be addressed via a global variable, for example "m.goStateManager", so it can easily be accessed from within menu SET SKIP OF and ON SELECTION commands, as well as from any toolbar button.

The state manager object is also responsible for maintaining a list of all open toolbars. Then whenever a form needs to refresh the toolbars, it can call the state manager and tell it to refresh all toolbars in the list. So now forms can easily refresh the toolbars when needed, without having to worry about figuring out which ones are currently open.

The code segments listed below show the beginnings of a simple state manager object implementation. They are not necessarily complete, but will give you a good start on developing your own complete state manager.

FUNCTION IsEnabled(cMethod)
  IF TYPE('_SCREEN.activeform.name') = 'C'
    RETURN PEMSTATUS(_SCREEN.activeform, m.cMethod, 5)
  ENDIF
  RETURN .F.
ENDFUNC

The IsEnabled() method is used to determine whether or not the current form has the requested method. It would be called from the Refresh() method of toolbar buttons, and from the SET SKIP OF clause of menu bars. For example, the refresh() method of a toolbar "Next" button would have code like:

 THIS.Enabled = m.goStageManager.IsEnabled("Next")

As a further step, you could enhance IsEnabled() to check for the existence of an IsEnabled() method in the form. If the method exists, call the form's IsEnabled() method to determine if the specified cMethod can be called at the current time. Then code in the form's IsEnabled() method can determine whether or not each supported value for cMethod is currently enabled or disabled. Then you have a completely generic mechanism to dynamically enable or disable any toolbar button or menu interface to your form.

FUNCTION Execute(cMethod)
  IF TYPE('_SCREEN.activeform.name') = 'C'
    IF PEMSTATUS(_SCREEN.activeform, m.cMethod, 5)
      RETURN EVALUATE('_SCREEN.Activeform.' + m.cMethod + '()')
    ENDIF
  ENDIF
ENDFUNC

The Execute() method is used to call the specified method in the active form. First, Execute() checks to make sure there is an active form, and then checks to make sure the form has the specified method. If a tests succeed, the target method is called. Now any toolbar or menu item can safely call a method in the active form with a single line of code.

FUNCTION RefreshToolbars()
  * Refresh every toolbar in the system
  FOR ix = 1 TO _SCREEN.FormCount
    IF _SCREEN.Forms[m.ix].Baseclass == 'Toolbar'
      _SCREEN.Forms[m.ix].Refresh()
    ENDIF
  ENDFOR
ENDFUNC

The RefreshToolbars() method provides an easy way for forms to refresh all the toolbars whenever needed. Adding this method to the state manager means your forms don't need to bother keeping track of which toolbars are currently open. Normally, a form should call goStatemanager.RefreshToolbars() whenever the form is refreshed, when it becomes active, when it is closed, and any other time something happens they may affect toolbar button settings.

Summary

A state manager object takes some extra work up front to design and create, but once you have one in place, it makes integrating forms, toolbars and menus much easier. Some of the many benefits include:

The State Manager object included with the Codemine Development System Framework includes many additional features for even greater flexibility. Most notably, it allows for automatic background refreshes of toolbars during system idle time whenever the active window changes. Using idle time to refresh toolbars improves the performance of your application, and also reduces the number of places your forms need to explicitly request toolbar refreshes. For more information and a sample application, visit the Soft Classics web site at www.Codemine.com

Author Info:

Dave Lehr
Soft Classics, Ltd.
Phone: 207-942-4112
Fax: 207-942-3693
email: Dave@Codemine.com