Friday, April 10, 2020

Controlling Controls in VFP (Part 1: Display Styles)

Published March 29, 2009 | By Andy Kramek

A question that often comes up is related to how to handle scenarios where controls on a form need to behave differently according to some condition. for example, in our applications our normal practice is to bring forms up in view–only mode. this means that the user has to take some positive action (click a toolbar button, or a button on the form) to put the form into either add (i.e. get a new blank record), or edit (i.e. change the existing data). this requirement conveys several distinct benefits in the context of an application.

first, it guards against accidental data changes. if a form came up in edit mode, with the first field selected, an accidental press of the delete key would delete the data. in our scenario this can't happen because they form is read-only by default.

second, it gives us an opportunity to determine whether the user has permission to carry out their intended action. as a general rule we don't like the 'magic disappearing button' approach, but we certainly want to be able to disable the "add" and "edit" buttons if the current user does not have anything other than "view-only" permission.

third, it allows us to control the functionality so that only the appropriate options are enabled. thus when the form first appears (assuming the appropriate permissions) the only options enabled might be add, edit, search, print and exit. upon entering either add or edit mode we would want to disable all the previously enabled options and enable only the save and cancel options instead. not only does this help the user know what they can do, it prevents them from navigating away from a half-finished entry and it obviates the need for testing to see if changes were made (and the need for the irritating "do you want to save your changes?" message that appears in so many applications when the user hasn't actually made a change).

fourth, it enables us to present a consistent and intuitive behavior for our interfaces. this is by no means an insignificant factor because the key to a good user interface is that it should be obvious to a user what they are supposed to do. after all, having a "save" button enabled does not make much sense unless the user has actually done something that might need to be saved!

so the question is how can we do all this without writing lots of repetitive code?

the first thing we need is have some means of determining what mode a form is in at any given time. fortunately this is simple enough as there really are only three options for this; view-only, edit (existing data) or add (new data). so all we need is, in our form class to add a property for the form mode. in our classes this is named "cmode" and takes a single letter, either "v", "e" or "a" and has a default value of "v".



note: since this is not a user-controllable value there is no absolute need to add checking or validation to the property, it will only ever be set in code. however, if you wanted to ensure that these values are the only ones ever applied, an assign method would be a great way to handle it. the following code does exactly this, defaulting to "v" if an invalid value is passed in.


***cmode_assign method:
lparameters tunewvalue
*** default to view if empty or not character otherwise get, and make upper case, the first letter
tunewvalue = iif (vartype( tunewvalue ) # "c" or empty( tunewvalue ), "v", upper(left( tunewvalue,1)))

*** default to "v" if not v, e or a
tunewvalue = iif (inlist( tunewvalue, "v", "e", "a" ), tunewvalue, "v" )

*** now set the mode
this.cmode = tunewvalue
return

Now that we have a form mode property all we need to do is to make our controls recognize it. the basic methodology is simple enough but the implementation is slightly different for different types of control. for editable controls (textboxes, edit boxes, combos and lists) we use colors to indicate the state of the control. for example a control that is read-only is displayed with black text on a light grey background, while a control where an entry is mandatory is displayed as black on yellow. (figure 1).


In order to implement mode-aware controls we need to modify the root classes. these are the first level sub-class from the vfp base class on which all your controls are based. by adding this functionality at the root level all sub classes will inherit it. we will need two properties, and a little method code. the properties are named "cobjmode" which is used to control the object's mode, and "cmodeexpr" which is used to define the rule for modifying the object mode at run time. the main code that we need goers into the refresh() method of the class and, is slightly different for different types of control:

Mode-aware textboxes, editboxes and combos


Have the same functionality for Refresh() and are set up like this:

with this
  do case
       case .cobjmode = 'o'  && optional
          .backcolor = rgb( 255, 255, 255 )  && white
          .forecolor = rgb( 0,0,0 )  && black
          .mousepointer = 0  && default
          .tabstop = .t.
          .readonly = .f.
       case .cobjmode = 'm'  && mandatory
          .backcolor = rgb( 255,255,0 )  && yellow

          .forecolor = rgb( 0,0,0 )  && black
          .mousepointer = 0  && default
          .tabstop = .t.
          .readonly = .f.
       case .cobjmode = 's'  && system set
          .backcolor = rgb( 128,128,128 )  && dark grey
          .forecolor = rgb( 255,255,255 )  && black
          .mousepointer = 1  && arrow
       case .cobjmode = 'r'  && read only
          .backcolor = rgb(192,192,192)  && grey
          .forecolor = rgb( 0,0,0 )  && black
          .mousepointer = 1  && arrow
          .tabstop = .f.
          .readonly = .t.
       otherwise     && default to optional
          .cobjmode = 'o'
          .backcolor = rgb( 255, 255, 255 )  && white
          .forecolor = rgb( 0,0,0 )  && black
          .mousepointer = 0  && default
          .tabstop = .t.
          .readonly = .f.
       endcase
endwith

Notice that this code ensures that a read-only control is removed from the tab order, but is still selectable when needed. this contrasts with the "system set" which is not even selectable. that is handle by a single line of code the when() event:

return not ( this.cobjmode = "s" )

that effectively disables the control completely when the object mode = "s".


Mode-aware lists

The code here is almost identical, but we must specify the item foreground and background colors:

with this
  do case
       case .cobjmode = 'o'  && optional
          .itembackcolor = rgb( 255, 255, 255 )  && white
          .itemforecolor = rgb( 0,0,0 )  && black
          .mousepointer = 0  && default
          .tabstop = .t.
       case .cobjmode = 'm'  && mandatory
          .itembackcolor = rgb( 255,255,0 )  && yellow
          .itemforecolor = rgb( 0,0,0 )  && black
          .mousepointer = 0  && default
          .tabstop = .t.
       case .cobjmode = 's'  && system set
          .itembackcolor = rgb( 128,128,128 )  && dark grey
          .itemforecolor = rgb( 255,255,255 )  && black
          .mousepointer = 1  && arrow
       case .cobjmode = 'r'  && read only
          .itembackcolor = rgb(192,192,192)  && grey
          .itemforecolor = rgb( 0,0,0 )  && black
          .mousepointer = 1  && arrow
          .tabstop = .f.
       otherwise     && default to optional

          .cobjmode = 'o'
          .itembackcolor = rgb( 255, 255, 255 )  && white
          .itemforecolor = rgb( 0,0,0 )  && black
          .mousepointer = 0  && default
          .tabstop = .t.
       endcase
endwith

and the code in the when() is identical.

Mode-aware action controls

Commandbuttons, commandgroups, optiongroups, optionbuttons, checkboxes, pages (not pageframes) and grids also get these properties. however, instead of appearance we use them to control the enabled, disabled and visibility properties. here is the code from the refresh() of the command button class:

with this
    do case
        case .cobjmode = "e"    && enabled
            .enabled = .t.
            .visible = .t.
        case .cobjmode = "d"    && disabled
            .enabled = .f.
            .visible = .t.
        case .cobjmode = "h"    && hidden
            .enabled = .f.
            .visible = .f.
        otherwise                && default to enabled
            .cobjmode = 'e'
            .enabled = .t.
            .visible = .t.
    endcase
endwith

Now we have our controls set up so that they have behavior built into them based on their individual mode setting. in order to implement that behavior we now need to go back to our form class and add some code there too. the first thing to do is to create a decorator method for the form level refresh() method. we do this by adding a couple of custom methods named "beforerefresh()", "afterrefresh()" and "refreshform()". the first two, as the names imply, are template hook methods which, at the class level, are empty (and therefore return .t.).

The RefreshForm() method is the one that contains the code. it takes an optional parameter (that defaults to .f. if not passed) as follows:

lparameters tlsetobjmode
with thisform
  .lockscreen = .t.
  if .beforerefresh()
       if tlsetobjmode
          .setobjmode( this, .t. )
       endif 
       .refresh()
       .afterrefresh()
  endif
  .lockscreen = .f.
endwith  

The first thing this method does is to manage the form's lockscreen property. next it calls out to the before refresh() hook. if that does not return .t. then the lockscreen is released and nothing else happens. if beforerefresh() returns .t. the next thing depends on the input parameter. if it is .f. (or was omitted) the form level refresh() is called immediately, otherwise an extra call to the form's 'setobjmode()' method, passing a reference to the form itself and a logical .t. (to suppress immediate refreshes), is made first. as the name implies, this method is responsible for setting the cobjmode property on all controls on the form.

We use recursive code to handle this:

lparameters toobject, tlnorefresh
local loref, lncnt, lopage, locolumn, lcexpr, lulastmode

do case
  case toobject.baseclass = 'pageframe'
    *** validate each page
    for each lopage in toobject.pages
      if lopage.pageorder = toobject.activepage
        *** loop through the objects on the pages
        for each loref in lopage.controls
          *** validate form objects that require validation
          thisform.setobjmode( loref, tlnorefresh )
        endfor
        exit
      endif
    endfor


  case inlist( toobject.baseclass, 'form', 'container' )
    *** loop through all the objects on the form or in the container
    for each loref in toobject.controls
      *** validate objects that require validation
      thisform.setobjmode( loref, tlnorefresh )
    endfor

  otherwise
    *** evaluate the mode expression and refresh the control's cobjmode
    if pemstatus( toobject, 'cmodeexpr', 5 )
      if ! empty( toobject.cmodeexpr )
        lcexpr = toobject.cmodeexpr
        lulastmode = toobject.cobjmode
        toobject.cobjmode = &lcexpr
        *** only refresh if value has changed
        if ! tlnorefresh and toobject.cobjmode # lulastmode
          toobject.refresh()
        endif
      endif
    endif
endcase 
return

This method loops through the form's controls collection and for container classes calls itself, recursively. for any control that it finds that has a non-empty cmodeexpr property it evaluates the expression and sets the cobjmode accordingly.

Now you may be wondering why we have the extra parameter to suppress the immediate refresh of the control. the reason is that this method could be called from other places than refreshform(). when called from refreshform(), the next thing will be always a full form-level refresh, so there is no point in immediately refreshing each control individually. however, when called directly from a control (when the value in some control that affects another control directly has changed for instance) there will be no need to call a full form-level refresh and so we only want to refresh affected controls.

When all is done the refreshform() method calls the native form level refresh(), then the afterrefresh() hook and finally explicitly releases the lockscreen that it placed.

We are now finished! all that we have to do to implement this functionality in a form is to set the initial values for the cobjmode property to whatever is appropriate and then add the logical expression to handle changes to the cmodeexpr property like this, for a "save" button which will enable itself in add or edit mode but be disabled in view mode:

iif( thisform.cmode = 'v', 'd', 'e' )

or this for a textbox which is mandatory in add mode, optional in edit mode and read-only in view:

icase( thisform.cmode = 'v', 'r', thisform.cmode = 'e', 'o', 'm' )

Wherever you need to use the functionality simply replace calls to thisform.refresh() with calls to thisform.refreshform(.t.). so in an "add" button we would use code like this:

append blank
thisform.cmode = 'a'
thisform.refreshform(.t.)

while for an "edit" button the equivalent code would simply be:

thisform.cmode = 'e'
thisform.refreshform(.t.)

Using this methodology you never have to worry about enabling/disabling controls again. each control looks after itself based on its current environment. there are many other ways in which this type of approach can be used and i would be interested to hear of any that you come up with.

No comments:

Post a Comment

Writing better code (Part 1)

Writing better code (Part 1) As we all know, Visual FoxPro provides an extremely rich and varied development environment but sometimes to...